@gxp-dev/tools 2.0.72 → 2.0.74
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/bin/gx-devtools.js +71 -99
- package/bin/lib/cli.js +62 -7
- package/bin/lib/commands/init.js +81 -82
- package/bin/lib/utils/ai-scaffold.js +137 -0
- package/mcp/gxp-api-server.js +31 -2
- package/mcp/lib/api-tools.js +87 -0
- package/package.json +1 -1
- package/runtime/stores/gxpPortalConfigStore.js +88 -87
- package/template/.claude/agents/gxp-developer.md +377 -50
- package/template/AGENTS.md +265 -21
- package/template/GEMINI.md +181 -19
package/mcp/gxp-api-server.js
CHANGED
|
@@ -63,12 +63,40 @@ function searchEndpoints(spec, query) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Search AsyncAPI spec for channels/events matching a query
|
|
66
|
+
* Search AsyncAPI spec for channels/events matching a query.
|
|
67
|
+
*
|
|
68
|
+
* Matches across:
|
|
69
|
+
* - components.messages (event name, summary, description, x-triggered-by)
|
|
70
|
+
* - channels (channel name, description)
|
|
71
|
+
*
|
|
72
|
+
* For messages, the returned `eventName` is what you pass to
|
|
73
|
+
* store.listen(eventName, permissionIdentifier, callback) on the client.
|
|
67
74
|
*/
|
|
68
75
|
function searchEvents(spec, query) {
|
|
69
76
|
const results = []
|
|
70
77
|
const queryLower = query.toLowerCase()
|
|
71
78
|
|
|
79
|
+
const messages = spec?.components?.messages || {}
|
|
80
|
+
for (const [eventName, message] of Object.entries(messages)) {
|
|
81
|
+
if (typeof message !== "object" || message === null) continue
|
|
82
|
+
const trigger = message["x-triggered-by"] || ""
|
|
83
|
+
if (
|
|
84
|
+
eventName.toLowerCase().includes(queryLower) ||
|
|
85
|
+
message.summary?.toLowerCase().includes(queryLower) ||
|
|
86
|
+
message.description?.toLowerCase().includes(queryLower) ||
|
|
87
|
+
trigger.toLowerCase().includes(queryLower)
|
|
88
|
+
) {
|
|
89
|
+
results.push({
|
|
90
|
+
kind: "event",
|
|
91
|
+
eventName,
|
|
92
|
+
summary: message.summary || "",
|
|
93
|
+
description: message.description || "",
|
|
94
|
+
triggeredBy: trigger || null,
|
|
95
|
+
payloadRef: message.payload?.$ref || null,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
if (spec.channels) {
|
|
73
101
|
for (const [channel, details] of Object.entries(spec.channels)) {
|
|
74
102
|
if (
|
|
@@ -92,6 +120,7 @@ function searchEvents(spec, query) {
|
|
|
92
120
|
}
|
|
93
121
|
|
|
94
122
|
results.push({
|
|
123
|
+
kind: "channel",
|
|
95
124
|
channel,
|
|
96
125
|
description: details.description || "",
|
|
97
126
|
operations,
|
|
@@ -200,7 +229,7 @@ const API_TOOLS = [
|
|
|
200
229
|
{
|
|
201
230
|
name: "search_websocket_events",
|
|
202
231
|
description:
|
|
203
|
-
"Search
|
|
232
|
+
"Search AsyncAPI events matching a query. Searches components.messages (event name, summary, description, x-triggered-by) and channel definitions. The returned eventName is what you pass to store.listen(eventName, permissionIdentifier, callback).",
|
|
204
233
|
inputSchema: {
|
|
205
234
|
type: "object",
|
|
206
235
|
properties: {
|
package/mcp/lib/api-tools.js
CHANGED
|
@@ -158,6 +158,38 @@ const EXT_API_TOOLS = [
|
|
|
158
158
|
required: [],
|
|
159
159
|
},
|
|
160
160
|
},
|
|
161
|
+
{
|
|
162
|
+
name: "api_find_events_for_operation",
|
|
163
|
+
description:
|
|
164
|
+
"Given an OpenAPI operationId, return every AsyncAPI message whose x-triggered-by matches. Use this after adding a callApi(operationId, ...) call to discover whether a socket event is fired server-side — subscribe to it with store.listen(eventName, permissionIdentifier, cb) instead of polling.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
operationId: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description:
|
|
171
|
+
"OpenAPI operationId (e.g. 'posts.store'). Bare ids and 'portal.v1.project.<id>' are both accepted.",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ["operationId"],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "api_list_events",
|
|
179
|
+
description:
|
|
180
|
+
"List all AsyncAPI events from components.messages. Each entry includes the event name, summary/description, and x-triggered-by (if declared). Optionally filter by triggeredBy operationId.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
triggeredBy: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description:
|
|
187
|
+
"Only return events whose x-triggered-by equals this operationId. Omit for all events.",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: [],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
161
193
|
{
|
|
162
194
|
name: "api_generate_dependency",
|
|
163
195
|
description:
|
|
@@ -312,6 +344,50 @@ async function findEndpointsBySchema(filters) {
|
|
|
312
344
|
return out
|
|
313
345
|
}
|
|
314
346
|
|
|
347
|
+
function normalizeOperationId(id) {
|
|
348
|
+
if (!id) return id
|
|
349
|
+
return id.replace(/^portal\.v1\.project\./, "")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function listAsyncApiEvents(triggeredBy) {
|
|
353
|
+
const spec = await fetchSpec("asyncapi")
|
|
354
|
+
const messages = spec?.components?.messages || {}
|
|
355
|
+
const targetTrigger = triggeredBy ? normalizeOperationId(triggeredBy) : null
|
|
356
|
+
|
|
357
|
+
const out = []
|
|
358
|
+
for (const [eventName, message] of Object.entries(messages)) {
|
|
359
|
+
if (typeof message !== "object" || message === null) continue
|
|
360
|
+
const trigger = message["x-triggered-by"] || null
|
|
361
|
+
if (targetTrigger && normalizeOperationId(trigger) !== targetTrigger) {
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
out.push({
|
|
365
|
+
eventName,
|
|
366
|
+
summary: message.summary || "",
|
|
367
|
+
description: message.description || "",
|
|
368
|
+
triggeredBy: trigger,
|
|
369
|
+
payloadRef: message.payload?.$ref || null,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
return out
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function findEventsForOperation(operationId) {
|
|
376
|
+
if (!operationId) {
|
|
377
|
+
return { ok: false, error: "operationId is required" }
|
|
378
|
+
}
|
|
379
|
+
const events = await listAsyncApiEvents(operationId)
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
operationId: normalizeOperationId(operationId),
|
|
383
|
+
events,
|
|
384
|
+
note:
|
|
385
|
+
events.length === 0
|
|
386
|
+
? "No AsyncAPI messages declare x-triggered-by for this operationId. Either the operation does not fire a platform event, or the spec has not declared the trigger yet."
|
|
387
|
+
: "Subscribe with store.listen(eventName, permissionIdentifier, callback) to receive these events live.",
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
315
391
|
async function generateDependency({
|
|
316
392
|
identifier,
|
|
317
393
|
tag,
|
|
@@ -429,6 +505,14 @@ async function handleExtApiToolCall(name, args = {}) {
|
|
|
429
505
|
results: await findEndpointsBySchema(args || {}),
|
|
430
506
|
})
|
|
431
507
|
|
|
508
|
+
case "api_find_events_for_operation":
|
|
509
|
+
return contentResult(await findEventsForOperation(args.operationId))
|
|
510
|
+
|
|
511
|
+
case "api_list_events":
|
|
512
|
+
return contentResult({
|
|
513
|
+
events: await listAsyncApiEvents(args.triggeredBy),
|
|
514
|
+
})
|
|
515
|
+
|
|
432
516
|
case "api_generate_dependency":
|
|
433
517
|
return contentResult(await generateDependency(args))
|
|
434
518
|
|
|
@@ -453,4 +537,7 @@ module.exports = {
|
|
|
453
537
|
getOperationParameters,
|
|
454
538
|
findEndpointsBySchema,
|
|
455
539
|
generateDependency,
|
|
540
|
+
listAsyncApiEvents,
|
|
541
|
+
findEventsForOperation,
|
|
542
|
+
normalizeOperationId,
|
|
456
543
|
}
|
package/package.json
CHANGED
|
@@ -244,8 +244,8 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
244
244
|
|
|
245
245
|
if (manifest.dependencies && Array.isArray(manifest.dependencies)) {
|
|
246
246
|
dependencies.value = manifest.dependencies // Store full dependency objects
|
|
247
|
-
dependencyList.value = manifest.dependencies.reduce((acc,
|
|
248
|
-
acc[
|
|
247
|
+
dependencyList.value = manifest.dependencies.reduce((acc, permission) => {
|
|
248
|
+
acc[permission.identifier] = "1"
|
|
249
249
|
return acc
|
|
250
250
|
}, {})
|
|
251
251
|
console.log("[GxP Store] Dependency List:", dependencyList.value)
|
|
@@ -379,58 +379,16 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
379
379
|
|
|
380
380
|
// Initialize dependency-based sockets based on the new structure
|
|
381
381
|
if (Array.isArray(dependencies.value)) {
|
|
382
|
-
dependencies.value.forEach((
|
|
383
|
-
if (
|
|
384
|
-
dependency.operations &&
|
|
385
|
-
Object.keys(dependency.operations).length > 0
|
|
386
|
-
) {
|
|
387
|
-
// Object.keys(dependency.operations).forEach((operation) => {
|
|
388
|
-
// if (
|
|
389
|
-
// Object.keys(apiOperations.value[dependency.identifier]).every(
|
|
390
|
-
// (key) =>
|
|
391
|
-
// [
|
|
392
|
-
// "identifier",
|
|
393
|
-
// "model",
|
|
394
|
-
// "permissionKey",
|
|
395
|
-
// "operations",
|
|
396
|
-
// ].includes(key)
|
|
397
|
-
// )
|
|
398
|
-
// ) {
|
|
399
|
-
// let method = "get";
|
|
400
|
-
// let path = dependency.operations[operation];
|
|
401
|
-
// if (path.includes(":")) {
|
|
402
|
-
// let pathSplit = path.split(":");
|
|
403
|
-
// method = pathSplit[0];
|
|
404
|
-
// path = pathSplit[1];
|
|
405
|
-
// }
|
|
406
|
-
// path = path.replace(
|
|
407
|
-
// "{teamSlug}/{projectSlug}",
|
|
408
|
-
// pluginVars.value.projectId
|
|
409
|
-
// );
|
|
410
|
-
// path = path.replace(
|
|
411
|
-
// `{${dependency.permissionKey}}`,
|
|
412
|
-
// dependencyList.value[dependency.identifier]
|
|
413
|
-
// );
|
|
414
|
-
// if (!apiOperations.value[dependency.identifier]) {
|
|
415
|
-
// apiOperations.value[dependency.identifier] = {};
|
|
416
|
-
// }
|
|
417
|
-
// apiOperations.value[dependency.identifier][operation] = {
|
|
418
|
-
// method: method,
|
|
419
|
-
// path: path,
|
|
420
|
-
// model_key: dependency.permissionKey,
|
|
421
|
-
// };
|
|
422
|
-
// }
|
|
423
|
-
// });
|
|
424
|
-
}
|
|
425
|
-
if (dependency.events && Object.keys(dependency.events).length > 0) {
|
|
382
|
+
dependencies.value.forEach((permission) => {
|
|
383
|
+
if (permission.events && Object.keys(permission.events).length > 0) {
|
|
426
384
|
// Create socket listeners for each event type
|
|
427
|
-
sockets[
|
|
385
|
+
sockets[permission.identifier] = {}
|
|
428
386
|
|
|
429
|
-
Object.keys(
|
|
430
|
-
const eventName =
|
|
431
|
-
const channel = `private.${
|
|
387
|
+
Object.keys(permission.events).forEach((eventType) => {
|
|
388
|
+
const eventName = permission.events[eventType]
|
|
389
|
+
const channel = `private.${permission.model}.${permission.identifier}`
|
|
432
390
|
|
|
433
|
-
sockets[
|
|
391
|
+
sockets[permission.identifier][eventType] = {
|
|
434
392
|
listen: function (callback) {
|
|
435
393
|
// Listen for the specific event on the primary socket
|
|
436
394
|
return primarySocket.on(eventName, (data) => {
|
|
@@ -445,7 +403,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
445
403
|
})
|
|
446
404
|
} else {
|
|
447
405
|
// For dependencies without events, create empty listeners
|
|
448
|
-
sockets[
|
|
406
|
+
sockets[permission.identifier] = {
|
|
449
407
|
created: { listen: () => () => {} },
|
|
450
408
|
updated: { listen: () => () => {} },
|
|
451
409
|
deleted: { listen: () => () => {} },
|
|
@@ -500,7 +458,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
500
458
|
throw new Error(`DELETE ${endpoint}: ${error.message}`)
|
|
501
459
|
}
|
|
502
460
|
}
|
|
503
|
-
async function callApi(operationId,
|
|
461
|
+
async function callApi(operationId, permissionIdentifier, data = {}) {
|
|
504
462
|
// Initialize operations if not done
|
|
505
463
|
if (Object.keys(apiOperations.value).length === 0) {
|
|
506
464
|
await initializeApiOperations()
|
|
@@ -520,7 +478,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
520
478
|
|
|
521
479
|
// Build context parameters from multiple sources:
|
|
522
480
|
// 1. Auto-inject teamSlug and projectSlug from portal context
|
|
523
|
-
// 2. Look up
|
|
481
|
+
// 2. Look up permissionIdentifier value from dependencyList (if permissionIdentifier provided)
|
|
524
482
|
// 3. Merge in additional data parameters
|
|
525
483
|
|
|
526
484
|
let projectTeamId = pluginVars.value?.projectId?.split("/")
|
|
@@ -537,14 +495,19 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
537
495
|
if (parameters.includes("form") && pluginVars.value?.formId) {
|
|
538
496
|
contextParams["form"] = pluginVars.value?.formId
|
|
539
497
|
}
|
|
540
|
-
// If
|
|
541
|
-
// dependencyList stores parent object IDs as { '
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
498
|
+
// If permissionIdentifier is provided, look up its value from dependencyList
|
|
499
|
+
// dependencyList stores parent object IDs as { 'permissionIdentifier': idValue }
|
|
500
|
+
if (
|
|
501
|
+
permissionIdentifier !== null &&
|
|
502
|
+
permissionIdentifier !== undefined &&
|
|
503
|
+
permissionIdentifier !== "project"
|
|
504
|
+
) {
|
|
505
|
+
const permissionIdentifierValue =
|
|
506
|
+
dependencyList.value?.[permissionIdentifier]
|
|
507
|
+
if (permissionIdentifierValue !== undefined) {
|
|
508
|
+
// Add the permissionIdentifier value using the permissionIdentifier key as the param name
|
|
509
|
+
// e.g., permissionIdentifier='form' with dependencyList.form='quiz-123' adds { form: 'quiz-123' }
|
|
510
|
+
contextParams[permissionIdentifier] = permissionIdentifierValue
|
|
548
511
|
}
|
|
549
512
|
}
|
|
550
513
|
const parsedData = {}
|
|
@@ -582,9 +545,9 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
582
545
|
for (const param of parameters) {
|
|
583
546
|
delete bodyData[param]
|
|
584
547
|
}
|
|
585
|
-
// Also remove
|
|
586
|
-
if (
|
|
587
|
-
delete bodyData[
|
|
548
|
+
// Also remove permissionIdentifier from body if it was in data
|
|
549
|
+
if (permissionIdentifier && bodyData[permissionIdentifier] !== undefined) {
|
|
550
|
+
delete bodyData[permissionIdentifier]
|
|
588
551
|
}
|
|
589
552
|
|
|
590
553
|
try {
|
|
@@ -611,20 +574,6 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
611
574
|
)
|
|
612
575
|
throw new Error(`${method.toUpperCase()} ${resolvedPath}: ${message}`)
|
|
613
576
|
}
|
|
614
|
-
|
|
615
|
-
// try {
|
|
616
|
-
// const operationConfig = apiOperations.value[identifier][operation];
|
|
617
|
-
// if (!operationConfig) {
|
|
618
|
-
// throw new Error(`Operation not found: ${operation}`);
|
|
619
|
-
// }
|
|
620
|
-
// const response = await apiClient[operationConfig.method](
|
|
621
|
-
// operationConfig.path,
|
|
622
|
-
// data
|
|
623
|
-
// );
|
|
624
|
-
// return response.data;
|
|
625
|
-
// } catch (error) {
|
|
626
|
-
// throw new Error(`${method} ${endpoint}: ${error.message}`);
|
|
627
|
-
// }
|
|
628
577
|
}
|
|
629
578
|
|
|
630
579
|
// Utility methods
|
|
@@ -643,8 +592,8 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
643
592
|
function getState(key, fallback = null) {
|
|
644
593
|
return triggerState.value[key] || fallback
|
|
645
594
|
}
|
|
646
|
-
function findDependency(
|
|
647
|
-
return dependencyList.value[
|
|
595
|
+
function findDependency(permissionIdentifier) {
|
|
596
|
+
return dependencyList.value[permissionIdentifier]
|
|
648
597
|
}
|
|
649
598
|
function hasPermission(flag) {
|
|
650
599
|
return permissionFlags.value.includes(flag)
|
|
@@ -661,13 +610,65 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
661
610
|
}
|
|
662
611
|
|
|
663
612
|
// Standard Socket helper methods
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
613
|
+
//
|
|
614
|
+
// Polymorphic — supports two forms:
|
|
615
|
+
//
|
|
616
|
+
// 1. listen(socketName, event, callback)
|
|
617
|
+
// Subscribes to `event` on the named socket (e.g. 'primary' or a
|
|
618
|
+
// dependency identifier whose socket was initialized via
|
|
619
|
+
// initializeDependencySockets). This matches the legacy shape.
|
|
620
|
+
//
|
|
621
|
+
// 2. listen(eventName, permissionIdentifier, callback)
|
|
622
|
+
// Subscribes to an AsyncAPI-defined platform event on the primary
|
|
623
|
+
// socket, scoped to a permission identifier from dependencyList
|
|
624
|
+
// (or the reserved "project" identifier). Use this for events whose
|
|
625
|
+
// `x-triggered-by` matches a callApi operationId.
|
|
626
|
+
//
|
|
627
|
+
// Disambiguation: if arg1 names a registered socket we take form 1,
|
|
628
|
+
// otherwise we fall through to form 2.
|
|
629
|
+
function listen(arg1, arg2, arg3) {
|
|
630
|
+
const hasRegisteredSocket =
|
|
631
|
+
sockets[arg1] && typeof sockets[arg1].listen === "function"
|
|
632
|
+
|
|
633
|
+
if (hasRegisteredSocket && typeof arg3 === "function") {
|
|
634
|
+
return sockets[arg1].listen(arg2, arg3)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (typeof arg3 === "function") {
|
|
638
|
+
const eventName = arg1
|
|
639
|
+
const permissionIdentifier = arg2
|
|
640
|
+
const callback = arg3
|
|
641
|
+
const primary = socketConnections.primary
|
|
642
|
+
if (!primary) {
|
|
643
|
+
console.warn(
|
|
644
|
+
"[GxP Store] listen(): primary socket not initialized",
|
|
645
|
+
)
|
|
646
|
+
return () => {}
|
|
647
|
+
}
|
|
648
|
+
if (
|
|
649
|
+
permissionIdentifier !== "project" &&
|
|
650
|
+
dependencyList.value?.[permissionIdentifier] === undefined
|
|
651
|
+
) {
|
|
652
|
+
console.warn(
|
|
653
|
+
`[GxP Store] listen("${eventName}", "${permissionIdentifier}"): permission identifier not bound in dependencyList`,
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
const handler = (data) => {
|
|
657
|
+
try {
|
|
658
|
+
callback(data)
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(
|
|
661
|
+
`[GxP Store] listen callback error for ${eventName}:`,
|
|
662
|
+
err,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
primary.on(eventName, handler)
|
|
667
|
+
return () => primary.off(eventName, handler)
|
|
670
668
|
}
|
|
669
|
+
|
|
670
|
+
console.warn(`Socket not found: ${arg1}`)
|
|
671
|
+
return () => {}
|
|
671
672
|
}
|
|
672
673
|
|
|
673
674
|
function broadcast(socketName, event, data) {
|