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.
@@ -81,7 +81,7 @@ class KnowledgeAction(str, Enum):
81
81
  )
82
82
  WORK_MINE = (
83
83
  "work_mine",
84
- "Lists work items assigned to the current user. Automatically excludes items with status 'done' or 'cancelled'. Supports pagination via limit and offset parameters.",
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 = ("rule_create", "Creates a rule with content and metadata.")
123
- RULE_LIST = ("rule_list", "Lists registered rules.")
124
- RULE_GET = ("rule_get", "Gets rule details.")
125
- RULE_UPDATE = ("rule_update", "Updates an existing rule.")
126
- RULE_DELETE = ("rule_delete", "Removes a rule.")
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 or doc_children first.",
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
- "Retrieves complete documentation tree. Start here to find documents.",
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. Choose one of the values: " + ", ".join(
181
- f"`{member.value}` ({member.description.rstrip('.')})."
182
- for member in KnowledgeAction
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 Cloud knowledge operations (Work Items, Boards, Sprints, Modes, Rules, Docs)."
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 or not payload.rule_content:
958
- return text("❌ Provide rule_name and rule_content.")
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
- "description": payload.rule_description,
964
+ "slug": sanitize_null(payload.rule_slug),
965
+ "description": sanitize_null(payload.rule_description),
963
966
  "content": payload.rule_content,
964
- "is_default": payload.rule_is_default,
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
- return_description=payload.return_description,
973
- return_metadata=payload.return_metadata,
974
- return_modes=payload.return_metadata,
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("📋 No rules found.")
980
+ return text("📜 No rules found.")
978
981
  body = "\n\n".join(_format_rule(rule) for rule in rules)
979
- return text(f"📋 **Rules ({len(rules)}):**\n\n{body}")
982
+ return text(f"📜 **Rules ({len(rules)}):**\n\n{body}")
980
983
 
981
984
  if action is KnowledgeAction.RULE_GET:
982
- if not payload.rule_id:
983
- return text("❌ Provide rule_id to get details.")
984
- rule = await self._service.rule_get(
985
- payload.rule_id,
986
- return_description=payload.return_description,
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
- if not payload.rule_id:
994
- return text("❌ Provide rule_id to update.")
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
- payload.rule_id,
996
+ rule_id,
997
997
  {
998
- "name": payload.rule_name,
999
- "description": payload.rule_description,
1000
- "content": payload.rule_content,
1001
- "is_default": payload.rule_is_default,
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
- if not payload.rule_id:
1009
- return text("❌ Provide rule_id to remove.")
1010
- await self._service.rule_delete(payload.rule_id)
1011
- return text(f"🗑️ Rule {payload.rule_id} removed.")
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 _format_mode(
1365
- mode: Dict[str, Any],
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"🎭 **{mode.get('name', 'Unnamed')}**",
1377
- f"ID: {mode.get('id', 'N/A')}",
1378
- f"Default: {mode.get('is_default', False)}",
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 mode.get("description"):
1382
- lines.append(f"Description: {mode['description']}")
1383
- if show_content and mode.get("content"):
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(mode.get("content"))
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"📋 **{rule.get('name', 'Unnamed')}**",
1521
+ f"{scope_emoji} **{name}**",
1403
1522
  f"ID: {rule.get('id', 'N/A')}",
1404
- f"Default: {rule.get('is_default', False)}",
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['description']}")
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 _format_doc(
1417
- doc: Dict[str, Any],
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
- if header:
1424
- lines.append(header)
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
- lines.extend(
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
- # Show content only when explicitly requested (e.g., doc_get)
1440
- if show_content and doc.get("content"):
1695
+ # Tags
1696
+ tags = api.get("tags", [])
1697
+ if tags:
1441
1698
  lines.append("")
1442
- lines.append("**Content:**")
1443
- lines.append(doc.get("content"))
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