fenix-mcp 0.6.0__py3-none-any.whl → 1.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.
@@ -22,6 +22,8 @@ from fenix_mcp.application.tool_base import (
22
22
  ToolRequest,
23
23
  UUIDStr,
24
24
  VersionStr,
25
+ sanitize_null,
26
+ sanitize_null_list,
25
27
  )
26
28
  from fenix_mcp.domain.knowledge import KnowledgeService, _format_date
27
29
  from fenix_mcp.infrastructure.context import AppContext
@@ -37,90 +39,93 @@ class KnowledgeAction(str, Enum):
37
39
  # Work items
38
40
  WORK_CREATE = (
39
41
  "work_create",
40
- "Cria um work item com título, status e vínculos opcionais.",
42
+ "Creates a work item with title, status and optional links.",
41
43
  )
42
44
  WORK_LIST = (
43
45
  "work_list",
44
- "Lista work items com filtros de status, prioridade e contexto.",
46
+ "Lists work items with status, priority and context filters.",
45
47
  )
46
- WORK_GET = ("work_get", "Obtém detalhes completos de um work item pelo ID.")
48
+ WORK_GET = ("work_get", "Gets full details of a work item by ID.")
47
49
  WORK_UPDATE = (
48
50
  "work_update",
49
- "Atualiza campos específicos de um work item existente.",
51
+ "Updates specific fields of an existing work item.",
50
52
  )
51
- WORK_DELETE = ("work_delete", "Remove um work item definitivamente.")
52
- WORK_BACKLOG = ("work_backlog", "Lista itens do backlog de um time.")
53
- WORK_SEARCH = ("work_search", "Busca work items por texto com filtros adicionais.")
54
- WORK_ANALYTICS = ("work_analytics", "Retorna métricas consolidadas de work items.")
55
- WORK_BY_BOARD = ("work_by_board", "Lista work items associados a um board.")
56
- WORK_BY_SPRINT = ("work_by_sprint", "Lista work items associados a um sprint.")
57
- WORK_BY_EPIC = ("work_by_epic", "Lista work items associados a um épico.")
58
- WORK_CHILDREN = ("work_children", "Lista work items filhos de um item pai.")
53
+ WORK_DELETE = ("work_delete", "Permanently removes a work item.")
54
+ WORK_BACKLOG = ("work_backlog", "Lists backlog items for a team.")
55
+ WORK_SEARCH = (
56
+ "work_search",
57
+ "Searches work items by text with additional filters.",
58
+ )
59
+ WORK_ANALYTICS = ("work_analytics", "Returns consolidated work item metrics.")
60
+ WORK_BY_BOARD = ("work_by_board", "Lists work items associated with a board.")
61
+ WORK_BY_SPRINT = ("work_by_sprint", "Lists work items associated with a sprint.")
62
+ WORK_BY_EPIC = ("work_by_epic", "Lists work items associated with an epic.")
63
+ WORK_CHILDREN = ("work_children", "Lists child work items of a parent item.")
59
64
  WORK_STATUS_UPDATE = (
60
65
  "work_status_update",
61
- "Atualiza apenas o status de um work item.",
66
+ "Updates only the status of a work item.",
62
67
  )
63
68
  WORK_ASSIGN_SPRINT = (
64
69
  "work_assign_sprint",
65
- "Atribui work items a um sprint.",
70
+ "Assigns work items to a sprint.",
66
71
  )
67
72
 
68
73
  # Boards
69
- BOARD_LIST = ("board_list", "Lista boards disponíveis com filtros opcionais.")
70
- BOARD_BY_TEAM = ("board_by_team", "Lista boards de um time específico.")
71
- BOARD_FAVORITES = ("board_favorites", "Lista boards marcados como favoritos.")
72
- BOARD_GET = ("board_get", "Obtém detalhes de um board pelo ID.")
73
- BOARD_COLUMNS = ("board_columns", "Lista colunas configuradas para um board.")
74
+ BOARD_LIST = ("board_list", "Lists available boards with optional filters.")
75
+ BOARD_BY_TEAM = ("board_by_team", "Lists boards for a specific team.")
76
+ BOARD_FAVORITES = ("board_favorites", "Lists boards marked as favorites.")
77
+ BOARD_GET = ("board_get", "Gets board details by ID.")
78
+ BOARD_COLUMNS = ("board_columns", "Lists columns configured for a board.")
74
79
 
75
80
  # Sprints
76
- SPRINT_LIST = ("sprint_list", "Lista sprints disponíveis com filtros opcionais.")
77
- SPRINT_BY_TEAM = ("sprint_by_team", "Lista sprints associados a um time.")
78
- SPRINT_ACTIVE = ("sprint_active", "Obtém o sprint ativo de um time.")
79
- SPRINT_GET = ("sprint_get", "Obtém detalhes de um sprint pelo ID.")
81
+ SPRINT_LIST = ("sprint_list", "Lists available sprints with optional filters.")
82
+ SPRINT_BY_TEAM = ("sprint_by_team", "Lists sprints associated with a team.")
83
+ SPRINT_ACTIVE = ("sprint_active", "Gets the active sprint for a team.")
84
+ SPRINT_GET = ("sprint_get", "Gets sprint details by ID.")
80
85
  SPRINT_WORK_ITEMS = (
81
86
  "sprint_work_items",
82
- "Lista work items vinculados a um sprint.",
87
+ "Lists work items linked to a sprint.",
83
88
  )
84
89
 
85
90
  # Modes
86
- MODE_CREATE = ("mode_create", "Cria um modo com conteúdo e metadados opcionais.")
87
- MODE_LIST = ("mode_list", "Lista modes cadastrados.")
88
- MODE_GET = ("mode_get", "Obtém detalhes completos de um modo.")
89
- MODE_UPDATE = ("mode_update", "Atualiza propriedades de um modo existente.")
90
- MODE_DELETE = ("mode_delete", "Remove um modo.")
91
- MODE_RULE_ADD = ("mode_rule_add", "Associa uma regra a um modo.")
91
+ MODE_CREATE = ("mode_create", "Creates a mode with content and optional metadata.")
92
+ MODE_LIST = ("mode_list", "Lists registered modes.")
93
+ MODE_GET = ("mode_get", "Gets full details of a mode.")
94
+ MODE_UPDATE = ("mode_update", "Updates properties of an existing mode.")
95
+ MODE_DELETE = ("mode_delete", "Removes a mode.")
96
+ MODE_RULE_ADD = ("mode_rule_add", "Associates a rule with a mode.")
92
97
  MODE_RULE_REMOVE = (
93
98
  "mode_rule_remove",
94
- "Remove a associação de uma regra com um modo.",
99
+ "Removes the association of a rule with a mode.",
95
100
  )
96
- MODE_RULES = ("mode_rules", "Lista regras associadas a um modo.")
101
+ MODE_RULES = ("mode_rules", "Lists rules associated with a mode.")
97
102
 
98
103
  # Rules
99
- RULE_CREATE = ("rule_create", "Cria uma regra com conteúdo e metadados.")
100
- RULE_LIST = ("rule_list", "Lista regras cadastradas.")
101
- RULE_GET = ("rule_get", "Obtém detalhes de uma regra.")
102
- RULE_UPDATE = ("rule_update", "Atualiza uma regra existente.")
103
- RULE_DELETE = ("rule_delete", "Remove uma regra.")
104
+ RULE_CREATE = ("rule_create", "Creates a rule with content and metadata.")
105
+ RULE_LIST = ("rule_list", "Lists registered rules.")
106
+ RULE_GET = ("rule_get", "Gets rule details.")
107
+ RULE_UPDATE = ("rule_update", "Updates an existing rule.")
108
+ RULE_DELETE = ("rule_delete", "Removes a rule.")
104
109
 
105
110
  # Documentation
106
- DOC_CREATE = ("doc_create", "Cria um item de documentação.")
107
- DOC_LIST = ("doc_list", "Lista itens de documentação com filtros.")
108
- DOC_GET = ("doc_get", "Obtém detalhes de um item de documentação.")
109
- DOC_UPDATE = ("doc_update", "Atualiza um item de documentação.")
110
- DOC_DELETE = ("doc_delete", "Remove um item de documentação.")
111
- DOC_SEARCH = ("doc_search", "Busca itens de documentação por texto.")
112
- DOC_ROOTS = ("doc_roots", "Lista documentos raiz disponíveis.")
113
- DOC_RECENT = ("doc_recent", "Lista documentos acessados recentemente.")
114
- DOC_ANALYTICS = ("doc_analytics", "Retorna analytics dos documentos.")
115
- DOC_CHILDREN = ("doc_children", "Lista documentos filhos de um item.")
116
- DOC_TREE = ("doc_tree", "Recupera a árvore direta de um documento.")
117
- DOC_FULL_TREE = ("doc_full_tree", "Recupera a árvore completa de documentação.")
118
- DOC_MOVE = ("doc_move", "Move um documento para outro pai.")
119
- DOC_PUBLISH = ("doc_publish", "Altera status de publicação de um documento.")
120
- DOC_VERSION = ("doc_version", "Gera ou recupera versão de um documento.")
121
- DOC_DUPLICATE = ("doc_duplicate", "Duplica um documento existente.")
122
-
123
- HELP = ("knowledge_help", "Mostra as ações disponíveis e seus usos.")
111
+ DOC_CREATE = ("doc_create", "Creates a documentation item.")
112
+ DOC_LIST = ("doc_list", "Lists documentation items with filters.")
113
+ DOC_GET = ("doc_get", "Gets documentation item details.")
114
+ DOC_UPDATE = ("doc_update", "Updates a documentation item.")
115
+ DOC_DELETE = ("doc_delete", "Removes a documentation item.")
116
+ DOC_SEARCH = ("doc_search", "Searches documentation items by text.")
117
+ DOC_ROOTS = ("doc_roots", "Lists available root documents.")
118
+ DOC_RECENT = ("doc_recent", "Lists recently accessed documents.")
119
+ DOC_ANALYTICS = ("doc_analytics", "Returns document analytics.")
120
+ DOC_CHILDREN = ("doc_children", "Lists child documents of an item.")
121
+ DOC_TREE = ("doc_tree", "Retrieves the direct tree of a document.")
122
+ DOC_FULL_TREE = ("doc_full_tree", "Retrieves the full documentation tree.")
123
+ DOC_MOVE = ("doc_move", "Moves a document to another parent.")
124
+ DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
125
+ DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
126
+ DOC_DUPLICATE = ("doc_duplicate", "Duplicates an existing document.")
127
+
128
+ HELP = ("knowledge_help", "Shows available actions and their uses.")
124
129
 
125
130
  @classmethod
126
131
  def choices(cls) -> List[str]:
@@ -129,7 +134,7 @@ class KnowledgeAction(str, Enum):
129
134
  @classmethod
130
135
  def formatted_help(cls) -> str:
131
136
  lines = [
132
- "| **Ação** | **Descrição** |",
137
+ "| **Action** | **Description** |",
133
138
  "| --- | --- |",
134
139
  ]
135
140
  for member in cls:
@@ -137,7 +142,7 @@ class KnowledgeAction(str, Enum):
137
142
  return "\n".join(lines)
138
143
 
139
144
 
140
- ACTION_FIELD_DESCRIPTION = "Ação de conhecimento. Escolha um dos valores: " + ", ".join(
145
+ ACTION_FIELD_DESCRIPTION = "Knowledge action. Choose one of the values: " + ", ".join(
141
146
  f"`{member.value}` ({member.description.rstrip('.')})."
142
147
  for member in KnowledgeAction
143
148
  )
@@ -154,165 +159,158 @@ _ALLOWED_DOC_TYPES = {
154
159
  class KnowledgeRequest(ToolRequest):
155
160
  action: KnowledgeAction = Field(description=ACTION_FIELD_DESCRIPTION)
156
161
  id: Optional[UUIDStr] = Field(
157
- default=None, description="ID principal do recurso (UUID)."
162
+ default=None, description="Primary resource ID (UUID)."
158
163
  )
159
- limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
164
+ limit: int = Field(default=20, ge=1, le=100, description="Result limit.")
160
165
  offset: int = Field(
161
- default=0, ge=0, description="Offset de paginação (quando suportado)."
162
- )
163
- team_id: Optional[UUIDStr] = Field(
164
- default=None, description="ID do time para filtros (UUID)."
166
+ default=0, ge=0, description="Pagination offset (when supported)."
165
167
  )
166
168
  board_id: Optional[UUIDStr] = Field(
167
- default=None, description="ID do board associado (UUID)."
169
+ default=None, description="Associated board ID (UUID)."
168
170
  )
169
171
  sprint_id: Optional[UUIDStr] = Field(
170
- default=None, description="ID do sprint associado (UUID)."
172
+ default=None, description="Associated sprint ID (UUID)."
171
173
  )
172
174
  epic_id: Optional[UUIDStr] = Field(
173
- default=None, description="ID do épico associado (UUID)."
175
+ default=None, description="Associated epic ID (UUID)."
174
176
  )
175
- query: Optional[str] = Field(default=None, description="Filtro/busca.")
177
+ query: Optional[str] = Field(default=None, description="Filter/search term.")
176
178
  return_content: Optional[bool] = Field(
177
- default=None, description="Retorna conteúdo completo."
179
+ default=None, description="Return full content."
178
180
  )
179
181
  return_description: Optional[bool] = Field(
180
- default=None, description="Retorna descrição completa."
182
+ default=None, description="Return full description."
181
183
  )
182
184
  return_metadata: Optional[bool] = Field(
183
- default=None, description="Retorna metadados completos."
185
+ default=None, description="Return full metadata."
184
186
  )
185
187
 
186
188
  # Work item fields
187
- work_title: Optional[TitleStr] = Field(
188
- default=None, description="Título do work item."
189
- )
189
+ work_title: Optional[TitleStr] = Field(default=None, description="Work item title.")
190
190
  work_description: Optional[MarkdownStr] = Field(
191
- default=None, description="Descrição do work item (Markdown)."
191
+ default=None, description="Work item description (Markdown)."
192
192
  )
193
193
  work_type: Optional[str] = Field(
194
194
  default="task",
195
- description="Tipo do work item (epic, feature, story, task, subtask, bug).",
195
+ description="Work item type (epic, feature, story, task, subtask, bug).",
196
196
  )
197
197
  work_status: Optional[str] = Field(
198
198
  default=None,
199
- description="Status do work item (backlog, todo, in_progress, review, testing, done, cancelled).",
199
+ description="Work item status (backlog, todo, in_progress, review, testing, done, cancelled).",
200
200
  )
201
201
  work_priority: Optional[str] = Field(
202
202
  default=None,
203
- description="Prioridade do work item (critical, high, medium, low).",
203
+ description="Work item priority (critical, high, medium, low).",
204
204
  )
205
205
  story_points: Optional[int] = Field(
206
206
  default=None, ge=0, le=100, description="Story points (0-100)."
207
207
  )
208
208
  assignee_id: Optional[UUIDStr] = Field(
209
- default=None, description="ID do responsável (UUID)."
209
+ default=None, description="Assignee ID (UUID)."
210
210
  )
211
211
  parent_id: Optional[UUIDStr] = Field(
212
- default=None, description="ID do item pai (UUID)."
212
+ default=None, description="Parent item ID (UUID)."
213
213
  )
214
214
  work_due_date: Optional[DateTimeStr] = Field(
215
- default=None, description="Data de vencimento do work item (ISO 8601)."
215
+ default=None, description="Work item due date (ISO 8601)."
216
216
  )
217
217
  work_tags: Optional[List[TagStr]] = Field(
218
- default=None, description="Tags do work item."
218
+ default=None, description="Work item tags."
219
219
  )
220
220
  work_item_ids: Optional[List[UUIDStr]] = Field(
221
221
  default=None,
222
- description="Lista de IDs de work items para operações em lote (UUIDs).",
222
+ description="List of work item IDs for batch operations (UUIDs).",
223
223
  )
224
224
 
225
225
  # Mode fields
226
226
  mode_id: Optional[UUIDStr] = Field(
227
- default=None, description="ID do modo relacionado (UUID)."
227
+ default=None, description="Related mode ID (UUID)."
228
228
  )
229
- mode_name: Optional[TitleStr] = Field(default=None, description="Nome do modo.")
229
+ mode_name: Optional[TitleStr] = Field(default=None, description="Mode name.")
230
230
  mode_description: Optional[DescriptionStr] = Field(
231
- default=None, description="Descrição do modo."
231
+ default=None, description="Mode description."
232
232
  )
233
233
  mode_content: Optional[MarkdownStr] = Field(
234
- default=None, description="Conteúdo do modo (Markdown)."
234
+ default=None, description="Mode content (Markdown)."
235
235
  )
236
236
  mode_is_default: Optional[bool] = Field(
237
- default=None, description="Indica se o modo é padrão."
237
+ default=None, description="Indicates if the mode is default."
238
238
  )
239
239
  mode_metadata: Optional[MarkdownStr] = Field(
240
- default=None, description="Metadata do modo para processamento de IA."
240
+ default=None, description="Mode metadata for AI processing."
241
241
  )
242
242
 
243
243
  # Rule fields
244
244
  rule_id: Optional[UUIDStr] = Field(
245
- default=None, description="ID da regra relacionada (UUID)."
245
+ default=None, description="Related rule ID (UUID)."
246
246
  )
247
- rule_name: Optional[TitleStr] = Field(default=None, description="Nome da regra.")
247
+ rule_name: Optional[TitleStr] = Field(default=None, description="Rule name.")
248
248
  rule_description: Optional[DescriptionStr] = Field(
249
- default=None, description="Descrição da regra."
249
+ default=None, description="Rule description."
250
250
  )
251
251
  rule_content: Optional[MarkdownStr] = Field(
252
- default=None, description="Conteúdo da regra (Markdown)."
252
+ default=None, description="Rule content (Markdown)."
253
253
  )
254
- rule_is_default: Optional[bool] = Field(default=None, description="Regra padrão.")
254
+ rule_is_default: Optional[bool] = Field(default=None, description="Default rule.")
255
255
  rule_metadata: Optional[MarkdownStr] = Field(
256
- default=None, description="Metadata da regra para processamento de IA."
256
+ default=None, description="Rule metadata for AI processing."
257
257
  )
258
258
 
259
259
  # Documentation fields
260
260
  doc_title: Optional[TitleStr] = Field(
261
- default=None, description="Título da documentação."
261
+ default=None, description="Documentation title."
262
262
  )
263
263
  doc_description: Optional[DescriptionStr] = Field(
264
- default=None, description="Descrição da documentação."
264
+ default=None, description="Documentation description."
265
265
  )
266
266
  doc_content: Optional[MarkdownStr] = Field(
267
- default=None, description="Conteúdo da documentação (Markdown)."
267
+ default=None, description="Documentation content (Markdown)."
268
268
  )
269
269
  doc_status: Optional[str] = Field(
270
270
  default=None,
271
- description="Status da documentação (draft, review, published, archived).",
271
+ description="Documentation status (draft, review, published, archived).",
272
272
  )
273
273
  doc_type: Optional[str] = Field(
274
- default=None, description="Tipo da documentação (folder, page, api_doc, guide)."
274
+ default=None,
275
+ description="Documentation type. Values: folder, page, api_doc, guide.",
275
276
  )
276
277
  doc_language: Optional[LanguageStr] = Field(
277
- default=None, description="Idioma da documentação (ex: pt, en, pt-BR)."
278
+ default=None, description="Documentation language (e.g.: pt, en, pt-BR)."
278
279
  )
279
280
  doc_parent_id: Optional[UUIDStr] = Field(
280
- default=None, description="ID do documento pai (UUID)."
281
- )
282
- doc_team_id: Optional[UUIDStr] = Field(
283
- default=None, description="ID do time responsável pela documentação (UUID)."
281
+ default=None, description="Parent document ID (UUID)."
284
282
  )
285
283
  doc_owner_id: Optional[UUIDStr] = Field(
286
- default=None, description="ID do dono (UUID)."
284
+ default=None, description="Owner ID (UUID)."
287
285
  )
288
286
  doc_reviewer_id: Optional[UUIDStr] = Field(
289
- default=None, description="ID do revisor (UUID)."
287
+ default=None, description="Reviewer ID (UUID)."
290
288
  )
291
289
  doc_version: Optional[VersionStr] = Field(
292
- default=None, description="Versão do documento (ex: 1.0, 2.1.3)."
290
+ default=None, description="Document version (e.g.: 1.0, 2.1.3)."
293
291
  )
294
- doc_category: Optional[CategoryStr] = Field(default=None, description="Categoria.")
292
+ doc_category: Optional[CategoryStr] = Field(default=None, description="Category.")
295
293
  doc_tags: Optional[List[TagStr]] = Field(default=None, description="Tags.")
296
294
  doc_position: Optional[int] = Field(
297
- default=None, ge=0, description="Posição desejada ao mover documentos."
295
+ default=None, ge=0, description="Desired position when moving documents."
298
296
  )
299
297
  doc_emoji: Optional[EmojiStr] = Field(
300
- default=None, description="Emoji exibido junto ao documento."
298
+ default=None, description="Emoji displayed with the document."
301
299
  )
302
300
  doc_emote: Optional[EmojiStr] = Field(
303
- default=None, description="Alias para emoji, mantido por compatibilidade."
301
+ default=None, description="Alias for emoji, kept for compatibility."
304
302
  )
305
303
  doc_keywords: Optional[List[TagStr]] = Field(
306
- default=None, description="Palavras-chave para busca."
304
+ default=None, description="Keywords for search."
307
305
  )
308
306
  doc_is_public: Optional[bool] = Field(
309
- default=None, description="Se o documento é público."
307
+ default=None, description="Whether the document is public."
310
308
  )
311
309
 
312
310
 
313
311
  class KnowledgeTool(Tool):
314
312
  name = "knowledge"
315
- description = "Operações de conhecimento do Fênix Cloud (Work Items, Boards, Sprints, Modes, Rules, Docs)."
313
+ description = "Fenix Cloud knowledge operations (Work Items, Boards, Sprints, Modes, Rules, Docs)."
316
314
  request_model = KnowledgeRequest
317
315
 
318
316
  def __init__(self, context: AppContext):
@@ -336,7 +334,7 @@ class KnowledgeTool(Tool):
336
334
  if action.value.startswith("doc_"):
337
335
  return await self._run_doc(payload)
338
336
  return text(
339
- "❌ Ação inválida para knowledge.\n\nEscolha um dos valores:\n"
337
+ "❌ Invalid action for knowledge.\n\nChoose one of the values:\n"
340
338
  + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices())
341
339
  )
342
340
 
@@ -347,9 +345,7 @@ class KnowledgeTool(Tool):
347
345
  action = payload.action
348
346
  if action is KnowledgeAction.WORK_CREATE:
349
347
  if not payload.work_title:
350
- return text("❌ Informe work_title para criar o item.")
351
- if not payload.team_id:
352
- return text("❌ Informe team_id para criar o item.")
348
+ return text("❌ Provide work_title to create the item.")
353
349
  work = await self._service.work_create(
354
350
  {
355
351
  "title": payload.work_title,
@@ -362,12 +358,11 @@ class KnowledgeTool(Tool):
362
358
  "sprint_id": payload.sprint_id,
363
359
  "board_id": payload.board_id,
364
360
  "parent_id": payload.parent_id,
365
- "team_id": payload.team_id,
366
361
  "due_date": payload.work_due_date,
367
362
  "tags": payload.work_tags,
368
363
  }
369
364
  )
370
- return text(_format_work(work, header="✅ Work item criado"))
365
+ return text(_format_work(work, header="✅ Work item created"))
371
366
 
372
367
  if action is KnowledgeAction.WORK_LIST:
373
368
  items = await self._service.work_list(
@@ -380,19 +375,19 @@ class KnowledgeTool(Tool):
380
375
  board=payload.board_id,
381
376
  )
382
377
  if not items:
383
- return text("🎯 Nenhum work item encontrado.")
378
+ return text("🎯 No work items found.")
384
379
  body = "\n\n".join(_format_work(item) for item in items)
385
380
  return text(f"🎯 **Work items ({len(items)}):**\n\n{body}")
386
381
 
387
382
  if action is KnowledgeAction.WORK_GET:
388
383
  if not payload.id:
389
- return text("❌ Informe o ID do work item.")
384
+ return text("❌ Provide the work item ID.")
390
385
  work = await self._service.work_get(payload.id)
391
- return text(_format_work(work, header="🎯 Detalhes do work item"))
386
+ return text(_format_work(work, header="🎯 Work item details"))
392
387
 
393
388
  if action is KnowledgeAction.WORK_UPDATE:
394
389
  if not payload.id:
395
- return text("❌ Informe o ID do work item.")
390
+ return text("❌ Provide the work item ID.")
396
391
  work = await self._service.work_update(
397
392
  payload.id,
398
393
  {
@@ -406,102 +401,97 @@ class KnowledgeTool(Tool):
406
401
  "sprint_id": payload.sprint_id,
407
402
  "board_id": payload.board_id,
408
403
  "parent_id": payload.parent_id,
409
- "team_id": payload.team_id,
410
404
  "due_date": payload.work_due_date,
411
405
  "tags": payload.work_tags,
412
406
  },
413
407
  )
414
- return text(_format_work(work, header="✅ Work item atualizado"))
408
+ return text(_format_work(work, header="✅ Work item updated"))
415
409
 
416
410
  if action is KnowledgeAction.WORK_DELETE:
417
411
  if not payload.id:
418
- return text("❌ Informe o ID do work item.")
412
+ return text("❌ Provide the work item ID.")
419
413
  await self._service.work_delete(payload.id)
420
- return text(f"🗑️ Work item {payload.id} removido.")
414
+ return text(f"🗑️ Work item {payload.id} removed.")
421
415
 
422
416
  if action is KnowledgeAction.WORK_BACKLOG:
423
- if not payload.team_id:
424
- return text("❌ Informe team_id para consultar o backlog.")
425
- items = await self._service.work_backlog(team_id=payload.team_id)
417
+ items = await self._service.work_backlog()
426
418
  if not items:
427
- return text("📋 Backlog vazio para o time informado.")
419
+ return text("📋 Backlog empty.")
428
420
  body = "\n\n".join(_format_work(item) for item in items)
429
421
  return text(f"📋 **Backlog ({len(items)}):**\n\n{body}")
430
422
 
431
423
  if action is KnowledgeAction.WORK_SEARCH:
432
- if not payload.query or not payload.team_id:
433
- return text("❌ Informe query e team_id para buscar work items.")
424
+ query = sanitize_null(payload.query)
425
+ if not query:
426
+ return text("❌ Provide query to search work items.")
434
427
  items = await self._service.work_search(
435
- query=payload.query,
436
- team_id=payload.team_id,
428
+ query=query,
437
429
  limit=payload.limit,
438
430
  )
439
431
  if not items:
440
- return text("🔍 Nenhum work item encontrado.")
432
+ return text("🔍 No work items found.")
441
433
  body = "\n\n".join(_format_work(item) for item in items)
442
- return text(f"🔍 **Resultados ({len(items)}):**\n\n{body}")
434
+ return text(f"🔍 **Results ({len(items)}):**\n\n{body}")
443
435
 
444
436
  if action is KnowledgeAction.WORK_ANALYTICS:
445
- if not payload.team_id:
446
- return text(" Informe team_id para obter analytics.")
447
- analytics = await self._service.work_analytics(team_id=payload.team_id)
448
- lines = ["📊 **Analytics de Work Items**"]
437
+ analytics = await self._service.work_analytics()
438
+ lines = ["📊 **Work Items Analytics**"]
449
439
  for key, value in analytics.items():
450
440
  lines.append(f"- {key}: {value}")
451
441
  return text("\n".join(lines))
452
442
 
453
443
  if action is KnowledgeAction.WORK_BY_BOARD:
454
444
  if not payload.board_id:
455
- return text("❌ Informe board_id para listar os itens.")
445
+ return text("❌ Provide board_id to list items.")
456
446
  items = await self._service.work_by_board(board_id=payload.board_id)
457
447
  if not items:
458
- return text("🗂️ Nenhum work item para o board informado.")
448
+ return text("🗂️ No work items for the specified board.")
459
449
  body = "\n\n".join(_format_work(item) for item in items)
460
- return text(f"🗂️ **Itens do board ({len(items)}):**\n\n{body}")
450
+ return text(f"🗂️ **Board items ({len(items)}):**\n\n{body}")
461
451
 
462
452
  if action is KnowledgeAction.WORK_BY_SPRINT:
463
453
  if not payload.sprint_id:
464
- return text("❌ Informe sprint_id para listar os itens.")
454
+ return text("❌ Provide sprint_id to list items.")
465
455
  items = await self._service.work_by_sprint(sprint_id=payload.sprint_id)
466
456
  if not items:
467
- return text("🏃 Nenhum item vinculado ao sprint informado.")
457
+ return text("🏃 No items linked to the specified sprint.")
468
458
  body = "\n\n".join(_format_work(item) for item in items)
469
- return text(f"🏃 **Work items do sprint ({len(items)}):**\n\n{body}")
459
+ return text(f"🏃 **Sprint work items ({len(items)}):**\n\n{body}")
470
460
 
471
461
  if action is KnowledgeAction.WORK_BY_EPIC:
472
462
  if not payload.epic_id:
473
- return text("❌ Informe epic_id para listar os itens.")
463
+ return text("❌ Provide epic_id to list items.")
474
464
  items = await self._service.work_by_epic(epic_id=payload.epic_id)
475
465
  if not items:
476
- return text("📦 Nenhum item vinculado ao épico informado.")
466
+ return text("📦 No items linked to the specified epic.")
477
467
  body = "\n\n".join(_format_work(item) for item in items)
478
- return text(f"📦 **Work items do épico ({len(items)}):**\n\n{body}")
468
+ return text(f"📦 **Epic work items ({len(items)}):**\n\n{body}")
479
469
 
480
470
  if action is KnowledgeAction.WORK_CHILDREN:
481
471
  if not payload.id:
482
- return text("❌ Informe o ID do work item pai.")
472
+ return text("❌ Provide the parent work item ID.")
483
473
  items = await self._service.work_children(payload.id)
484
474
  if not items:
485
- return text("👶 Nenhum item filho encontrado.")
475
+ return text("👶 No child items found.")
486
476
  body = "\n\n".join(_format_work(item) for item in items)
487
- return text(f"👶 **Work items filhos ({len(items)}):**\n\n{body}")
477
+ return text(f"👶 **Child work items ({len(items)}):**\n\n{body}")
488
478
 
489
479
  if action is KnowledgeAction.WORK_STATUS_UPDATE:
490
480
  if not payload.id:
491
- return text("❌ Informe o ID do work item.")
481
+ return text("❌ Provide the work item ID.")
492
482
  if not payload.work_status:
493
- return text("❌ Informe work_status para atualizar.")
483
+ return text("❌ Provide work_status to update.")
494
484
  work = await self._service.work_update_status(
495
485
  payload.id,
496
486
  {"status": payload.work_status},
497
487
  )
498
- return text(_format_work(work, header="✅ Status atualizado"))
488
+ return text(_format_work(work, header="✅ Status updated"))
499
489
 
500
490
  if action is KnowledgeAction.WORK_ASSIGN_SPRINT:
501
491
  if not payload.sprint_id:
502
- return text("❌ Informe sprint_id para atribuir os itens.")
492
+ return text("❌ Provide sprint_id to assign items.")
503
493
  if not payload.work_item_ids:
504
- return text("❌ Informe work_item_ids com a lista de IDs.")
494
+ return text("❌ Provide work_item_ids with the list of IDs.")
505
495
  await self._service.work_assign_to_sprint(
506
496
  {
507
497
  "sprint_id": payload.sprint_id,
@@ -509,10 +499,10 @@ class KnowledgeTool(Tool):
509
499
  }
510
500
  )
511
501
  count = len(payload.work_item_ids)
512
- return text(f"✅ {count} work item(s) atribuído(s) ao sprint.")
502
+ return text(f"✅ {count} work item(s) assigned to sprint.")
513
503
 
514
504
  return text(
515
- "❌ Ação de work item não suportada.\n\nEscolha um dos valores:\n"
505
+ "❌ Unsupported work item action.\n\nChoose one of the values:\n"
516
506
  + "\n".join(
517
507
  f"- `{value}`"
518
508
  for value in KnowledgeAction.choices()
@@ -530,46 +520,44 @@ class KnowledgeTool(Tool):
530
520
  limit=payload.limit, offset=payload.offset
531
521
  )
532
522
  if not boards:
533
- return text("🗂️ Nenhum board encontrado.")
523
+ return text("🗂️ No boards found.")
534
524
  body = "\n\n".join(_format_board(board) for board in boards)
535
525
  return text(f"🗂️ **Boards ({len(boards)}):**\n\n{body}")
536
526
 
537
527
  if action is KnowledgeAction.BOARD_BY_TEAM:
538
- if not payload.team_id:
539
- return text("❌ Informe team_id para listar boards do time.")
540
- boards = await self._service.board_list_by_team(payload.team_id)
528
+ boards = await self._service.board_list_by_team()
541
529
  if not boards:
542
- return text("🗂️ Nenhum board cadastrado para o time.")
530
+ return text("🗂️ No boards registered for the team.")
543
531
  body = "\n\n".join(_format_board(board) for board in boards)
544
- return text(f"🗂️ **Boards do time ({len(boards)}):**\n\n{body}")
532
+ return text(f"🗂️ **Team boards ({len(boards)}):**\n\n{body}")
545
533
 
546
534
  if action is KnowledgeAction.BOARD_FAVORITES:
547
535
  boards = await self._service.board_favorites()
548
536
  if not boards:
549
- return text("⭐ Nenhum board favorito cadastrado.")
537
+ return text("⭐ No favorite boards registered.")
550
538
  body = "\n\n".join(_format_board(board) for board in boards)
551
- return text(f"⭐ **Boards favoritos ({len(boards)}):**\n\n{body}")
539
+ return text(f"⭐ **Favorite boards ({len(boards)}):**\n\n{body}")
552
540
 
553
541
  if action is KnowledgeAction.BOARD_GET:
554
542
  if not payload.board_id:
555
- return text("❌ Informe board_id para consultar detalhes.")
543
+ return text("❌ Provide board_id to get details.")
556
544
  board = await self._service.board_get(payload.board_id)
557
- return text(_format_board(board, header="🗂️ Detalhes do board"))
545
+ return text(_format_board(board, header="🗂️ Board details"))
558
546
 
559
547
  if action is KnowledgeAction.BOARD_COLUMNS:
560
548
  if not payload.board_id:
561
- return text("❌ Informe board_id para listar colunas.")
549
+ return text("❌ Provide board_id to list columns.")
562
550
  columns = await self._service.board_columns(payload.board_id)
563
551
  if not columns:
564
- return text("📊 Board sem colunas cadastradas.")
552
+ return text("📊 Board has no columns registered.")
565
553
  body = "\n".join(
566
- f"- {col.get('name', 'Sem nome')} (ID: {col.get('id')})"
554
+ f"- {col.get('name', 'Unnamed')} (ID: {col.get('id')})"
567
555
  for col in columns
568
556
  )
569
- return text(f"📊 **Colunas do board:**\n{body}")
557
+ return text(f"📊 **Board columns:**\n{body}")
570
558
 
571
559
  return text(
572
- "❌ Ação de board não suportada.\n\nEscolha um dos valores:\n"
560
+ "❌ Unsupported board action.\n\nChoose one of the values:\n"
573
561
  + "\n".join(
574
562
  f"- `{value}`"
575
563
  for value in KnowledgeAction.choices()
@@ -587,44 +575,40 @@ class KnowledgeTool(Tool):
587
575
  limit=payload.limit, offset=payload.offset
588
576
  )
589
577
  if not sprints:
590
- return text("🏃 Nenhum sprint encontrado.")
578
+ return text("🏃 No sprints found.")
591
579
  body = "\n\n".join(_format_sprint(sprint) for sprint in sprints)
592
580
  return text(f"🏃 **Sprints ({len(sprints)}):**\n\n{body}")
593
581
 
594
582
  if action is KnowledgeAction.SPRINT_BY_TEAM:
595
- if not payload.team_id:
596
- return text("❌ Informe team_id para listar sprints do time.")
597
- sprints = await self._service.sprint_list_by_team(payload.team_id)
583
+ sprints = await self._service.sprint_list_by_team()
598
584
  if not sprints:
599
- return text("🏃 Nenhum sprint cadastrado para o time.")
585
+ return text("🏃 No sprints registered for the team.")
600
586
  body = "\n\n".join(_format_sprint(sprint) for sprint in sprints)
601
- return text(f"🏃 **Sprints do time ({len(sprints)}):**\n\n{body}")
587
+ return text(f"🏃 **Team sprints ({len(sprints)}):**\n\n{body}")
602
588
 
603
589
  if action is KnowledgeAction.SPRINT_ACTIVE:
604
- if not payload.team_id:
605
- return text("❌ Informe team_id para consultar o sprint ativo.")
606
- sprint = await self._service.sprint_active(payload.team_id)
590
+ sprint = await self._service.sprint_active()
607
591
  if not sprint:
608
- return text("⏳ Nenhum sprint ativo no momento.")
609
- return text(_format_sprint(sprint, header="⏳ Sprint ativo"))
592
+ return text("⏳ No active sprint at the moment.")
593
+ return text(_format_sprint(sprint, header="⏳ Active sprint"))
610
594
 
611
595
  if action is KnowledgeAction.SPRINT_GET:
612
596
  if not payload.sprint_id:
613
- return text("❌ Informe sprint_id para consultar detalhes.")
597
+ return text("❌ Provide sprint_id to get details.")
614
598
  sprint = await self._service.sprint_get(payload.sprint_id)
615
- return text(_format_sprint(sprint, header="🏃 Detalhes do sprint"))
599
+ return text(_format_sprint(sprint, header="🏃 Sprint details"))
616
600
 
617
601
  if action is KnowledgeAction.SPRINT_WORK_ITEMS:
618
602
  if not payload.sprint_id:
619
- return text("❌ Informe sprint_id para listar os itens.")
603
+ return text("❌ Provide sprint_id to list items.")
620
604
  items = await self._service.sprint_work_items(payload.sprint_id)
621
605
  if not items:
622
- return text("🏃 Nenhum item vinculado ao sprint informado.")
606
+ return text("🏃 No items linked to the specified sprint.")
623
607
  body = "\n\n".join(_format_work(item) for item in items)
624
- return text(f"🏃 **Itens do sprint ({len(items)}):**\n\n{body}")
608
+ return text(f"🏃 **Sprint items ({len(items)}):**\n\n{body}")
625
609
 
626
610
  return text(
627
- "❌ Ação de sprint não suportada.\n\nEscolha um dos valores:\n"
611
+ "❌ Unsupported sprint action.\n\nChoose one of the values:\n"
628
612
  + "\n".join(
629
613
  f"- `{value}`"
630
614
  for value in KnowledgeAction.choices()
@@ -639,7 +623,7 @@ class KnowledgeTool(Tool):
639
623
  action = payload.action
640
624
  if action is KnowledgeAction.MODE_CREATE:
641
625
  if not payload.mode_name:
642
- return text("❌ Informe mode_name para criar o modo.")
626
+ return text("❌ Provide mode_name to create the mode.")
643
627
  mode = await self._service.mode_create(
644
628
  {
645
629
  "name": payload.mode_name,
@@ -649,7 +633,7 @@ class KnowledgeTool(Tool):
649
633
  "metadata": payload.mode_metadata,
650
634
  }
651
635
  )
652
- return text(_format_mode(mode, header="✅ Modo criado"))
636
+ return text(_format_mode(mode, header="✅ Mode created"))
653
637
 
654
638
  if action is KnowledgeAction.MODE_LIST:
655
639
  modes = await self._service.mode_list(
@@ -658,23 +642,23 @@ class KnowledgeTool(Tool):
658
642
  return_metadata=payload.return_metadata,
659
643
  )
660
644
  if not modes:
661
- return text("🎭 Nenhum modo encontrado.")
645
+ return text("🎭 No modes found.")
662
646
  body = "\n\n".join(_format_mode(mode) for mode in modes)
663
647
  return text(f"🎭 **Modes ({len(modes)}):**\n\n{body}")
664
648
 
665
649
  if action is KnowledgeAction.MODE_GET:
666
650
  if not payload.mode_id:
667
- return text("❌ Informe mode_id para consultar detalhes.")
651
+ return text("❌ Provide mode_id to get details.")
668
652
  mode = await self._service.mode_get(
669
653
  payload.mode_id,
670
654
  return_description=payload.return_description,
671
655
  return_metadata=payload.return_metadata,
672
656
  )
673
- return text(_format_mode(mode, header="🎭 Detalhes do modo"))
657
+ return text(_format_mode(mode, header="🎭 Mode details"))
674
658
 
675
659
  if action is KnowledgeAction.MODE_UPDATE:
676
660
  if not payload.mode_id:
677
- return text("❌ Informe mode_id para atualizar.")
661
+ return text("❌ Provide mode_id to update.")
678
662
  mode = await self._service.mode_update(
679
663
  payload.mode_id,
680
664
  {
@@ -685,53 +669,53 @@ class KnowledgeTool(Tool):
685
669
  "metadata": payload.mode_metadata,
686
670
  },
687
671
  )
688
- return text(_format_mode(mode, header="✅ Modo atualizado"))
672
+ return text(_format_mode(mode, header="✅ Mode updated"))
689
673
 
690
674
  if action is KnowledgeAction.MODE_DELETE:
691
675
  if not payload.mode_id:
692
- return text("❌ Informe mode_id para remover.")
676
+ return text("❌ Provide mode_id to remove.")
693
677
  await self._service.mode_delete(payload.mode_id)
694
- return text(f"🗑️ Modo {payload.mode_id} removido.")
678
+ return text(f"🗑️ Mode {payload.mode_id} removed.")
695
679
 
696
680
  if action is KnowledgeAction.MODE_RULE_ADD:
697
681
  if not payload.mode_id or not payload.rule_id:
698
- return text("❌ Informe mode_id e rule_id para associar.")
682
+ return text("❌ Provide mode_id and rule_id to associate.")
699
683
  link = await self._service.mode_rule_add(payload.mode_id, payload.rule_id)
700
684
  return text(
701
685
  "\n".join(
702
686
  [
703
- "🔗 **Regra associada ao modo!**",
704
- f"Modo: {link.get('modeId', payload.mode_id)}",
705
- f"Regra: {link.get('ruleId', payload.rule_id)}",
687
+ "🔗 **Rule associated with mode!**",
688
+ f"Mode: {link.get('modeId', payload.mode_id)}",
689
+ f"Rule: {link.get('ruleId', payload.rule_id)}",
706
690
  ]
707
691
  )
708
692
  )
709
693
 
710
694
  if action is KnowledgeAction.MODE_RULE_REMOVE:
711
695
  if not payload.mode_id or not payload.rule_id:
712
- return text("❌ Informe mode_id e rule_id para remover a associação.")
696
+ return text("❌ Provide mode_id and rule_id to remove the association.")
713
697
  await self._service.mode_rule_remove(payload.mode_id, payload.rule_id)
714
- return text("🔗 Associação removida.")
698
+ return text("🔗 Association removed.")
715
699
 
716
700
  if action is KnowledgeAction.MODE_RULES:
717
701
  if payload.mode_id:
718
702
  rules = await self._service.mode_rules(payload.mode_id)
719
- context_label = f"modo {payload.mode_id}"
703
+ context_label = f"mode {payload.mode_id}"
720
704
  elif payload.rule_id:
721
705
  rules = await self._service.mode_rules_for_rule(payload.rule_id)
722
- context_label = f"regra {payload.rule_id}"
706
+ context_label = f"rule {payload.rule_id}"
723
707
  else:
724
- return text("❌ Informe mode_id ou rule_id para listar associações.")
708
+ return text("❌ Provide mode_id or rule_id to list associations.")
725
709
  if not rules:
726
- return text("🔗 Nenhuma associação encontrada.")
710
+ return text("🔗 No associations found.")
727
711
  body = "\n".join(
728
- f"- {item.get('name', 'Sem nome')} (ID: {item.get('id')})"
712
+ f"- {item.get('name', 'Unnamed')} (ID: {item.get('id')})"
729
713
  for item in rules
730
714
  )
731
- return text(f"🔗 **Associações para {context_label}:**\n{body}")
715
+ return text(f"🔗 **Associations for {context_label}:**\n{body}")
732
716
 
733
717
  return text(
734
- "❌ Ação de modo não suportada.\n\nEscolha um dos valores:\n"
718
+ "❌ Unsupported mode action.\n\nChoose one of the values:\n"
735
719
  + "\n".join(
736
720
  f"- `{value}`"
737
721
  for value in KnowledgeAction.choices()
@@ -746,7 +730,7 @@ class KnowledgeTool(Tool):
746
730
  action = payload.action
747
731
  if action is KnowledgeAction.RULE_CREATE:
748
732
  if not payload.rule_name or not payload.rule_content:
749
- return text("❌ Informe rule_name e rule_content.")
733
+ return text("❌ Provide rule_name and rule_content.")
750
734
  rule = await self._service.rule_create(
751
735
  {
752
736
  "name": payload.rule_name,
@@ -756,7 +740,7 @@ class KnowledgeTool(Tool):
756
740
  "metadata": payload.rule_metadata,
757
741
  }
758
742
  )
759
- return text(_format_rule(rule, header="✅ Regra criada"))
743
+ return text(_format_rule(rule, header="✅ Rule created"))
760
744
 
761
745
  if action is KnowledgeAction.RULE_LIST:
762
746
  rules = await self._service.rule_list(
@@ -765,24 +749,24 @@ class KnowledgeTool(Tool):
765
749
  return_modes=payload.return_metadata,
766
750
  )
767
751
  if not rules:
768
- return text("📋 Nenhuma regra encontrada.")
752
+ return text("📋 No rules found.")
769
753
  body = "\n\n".join(_format_rule(rule) for rule in rules)
770
- return text(f"📋 **Regras ({len(rules)}):**\n\n{body}")
754
+ return text(f"📋 **Rules ({len(rules)}):**\n\n{body}")
771
755
 
772
756
  if action is KnowledgeAction.RULE_GET:
773
757
  if not payload.rule_id:
774
- return text("❌ Informe rule_id para consultar detalhes.")
758
+ return text("❌ Provide rule_id to get details.")
775
759
  rule = await self._service.rule_get(
776
760
  payload.rule_id,
777
761
  return_description=payload.return_description,
778
762
  return_metadata=payload.return_metadata,
779
763
  return_modes=payload.return_metadata,
780
764
  )
781
- return text(_format_rule(rule, header="📋 Detalhes da regra"))
765
+ return text(_format_rule(rule, header="📋 Rule details"))
782
766
 
783
767
  if action is KnowledgeAction.RULE_UPDATE:
784
768
  if not payload.rule_id:
785
- return text("❌ Informe rule_id para atualizar.")
769
+ return text("❌ Provide rule_id to update.")
786
770
  rule = await self._service.rule_update(
787
771
  payload.rule_id,
788
772
  {
@@ -793,16 +777,16 @@ class KnowledgeTool(Tool):
793
777
  "metadata": payload.rule_metadata,
794
778
  },
795
779
  )
796
- return text(_format_rule(rule, header="✅ Regra atualizada"))
780
+ return text(_format_rule(rule, header="✅ Rule updated"))
797
781
 
798
782
  if action is KnowledgeAction.RULE_DELETE:
799
783
  if not payload.rule_id:
800
- return text("❌ Informe rule_id para remover.")
784
+ return text("❌ Provide rule_id to remove.")
801
785
  await self._service.rule_delete(payload.rule_id)
802
- return text(f"🗑️ Regra {payload.rule_id} removida.")
786
+ return text(f"🗑️ Rule {payload.rule_id} removed.")
803
787
 
804
788
  return text(
805
- "❌ Ação de regra não suportada.\n\nEscolha um dos valores:\n"
789
+ "❌ Unsupported rule action.\n\nChoose one of the values:\n"
806
790
  + "\n".join(
807
791
  f"- `{value}`"
808
792
  for value in KnowledgeAction.choices()
@@ -817,33 +801,32 @@ class KnowledgeTool(Tool):
817
801
  action = payload.action
818
802
  if action is KnowledgeAction.DOC_CREATE:
819
803
  if not payload.doc_title:
820
- return text("❌ Informe doc_title para criar a documentação.")
804
+ return text("❌ Provide doc_title to create the documentation.")
821
805
  if payload.doc_type and payload.doc_type not in _ALLOWED_DOC_TYPES:
822
806
  allowed = ", ".join(sorted(_ALLOWED_DOC_TYPES))
823
807
  return text(
824
- "❌ doc_type inválido. Use um dos valores suportados: " + allowed
808
+ "❌ Invalid doc_type. Use one of the supported values: " + allowed
825
809
  )
826
810
  doc = await self._service.doc_create(
827
811
  {
828
812
  "title": payload.doc_title,
829
- "description": payload.doc_description,
830
- "content": payload.doc_content,
831
- "status": payload.doc_status,
832
- "doc_type": payload.doc_type,
833
- "language": payload.doc_language,
834
- "parent_id": payload.doc_parent_id,
835
- "team_id": payload.doc_team_id or payload.team_id,
836
- "owner_user_id": payload.doc_owner_id,
837
- "reviewer_user_id": payload.doc_reviewer_id,
838
- "version": payload.doc_version,
839
- "category": payload.doc_category,
840
- "tags": payload.doc_tags,
841
- "emoji": payload.doc_emoji or payload.doc_emote,
842
- "keywords": payload.doc_keywords,
813
+ "description": sanitize_null(payload.doc_description),
814
+ "content": sanitize_null(payload.doc_content),
815
+ "status": sanitize_null(payload.doc_status),
816
+ "doc_type": sanitize_null(payload.doc_type),
817
+ "language": sanitize_null(payload.doc_language),
818
+ "parent_id": sanitize_null(payload.doc_parent_id),
819
+ "owner_user_id": sanitize_null(payload.doc_owner_id),
820
+ "reviewer_user_id": sanitize_null(payload.doc_reviewer_id),
821
+ "version": sanitize_null(payload.doc_version),
822
+ "category": sanitize_null(payload.doc_category),
823
+ "tags": sanitize_null_list(payload.doc_tags),
824
+ "emoji": sanitize_null(payload.doc_emoji or payload.doc_emote),
825
+ "keywords": sanitize_null_list(payload.doc_keywords),
843
826
  "is_public": payload.doc_is_public,
844
827
  }
845
828
  )
846
- return text(_format_doc(doc, header="✅ Documentação criada"))
829
+ return text(_format_doc(doc, header="✅ Documentation created"))
847
830
 
848
831
  if action is KnowledgeAction.DOC_LIST:
849
832
  docs = await self._service.doc_list(
@@ -852,156 +835,140 @@ class KnowledgeTool(Tool):
852
835
  returnContent=payload.return_content,
853
836
  )
854
837
  if not docs:
855
- return text("📄 Nenhuma documentação encontrada.")
838
+ return text("📄 No documentation found.")
856
839
  body = "\n\n".join(_format_doc(doc) for doc in docs)
857
- return text(f"📄 **Documentos ({len(docs)}):**\n\n{body}")
840
+ return text(f"📄 **Documents ({len(docs)}):**\n\n{body}")
858
841
 
859
842
  if action is KnowledgeAction.DOC_GET:
860
843
  if not payload.id:
861
- return text("❌ Informe o ID da documentação.")
844
+ return text("❌ Provide the documentation ID.")
862
845
  doc = await self._service.doc_get(
863
846
  payload.id,
864
847
  returnContent=payload.return_content,
865
848
  )
866
- return text(_format_doc(doc, header="📄 Detalhes da documentação"))
849
+ return text(_format_doc(doc, header="📄 Documentation details"))
867
850
 
868
851
  if action is KnowledgeAction.DOC_UPDATE:
869
852
  if not payload.id:
870
- return text("❌ Informe o ID da documentação.")
853
+ return text("❌ Provide the documentation ID.")
871
854
  if payload.doc_type and payload.doc_type not in _ALLOWED_DOC_TYPES:
872
855
  allowed = ", ".join(sorted(_ALLOWED_DOC_TYPES))
873
856
  return text(
874
- "❌ doc_type inválido. Use um dos valores suportados: " + allowed
857
+ "❌ Invalid doc_type. Use one of the supported values: " + allowed
875
858
  )
876
859
  doc = await self._service.doc_update(
877
860
  payload.id,
878
861
  {
879
- "title": payload.doc_title,
880
- "description": payload.doc_description,
881
- "content": payload.doc_content,
882
- "status": payload.doc_status,
883
- "doc_type": payload.doc_type,
884
- "language": payload.doc_language,
885
- "parent_id": payload.doc_parent_id,
886
- "team_id": payload.doc_team_id or payload.team_id,
887
- "owner_user_id": payload.doc_owner_id,
888
- "reviewer_user_id": payload.doc_reviewer_id,
889
- "version": payload.doc_version,
890
- "category": payload.doc_category,
891
- "tags": payload.doc_tags,
892
- "emoji": payload.doc_emoji or payload.doc_emote,
893
- "keywords": payload.doc_keywords,
862
+ "title": sanitize_null(payload.doc_title),
863
+ "description": sanitize_null(payload.doc_description),
864
+ "content": sanitize_null(payload.doc_content),
865
+ "status": sanitize_null(payload.doc_status),
866
+ "doc_type": sanitize_null(payload.doc_type),
867
+ "language": sanitize_null(payload.doc_language),
868
+ "parent_id": sanitize_null(payload.doc_parent_id),
869
+ "owner_user_id": sanitize_null(payload.doc_owner_id),
870
+ "reviewer_user_id": sanitize_null(payload.doc_reviewer_id),
871
+ "version": sanitize_null(payload.doc_version),
872
+ "category": sanitize_null(payload.doc_category),
873
+ "tags": sanitize_null_list(payload.doc_tags),
874
+ "emoji": sanitize_null(payload.doc_emoji or payload.doc_emote),
875
+ "keywords": sanitize_null_list(payload.doc_keywords),
894
876
  "is_public": payload.doc_is_public,
895
877
  },
896
878
  )
897
- return text(_format_doc(doc, header="✅ Documentação atualizada"))
879
+ return text(_format_doc(doc, header="✅ Documentation updated"))
898
880
 
899
881
  if action is KnowledgeAction.DOC_DELETE:
900
882
  if not payload.id:
901
- return text("❌ Informe o ID da documentação.")
883
+ return text("❌ Provide the documentation ID.")
902
884
  await self._service.doc_delete(payload.id)
903
- return text(f"🗑️ Documentação {payload.id} removida.")
885
+ return text(f"🗑️ Documentation {payload.id} removed.")
904
886
 
905
887
  if action is KnowledgeAction.DOC_SEARCH:
906
- if not payload.query or not (payload.doc_team_id or payload.team_id):
907
- return text("❌ Informe query e team_id para buscar documentação.")
888
+ query = sanitize_null(payload.query)
889
+ if not query:
890
+ return text("❌ Provide query to search documentation.")
908
891
  docs = await self._service.doc_search(
909
- query=payload.query,
910
- team_id=payload.doc_team_id or payload.team_id,
892
+ query=query,
911
893
  limit=payload.limit,
912
894
  )
913
895
  if not docs:
914
- return text(
915
- "🔍 Nenhum documento encontrado para os filtros informados."
916
- )
896
+ return text("🔍 No documents found for the specified filters.")
917
897
  body = "\n\n".join(_format_doc(doc) for doc in docs)
918
- return text(f"🔍 **Resultados ({len(docs)}):**\n\n{body}")
898
+ return text(f"🔍 **Results ({len(docs)}):**\n\n{body}")
919
899
 
920
900
  if action is KnowledgeAction.DOC_ROOTS:
921
- if not (payload.doc_team_id or payload.team_id):
922
- return text("❌ Informe team_id para listar raízes.")
923
- docs = await self._service.doc_roots(
924
- team_id=payload.doc_team_id or payload.team_id
925
- )
901
+ docs = await self._service.doc_roots()
926
902
  if not docs:
927
- return text("📚 Nenhuma raiz encontrada.")
903
+ return text("📚 No roots found.")
928
904
  body = "\n".join(
929
- f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})"
905
+ f"- {doc.get('title', 'Untitled')} (ID: {doc.get('id')})"
930
906
  for doc in docs
931
907
  )
932
- return text(f"📚 **Raízes de documentação:**\n{body}")
908
+ return text(f"📚 **Documentation roots:**\n{body}")
933
909
 
934
910
  if action is KnowledgeAction.DOC_RECENT:
935
- if not (payload.doc_team_id or payload.team_id):
936
- return text("❌ Informe team_id para listar documentos recentes.")
937
911
  docs = await self._service.doc_recent(
938
- team_id=payload.doc_team_id or payload.team_id,
939
912
  limit=payload.limit,
940
913
  )
941
914
  if not docs:
942
- return text("🕒 Nenhuma documentação recente encontrada.")
915
+ return text("🕒 No recent documentation found.")
943
916
  body = "\n\n".join(_format_doc(doc) for doc in docs)
944
- return text(f"🕒 **Documentos recentes ({len(docs)}):**\n\n{body}")
917
+ return text(f"🕒 **Recent documents ({len(docs)}):**\n\n{body}")
945
918
 
946
919
  if action is KnowledgeAction.DOC_ANALYTICS:
947
- if not (payload.doc_team_id or payload.team_id):
948
- return text(" Informe team_id para obter analytics.")
949
- analytics = await self._service.doc_analytics(
950
- team_id=payload.doc_team_id or payload.team_id
951
- )
952
- lines = ["📊 **Analytics de Documentação**"]
920
+ analytics = await self._service.doc_analytics()
921
+ lines = ["📊 **Documentation Analytics**"]
953
922
  for key, value in analytics.items():
954
923
  lines.append(f"- {key}: {value}")
955
924
  return text("\n".join(lines))
956
925
 
957
926
  if action is KnowledgeAction.DOC_CHILDREN:
958
927
  if not payload.id:
959
- return text("❌ Informe o ID da documentação.")
928
+ return text("❌ Provide the documentation ID.")
960
929
  docs = await self._service.doc_children(payload.id)
961
930
  if not docs:
962
- return text("📄 Nenhum filho cadastrado para o documento informado.")
931
+ return text("📄 No children registered for the specified document.")
963
932
  body = "\n".join(
964
- f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})"
933
+ f"- {doc.get('title', 'Untitled')} (ID: {doc.get('id')})"
965
934
  for doc in docs
966
935
  )
967
- return text(f"📄 **Filhos:**\n{body}")
936
+ return text(f"📄 **Children:**\n{body}")
968
937
 
969
938
  if action is KnowledgeAction.DOC_TREE:
970
939
  if not payload.id:
971
- return text("❌ Informe o ID da documentação.")
940
+ return text("❌ Provide the documentation ID.")
972
941
  tree = await self._service.doc_tree(payload.id)
973
- return text(f"🌳 **Árvore de documentação para {payload.id}:**\n{tree}")
942
+ return text(f"🌳 **Documentation tree for {payload.id}:**\n{tree}")
974
943
 
975
944
  if action is KnowledgeAction.DOC_FULL_TREE:
976
945
  tree = await self._service.doc_full_tree()
977
- return text(f"🌳 **Árvore completa de documentação:**\n{tree}")
946
+ return text(f"🌳 **Full documentation tree:**\n{tree}")
978
947
 
979
948
  if action is KnowledgeAction.DOC_MOVE:
980
949
  if not payload.id:
981
- return text("❌ Informe o ID da documentação.")
950
+ return text("❌ Provide the documentation ID.")
982
951
  if payload.doc_parent_id is None and payload.doc_position is None:
983
- return text(
984
- "❌ Informe doc_parent_id, doc_position ou ambos para mover."
985
- )
952
+ return text("❌ Provide doc_parent_id, doc_position or both to move.")
986
953
  move_payload = {
987
954
  "new_parent_id": payload.doc_parent_id,
988
955
  "new_position": payload.doc_position,
989
956
  }
990
957
  doc = await self._service.doc_move(payload.id, move_payload)
991
- return text(_format_doc(doc, header="📦 Documentação movida"))
958
+ return text(_format_doc(doc, header="📦 Documentation moved"))
992
959
 
993
960
  if action is KnowledgeAction.DOC_PUBLISH:
994
961
  if not payload.id:
995
- return text("❌ Informe o ID da documentação.")
962
+ return text("❌ Provide the documentation ID.")
996
963
  result = await self._service.doc_publish(payload.id)
997
- return text(f"🗞️ Documento publicado: {result}")
964
+ return text(f"🗞️ Document published: {result}")
998
965
 
999
966
  if action is KnowledgeAction.DOC_VERSION:
1000
967
  if not payload.id:
1001
- return text("❌ Informe o ID da documentação.")
968
+ return text("❌ Provide the documentation ID.")
1002
969
  if not payload.doc_version:
1003
970
  return text(
1004
- "❌ Informe doc_version com o número/identificador da versão."
971
+ "❌ Provide doc_version with the version number/identifier."
1005
972
  )
1006
973
  version_payload = {
1007
974
  "title": payload.doc_title or f"Version {payload.doc_version}",
@@ -1009,24 +976,23 @@ class KnowledgeTool(Tool):
1009
976
  "content": payload.doc_content,
1010
977
  }
1011
978
  doc = await self._service.doc_version(payload.id, version_payload)
1012
- return text(_format_doc(doc, header="🗞️ Nova versão criada"))
979
+ return text(_format_doc(doc, header="🗞️ New version created"))
1013
980
 
1014
981
  if action is KnowledgeAction.DOC_DUPLICATE:
1015
982
  if not payload.id:
1016
- return text("❌ Informe o ID da documentação.")
983
+ return text("❌ Provide the documentation ID.")
1017
984
  if not payload.doc_title:
1018
- return text("❌ Informe doc_title para nomear a cópia.")
985
+ return text("❌ Provide doc_title to name the copy.")
1019
986
  doc = await self._service.doc_duplicate(
1020
987
  payload.id,
1021
988
  {
1022
989
  "title": payload.doc_title,
1023
- "team_id": payload.doc_team_id or payload.team_id,
1024
990
  },
1025
991
  )
1026
- return text(_format_doc(doc, header="🗂️ Documento duplicado"))
992
+ return text(_format_doc(doc, header="🗂️ Document duplicated"))
1027
993
 
1028
994
  return text(
1029
- "❌ Ação de documentação não suportada.\n\nEscolha um dos valores:\n"
995
+ "❌ Unsupported documentation action.\n\nChoose one of the values:\n"
1030
996
  + "\n".join(
1031
997
  f"- `{value}`"
1032
998
  for value in KnowledgeAction.choices()
@@ -1036,7 +1002,7 @@ class KnowledgeTool(Tool):
1036
1002
 
1037
1003
  async def _handle_help(self):
1038
1004
  return text(
1039
- "📚 **Ações disponíveis para knowledge**\n\n"
1005
+ "📚 **Available actions for knowledge**\n\n"
1040
1006
  + KnowledgeAction.formatted_help()
1041
1007
  )
1042
1008
 
@@ -1048,13 +1014,13 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1048
1014
  lines.append("")
1049
1015
 
1050
1016
  # Extract title
1051
- title = item.get("title") or item.get("name") or "Sem título"
1017
+ title = item.get("title") or item.get("name") or "Untitled"
1052
1018
 
1053
1019
  # Extract status
1054
- status = item.get("status") or item.get("state") or "desconhecido"
1020
+ status = item.get("status") or item.get("state") or "unknown"
1055
1021
 
1056
1022
  # Extract priority
1057
- priority = item.get("priority") or item.get("priority_level") or "indefinido"
1023
+ priority = item.get("priority") or item.get("priority_level") or "undefined"
1058
1024
 
1059
1025
  # Extract ID
1060
1026
  item_id = item.get("id", "N/A")
@@ -1073,13 +1039,13 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1073
1039
  f"🎯 **{title}**",
1074
1040
  f"ID: {item_id}",
1075
1041
  f"Status: {status}",
1076
- f"Prioridade: {priority}",
1077
- f"Responsável: {assignee}",
1042
+ f"Priority: {priority}",
1043
+ f"Assignee: {assignee}",
1078
1044
  ]
1079
1045
  )
1080
1046
  if item.get("due_date") or item.get("dueDate"):
1081
1047
  lines.append(
1082
- f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}"
1048
+ f"Due date: {_format_date(item.get('due_date') or item.get('dueDate'))}"
1083
1049
  )
1084
1050
  if item.get("tags"):
1085
1051
  tags = item.get("tags", [])
@@ -1095,10 +1061,10 @@ def _format_board(board: Dict[str, Any], header: Optional[str] = None) -> str:
1095
1061
  lines.append("")
1096
1062
  lines.extend(
1097
1063
  [
1098
- f"🗂️ **{board.get('name', 'Sem nome')}**",
1064
+ f"🗂️ **{board.get('name', 'Unnamed')}**",
1099
1065
  f"ID: {board.get('id', 'N/A')}",
1100
- f"Time: {board.get('team_id', 'N/A')}",
1101
- f"Colunas: {len(board.get('columns', []))}",
1066
+ f"Team: {board.get('team_id', 'N/A')}",
1067
+ f"Columns: {len(board.get('columns', []))}",
1102
1068
  ]
1103
1069
  )
1104
1070
  return "\n".join(lines)
@@ -1111,19 +1077,19 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
1111
1077
  lines.append("")
1112
1078
  lines.extend(
1113
1079
  [
1114
- f"🏃 **{sprint.get('name', 'Sem nome')}**",
1080
+ f"🏃 **{sprint.get('name', 'Unnamed')}**",
1115
1081
  f"ID: {sprint.get('id', 'N/A')}",
1116
1082
  f"Status: {sprint.get('status', 'N/A')}",
1117
- f"Time: {sprint.get('team_id', 'N/A')}",
1083
+ f"Team: {sprint.get('team_id', 'N/A')}",
1118
1084
  ]
1119
1085
  )
1120
1086
  if sprint.get("start_date") or sprint.get("startDate"):
1121
1087
  lines.append(
1122
- f"Início: {_format_date(sprint.get('start_date') or sprint.get('startDate'))}"
1088
+ f"Start: {_format_date(sprint.get('start_date') or sprint.get('startDate'))}"
1123
1089
  )
1124
1090
  if sprint.get("end_date") or sprint.get("endDate"):
1125
1091
  lines.append(
1126
- f"Fim: {_format_date(sprint.get('end_date') or sprint.get('endDate'))}"
1092
+ f"End: {_format_date(sprint.get('end_date') or sprint.get('endDate'))}"
1127
1093
  )
1128
1094
  return "\n".join(lines)
1129
1095
 
@@ -1135,13 +1101,13 @@ def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
1135
1101
  lines.append("")
1136
1102
  lines.extend(
1137
1103
  [
1138
- f"🎭 **{mode.get('name', 'Sem nome')}**",
1104
+ f"🎭 **{mode.get('name', 'Unnamed')}**",
1139
1105
  f"ID: {mode.get('id', 'N/A')}",
1140
- f"Padrão: {mode.get('is_default', False)}",
1106
+ f"Default: {mode.get('is_default', False)}",
1141
1107
  ]
1142
1108
  )
1143
1109
  if mode.get("description"):
1144
- lines.append(f"Descrição: {mode['description']}")
1110
+ lines.append(f"Description: {mode['description']}")
1145
1111
  return "\n".join(lines)
1146
1112
 
1147
1113
 
@@ -1152,13 +1118,13 @@ def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
1152
1118
  lines.append("")
1153
1119
  lines.extend(
1154
1120
  [
1155
- f"📋 **{rule.get('name', 'Sem nome')}**",
1121
+ f"📋 **{rule.get('name', 'Unnamed')}**",
1156
1122
  f"ID: {rule.get('id', 'N/A')}",
1157
- f"Padrão: {rule.get('is_default', False)}",
1123
+ f"Default: {rule.get('is_default', False)}",
1158
1124
  ]
1159
1125
  )
1160
1126
  if rule.get("description"):
1161
- lines.append(f"Descrição: {rule['description']}")
1127
+ lines.append(f"Description: {rule['description']}")
1162
1128
  return "\n".join(lines)
1163
1129
 
1164
1130
 
@@ -1169,15 +1135,15 @@ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
1169
1135
  lines.append("")
1170
1136
  lines.extend(
1171
1137
  [
1172
- f"📄 **{doc.get('title') or doc.get('name', 'Sem título')}**",
1138
+ f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
1173
1139
  f"ID: {doc.get('id', 'N/A')}",
1174
1140
  f"Status: {doc.get('status', 'N/A')}",
1175
- f"Time: {doc.get('team_id', 'N/A')}",
1141
+ f"Team: {doc.get('team_id', 'N/A')}",
1176
1142
  ]
1177
1143
  )
1178
1144
  if doc.get("updated_at") or doc.get("updatedAt"):
1179
1145
  lines.append(
1180
- f"Atualizado em: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1146
+ f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1181
1147
  )
1182
1148
  return "\n".join(lines)
1183
1149