@gxp-dev/tools 2.0.71 → 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/README.md +108 -81
- package/bin/lib/cli.js +18 -0
- package/bin/lib/commands/index.js +2 -0
- package/bin/lib/commands/init.js +104 -82
- package/bin/lib/commands/lint.js +77 -0
- package/bin/lib/constants.js +12 -0
- package/bin/lib/lint/formatter.js +91 -0
- package/bin/lib/lint/index.js +284 -0
- package/bin/lib/lint/schemas/app-manifest.schema.json +124 -0
- package/bin/lib/lint/schemas/card.schema.json +165 -0
- package/bin/lib/lint/schemas/common.schema.json +62 -0
- package/bin/lib/lint/schemas/configuration.schema.json +19 -0
- package/bin/lib/lint/schemas/field.schema.json +230 -0
- package/bin/lib/utils/ai-scaffold.js +137 -0
- package/mcp/gxp-api-server.js +87 -129
- package/mcp/lib/api-tools.js +543 -0
- package/mcp/lib/config-ops.js +234 -0
- package/mcp/lib/config-tools.js +549 -0
- package/mcp/lib/docs-tools.js +142 -0
- package/mcp/lib/docs.js +263 -0
- package/mcp/lib/specs.js +135 -0
- package/mcp/lib/test-tools.js +358 -0
- package/package.json +3 -1
- package/runtime/stores/gxpPortalConfigStore.js +88 -87
- package/runtime/vite.config.js +5 -3
- package/template/.claude/agents/gxp-developer.md +377 -50
- package/template/.prettierrc +10 -0
- package/template/AGENTS.md +265 -21
- package/template/GEMINI.md +181 -19
- package/template/README.md +205 -240
- package/template/app-instructions.md +91 -0
- package/template/eslint.config.js +32 -0
- package/template/githooks/pre-commit +37 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended MCP API tools (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Builds on the existing search_api_endpoints / get_endpoint_details to add:
|
|
5
|
+
* - api_list_tags : enumerate all OpenAPI tags
|
|
6
|
+
* - api_list_operation_ids : enumerate operations, optionally filtered by tag
|
|
7
|
+
* - api_get_operation_parameters : deep detail for a single operation id
|
|
8
|
+
* - api_find_endpoints_by_schema : search by request/response field names
|
|
9
|
+
* - api_generate_dependency : build the GxP dependency JSON from a
|
|
10
|
+
* tag + selected operations/events,
|
|
11
|
+
* optionally appending to app-manifest.json
|
|
12
|
+
*
|
|
13
|
+
* The dependency shape mirrors bin/lib/commands/add-dependency.js:
|
|
14
|
+
* { identifier, model, permissionKey, permissions: [], operations: {}, events: {} }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require("fs")
|
|
18
|
+
const path = require("path")
|
|
19
|
+
const { fetchSpec } = require("./specs")
|
|
20
|
+
|
|
21
|
+
/* ---------------------------------- utils --------------------------------- */
|
|
22
|
+
|
|
23
|
+
function contentResult(obj) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walkOperations(spec) {
|
|
30
|
+
const out = []
|
|
31
|
+
if (!spec || !spec.paths) return out
|
|
32
|
+
for (const [p, methods] of Object.entries(spec.paths)) {
|
|
33
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
34
|
+
if (typeof op !== "object" || op === null) continue
|
|
35
|
+
if (
|
|
36
|
+
!["get", "post", "put", "patch", "delete", "options", "head"].includes(
|
|
37
|
+
method,
|
|
38
|
+
)
|
|
39
|
+
) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
out.push({ path: p, method: method.toUpperCase(), op })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Return the list of distinct field names mentioned anywhere in a JSON-ish
|
|
50
|
+
* object, walking $ref via the components registry shallowly.
|
|
51
|
+
*/
|
|
52
|
+
function schemaFieldNames(schema, components, depth = 0, seen = new Set()) {
|
|
53
|
+
if (!schema || depth > 4) return []
|
|
54
|
+
if (schema.$ref && components) {
|
|
55
|
+
const refName = schema.$ref.split("/").pop()
|
|
56
|
+
if (seen.has(refName)) return []
|
|
57
|
+
seen.add(refName)
|
|
58
|
+
const target = components?.schemas?.[refName]
|
|
59
|
+
return schemaFieldNames(target, components, depth + 1, seen)
|
|
60
|
+
}
|
|
61
|
+
const names = []
|
|
62
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
63
|
+
for (const [name, subSchema] of Object.entries(schema.properties)) {
|
|
64
|
+
names.push(name)
|
|
65
|
+
names.push(...schemaFieldNames(subSchema, components, depth + 1, seen))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (schema.items) {
|
|
69
|
+
names.push(...schemaFieldNames(schema.items, components, depth + 1, seen))
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(schema.allOf)) {
|
|
72
|
+
for (const sub of schema.allOf) {
|
|
73
|
+
names.push(...schemaFieldNames(sub, components, depth + 1, seen))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return names
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findOperationById(spec, operationId) {
|
|
80
|
+
for (const { path: p, method, op } of walkOperations(spec)) {
|
|
81
|
+
if (op.operationId === operationId) {
|
|
82
|
+
return { path: p, method, op }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ------------------------------ tool schemas ------------------------------ */
|
|
89
|
+
|
|
90
|
+
const EXT_API_TOOLS = [
|
|
91
|
+
{
|
|
92
|
+
name: "api_list_tags",
|
|
93
|
+
description:
|
|
94
|
+
"List every tag in the OpenAPI spec with endpoint counts. Use this to discover platform 'models' (attendees, projects, events, etc.) before building a dependency.",
|
|
95
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "api_list_operation_ids",
|
|
99
|
+
description:
|
|
100
|
+
"Enumerate operation IDs in the OpenAPI spec. Optionally filter by tag.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
tag: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description:
|
|
107
|
+
"Filter to operations under this tag (e.g. 'Attendees'). Omit for all.",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: [],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "api_get_operation_parameters",
|
|
115
|
+
description:
|
|
116
|
+
"Look up a single operation by id and return its path, method, parameters, requestBody schema, response schemas, and required permissions.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
operationId: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "OpenAPI operationId (e.g. 'attendees.index').",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ["operationId"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "api_find_endpoints_by_schema",
|
|
130
|
+
description:
|
|
131
|
+
"Search endpoints by structural hints: field names present in the request or response bodies, URL path substrings, or HTTP method. Combine any filters — all are AND'd.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
request_field: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description:
|
|
138
|
+
"Find endpoints whose request body schema mentions this field name.",
|
|
139
|
+
},
|
|
140
|
+
response_field: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description:
|
|
143
|
+
"Find endpoints whose response schema mentions this field name.",
|
|
144
|
+
},
|
|
145
|
+
path_pattern: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Substring that must appear in the path.",
|
|
148
|
+
},
|
|
149
|
+
method: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "HTTP method filter (GET, POST, etc.).",
|
|
152
|
+
},
|
|
153
|
+
tag: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Restrict to one tag.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: [],
|
|
159
|
+
},
|
|
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
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "api_generate_dependency",
|
|
195
|
+
description:
|
|
196
|
+
"Build the GxP dependency JSON (for app-manifest.json dependencies[]) from a tag and an explicit list of operationIds and/or asyncapi event names. Optionally append it to an app-manifest.json file.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
identifier: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description:
|
|
203
|
+
"Short slug used by plugin code to reference the dependency (e.g. 'attendees').",
|
|
204
|
+
},
|
|
205
|
+
tag: {
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "OpenAPI tag name — becomes the dependency's 'model'.",
|
|
208
|
+
},
|
|
209
|
+
operationIds: {
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
description:
|
|
213
|
+
"Operation IDs to include. Omit to include every operation under the tag.",
|
|
214
|
+
},
|
|
215
|
+
eventNames: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: { type: "string" },
|
|
218
|
+
description:
|
|
219
|
+
"AsyncAPI event names to wire up in the dependency's events map.",
|
|
220
|
+
},
|
|
221
|
+
writeTo: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description:
|
|
224
|
+
"Optional path to an app-manifest.json. When provided, the dependency is appended/replaced in manifest.dependencies[] and the file is rewritten.",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["identifier", "tag"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
/* ---------------------------------- core ---------------------------------- */
|
|
233
|
+
|
|
234
|
+
async function listTags() {
|
|
235
|
+
const spec = await fetchSpec("openapi")
|
|
236
|
+
const tagMeta = new Map()
|
|
237
|
+
for (const t of spec.tags || []) {
|
|
238
|
+
tagMeta.set(t.name, {
|
|
239
|
+
name: t.name,
|
|
240
|
+
description: t.description || "",
|
|
241
|
+
pathCount: 0,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
for (const { op } of walkOperations(spec)) {
|
|
245
|
+
for (const tag of op.tags || []) {
|
|
246
|
+
if (!tagMeta.has(tag)) {
|
|
247
|
+
tagMeta.set(tag, { name: tag, description: "", pathCount: 0 })
|
|
248
|
+
}
|
|
249
|
+
tagMeta.get(tag).pathCount++
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return Array.from(tagMeta.values()).sort((a, b) =>
|
|
253
|
+
a.name.localeCompare(b.name),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function listOperationIds(tag) {
|
|
258
|
+
const spec = await fetchSpec("openapi")
|
|
259
|
+
const out = []
|
|
260
|
+
for (const { path: p, method, op } of walkOperations(spec)) {
|
|
261
|
+
if (!op.operationId) continue
|
|
262
|
+
if (tag && !(op.tags || []).includes(tag)) continue
|
|
263
|
+
out.push({
|
|
264
|
+
operationId: op.operationId,
|
|
265
|
+
method,
|
|
266
|
+
path: p,
|
|
267
|
+
tags: op.tags || [],
|
|
268
|
+
summary: op.summary || "",
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
return out
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function getOperationParameters(operationId) {
|
|
275
|
+
const spec = await fetchSpec("openapi")
|
|
276
|
+
const found = findOperationById(spec, operationId)
|
|
277
|
+
if (!found) return null
|
|
278
|
+
|
|
279
|
+
const { path: p, method, op } = found
|
|
280
|
+
const permission =
|
|
281
|
+
op["x-permission"] ||
|
|
282
|
+
op["x-permissionKey"] ||
|
|
283
|
+
op.security?.[0]?.permission ||
|
|
284
|
+
null
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
operationId,
|
|
288
|
+
path: p,
|
|
289
|
+
method,
|
|
290
|
+
tags: op.tags || [],
|
|
291
|
+
summary: op.summary || "",
|
|
292
|
+
description: op.description || "",
|
|
293
|
+
parameters: op.parameters || [],
|
|
294
|
+
requestBody: op.requestBody || null,
|
|
295
|
+
responses: op.responses || {},
|
|
296
|
+
permission,
|
|
297
|
+
security: op.security || spec.security || [],
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function findEndpointsBySchema(filters) {
|
|
302
|
+
const spec = await fetchSpec("openapi")
|
|
303
|
+
const components = spec.components || {}
|
|
304
|
+
const {
|
|
305
|
+
request_field: reqField,
|
|
306
|
+
response_field: respField,
|
|
307
|
+
path_pattern: pathPattern,
|
|
308
|
+
method: methodFilter,
|
|
309
|
+
tag,
|
|
310
|
+
} = filters || {}
|
|
311
|
+
|
|
312
|
+
const out = []
|
|
313
|
+
for (const { path: p, method, op } of walkOperations(spec)) {
|
|
314
|
+
if (methodFilter && methodFilter.toUpperCase() !== method) continue
|
|
315
|
+
if (pathPattern && !p.includes(pathPattern)) continue
|
|
316
|
+
if (tag && !(op.tags || []).includes(tag)) continue
|
|
317
|
+
|
|
318
|
+
if (reqField) {
|
|
319
|
+
const reqSchema = op.requestBody?.content?.["application/json"]?.schema
|
|
320
|
+
const fields = schemaFieldNames(reqSchema, components)
|
|
321
|
+
if (!fields.includes(reqField)) continue
|
|
322
|
+
}
|
|
323
|
+
if (respField) {
|
|
324
|
+
// Prefer a 2xx response; fall back to any.
|
|
325
|
+
const responses = op.responses || {}
|
|
326
|
+
const preferred =
|
|
327
|
+
responses["200"] ||
|
|
328
|
+
responses["201"] ||
|
|
329
|
+
responses["204"] ||
|
|
330
|
+
Object.values(responses)[0]
|
|
331
|
+
const respSchema = preferred?.content?.["application/json"]?.schema
|
|
332
|
+
const fields = schemaFieldNames(respSchema, components)
|
|
333
|
+
if (!fields.includes(respField)) continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
out.push({
|
|
337
|
+
operationId: op.operationId || null,
|
|
338
|
+
method,
|
|
339
|
+
path: p,
|
|
340
|
+
tags: op.tags || [],
|
|
341
|
+
summary: op.summary || "",
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
return out
|
|
345
|
+
}
|
|
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
|
+
|
|
391
|
+
async function generateDependency({
|
|
392
|
+
identifier,
|
|
393
|
+
tag,
|
|
394
|
+
operationIds,
|
|
395
|
+
eventNames,
|
|
396
|
+
writeTo,
|
|
397
|
+
}) {
|
|
398
|
+
const openapi = await fetchSpec("openapi")
|
|
399
|
+
|
|
400
|
+
// Discover operations under the tag.
|
|
401
|
+
const scoped = []
|
|
402
|
+
for (const { path: p, method, op } of walkOperations(openapi)) {
|
|
403
|
+
if (!(op.tags || []).includes(tag)) continue
|
|
404
|
+
if (operationIds && operationIds.length > 0) {
|
|
405
|
+
if (!operationIds.includes(op.operationId)) continue
|
|
406
|
+
}
|
|
407
|
+
scoped.push({ path: p, method, op })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (scoped.length === 0) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
error: `No operations found for tag="${tag}"${
|
|
414
|
+
operationIds
|
|
415
|
+
? ` matching operationIds=${JSON.stringify(operationIds)}`
|
|
416
|
+
: ""
|
|
417
|
+
}.`,
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const operations = {}
|
|
422
|
+
const permissions = new Set()
|
|
423
|
+
let permissionKey = null
|
|
424
|
+
for (const { path: p, method, op } of scoped) {
|
|
425
|
+
if (!op.operationId) continue
|
|
426
|
+
const cleanOpId = op.operationId.replace(/^portal\.v1\.project\./, "")
|
|
427
|
+
operations[cleanOpId] = `${method.toLowerCase()}:${p}`
|
|
428
|
+
|
|
429
|
+
const perm = op["x-permission"] || op.security?.[0]?.permission || null
|
|
430
|
+
if (perm) permissions.add(perm)
|
|
431
|
+
|
|
432
|
+
const key = op["x-permission-key"] || op["x-permissionKey"] || null
|
|
433
|
+
if (!permissionKey && key) permissionKey = key
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const events = {}
|
|
437
|
+
if (Array.isArray(eventNames)) {
|
|
438
|
+
for (const name of eventNames) {
|
|
439
|
+
events[name] = name
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const dependency = {
|
|
444
|
+
identifier,
|
|
445
|
+
model: tag,
|
|
446
|
+
permissionKey,
|
|
447
|
+
permissions: Array.from(permissions).sort(),
|
|
448
|
+
operations,
|
|
449
|
+
events,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const result = { ok: true, dependency, wrote: false }
|
|
453
|
+
|
|
454
|
+
if (writeTo) {
|
|
455
|
+
const absPath = path.resolve(process.cwd(), writeTo)
|
|
456
|
+
let manifest = { dependencies: [] }
|
|
457
|
+
if (fs.existsSync(absPath)) {
|
|
458
|
+
manifest = JSON.parse(fs.readFileSync(absPath, "utf-8"))
|
|
459
|
+
}
|
|
460
|
+
if (!Array.isArray(manifest.dependencies)) {
|
|
461
|
+
manifest.dependencies = []
|
|
462
|
+
}
|
|
463
|
+
const existingIdx = manifest.dependencies.findIndex(
|
|
464
|
+
(d) => d.identifier === identifier,
|
|
465
|
+
)
|
|
466
|
+
if (existingIdx >= 0) {
|
|
467
|
+
manifest.dependencies[existingIdx] = dependency
|
|
468
|
+
result.replaced = true
|
|
469
|
+
} else {
|
|
470
|
+
manifest.dependencies.push(dependency)
|
|
471
|
+
result.replaced = false
|
|
472
|
+
}
|
|
473
|
+
fs.writeFileSync(absPath, JSON.stringify(manifest, null, "\t"))
|
|
474
|
+
result.wrote = true
|
|
475
|
+
result.file = absPath
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return result
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* --------------------------------- dispatch ------------------------------- */
|
|
482
|
+
|
|
483
|
+
async function handleExtApiToolCall(name, args = {}) {
|
|
484
|
+
switch (name) {
|
|
485
|
+
case "api_list_tags":
|
|
486
|
+
return contentResult({ tags: await listTags() })
|
|
487
|
+
|
|
488
|
+
case "api_list_operation_ids":
|
|
489
|
+
return contentResult({
|
|
490
|
+
operations: await listOperationIds(args.tag),
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
case "api_get_operation_parameters": {
|
|
494
|
+
const detail = await getOperationParameters(args.operationId)
|
|
495
|
+
if (!detail) {
|
|
496
|
+
return contentResult({
|
|
497
|
+
error: `Operation not found: ${args.operationId}`,
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
return contentResult(detail)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case "api_find_endpoints_by_schema":
|
|
504
|
+
return contentResult({
|
|
505
|
+
results: await findEndpointsBySchema(args || {}),
|
|
506
|
+
})
|
|
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
|
+
|
|
516
|
+
case "api_generate_dependency":
|
|
517
|
+
return contentResult(await generateDependency(args))
|
|
518
|
+
|
|
519
|
+
default:
|
|
520
|
+
throw new Error(`Unknown ext api tool: ${name}`)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function isExtApiTool(name) {
|
|
525
|
+
return EXT_API_TOOLS.some((t) => t.name === name)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
module.exports = {
|
|
529
|
+
EXT_API_TOOLS,
|
|
530
|
+
handleExtApiToolCall,
|
|
531
|
+
isExtApiTool,
|
|
532
|
+
// Exported for testing
|
|
533
|
+
walkOperations,
|
|
534
|
+
schemaFieldNames,
|
|
535
|
+
listTags,
|
|
536
|
+
listOperationIds,
|
|
537
|
+
getOperationParameters,
|
|
538
|
+
findEndpointsBySchema,
|
|
539
|
+
generateDependency,
|
|
540
|
+
listAsyncApiEvents,
|
|
541
|
+
findEventsForOperation,
|
|
542
|
+
normalizeOperationId,
|
|
543
|
+
}
|