fenix-mcp 1.13.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- fenix_mcp/__init__.py +1 -1
- fenix_mcp/application/tools/initialize.py +18 -56
- fenix_mcp/application/tools/intelligence.py +130 -304
- fenix_mcp/application/tools/knowledge.py +567 -257
- fenix_mcp/domain/initialization.py +11 -112
- fenix_mcp/domain/intelligence.py +57 -247
- fenix_mcp/domain/knowledge.py +56 -117
- fenix_mcp/infrastructure/fenix_api/client.py +158 -122
- fenix_mcp/interface/mcp_server.py +12 -0
- fenix_mcp-2.0.0.dist-info/METADATA +341 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/RECORD +14 -14
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/WHEEL +1 -1
- fenix_mcp-1.13.0.dist-info/METADATA +0 -258
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -81,7 +81,7 @@ class KnowledgeAction(str, Enum):
|
|
|
81
81
|
)
|
|
82
82
|
WORK_MINE = (
|
|
83
83
|
"work_mine",
|
|
84
|
-
"Lists work items assigned to
|
|
84
|
+
"START HERE for 'my tasks' or 'what am I working on'. Lists work items assigned to current user. Excludes done/cancelled. Supports limit and offset.",
|
|
85
85
|
)
|
|
86
86
|
WORK_BULK_CREATE = (
|
|
87
87
|
"work_bulk_create",
|
|
@@ -105,25 +105,39 @@ class KnowledgeAction(str, Enum):
|
|
|
105
105
|
"Lists work items linked to a sprint.",
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
# Modes
|
|
109
|
-
MODE_CREATE = ("mode_create", "Creates a mode with content and optional metadata.")
|
|
110
|
-
MODE_LIST = ("mode_list", "Lists registered modes.")
|
|
111
|
-
MODE_GET = ("mode_get", "Gets full details of a mode.")
|
|
112
|
-
MODE_UPDATE = ("mode_update", "Updates properties of an existing mode.")
|
|
113
|
-
MODE_DELETE = ("mode_delete", "Removes a mode.")
|
|
114
|
-
MODE_RULE_ADD = ("mode_rule_add", "Associates a rule with a mode.")
|
|
115
|
-
MODE_RULE_REMOVE = (
|
|
116
|
-
"mode_rule_remove",
|
|
117
|
-
"Removes the association of a rule with a mode.",
|
|
118
|
-
)
|
|
119
|
-
MODE_RULES = ("mode_rules", "Lists rules associated with a mode.")
|
|
120
|
-
|
|
121
108
|
# Rules
|
|
122
|
-
RULE_CREATE = (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
109
|
+
RULE_CREATE = (
|
|
110
|
+
"rule_create",
|
|
111
|
+
"Creates a new rule. REQUIRED: rule_scope (personal|team|organization|marketplace), rule_name, rule_content. Optional: rule_description, rule_slug. If scope=team, rule_team_id is also required.",
|
|
112
|
+
)
|
|
113
|
+
RULE_LIST = (
|
|
114
|
+
"rule_list",
|
|
115
|
+
"Lists rules accessible to the current user. Optional filters: rule_scope, query, limit, offset.",
|
|
116
|
+
)
|
|
117
|
+
RULE_GET = (
|
|
118
|
+
"rule_get",
|
|
119
|
+
"Gets full details of a rule. REQUIRED: rule_id (or id).",
|
|
120
|
+
)
|
|
121
|
+
RULE_UPDATE = (
|
|
122
|
+
"rule_update",
|
|
123
|
+
"Updates an existing rule. REQUIRED: rule_id (or id). Optional: rule_name, rule_description, rule_content, rule_is_active.",
|
|
124
|
+
)
|
|
125
|
+
RULE_DELETE = (
|
|
126
|
+
"rule_delete",
|
|
127
|
+
"Deletes a rule. REQUIRED: rule_id (or id).",
|
|
128
|
+
)
|
|
129
|
+
RULE_MARKETPLACE = (
|
|
130
|
+
"rule_marketplace",
|
|
131
|
+
"Lists public marketplace rules. Optional filters: query, limit, offset.",
|
|
132
|
+
)
|
|
133
|
+
RULE_FORK = (
|
|
134
|
+
"rule_fork",
|
|
135
|
+
"Forks a rule to a new scope. REQUIRED: rule_id (or id), rule_scope (personal|team|organization). Optional: rule_name, rule_team_id (required if scope=team).",
|
|
136
|
+
)
|
|
137
|
+
RULE_EXPORT = (
|
|
138
|
+
"rule_export",
|
|
139
|
+
"Exports a rule to a specific format. REQUIRED: rule_id (or id), rule_export_format (cursor|claude|copilot|windsurf).",
|
|
140
|
+
)
|
|
127
141
|
|
|
128
142
|
# Documentation - Navigation workflow: doc_full_tree -> doc_children -> doc_get
|
|
129
143
|
DOC_CREATE = (
|
|
@@ -136,7 +150,7 @@ class KnowledgeAction(str, Enum):
|
|
|
136
150
|
)
|
|
137
151
|
DOC_GET = (
|
|
138
152
|
"doc_get",
|
|
139
|
-
"Reads document content by ID. Get the ID from doc_full_tree
|
|
153
|
+
"Reads document content by ID. PREREQUISITE: Get the ID from doc_full_tree first. Do NOT guess IDs.",
|
|
140
154
|
)
|
|
141
155
|
DOC_UPDATE = ("doc_update", "Updates a documentation item.")
|
|
142
156
|
DOC_DELETE = ("doc_delete", "Removes a documentation item.")
|
|
@@ -153,13 +167,27 @@ class KnowledgeAction(str, Enum):
|
|
|
153
167
|
DOC_TREE = ("doc_tree", "Retrieves tree starting from a specific document.")
|
|
154
168
|
DOC_FULL_TREE = (
|
|
155
169
|
"doc_full_tree",
|
|
156
|
-
"
|
|
170
|
+
"START HERE when looking for documentation. Returns complete folder structure with IDs. You NEED the ID from this to use doc_get. Workflow: 1) doc_full_tree to see all docs with IDs, 2) doc_get(id) to read content.",
|
|
157
171
|
)
|
|
158
172
|
DOC_MOVE = ("doc_move", "Moves a document to another parent.")
|
|
159
173
|
DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
|
|
160
174
|
DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
|
|
161
175
|
DOC_DUPLICATE = ("doc_duplicate", "Duplicates an existing document.")
|
|
162
176
|
|
|
177
|
+
# API Catalog
|
|
178
|
+
API_CATALOG_SEARCH = (
|
|
179
|
+
"api_catalog_search",
|
|
180
|
+
"Semantic search to find APIs by natural language query (e.g., 'API de usuários').",
|
|
181
|
+
)
|
|
182
|
+
API_CATALOG_LIST = (
|
|
183
|
+
"api_catalog_list",
|
|
184
|
+
"Lists API specifications with optional filters (status, tags).",
|
|
185
|
+
)
|
|
186
|
+
API_CATALOG_GET = (
|
|
187
|
+
"api_catalog_get",
|
|
188
|
+
"Gets full details of an API including all endpoints and environments.",
|
|
189
|
+
)
|
|
190
|
+
|
|
163
191
|
HELP = ("knowledge_help", "Shows available actions and their uses.")
|
|
164
192
|
|
|
165
193
|
@classmethod
|
|
@@ -177,10 +205,73 @@ class KnowledgeAction(str, Enum):
|
|
|
177
205
|
return "\n".join(lines)
|
|
178
206
|
|
|
179
207
|
|
|
180
|
-
ACTION_FIELD_DESCRIPTION = "Knowledge action
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
208
|
+
ACTION_FIELD_DESCRIPTION = """Knowledge action grouped by category:
|
|
209
|
+
|
|
210
|
+
**WORK ITEMS** (tasks, bugs, features):
|
|
211
|
+
- `work_mine`: YOUR assigned tasks (START HERE for "my tasks")
|
|
212
|
+
- `work_list`: List with filters (status, priority, type)
|
|
213
|
+
- `work_get`: Get details by ID or key (use work_key for "FENIX-123")
|
|
214
|
+
- `work_create`: Create new (requires: work_title, work_category)
|
|
215
|
+
- `work_update`: Update fields (title, description, priority, story_points, tags, due_date)
|
|
216
|
+
- `work_search`: Search by text
|
|
217
|
+
- `work_bulk_create`: Create multiple with hierarchy
|
|
218
|
+
- `work_backlog`: Team backlog items
|
|
219
|
+
- `work_children`: Child items of a parent
|
|
220
|
+
- `work_by_sprint`: Items in a sprint
|
|
221
|
+
- `work_by_board`: Items in a board
|
|
222
|
+
- `work_by_epic`: Items in an epic
|
|
223
|
+
- `work_status_update`: Update only status
|
|
224
|
+
- `work_assign_sprint`: Assign items to sprint
|
|
225
|
+
- `work_assign_to_me`: Assign item to yourself
|
|
226
|
+
- `work_analytics`: Work item metrics
|
|
227
|
+
|
|
228
|
+
**DOCUMENTATION**:
|
|
229
|
+
- `doc_full_tree`: Complete tree (START HERE to find docs and get IDs)
|
|
230
|
+
- `doc_get`: Read content (needs ID from doc_full_tree)
|
|
231
|
+
- `doc_children`: Folder contents
|
|
232
|
+
- `doc_create`: Create new doc (requires: doc_title, doc_emoji for non-folders)
|
|
233
|
+
- `doc_update`: Update doc
|
|
234
|
+
- `doc_delete`: Remove doc
|
|
235
|
+
- `doc_roots`: Root folders
|
|
236
|
+
- `doc_recent`: Recently accessed
|
|
237
|
+
- `doc_tree`: Tree from specific doc
|
|
238
|
+
- `doc_move`: Move to another parent
|
|
239
|
+
- `doc_publish`: Change publication status
|
|
240
|
+
- `doc_version`: Create/get version
|
|
241
|
+
- `doc_duplicate`: Duplicate doc
|
|
242
|
+
- `doc_analytics`: Doc metrics
|
|
243
|
+
|
|
244
|
+
**SPRINTS**:
|
|
245
|
+
- `sprint_active`: Current sprint (START HERE)
|
|
246
|
+
- `sprint_list`: All sprints
|
|
247
|
+
- `sprint_get`: Sprint details
|
|
248
|
+
- `sprint_by_team`: Team sprints
|
|
249
|
+
- `sprint_work_items`: Items in sprint
|
|
250
|
+
|
|
251
|
+
**BOARDS**:
|
|
252
|
+
- `board_list`: All boards
|
|
253
|
+
- `board_get`: Board details
|
|
254
|
+
- `board_by_team`: Team boards
|
|
255
|
+
- `board_favorites`: Favorite boards
|
|
256
|
+
- `board_columns`: Board columns
|
|
257
|
+
|
|
258
|
+
**RULES** (coding standards):
|
|
259
|
+
- `rule_list`: Team rules
|
|
260
|
+
- `rule_get`: Rule content
|
|
261
|
+
- `rule_create`: Create rule (requires: rule_scope, rule_name, rule_content)
|
|
262
|
+
- `rule_update`: Update rule
|
|
263
|
+
- `rule_delete`: Remove rule
|
|
264
|
+
- `rule_export`: Export for IDE (cursor, claude, copilot, windsurf)
|
|
265
|
+
- `rule_marketplace`: Public rules
|
|
266
|
+
- `rule_fork`: Fork a rule
|
|
267
|
+
|
|
268
|
+
**API CATALOG**:
|
|
269
|
+
- `api_catalog_search`: Semantic search for APIs (natural language)
|
|
270
|
+
- `api_catalog_list`: List all APIs
|
|
271
|
+
- `api_catalog_get`: API details with endpoints
|
|
272
|
+
|
|
273
|
+
`knowledge_help`: Show all actions with descriptions
|
|
274
|
+
"""
|
|
184
275
|
|
|
185
276
|
|
|
186
277
|
_ALLOWED_DOC_TYPES = {
|
|
@@ -283,40 +374,6 @@ class KnowledgeRequest(ToolRequest):
|
|
|
283
374
|
),
|
|
284
375
|
)
|
|
285
376
|
|
|
286
|
-
# Mode fields
|
|
287
|
-
mode_id: Optional[UUIDStr] = Field(
|
|
288
|
-
default=None, description="Related mode ID (UUID)."
|
|
289
|
-
)
|
|
290
|
-
mode_name: Optional[TitleStr] = Field(default=None, description="Mode name.")
|
|
291
|
-
mode_description: Optional[DescriptionStr] = Field(
|
|
292
|
-
default=None, description="Mode description."
|
|
293
|
-
)
|
|
294
|
-
mode_content: Optional[MarkdownStr] = Field(
|
|
295
|
-
default=None, description="Mode content (Markdown)."
|
|
296
|
-
)
|
|
297
|
-
mode_is_default: Optional[bool] = Field(
|
|
298
|
-
default=None, description="Indicates if the mode is default."
|
|
299
|
-
)
|
|
300
|
-
mode_metadata: Optional[MarkdownStr] = Field(
|
|
301
|
-
default=None, description="Mode metadata for AI processing."
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
# Rule fields
|
|
305
|
-
rule_id: Optional[UUIDStr] = Field(
|
|
306
|
-
default=None, description="Related rule ID (UUID)."
|
|
307
|
-
)
|
|
308
|
-
rule_name: Optional[TitleStr] = Field(default=None, description="Rule name.")
|
|
309
|
-
rule_description: Optional[DescriptionStr] = Field(
|
|
310
|
-
default=None, description="Rule description."
|
|
311
|
-
)
|
|
312
|
-
rule_content: Optional[MarkdownStr] = Field(
|
|
313
|
-
default=None, description="Rule content (Markdown)."
|
|
314
|
-
)
|
|
315
|
-
rule_is_default: Optional[bool] = Field(default=None, description="Default rule.")
|
|
316
|
-
rule_metadata: Optional[MarkdownStr] = Field(
|
|
317
|
-
default=None, description="Rule metadata for AI processing."
|
|
318
|
-
)
|
|
319
|
-
|
|
320
377
|
# Documentation fields
|
|
321
378
|
doc_title: Optional[TitleStr] = Field(
|
|
322
379
|
default=None, description="Documentation title."
|
|
@@ -369,10 +426,71 @@ class KnowledgeRequest(ToolRequest):
|
|
|
369
426
|
default=None, description="Whether the document is public."
|
|
370
427
|
)
|
|
371
428
|
|
|
429
|
+
# Rule fields
|
|
430
|
+
rule_id: Optional[UUIDStr] = Field(default=None, description="Rule ID (UUID).")
|
|
431
|
+
rule_scope: Optional[str] = Field(
|
|
432
|
+
default=None,
|
|
433
|
+
description="Rule scope. Values: personal, team, organization, marketplace.",
|
|
434
|
+
)
|
|
435
|
+
rule_name: Optional[TitleStr] = Field(default=None, description="Rule name.")
|
|
436
|
+
rule_slug: Optional[str] = Field(
|
|
437
|
+
default=None, description="Rule slug (auto-generated if not provided)."
|
|
438
|
+
)
|
|
439
|
+
rule_description: Optional[DescriptionStr] = Field(
|
|
440
|
+
default=None, description="Rule description."
|
|
441
|
+
)
|
|
442
|
+
rule_content: Optional[MarkdownStr] = Field(
|
|
443
|
+
default=None, description=f"Rule content (Markdown).{MERMAID_HINT}"
|
|
444
|
+
)
|
|
445
|
+
rule_team_id: Optional[UUIDStr] = Field(
|
|
446
|
+
default=None, description="Team ID for team-scoped rules (UUID)."
|
|
447
|
+
)
|
|
448
|
+
rule_export_format: Optional[str] = Field(
|
|
449
|
+
default=None,
|
|
450
|
+
description="Export format. Values: cursor, claude, copilot, windsurf.",
|
|
451
|
+
)
|
|
452
|
+
rule_is_active: Optional[bool] = Field(
|
|
453
|
+
default=None, description="Whether the rule is active."
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# API Catalog fields
|
|
457
|
+
api_spec_id: Optional[UUIDStr] = Field(
|
|
458
|
+
default=None,
|
|
459
|
+
description="API specification ID (UUID). Can also use 'id' field instead.",
|
|
460
|
+
)
|
|
461
|
+
|
|
372
462
|
|
|
373
463
|
class KnowledgeTool(Tool):
|
|
374
464
|
name = "knowledge"
|
|
375
|
-
description = "Fenix
|
|
465
|
+
description = """Fenix knowledge base - use this tool when user asks about:
|
|
466
|
+
|
|
467
|
+
WORK ITEMS (tasks, bugs, features):
|
|
468
|
+
- "what are my tasks?" -> action: work_mine
|
|
469
|
+
- "show task FENIX-123" -> action: work_get, work_key: "FENIX-123"
|
|
470
|
+
- "what's in the backlog?" -> action: work_backlog
|
|
471
|
+
- "create a task" -> action: work_create (requires: work_title, work_category)
|
|
472
|
+
|
|
473
|
+
DOCUMENTATION:
|
|
474
|
+
- "find docs about X" -> action: doc_full_tree (START HERE to get IDs)
|
|
475
|
+
- "read the architecture doc" -> action: doc_get (needs ID from doc_full_tree)
|
|
476
|
+
|
|
477
|
+
SPRINTS:
|
|
478
|
+
- "current sprint?" -> action: sprint_active
|
|
479
|
+
- "sprint items?" -> action: sprint_work_items
|
|
480
|
+
|
|
481
|
+
RULES (coding standards):
|
|
482
|
+
- "team coding standards?" -> action: rule_list
|
|
483
|
+
- "export rules for cursor" -> action: rule_export
|
|
484
|
+
|
|
485
|
+
API CATALOG:
|
|
486
|
+
- "find user API" -> action: api_catalog_search
|
|
487
|
+
- "list all APIs" -> action: api_catalog_list
|
|
488
|
+
|
|
489
|
+
IMPORTANT WORKFLOWS:
|
|
490
|
+
1. For MY tasks: Always use work_mine first
|
|
491
|
+
2. For docs: Always use doc_full_tree first to get IDs, then doc_get
|
|
492
|
+
3. For work item by key: Use work_key parameter (e.g., "FENIX-123")
|
|
493
|
+
"""
|
|
376
494
|
request_model = KnowledgeRequest
|
|
377
495
|
|
|
378
496
|
def __init__(self, context: AppContext):
|
|
@@ -389,12 +507,12 @@ class KnowledgeTool(Tool):
|
|
|
389
507
|
return await self._run_board(payload)
|
|
390
508
|
if action.value.startswith("sprint_"):
|
|
391
509
|
return await self._run_sprint(payload)
|
|
392
|
-
if action.value.startswith("mode_"):
|
|
393
|
-
return await self._run_mode(payload)
|
|
394
510
|
if action.value.startswith("rule_"):
|
|
395
511
|
return await self._run_rule(payload)
|
|
396
512
|
if action.value.startswith("doc_"):
|
|
397
513
|
return await self._run_doc(payload)
|
|
514
|
+
if action.value.startswith("api_catalog_"):
|
|
515
|
+
return await self._run_api_catalog(payload)
|
|
398
516
|
return text(
|
|
399
517
|
"❌ Invalid action for knowledge.\n\nChoose one of the values:\n"
|
|
400
518
|
+ "\n".join(f"- `{value}`" for value in KnowledgeAction.choices())
|
|
@@ -820,195 +938,123 @@ class KnowledgeTool(Tool):
|
|
|
820
938
|
)
|
|
821
939
|
)
|
|
822
940
|
|
|
823
|
-
# ------------------------------------------------------------------
|
|
824
|
-
# Modes
|
|
825
|
-
# ------------------------------------------------------------------
|
|
826
|
-
async def _run_mode(self, payload: KnowledgeRequest):
|
|
827
|
-
action = payload.action
|
|
828
|
-
if action is KnowledgeAction.MODE_CREATE:
|
|
829
|
-
if not payload.mode_name:
|
|
830
|
-
return text("❌ Provide mode_name to create the mode.")
|
|
831
|
-
mode = await self._service.mode_create(
|
|
832
|
-
{
|
|
833
|
-
"name": payload.mode_name,
|
|
834
|
-
"description": payload.mode_description,
|
|
835
|
-
"content": payload.mode_content,
|
|
836
|
-
"is_default": payload.mode_is_default,
|
|
837
|
-
"metadata": payload.mode_metadata,
|
|
838
|
-
}
|
|
839
|
-
)
|
|
840
|
-
return text(_format_mode(mode, header="✅ Mode created"))
|
|
841
|
-
|
|
842
|
-
if action is KnowledgeAction.MODE_LIST:
|
|
843
|
-
modes = await self._service.mode_list(
|
|
844
|
-
include_rules=payload.return_metadata,
|
|
845
|
-
return_description=payload.return_description,
|
|
846
|
-
return_metadata=payload.return_metadata,
|
|
847
|
-
)
|
|
848
|
-
if not modes:
|
|
849
|
-
return text("🎭 No modes found.")
|
|
850
|
-
body = "\n\n".join(_format_mode(mode) for mode in modes)
|
|
851
|
-
return text(f"🎭 **Modes ({len(modes)}):**\n\n{body}")
|
|
852
|
-
|
|
853
|
-
if action is KnowledgeAction.MODE_GET:
|
|
854
|
-
if not payload.mode_id:
|
|
855
|
-
return text("❌ Provide mode_id to get details.")
|
|
856
|
-
mode = await self._service.mode_get(
|
|
857
|
-
payload.mode_id,
|
|
858
|
-
return_description=payload.return_description,
|
|
859
|
-
return_metadata=payload.return_metadata,
|
|
860
|
-
)
|
|
861
|
-
# Buscar rules associadas ao mode
|
|
862
|
-
associations = await self._service.mode_rules(payload.mode_id)
|
|
863
|
-
rules = [assoc.get("rule", assoc) for assoc in associations]
|
|
864
|
-
|
|
865
|
-
# Formatar resposta com rules (incluindo content)
|
|
866
|
-
output = _format_mode(mode, header="🎭 Mode details", show_content=True)
|
|
867
|
-
if rules:
|
|
868
|
-
rules_parts = []
|
|
869
|
-
for r in rules:
|
|
870
|
-
rule_text = f"### 📋 {r.get('name', 'Unnamed')}\n"
|
|
871
|
-
rule_text += f"ID: {r.get('id')}\n"
|
|
872
|
-
if r.get("content"):
|
|
873
|
-
rule_text += f"\n{r.get('content')}"
|
|
874
|
-
rules_parts.append(rule_text)
|
|
875
|
-
output += f"\n\n**Rules ({len(rules)}):**\n\n" + "\n\n---\n\n".join(
|
|
876
|
-
rules_parts
|
|
877
|
-
)
|
|
878
|
-
return text(output)
|
|
879
|
-
|
|
880
|
-
if action is KnowledgeAction.MODE_UPDATE:
|
|
881
|
-
if not payload.mode_id:
|
|
882
|
-
return text("❌ Provide mode_id to update.")
|
|
883
|
-
mode = await self._service.mode_update(
|
|
884
|
-
payload.mode_id,
|
|
885
|
-
{
|
|
886
|
-
"name": payload.mode_name,
|
|
887
|
-
"description": payload.mode_description,
|
|
888
|
-
"content": payload.mode_content,
|
|
889
|
-
"is_default": payload.mode_is_default,
|
|
890
|
-
"metadata": payload.mode_metadata,
|
|
891
|
-
},
|
|
892
|
-
)
|
|
893
|
-
return text(_format_mode(mode, header="✅ Mode updated"))
|
|
894
|
-
|
|
895
|
-
if action is KnowledgeAction.MODE_DELETE:
|
|
896
|
-
if not payload.mode_id:
|
|
897
|
-
return text("❌ Provide mode_id to remove.")
|
|
898
|
-
await self._service.mode_delete(payload.mode_id)
|
|
899
|
-
return text(f"🗑️ Mode {payload.mode_id} removed.")
|
|
900
|
-
|
|
901
|
-
if action is KnowledgeAction.MODE_RULE_ADD:
|
|
902
|
-
if not payload.mode_id or not payload.rule_id:
|
|
903
|
-
return text("❌ Provide mode_id and rule_id to associate.")
|
|
904
|
-
link = await self._service.mode_rule_add(payload.mode_id, payload.rule_id)
|
|
905
|
-
return text(
|
|
906
|
-
"\n".join(
|
|
907
|
-
[
|
|
908
|
-
"🔗 **Rule associated with mode!**",
|
|
909
|
-
f"Mode: {link.get('modeId', payload.mode_id)}",
|
|
910
|
-
f"Rule: {link.get('ruleId', payload.rule_id)}",
|
|
911
|
-
]
|
|
912
|
-
)
|
|
913
|
-
)
|
|
914
|
-
|
|
915
|
-
if action is KnowledgeAction.MODE_RULE_REMOVE:
|
|
916
|
-
if not payload.mode_id or not payload.rule_id:
|
|
917
|
-
return text("❌ Provide mode_id and rule_id to remove the association.")
|
|
918
|
-
await self._service.mode_rule_remove(payload.mode_id, payload.rule_id)
|
|
919
|
-
return text("🔗 Association removed.")
|
|
920
|
-
|
|
921
|
-
if action is KnowledgeAction.MODE_RULES:
|
|
922
|
-
if payload.mode_id:
|
|
923
|
-
associations = await self._service.mode_rules(payload.mode_id)
|
|
924
|
-
context_label = f"mode {payload.mode_id}"
|
|
925
|
-
# API retorna [{id, mode_id, rule_id, rule: {...}}] - extrair rule
|
|
926
|
-
items = [assoc.get("rule", assoc) for assoc in associations]
|
|
927
|
-
elif payload.rule_id:
|
|
928
|
-
associations = await self._service.mode_rules_for_rule(payload.rule_id)
|
|
929
|
-
context_label = f"rule {payload.rule_id}"
|
|
930
|
-
# API retorna [{id, mode_id, rule_id, mode: {...}}] - extrair mode
|
|
931
|
-
items = [assoc.get("mode", assoc) for assoc in associations]
|
|
932
|
-
else:
|
|
933
|
-
return text("❌ Provide mode_id or rule_id to list associations.")
|
|
934
|
-
if not items:
|
|
935
|
-
return text("🔗 No associations found.")
|
|
936
|
-
body = "\n".join(
|
|
937
|
-
f"- {item.get('name', 'Unnamed')} (ID: {item.get('id')})"
|
|
938
|
-
for item in items
|
|
939
|
-
)
|
|
940
|
-
return text(f"🔗 **Associations for {context_label}:**\n{body}")
|
|
941
|
-
|
|
942
|
-
return text(
|
|
943
|
-
"❌ Unsupported mode action.\n\nChoose one of the values:\n"
|
|
944
|
-
+ "\n".join(
|
|
945
|
-
f"- `{value}`"
|
|
946
|
-
for value in KnowledgeAction.choices()
|
|
947
|
-
if value.startswith("mode_")
|
|
948
|
-
)
|
|
949
|
-
)
|
|
950
|
-
|
|
951
941
|
# ------------------------------------------------------------------
|
|
952
942
|
# Rules
|
|
953
943
|
# ------------------------------------------------------------------
|
|
954
944
|
async def _run_rule(self, payload: KnowledgeRequest):
|
|
955
945
|
action = payload.action
|
|
946
|
+
|
|
956
947
|
if action is KnowledgeAction.RULE_CREATE:
|
|
957
|
-
if not payload.rule_name
|
|
958
|
-
return text("❌ Provide rule_name
|
|
948
|
+
if not payload.rule_name:
|
|
949
|
+
return text("❌ Provide rule_name to create the rule.")
|
|
950
|
+
if not payload.rule_content:
|
|
951
|
+
return text("❌ Provide rule_content to create the rule.")
|
|
952
|
+
if not payload.rule_scope:
|
|
953
|
+
return text(
|
|
954
|
+
"❌ Provide rule_scope to create the rule. "
|
|
955
|
+
"Values: personal, team, organization, marketplace."
|
|
956
|
+
)
|
|
957
|
+
if payload.rule_scope == "team" and not payload.rule_team_id:
|
|
958
|
+
return text("❌ Provide rule_team_id for team-scoped rules.")
|
|
959
|
+
|
|
959
960
|
rule = await self._service.rule_create(
|
|
960
961
|
{
|
|
962
|
+
"scope": payload.rule_scope,
|
|
961
963
|
"name": payload.rule_name,
|
|
962
|
-
"
|
|
964
|
+
"slug": sanitize_null(payload.rule_slug),
|
|
965
|
+
"description": sanitize_null(payload.rule_description),
|
|
963
966
|
"content": payload.rule_content,
|
|
964
|
-
"
|
|
965
|
-
"metadata": payload.rule_metadata,
|
|
967
|
+
"team_id": sanitize_null(payload.rule_team_id),
|
|
966
968
|
}
|
|
967
969
|
)
|
|
968
970
|
return text(_format_rule(rule, header="✅ Rule created"))
|
|
969
971
|
|
|
970
972
|
if action is KnowledgeAction.RULE_LIST:
|
|
971
973
|
rules = await self._service.rule_list(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
974
|
+
limit=payload.limit,
|
|
975
|
+
offset=payload.offset,
|
|
976
|
+
scope=sanitize_null(payload.rule_scope),
|
|
977
|
+
query=sanitize_null(payload.query),
|
|
975
978
|
)
|
|
976
979
|
if not rules:
|
|
977
|
-
return text("
|
|
980
|
+
return text("📜 No rules found.")
|
|
978
981
|
body = "\n\n".join(_format_rule(rule) for rule in rules)
|
|
979
|
-
return text(f"
|
|
982
|
+
return text(f"📜 **Rules ({len(rules)}):**\n\n{body}")
|
|
980
983
|
|
|
981
984
|
if action is KnowledgeAction.RULE_GET:
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
return_metadata=payload.return_metadata,
|
|
988
|
-
return_modes=payload.return_metadata,
|
|
989
|
-
)
|
|
990
|
-
return text(_format_rule(rule, header="📋 Rule details", show_content=True))
|
|
985
|
+
rule_id = payload.rule_id or payload.id
|
|
986
|
+
if not rule_id:
|
|
987
|
+
return text("❌ Provide rule_id or id to get the rule.")
|
|
988
|
+
rule = await self._service.rule_get(rule_id)
|
|
989
|
+
return text(_format_rule(rule, header="📜 Rule details", show_content=True))
|
|
991
990
|
|
|
992
991
|
if action is KnowledgeAction.RULE_UPDATE:
|
|
993
|
-
|
|
994
|
-
|
|
992
|
+
rule_id = payload.rule_id or payload.id
|
|
993
|
+
if not rule_id:
|
|
994
|
+
return text("❌ Provide rule_id or id to update the rule.")
|
|
995
995
|
rule = await self._service.rule_update(
|
|
996
|
-
|
|
996
|
+
rule_id,
|
|
997
997
|
{
|
|
998
|
-
"name": payload.rule_name,
|
|
999
|
-
"description": payload.rule_description,
|
|
1000
|
-
"content": payload.rule_content,
|
|
1001
|
-
"
|
|
1002
|
-
"metadata": payload.rule_metadata,
|
|
998
|
+
"name": sanitize_null(payload.rule_name),
|
|
999
|
+
"description": sanitize_null(payload.rule_description),
|
|
1000
|
+
"content": sanitize_null(payload.rule_content),
|
|
1001
|
+
"is_active": payload.rule_is_active,
|
|
1003
1002
|
},
|
|
1004
1003
|
)
|
|
1005
1004
|
return text(_format_rule(rule, header="✅ Rule updated"))
|
|
1006
1005
|
|
|
1007
1006
|
if action is KnowledgeAction.RULE_DELETE:
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1007
|
+
rule_id = payload.rule_id or payload.id
|
|
1008
|
+
if not rule_id:
|
|
1009
|
+
return text("❌ Provide rule_id or id to delete the rule.")
|
|
1010
|
+
await self._service.rule_delete(rule_id)
|
|
1011
|
+
return text(f"🗑️ Rule {rule_id} removed.")
|
|
1012
|
+
|
|
1013
|
+
if action is KnowledgeAction.RULE_MARKETPLACE:
|
|
1014
|
+
rules = await self._service.rule_marketplace(
|
|
1015
|
+
limit=payload.limit,
|
|
1016
|
+
offset=payload.offset,
|
|
1017
|
+
query=sanitize_null(payload.query),
|
|
1018
|
+
)
|
|
1019
|
+
if not rules:
|
|
1020
|
+
return text("🏪 No marketplace rules found.")
|
|
1021
|
+
body = "\n\n".join(_format_marketplace_rule(rule) for rule in rules)
|
|
1022
|
+
return text(f"🏪 **Marketplace Rules ({len(rules)}):**\n\n{body}")
|
|
1023
|
+
|
|
1024
|
+
if action is KnowledgeAction.RULE_FORK:
|
|
1025
|
+
rule_id = payload.rule_id or payload.id
|
|
1026
|
+
if not rule_id:
|
|
1027
|
+
return text("❌ Provide rule_id or id to fork the rule.")
|
|
1028
|
+
if not payload.rule_scope:
|
|
1029
|
+
return text(
|
|
1030
|
+
"❌ Provide rule_scope for the forked rule. "
|
|
1031
|
+
"Values: personal, team, organization."
|
|
1032
|
+
)
|
|
1033
|
+
rule = await self._service.rule_fork(
|
|
1034
|
+
rule_id,
|
|
1035
|
+
{
|
|
1036
|
+
"scope": payload.rule_scope,
|
|
1037
|
+
"name": sanitize_null(payload.rule_name),
|
|
1038
|
+
"team_id": sanitize_null(payload.rule_team_id),
|
|
1039
|
+
},
|
|
1040
|
+
)
|
|
1041
|
+
return text(_format_rule(rule, header="✅ Rule forked"))
|
|
1042
|
+
|
|
1043
|
+
if action is KnowledgeAction.RULE_EXPORT:
|
|
1044
|
+
rule_id = payload.rule_id or payload.id
|
|
1045
|
+
if not rule_id:
|
|
1046
|
+
return text("❌ Provide rule_id or id to export the rule.")
|
|
1047
|
+
if not payload.rule_export_format:
|
|
1048
|
+
return text(
|
|
1049
|
+
"❌ Provide rule_export_format. "
|
|
1050
|
+
"Values: cursor, claude, copilot, windsurf."
|
|
1051
|
+
)
|
|
1052
|
+
content = await self._service.rule_export(
|
|
1053
|
+
rule_id, payload.rule_export_format
|
|
1054
|
+
)
|
|
1055
|
+
return text(
|
|
1056
|
+
f"📤 **Export ({payload.rule_export_format}):**\n\n```\n{content}\n```"
|
|
1057
|
+
)
|
|
1012
1058
|
|
|
1013
1059
|
return text(
|
|
1014
1060
|
"❌ Unsupported rule action.\n\nChoose one of the values:\n"
|
|
@@ -1219,6 +1265,63 @@ class KnowledgeTool(Tool):
|
|
|
1219
1265
|
)
|
|
1220
1266
|
)
|
|
1221
1267
|
|
|
1268
|
+
# ------------------------------------------------------------------
|
|
1269
|
+
# API Catalog
|
|
1270
|
+
# ------------------------------------------------------------------
|
|
1271
|
+
async def _run_api_catalog(self, payload: KnowledgeRequest):
|
|
1272
|
+
action = payload.action
|
|
1273
|
+
|
|
1274
|
+
if action is KnowledgeAction.API_CATALOG_SEARCH:
|
|
1275
|
+
query = sanitize_null(payload.query)
|
|
1276
|
+
if not query:
|
|
1277
|
+
return text("❌ Provide query to search APIs.")
|
|
1278
|
+
|
|
1279
|
+
result = await self._service.api_catalog_search(
|
|
1280
|
+
query=query,
|
|
1281
|
+
limit=payload.limit,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
data = result.get("data", [])
|
|
1285
|
+
if not data:
|
|
1286
|
+
return text(
|
|
1287
|
+
f"🔍 No APIs found for: **{query}**\n\n"
|
|
1288
|
+
"Try a different search term or use `api_catalog_list` to see all APIs."
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
body = "\n\n".join(_format_api_search_result(api) for api in data)
|
|
1292
|
+
total = len(data)
|
|
1293
|
+
|
|
1294
|
+
return text(f"🔍 **APIs found for '{query}'** ({total} results)\n\n{body}")
|
|
1295
|
+
|
|
1296
|
+
if action is KnowledgeAction.API_CATALOG_LIST:
|
|
1297
|
+
apis = await self._service.api_catalog_list(
|
|
1298
|
+
limit=payload.limit,
|
|
1299
|
+
offset=payload.offset,
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
if not apis:
|
|
1303
|
+
return text("📚 No APIs found in the catalog.")
|
|
1304
|
+
|
|
1305
|
+
body = "\n\n".join(_format_api_item(api) for api in apis)
|
|
1306
|
+
return text(f"📚 **API Catalog ({len(apis)} APIs)**\n\n{body}")
|
|
1307
|
+
|
|
1308
|
+
if action is KnowledgeAction.API_CATALOG_GET:
|
|
1309
|
+
spec_id = payload.api_spec_id or payload.id
|
|
1310
|
+
if not spec_id:
|
|
1311
|
+
return text("❌ Provide api_spec_id or id to get the API details.")
|
|
1312
|
+
|
|
1313
|
+
api = await self._service.api_catalog_get(spec_id)
|
|
1314
|
+
return text(_format_api_detail(api))
|
|
1315
|
+
|
|
1316
|
+
return text(
|
|
1317
|
+
"❌ Unsupported API Catalog action.\n\nChoose one of the values:\n"
|
|
1318
|
+
+ "\n".join(
|
|
1319
|
+
f"- `{value}`"
|
|
1320
|
+
for value in KnowledgeAction.choices()
|
|
1321
|
+
if value.startswith("api_catalog_")
|
|
1322
|
+
)
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1222
1325
|
async def _handle_help(self):
|
|
1223
1326
|
workflow_guide = """
|
|
1224
1327
|
## 📖 How to find and read a document
|
|
@@ -1361,8 +1464,8 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
|
1361
1464
|
return "\n".join(lines)
|
|
1362
1465
|
|
|
1363
1466
|
|
|
1364
|
-
def
|
|
1365
|
-
|
|
1467
|
+
def _format_doc(
|
|
1468
|
+
doc: Dict[str, Any],
|
|
1366
1469
|
*,
|
|
1367
1470
|
header: Optional[str] = None,
|
|
1368
1471
|
show_content: bool = False,
|
|
@@ -1373,17 +1476,23 @@ def _format_mode(
|
|
|
1373
1476
|
lines.append("")
|
|
1374
1477
|
lines.extend(
|
|
1375
1478
|
[
|
|
1376
|
-
f"
|
|
1377
|
-
f"ID: {
|
|
1378
|
-
f"
|
|
1479
|
+
f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
|
|
1480
|
+
f"ID: {doc.get('id', 'N/A')}",
|
|
1481
|
+
f"Status: {doc.get('status', 'N/A')}",
|
|
1482
|
+
f"Team: {doc.get('team_id', 'N/A')}",
|
|
1379
1483
|
]
|
|
1380
1484
|
)
|
|
1381
|
-
if
|
|
1382
|
-
lines.append(
|
|
1383
|
-
|
|
1485
|
+
if doc.get("updated_at") or doc.get("updatedAt"):
|
|
1486
|
+
lines.append(
|
|
1487
|
+
f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
# Show content only when explicitly requested (e.g., doc_get)
|
|
1491
|
+
if show_content and doc.get("content"):
|
|
1384
1492
|
lines.append("")
|
|
1385
1493
|
lines.append("**Content:**")
|
|
1386
|
-
lines.append(
|
|
1494
|
+
lines.append(doc.get("content"))
|
|
1495
|
+
|
|
1387
1496
|
return "\n".join(lines)
|
|
1388
1497
|
|
|
1389
1498
|
|
|
@@ -1397,50 +1506,251 @@ def _format_rule(
|
|
|
1397
1506
|
if header:
|
|
1398
1507
|
lines.append(header)
|
|
1399
1508
|
lines.append("")
|
|
1509
|
+
|
|
1510
|
+
name = rule.get("name", "Untitled")
|
|
1511
|
+
scope = rule.get("scope", "unknown")
|
|
1512
|
+
scope_emoji = {
|
|
1513
|
+
"personal": "👤",
|
|
1514
|
+
"team": "👥",
|
|
1515
|
+
"organization": "🏢",
|
|
1516
|
+
"marketplace": "🌍",
|
|
1517
|
+
}.get(scope, "📜")
|
|
1518
|
+
|
|
1400
1519
|
lines.extend(
|
|
1401
1520
|
[
|
|
1402
|
-
f"
|
|
1521
|
+
f"{scope_emoji} **{name}**",
|
|
1403
1522
|
f"ID: {rule.get('id', 'N/A')}",
|
|
1404
|
-
f"
|
|
1523
|
+
f"Scope: {scope}",
|
|
1524
|
+
f"Version: {rule.get('version', '1.0')}",
|
|
1405
1525
|
]
|
|
1406
1526
|
)
|
|
1527
|
+
|
|
1407
1528
|
if rule.get("description"):
|
|
1408
|
-
lines.append(f"Description: {rule
|
|
1529
|
+
lines.append(f"Description: {rule.get('description')}")
|
|
1530
|
+
|
|
1531
|
+
# Author info
|
|
1532
|
+
author = rule.get("author")
|
|
1533
|
+
if isinstance(author, dict):
|
|
1534
|
+
lines.append(f"Author: {author.get('name', 'Unknown')}")
|
|
1535
|
+
|
|
1536
|
+
# Forked from info
|
|
1537
|
+
forked_from = rule.get("forked_from")
|
|
1538
|
+
if isinstance(forked_from, dict):
|
|
1539
|
+
forked_author = forked_from.get("author", {})
|
|
1540
|
+
author_name = (
|
|
1541
|
+
forked_author.get("name", "Unknown")
|
|
1542
|
+
if isinstance(forked_author, dict)
|
|
1543
|
+
else "Unknown"
|
|
1544
|
+
)
|
|
1545
|
+
lines.append(
|
|
1546
|
+
f"Forked from: {forked_from.get('name', 'Unknown')} by {author_name}"
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
if rule.get("is_active") is False:
|
|
1550
|
+
lines.append("Status: Inactive")
|
|
1551
|
+
|
|
1552
|
+
if rule.get("updated_at") or rule.get("updatedAt"):
|
|
1553
|
+
lines.append(
|
|
1554
|
+
f"Updated: {_format_date(rule.get('updated_at') or rule.get('updatedAt'))}"
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
# Show content only when explicitly requested
|
|
1409
1558
|
if show_content and rule.get("content"):
|
|
1410
1559
|
lines.append("")
|
|
1411
1560
|
lines.append("**Content:**")
|
|
1412
1561
|
lines.append(rule.get("content"))
|
|
1562
|
+
|
|
1413
1563
|
return "\n".join(lines)
|
|
1414
1564
|
|
|
1415
1565
|
|
|
1416
|
-
def
|
|
1417
|
-
|
|
1418
|
-
*,
|
|
1419
|
-
header: Optional[str] = None,
|
|
1420
|
-
show_content: bool = False,
|
|
1421
|
-
) -> str:
|
|
1566
|
+
def _format_marketplace_rule(rule: Dict[str, Any]) -> str:
|
|
1567
|
+
"""Format a marketplace rule with download/rating info."""
|
|
1422
1568
|
lines: List[str] = []
|
|
1423
|
-
|
|
1424
|
-
|
|
1569
|
+
|
|
1570
|
+
name = rule.get("name", "Untitled")
|
|
1571
|
+
downloads = rule.get("downloads", 0)
|
|
1572
|
+
rating = rule.get("rating")
|
|
1573
|
+
|
|
1574
|
+
lines.append(f"🌍 **{name}**")
|
|
1575
|
+
lines.append(f"ID: {rule.get('id', 'N/A')}")
|
|
1576
|
+
|
|
1577
|
+
if rule.get("description"):
|
|
1578
|
+
lines.append(f"Description: {rule.get('description')}")
|
|
1579
|
+
|
|
1580
|
+
# Author info
|
|
1581
|
+
author = rule.get("author")
|
|
1582
|
+
if isinstance(author, dict):
|
|
1583
|
+
lines.append(f"Author: {author.get('name', 'Unknown')}")
|
|
1584
|
+
|
|
1585
|
+
# Stats
|
|
1586
|
+
stats_parts = [f"⬇️ {downloads}"]
|
|
1587
|
+
if rating is not None:
|
|
1588
|
+
stats_parts.append(f"⭐ {float(rating):.1f}")
|
|
1589
|
+
lines.append(" | ".join(stats_parts))
|
|
1590
|
+
|
|
1591
|
+
return "\n".join(lines)
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
def _format_api_search_result(api: Dict[str, Any]) -> str:
|
|
1595
|
+
"""Format an API search result with similarity and matched endpoint."""
|
|
1596
|
+
lines: List[str] = []
|
|
1597
|
+
|
|
1598
|
+
title = api.get("title", "Untitled API")
|
|
1599
|
+
slug = api.get("slug", "")
|
|
1600
|
+
similarity = api.get("similarity")
|
|
1601
|
+
|
|
1602
|
+
# Title with similarity
|
|
1603
|
+
if similarity is not None:
|
|
1604
|
+
pct = float(similarity) * 100
|
|
1605
|
+
lines.append(f"🔗 **{title}** ({pct:.0f}% match)")
|
|
1606
|
+
else:
|
|
1607
|
+
lines.append(f"🔗 **{title}**")
|
|
1608
|
+
|
|
1609
|
+
lines.append(f"Slug: {slug}")
|
|
1610
|
+
|
|
1611
|
+
# Status
|
|
1612
|
+
status = api.get("lifecycle_status", "unknown")
|
|
1613
|
+
lines.append(f"Status: {status}")
|
|
1614
|
+
|
|
1615
|
+
# Description (truncated)
|
|
1616
|
+
desc = api.get("description")
|
|
1617
|
+
if desc:
|
|
1618
|
+
truncated = desc[:150] + "..." if len(desc) > 150 else desc
|
|
1619
|
+
lines.append(f"Description: {truncated}")
|
|
1620
|
+
|
|
1621
|
+
# Team info
|
|
1622
|
+
team = api.get("team")
|
|
1623
|
+
if isinstance(team, dict):
|
|
1624
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1625
|
+
|
|
1626
|
+
# Matched endpoint (for semantic search)
|
|
1627
|
+
matched = api.get("matchedEndpoint")
|
|
1628
|
+
if matched:
|
|
1629
|
+
method = matched.get("method", "").upper()
|
|
1630
|
+
path = matched.get("path", "")
|
|
1631
|
+
summary = matched.get("summary", "")
|
|
1632
|
+
lines.append(f"Best match: **{method} {path}**")
|
|
1633
|
+
if summary:
|
|
1634
|
+
lines.append(f" └─ {summary}")
|
|
1635
|
+
|
|
1636
|
+
# Current version
|
|
1637
|
+
version = api.get("current_version")
|
|
1638
|
+
if isinstance(version, dict):
|
|
1639
|
+
lines.append(f"Version: {version.get('version', 'N/A')}")
|
|
1640
|
+
|
|
1641
|
+
return "\n".join(lines)
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _format_api_item(api: Dict[str, Any]) -> str:
|
|
1645
|
+
"""Format an API item for listing."""
|
|
1646
|
+
lines: List[str] = []
|
|
1647
|
+
|
|
1648
|
+
title = api.get("title", "Untitled API")
|
|
1649
|
+
slug = api.get("slug", "")
|
|
1650
|
+
status = api.get("lifecycle_status", "unknown")
|
|
1651
|
+
|
|
1652
|
+
lines.append(f"📦 **{title}**")
|
|
1653
|
+
lines.append(f"ID: {api.get('id', 'N/A')}")
|
|
1654
|
+
lines.append(f"Slug: {slug}")
|
|
1655
|
+
lines.append(f"Status: {status}")
|
|
1656
|
+
|
|
1657
|
+
# Tags
|
|
1658
|
+
tags = api.get("tags", [])
|
|
1659
|
+
if tags:
|
|
1660
|
+
lines.append(f"Tags: {', '.join(tags)}")
|
|
1661
|
+
|
|
1662
|
+
# Team
|
|
1663
|
+
team = api.get("team")
|
|
1664
|
+
if isinstance(team, dict):
|
|
1665
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1666
|
+
|
|
1667
|
+
# Version
|
|
1668
|
+
version = api.get("current_version")
|
|
1669
|
+
if isinstance(version, dict):
|
|
1670
|
+
lines.append(f"Version: {version.get('version', 'N/A')}")
|
|
1671
|
+
|
|
1672
|
+
return "\n".join(lines)
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _format_api_detail(api: Dict[str, Any]) -> str:
|
|
1676
|
+
"""Format detailed API specification view."""
|
|
1677
|
+
lines: List[str] = []
|
|
1678
|
+
|
|
1679
|
+
title = api.get("title", "Untitled API")
|
|
1680
|
+
slug = api.get("slug", "")
|
|
1681
|
+
|
|
1682
|
+
lines.append(f"📚 **{title}**")
|
|
1683
|
+
lines.append("")
|
|
1684
|
+
lines.append(f"ID: {api.get('id', 'N/A')}")
|
|
1685
|
+
lines.append(f"Slug: {slug}")
|
|
1686
|
+
lines.append(f"Status: {api.get('lifecycle_status', 'unknown')}")
|
|
1687
|
+
|
|
1688
|
+
# Description
|
|
1689
|
+
desc = api.get("description")
|
|
1690
|
+
if desc:
|
|
1425
1691
|
lines.append("")
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
|
|
1429
|
-
f"ID: {doc.get('id', 'N/A')}",
|
|
1430
|
-
f"Status: {doc.get('status', 'N/A')}",
|
|
1431
|
-
f"Team: {doc.get('team_id', 'N/A')}",
|
|
1432
|
-
]
|
|
1433
|
-
)
|
|
1434
|
-
if doc.get("updated_at") or doc.get("updatedAt"):
|
|
1435
|
-
lines.append(
|
|
1436
|
-
f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
|
|
1437
|
-
)
|
|
1692
|
+
lines.append("**Description:**")
|
|
1693
|
+
lines.append(desc)
|
|
1438
1694
|
|
|
1439
|
-
#
|
|
1440
|
-
|
|
1695
|
+
# Tags
|
|
1696
|
+
tags = api.get("tags", [])
|
|
1697
|
+
if tags:
|
|
1441
1698
|
lines.append("")
|
|
1442
|
-
lines.append("
|
|
1443
|
-
|
|
1699
|
+
lines.append(f"Tags: {', '.join(tags)}")
|
|
1700
|
+
|
|
1701
|
+
# Team
|
|
1702
|
+
team = api.get("team")
|
|
1703
|
+
if isinstance(team, dict):
|
|
1704
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1705
|
+
|
|
1706
|
+
# Current version
|
|
1707
|
+
version = api.get("current_version")
|
|
1708
|
+
if isinstance(version, dict):
|
|
1709
|
+
lines.append("")
|
|
1710
|
+
lines.append("**Current Version:**")
|
|
1711
|
+
lines.append(f"- Version: {version.get('version', 'N/A')}")
|
|
1712
|
+
lines.append(f"- OpenAPI: {version.get('openapi_version', 'N/A')}")
|
|
1713
|
+
|
|
1714
|
+
# Endpoints from current version
|
|
1715
|
+
endpoints = version.get("endpoint_embeddings", [])
|
|
1716
|
+
if endpoints:
|
|
1717
|
+
lines.append(f"- Endpoints: {len(endpoints)}")
|
|
1718
|
+
lines.append("")
|
|
1719
|
+
lines.append("**Endpoints:**")
|
|
1720
|
+
for ep in endpoints[:10]: # Limit to 10
|
|
1721
|
+
method = ep.get("method", "").upper()
|
|
1722
|
+
path = ep.get("path", "")
|
|
1723
|
+
summary = ep.get("summary", "")
|
|
1724
|
+
if summary:
|
|
1725
|
+
lines.append(f"- **{method}** {path} - {summary}")
|
|
1726
|
+
else:
|
|
1727
|
+
lines.append(f"- **{method}** {path}")
|
|
1728
|
+
if len(endpoints) > 10:
|
|
1729
|
+
lines.append(f" ... and {len(endpoints) - 10} more endpoints")
|
|
1730
|
+
|
|
1731
|
+
# Environments
|
|
1732
|
+
environments = api.get("environments", [])
|
|
1733
|
+
if environments:
|
|
1734
|
+
lines.append("")
|
|
1735
|
+
lines.append("**Environments:**")
|
|
1736
|
+
for env in environments:
|
|
1737
|
+
name = env.get("name", "Unnamed")
|
|
1738
|
+
url = env.get("base_url", "")
|
|
1739
|
+
lines.append(f"- {name}: {url}")
|
|
1740
|
+
|
|
1741
|
+
# Permissions
|
|
1742
|
+
permissions = api.get("permissions", {})
|
|
1743
|
+
if permissions:
|
|
1744
|
+
lines.append("")
|
|
1745
|
+
lines.append("**Your Permissions:**")
|
|
1746
|
+
if permissions.get("canEdit"):
|
|
1747
|
+
lines.append("- ✅ Edit")
|
|
1748
|
+
if permissions.get("canDelete"):
|
|
1749
|
+
lines.append("- ✅ Delete")
|
|
1750
|
+
if permissions.get("canPublish"):
|
|
1751
|
+
lines.append("- ✅ Publish")
|
|
1752
|
+
if permissions.get("canManageVersions"):
|
|
1753
|
+
lines.append("- ✅ Manage Versions")
|
|
1444
1754
|
|
|
1445
1755
|
return "\n".join(lines)
|
|
1446
1756
|
|