@gxp-dev/tools 2.0.72 → 2.0.73
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/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/bin/lib/commands/init.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* 3. Run interactive configuration:
|
|
10
10
|
* - App name (prepopulated from package.json)
|
|
11
11
|
* - Description (prepopulated from package.json)
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
12
|
+
* - SSL setup
|
|
13
|
+
* - Launch AI agent (2nd-to-last — replaces the start-server step when chosen)
|
|
14
|
+
* 4. Start Development (skipped if an AI agent was launched)
|
|
14
15
|
* 5. Prompt to launch browser with extension
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -34,6 +35,7 @@ const {
|
|
|
34
35
|
generateSSLCertificates,
|
|
35
36
|
updateEnvWithCertPaths,
|
|
36
37
|
runAIScaffolding,
|
|
38
|
+
launchInteractiveAISession,
|
|
37
39
|
getAvailableProviders,
|
|
38
40
|
} = require("../utils")
|
|
39
41
|
|
|
@@ -401,65 +403,100 @@ async function runInteractiveConfig(projectPath, initialName, isLocal = false) {
|
|
|
401
403
|
updatePackageJson(projectPath, appName, description)
|
|
402
404
|
updateAppManifest(projectPath, appName, description)
|
|
403
405
|
|
|
404
|
-
// 3.
|
|
406
|
+
// 3. SSL Setup
|
|
405
407
|
console.log("")
|
|
406
408
|
console.log("─".repeat(50))
|
|
407
|
-
console.log("
|
|
409
|
+
console.log("🔒 SSL Configuration")
|
|
408
410
|
console.log("─".repeat(50))
|
|
409
|
-
|
|
410
|
-
|
|
411
|
+
|
|
412
|
+
const sslChoice = await arrowSelectPrompt(
|
|
413
|
+
"Set up SSL certificates for HTTPS development?",
|
|
414
|
+
[
|
|
415
|
+
{
|
|
416
|
+
label: "Yes, set up SSL",
|
|
417
|
+
value: "yes",
|
|
418
|
+
description: "Recommended for full feature access",
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
label: "Skip SSL setup",
|
|
422
|
+
value: "no",
|
|
423
|
+
description: "Can be set up later with npm run setup-ssl",
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
let sslSetup = false
|
|
429
|
+
if (sslChoice === "yes") {
|
|
430
|
+
console.log("\n🔒 Setting up HTTPS development environment...")
|
|
431
|
+
ensureMkcertInstalled()
|
|
432
|
+
const certs = generateSSLCertificates(projectPath)
|
|
433
|
+
if (certs) {
|
|
434
|
+
updateEnvWithCertPaths(projectPath, certs)
|
|
435
|
+
sslSetup = true
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 4. Build with an AI agent (2nd-to-last step)
|
|
440
|
+
//
|
|
441
|
+
// Launches the selected CLI in interactive mode with an initial prompt
|
|
442
|
+
// that points the agent at AGENTS.md / GEMINI.md in the scaffolded project
|
|
443
|
+
// and instructs it to greet the user, ask clarifying questions until it
|
|
444
|
+
// has enough detail, then plan and implement. When an agent is launched
|
|
445
|
+
// we skip the "Start Development" step — the agent session replaces it.
|
|
446
|
+
console.log("")
|
|
447
|
+
console.log("─".repeat(50))
|
|
448
|
+
console.log("🤖 Build with an AI Agent")
|
|
449
|
+
console.log("─".repeat(50))
|
|
450
|
+
console.log(
|
|
451
|
+
" Launch an AI coding agent that already knows the GxP toolkit,",
|
|
452
|
+
)
|
|
453
|
+
console.log(
|
|
454
|
+
" MCP tools, and workflow. The agent will ask you what you want",
|
|
455
|
+
)
|
|
456
|
+
console.log(" to build and then plan and implement it with you.")
|
|
411
457
|
console.log("")
|
|
412
458
|
|
|
413
459
|
// Check available AI providers
|
|
414
460
|
const providers = await getAvailableProviders()
|
|
415
|
-
const
|
|
461
|
+
const interactiveProviders = providers.filter(
|
|
462
|
+
(p) => p.available && (p.id !== "gemini" || p.method === "cli"),
|
|
463
|
+
)
|
|
416
464
|
|
|
417
465
|
let aiChoice = "skip"
|
|
418
466
|
let selectedProvider = null
|
|
419
467
|
|
|
420
|
-
if (
|
|
421
|
-
console.log(" ⚠️ No AI
|
|
422
|
-
console.log(
|
|
468
|
+
if (interactiveProviders.length === 0) {
|
|
469
|
+
console.log(" ⚠️ No AI CLIs detected.")
|
|
470
|
+
console.log(
|
|
471
|
+
" To use an AI agent here, install one of the following and retry:",
|
|
472
|
+
)
|
|
423
473
|
console.log(
|
|
424
474
|
" • Claude CLI: npm install -g @anthropic-ai/claude-code && claude login",
|
|
425
475
|
)
|
|
426
476
|
console.log(" • Codex CLI: npm install -g @openai/codex && codex auth")
|
|
427
477
|
console.log(" • Gemini CLI: npm install -g @google/gemini-cli && gemini")
|
|
428
|
-
console.log(" • Gemini API: export GEMINI_API_KEY=your_key")
|
|
429
478
|
console.log("")
|
|
430
479
|
aiChoice = "skip"
|
|
431
480
|
} else {
|
|
432
481
|
// Build provider options
|
|
433
|
-
const providerOptions = [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
case "api_key":
|
|
443
|
-
authInfo = "via API key"
|
|
444
|
-
break
|
|
445
|
-
case "gcloud":
|
|
446
|
-
authInfo = "via gcloud"
|
|
447
|
-
break
|
|
448
|
-
default:
|
|
449
|
-
authInfo = ""
|
|
450
|
-
}
|
|
451
|
-
} else {
|
|
452
|
-
authInfo = "logged in"
|
|
453
|
-
}
|
|
482
|
+
const providerOptions = [
|
|
483
|
+
{
|
|
484
|
+
label: "Skip — I'll build it myself",
|
|
485
|
+
value: "skip",
|
|
486
|
+
description: "You can launch an AI agent later from the project root",
|
|
487
|
+
},
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
for (const provider of interactiveProviders) {
|
|
454
491
|
providerOptions.push({
|
|
455
|
-
label:
|
|
492
|
+
label: provider.name,
|
|
456
493
|
value: provider.id,
|
|
457
|
-
description:
|
|
494
|
+
description: "logged in",
|
|
458
495
|
})
|
|
459
496
|
}
|
|
460
497
|
|
|
461
498
|
aiChoice = await arrowSelectPrompt(
|
|
462
|
-
"
|
|
499
|
+
"Launch an AI agent to build your plugin?",
|
|
463
500
|
providerOptions,
|
|
464
501
|
)
|
|
465
502
|
if (aiChoice !== "skip") {
|
|
@@ -467,55 +504,17 @@ async function runInteractiveConfig(projectPath, initialName, isLocal = false) {
|
|
|
467
504
|
}
|
|
468
505
|
}
|
|
469
506
|
|
|
470
|
-
|
|
507
|
+
// If an AI agent was chosen, it becomes the last step — skip starting the
|
|
508
|
+
// dev server. The agent is responsible for driving the rest of the session.
|
|
471
509
|
if (selectedProvider) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
510
|
+
await launchInteractiveAISession(
|
|
511
|
+
projectPath,
|
|
512
|
+
appName,
|
|
513
|
+
description,
|
|
514
|
+
selectedProvider,
|
|
475
515
|
)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await runAIScaffolding(
|
|
479
|
-
projectPath,
|
|
480
|
-
appName,
|
|
481
|
-
description,
|
|
482
|
-
buildPrompt,
|
|
483
|
-
selectedProvider,
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 4. SSL Setup
|
|
489
|
-
console.log("")
|
|
490
|
-
console.log("─".repeat(50))
|
|
491
|
-
console.log("🔒 SSL Configuration")
|
|
492
|
-
console.log("─".repeat(50))
|
|
493
|
-
|
|
494
|
-
const sslChoice = await arrowSelectPrompt(
|
|
495
|
-
"Set up SSL certificates for HTTPS development?",
|
|
496
|
-
[
|
|
497
|
-
{
|
|
498
|
-
label: "Yes, set up SSL",
|
|
499
|
-
value: "yes",
|
|
500
|
-
description: "Recommended for full feature access",
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
label: "Skip SSL setup",
|
|
504
|
-
value: "no",
|
|
505
|
-
description: "Can be set up later with npm run setup-ssl",
|
|
506
|
-
},
|
|
507
|
-
],
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
let sslSetup = false
|
|
511
|
-
if (sslChoice === "yes") {
|
|
512
|
-
console.log("\n🔒 Setting up HTTPS development environment...")
|
|
513
|
-
ensureMkcertInstalled()
|
|
514
|
-
const certs = generateSSLCertificates(projectPath)
|
|
515
|
-
if (certs) {
|
|
516
|
-
updateEnvWithCertPaths(projectPath, certs)
|
|
517
|
-
sslSetup = true
|
|
518
|
-
}
|
|
516
|
+
printFinalInstructions(projectPath, appName, sslSetup, isLocal)
|
|
517
|
+
return null
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
// 5. Start App
|
|
@@ -953,6 +953,141 @@ async function runAIScaffolding(
|
|
|
953
953
|
return result.errors.length === 0
|
|
954
954
|
}
|
|
955
955
|
|
|
956
|
+
/**
|
|
957
|
+
* Build the initial prompt sent to an interactive AI CLI session.
|
|
958
|
+
* The prompt anchors the agent in the scaffolded project, points it at
|
|
959
|
+
* the project's instruction files (which describe the full tool set),
|
|
960
|
+
* and tells it to keep asking questions until it has enough detail.
|
|
961
|
+
*
|
|
962
|
+
* @param {string} projectName - Plugin name
|
|
963
|
+
* @param {string} description - Plugin description
|
|
964
|
+
* @param {string} provider - Selected AI provider (claude, codex, gemini)
|
|
965
|
+
* @returns {string} Prompt text
|
|
966
|
+
*/
|
|
967
|
+
function buildInteractiveInitialPrompt(projectName, description, provider) {
|
|
968
|
+
const instructionFile = provider === "gemini" ? "GEMINI.md" : "AGENTS.md"
|
|
969
|
+
const claudeAgentHint =
|
|
970
|
+
provider === "claude"
|
|
971
|
+
? " You can also delegate to the `gxp-developer` subagent defined in `.claude/agents/gxp-developer.md`."
|
|
972
|
+
: ""
|
|
973
|
+
|
|
974
|
+
return [
|
|
975
|
+
`I just ran \`gxdev init\` to scaffold a new GxP plugin called "${projectName}"${
|
|
976
|
+
description ? ` (${description})` : ""
|
|
977
|
+
} in this directory.`,
|
|
978
|
+
"",
|
|
979
|
+
`Start a new GxP plugin development session. First read \`${instructionFile}\` and \`app-instructions.md\` in this project — they describe the workflow, conventions, and the full set of tools available to you.${claudeAgentHint}`,
|
|
980
|
+
"",
|
|
981
|
+
"You have the `gxp-api` MCP server available with 29 tools across five families:",
|
|
982
|
+
"- **API spec discovery** — `search_api_endpoints`, `api_list_operation_ids`, `api_get_operation_parameters`, `api_find_endpoints_by_schema`, `api_generate_dependency`, `get_endpoint_details`.",
|
|
983
|
+
"- **WebSocket events** — `api_find_events_for_operation` (maps an operationId to the AsyncAPI events it triggers), `api_list_events`, `search_websocket_events`.",
|
|
984
|
+
"- **Config editing** — `config_add_card`, `config_add_field`, `config_list_field_types`, `config_get_field_schema`, `config_extract_strings`, `config_validate`, etc. Every mutation is linter-guarded against the schemas in `bin/lib/lint/schemas/`.",
|
|
985
|
+
"- **Docs search** — `docs_search`, `docs_get_page`, `docs_list_pages` (full-text search across docs.gxp.dev).",
|
|
986
|
+
"- **Test helpers** — `test_scaffold_component_test`, `test_api_route`.",
|
|
987
|
+
"",
|
|
988
|
+
"Follow the full workflow from the instructions: (1) understand the feature, (2) discover data sources via MCP, (3) plan including the admin configuration form, (4) implement, (5) **sync the manifest and build the admin form**, (6) test with real broadcasts, (7) final `gxdev lint --all`.",
|
|
989
|
+
"",
|
|
990
|
+
"Step 5 is not optional. Every time you add or change a `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src`, close the loop:",
|
|
991
|
+
'- Call `config_extract_strings` with `writeTo: "app-manifest.json"` — it scans `src/` and merges every directive, store getter, and dependency identifier into the manifest (same logic as `gxdev extract-config`, linter-guarded).',
|
|
992
|
+
"- For every entry now in the manifest, add a matching field in `configuration.json` using the MCP `config_*` tools. Default mapping: `strings.default.*` → `text`/`textarea`, `assets.*` → `selectAsset`, each declared `dependencies[]` identifier → `asyncSelect` bound to the resource's list endpoint, colors → `colorPicker`, numbers → `number`, toggles → `boolean`. Field `name` must match the manifest key exactly.",
|
|
993
|
+
"- Run `gxdev lint --all` before moving on.",
|
|
994
|
+
"",
|
|
995
|
+
"Do NOT start implementing until you have enough detail. Keep asking me clarifying questions until you know:",
|
|
996
|
+
"- The user-facing outcome and who uses it (attendee, staff, admin)",
|
|
997
|
+
"- What real-world data it reads/writes — identify the concrete platform operationIds via the MCP, never invent them",
|
|
998
|
+
"- Which real-time events matter (use `api_find_events_for_operation` for each planned operationId)",
|
|
999
|
+
"- Every piece of admin-editable content: strings, assets, colors/thresholds/settings, feature toggles",
|
|
1000
|
+
"",
|
|
1001
|
+
"Then propose a plan — screens/components, data flow, admin configuration form, and the exact keys you'll add to `app-manifest.json` — and get my confirmation before implementing.",
|
|
1002
|
+
"",
|
|
1003
|
+
"Begin now by greeting me briefly and asking what I want to build. Ask one focused question at a time rather than dumping a full questionnaire.",
|
|
1004
|
+
].join("\n")
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Launch the selected AI CLI in interactive mode with an initial prompt.
|
|
1009
|
+
* The user talks to the agent directly in the terminal; the agent is
|
|
1010
|
+
* responsible for eliciting the feature spec and building it.
|
|
1011
|
+
*
|
|
1012
|
+
* @param {string} projectPath - Project directory (cwd for the spawned CLI)
|
|
1013
|
+
* @param {string} projectName - Plugin name
|
|
1014
|
+
* @param {string} description - Plugin description
|
|
1015
|
+
* @param {string} provider - claude | codex | gemini
|
|
1016
|
+
* @returns {Promise<boolean>} True if the CLI exited successfully
|
|
1017
|
+
*/
|
|
1018
|
+
function launchInteractiveAISession(
|
|
1019
|
+
projectPath,
|
|
1020
|
+
projectName,
|
|
1021
|
+
description,
|
|
1022
|
+
provider,
|
|
1023
|
+
) {
|
|
1024
|
+
return new Promise((resolve) => {
|
|
1025
|
+
const initialPrompt = buildInteractiveInitialPrompt(
|
|
1026
|
+
projectName,
|
|
1027
|
+
description,
|
|
1028
|
+
provider,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
let command
|
|
1032
|
+
let args
|
|
1033
|
+
|
|
1034
|
+
switch (provider) {
|
|
1035
|
+
case "claude":
|
|
1036
|
+
command = "claude"
|
|
1037
|
+
args = [initialPrompt]
|
|
1038
|
+
break
|
|
1039
|
+
case "codex":
|
|
1040
|
+
command = "codex"
|
|
1041
|
+
args = [initialPrompt]
|
|
1042
|
+
break
|
|
1043
|
+
case "gemini":
|
|
1044
|
+
command = "gemini"
|
|
1045
|
+
args = ["-i", initialPrompt]
|
|
1046
|
+
break
|
|
1047
|
+
default:
|
|
1048
|
+
console.error(
|
|
1049
|
+
`❌ Interactive AI sessions are not supported for provider: ${provider}`,
|
|
1050
|
+
)
|
|
1051
|
+
resolve(false)
|
|
1052
|
+
return
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
console.log("")
|
|
1056
|
+
console.log("─".repeat(50))
|
|
1057
|
+
console.log(`🚀 Launching ${provider} in interactive mode...`)
|
|
1058
|
+
console.log("─".repeat(50))
|
|
1059
|
+
console.log("")
|
|
1060
|
+
console.log(
|
|
1061
|
+
" The agent will read this project's instruction files, greet you,",
|
|
1062
|
+
)
|
|
1063
|
+
console.log(
|
|
1064
|
+
" and ask what you want to build. Answer its questions — it will",
|
|
1065
|
+
)
|
|
1066
|
+
console.log(
|
|
1067
|
+
" keep asking until it has enough detail, then plan, confirm, and",
|
|
1068
|
+
)
|
|
1069
|
+
console.log(" implement. Exit the agent when you're done.")
|
|
1070
|
+
console.log("")
|
|
1071
|
+
|
|
1072
|
+
const child = spawn(command, args, {
|
|
1073
|
+
cwd: projectPath,
|
|
1074
|
+
stdio: "inherit",
|
|
1075
|
+
shell: true,
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
child.on("close", (code) => {
|
|
1079
|
+
console.log("")
|
|
1080
|
+
console.log(`✅ ${provider} session ended.`)
|
|
1081
|
+
resolve(code === 0)
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
child.on("error", (err) => {
|
|
1085
|
+
console.error(`❌ Failed to launch ${provider}: ${err.message}`)
|
|
1086
|
+
resolve(false)
|
|
1087
|
+
})
|
|
1088
|
+
})
|
|
1089
|
+
}
|
|
1090
|
+
|
|
956
1091
|
module.exports = {
|
|
957
1092
|
SCAFFOLD_SYSTEM_PROMPT,
|
|
958
1093
|
AI_PROVIDERS,
|
|
@@ -961,4 +1096,6 @@ module.exports = {
|
|
|
961
1096
|
applyScaffold,
|
|
962
1097
|
generateScaffold,
|
|
963
1098
|
runAIScaffolding,
|
|
1099
|
+
buildInteractiveInitialPrompt,
|
|
1100
|
+
launchInteractiveAISession,
|
|
964
1101
|
}
|
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
|
}
|