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.
- package/dist/.env.example +4 -0
- package/dist/locales/en.json +74 -2
- package/dist/locales/fi.json +74 -2
- package/dist/public/css/views/capabilities.css +211 -0
- package/dist/public/llms-template.txt +152 -89
- package/dist/public/spa.html +1 -0
- package/dist/public/views/admin/capabilities-tab.js +114 -0
- package/dist/public/views/admin.js +2 -0
- package/dist/public/views/aimeat-os.js +194 -34
- package/dist/public/views/profile/capabilities-tab.js +252 -0
- package/dist/public/views/profile/extensions-tab.js +404 -50
- package/dist/public/views/profile/landing-page.js +2 -0
- package/dist/public/views/profile.js +2 -0
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +11 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/mcp/apps.d.ts +18 -0
- package/dist/src/mcp/apps.d.ts.map +1 -0
- package/dist/src/mcp/apps.js +241 -0
- package/dist/src/mcp/apps.js.map +1 -0
- package/dist/src/mcp/capabilities.d.ts +12 -0
- package/dist/src/mcp/capabilities.d.ts.map +1 -0
- package/dist/src/mcp/capabilities.js +187 -0
- package/dist/src/mcp/capabilities.js.map +1 -0
- package/dist/src/mcp/cortex.d.ts +17 -0
- package/dist/src/mcp/cortex.d.ts.map +1 -0
- package/dist/src/mcp/cortex.js +237 -0
- package/dist/src/mcp/cortex.js.map +1 -0
- package/dist/src/mcp/extensions.d.ts +4 -3
- package/dist/src/mcp/extensions.d.ts.map +1 -1
- package/dist/src/mcp/extensions.js +233 -3
- package/dist/src/mcp/extensions.js.map +1 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +6 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/routes/admin-capabilities.d.ts +11 -0
- package/dist/src/routes/admin-capabilities.d.ts.map +1 -0
- package/dist/src/routes/admin-capabilities.js +54 -0
- package/dist/src/routes/admin-capabilities.js.map +1 -0
- package/dist/src/routes/capabilities.d.ts +11 -0
- package/dist/src/routes/capabilities.d.ts.map +1 -0
- package/dist/src/routes/capabilities.js +289 -0
- package/dist/src/routes/capabilities.js.map +1 -0
- package/dist/src/routes/cortex.d.ts.map +1 -1
- package/dist/src/routes/cortex.js +2 -0
- package/dist/src/routes/cortex.js.map +1 -1
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +9 -0
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/lib-capabilities.d.ts +9 -0
- package/dist/src/routes/lib-capabilities.d.ts.map +1 -0
- package/dist/src/routes/lib-capabilities.js +190 -0
- package/dist/src/routes/lib-capabilities.js.map +1 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +13 -0
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/verification.d.ts +10 -1
- package/dist/src/routes/verification.d.ts.map +1 -1
- package/dist/src/routes/verification.js +174 -24
- package/dist/src/routes/verification.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +35 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/capability-aggregator.d.ts +8 -0
- package/dist/src/services/capability-aggregator.d.ts.map +1 -0
- package/dist/src/services/capability-aggregator.js +239 -0
- package/dist/src/services/capability-aggregator.js.map +1 -0
- package/dist/src/services/capability-invoke.d.ts +20 -0
- package/dist/src/services/capability-invoke.d.ts.map +1 -0
- package/dist/src/services/capability-invoke.js +95 -0
- package/dist/src/services/capability-invoke.js.map +1 -0
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +4 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/core-jobs.d.ts.map +1 -1
- package/dist/src/services/core-jobs.js +16 -0
- package/dist/src/services/core-jobs.js.map +1 -1
- package/dist/src/services/did-document.d.ts +25 -0
- package/dist/src/services/did-document.d.ts.map +1 -0
- package/dist/src/services/did-document.js +31 -0
- package/dist/src/services/did-document.js.map +1 -0
- package/dist/src/services/eudiw.d.ts +11 -1
- package/dist/src/services/eudiw.d.ts.map +1 -1
- package/dist/src/services/eudiw.js +43 -48
- package/dist/src/services/eudiw.js.map +1 -1
- package/dist/src/services/extension-runtime.d.ts.map +1 -1
- package/dist/src/services/extension-runtime.js +8 -5
- package/dist/src/services/extension-runtime.js.map +1 -1
- package/dist/src/services/job-seeding.d.ts.map +1 -1
- package/dist/src/services/job-seeding.js +4 -0
- package/dist/src/services/job-seeding.js.map +1 -1
- package/dist/src/services/oidc-client.d.ts +32 -0
- package/dist/src/services/oidc-client.d.ts.map +1 -0
- package/dist/src/services/oidc-client.js +67 -0
- package/dist/src/services/oidc-client.js.map +1 -0
- package/dist/src/services/sd-jwt.d.ts +21 -0
- package/dist/src/services/sd-jwt.d.ts.map +1 -0
- package/dist/src/services/sd-jwt.js +87 -0
- package/dist/src/services/sd-jwt.js.map +1 -0
- package/dist/src/services/vc-issuer.d.ts +20 -1
- package/dist/src/services/vc-issuer.d.ts.map +1 -1
- package/dist/src/services/vc-issuer.js +75 -15
- package/dist/src/services/vc-issuer.js.map +1 -1
- package/dist/src/storage/interface.d.ts +118 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +37 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +282 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +37 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +241 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +77 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/capability.repository.d.ts +35 -0
- package/dist/src/storage/repositories/capability.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/capability.repository.js +2 -0
- package/dist/src/storage/repositories/capability.repository.js.map +1 -0
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/node.repository.d.ts +5 -1
- package/dist/src/storage/repositories/node.repository.d.ts.map +1 -1
- package/dist/static/app-catalog.html +185 -29
- package/package.json +6 -1
- 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
|
|
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
|
-
* -
|
|
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 +
|
|
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
|
-
//
|
|
126
|
-
const [
|
|
127
|
-
const [
|
|
128
|
-
const [
|
|
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();
|
|
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();
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
try {
|
|
165
|
-
} catch(e) {
|
|
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
|
|
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
|
-
|
|
174
|
-
if (
|
|
447
|
+
loadServerExtensions();
|
|
448
|
+
if (srvDetail?.name === name) loadSrvDetail(name);
|
|
175
449
|
} catch(e) { showToast(e.message, true); }
|
|
176
450
|
}
|
|
177
451
|
|
|
178
|
-
async function
|
|
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
|
-
|
|
184
|
-
if (
|
|
457
|
+
loadServerExtensions();
|
|
458
|
+
if (srvDetail?.name === name) loadSrvDetail(name);
|
|
185
459
|
} catch(e) { showToast(e.message, true); }
|
|
186
460
|
}
|
|
187
461
|
|
|
188
|
-
async function
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
438
|
-
if (
|
|
439
|
-
if (
|
|
440
|
-
const ext =
|
|
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=${() =>
|
|
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 {
|
|
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
|
-
${!
|
|
827
|
+
${!srvInstances ? html`<${Spinner} />` : srvInstances.length === 0
|
|
503
828
|
? html`<div class="ext-no-instances">${t('profile.v8ext.noInstances')}</div>`
|
|
504
|
-
:
|
|
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=${() =>
|
|
529
|
-
: html`<button class="btn-primary" onClick=${() =>
|
|
530
|
-
<button class="btn-danger-solid" onClick=${() =>
|
|
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
|
|
871
|
+
const hasSrv = srvExts && srvExts.length > 0;
|
|
547
872
|
|
|
548
873
|
return html`<div>
|
|
549
|
-
|
|
550
|
-
<div class="
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
${
|
|
883
|
+
${srvExts.map(ext => {
|
|
555
884
|
const isActive = ext.status === 'active';
|
|
556
885
|
return html`
|
|
557
|
-
<div class="ext-card" onClick=${() =>
|
|
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();
|
|
571
|
-
: html`<button onClick=${(e) => { e.stopPropagation();
|
|
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
|
-
|
|
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' },
|