@danhachuel/thunderbolt 0.5.17 → 0.5.19

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.
@@ -71,24 +71,50 @@ def _first_id(value: Any, keys: tuple[str, ...]) -> str:
71
71
  return ""
72
72
 
73
73
 
74
- def _first_text_element(value: Any) -> str:
74
+ def _first_text_element(value: Any, *, text_context: bool = False) -> str:
75
75
  if isinstance(value, Mapping):
76
- for key in ("element_id", "id"):
76
+ # The MCP returns rich text elements with slightly different ID
77
+ # names depending on the design type/version of the server.
78
+ text_markers = ("text", "richtext", "rich_text", "content", "paragraph")
79
+ mapping_text = str(value).lower()
80
+ is_text_mapping = text_context or any(
81
+ marker in str(key).lower() or marker in mapping_text
82
+ for key in value
83
+ for marker in text_markers
84
+ )
85
+ for key in ("element_id", "elementId", "richtext_id", "richTextId", "text_id", "textId", "id"):
77
86
  candidate = str(value.get(key) or "").strip()
78
- if candidate and any(token in str(value).lower() for token in ("text", "richtext", "content")):
87
+ if candidate and is_text_mapping:
79
88
  return candidate
80
- for child in value.values():
81
- found = _first_text_element(child)
89
+ for key, child in value.items():
90
+ child_context = text_context or any(marker in str(key).lower() for marker in text_markers)
91
+ found = _first_text_element(child, text_context=child_context)
82
92
  if found:
83
93
  return found
84
94
  elif isinstance(value, list):
85
95
  for child in value:
86
- found = _first_text_element(child)
96
+ found = _first_text_element(child, text_context=text_context)
87
97
  if found:
88
98
  return found
89
99
  return ""
90
100
 
91
101
 
102
+ def _has_richtext(value: Any) -> bool:
103
+ """Return whether a Canva payload contains at least one richtext item."""
104
+ if isinstance(value, Mapping):
105
+ for key, child in value.items():
106
+ if str(key).lower() in {"richtexts", "rich_texts", "text_elements", "textelements"}:
107
+ if isinstance(child, list) and any(isinstance(item, Mapping) for item in child):
108
+ return True
109
+ if isinstance(child, Mapping) and child:
110
+ return True
111
+ if _has_richtext(child):
112
+ return True
113
+ elif isinstance(value, list):
114
+ return any(_has_richtext(child) for child in value)
115
+ return False
116
+
117
+
92
118
  def _first_page(value: Any) -> tuple[str, list[dict[str, Any]]]:
93
119
  pages: list[dict[str, Any]] = []
94
120
  if isinstance(value, Mapping) and isinstance(value.get("pages"), list):
@@ -122,17 +148,27 @@ def run_direct_canva_thumbnail(
122
148
  candidates = _items(result)
123
149
  if not candidates:
124
150
  raise CanvaMCPError("Canva não encontrou designs para as palavras-chave da thumbnail.")
125
- selected = next((candidate for candidate in candidates if _design_id(candidate)), candidates[0])
126
- design_id = _design_id(selected)
127
- if not design_id:
151
+ candidates = [candidate for candidate in candidates if _design_id(candidate)]
152
+ if not candidates:
128
153
  raise CanvaMCPError("A pesquisa Canva devolveu um design sem ID.")
129
154
 
130
155
  get_name = "get-design-content" if "get-design-content" in available else "get_design_content"
131
- content = _payload(client.call(get_name, {
132
- "design_id": design_id,
133
- "content_types": ["richtexts"],
134
- "user_intent": "Inspect the selected Canva design before applying the thumbnail edit.",
135
- }))
156
+ selected = None
157
+ content: Any = {}
158
+ for candidate in candidates:
159
+ candidate_id = _design_id(candidate)
160
+ candidate_content = _payload(client.call(get_name, {
161
+ "design_id": candidate_id,
162
+ "content_types": ["richtexts"],
163
+ "user_intent": "Inspect the selected Canva design before applying the thumbnail edit.",
164
+ }))
165
+ if _has_richtext(candidate_content):
166
+ selected = candidate
167
+ content = candidate_content
168
+ break
169
+ if selected is None:
170
+ raise CanvaMCPError("Os designs Canva encontrados não têm elementos de texto editáveis.")
171
+ design_id = _design_id(selected)
136
172
  start_name = "start-editing-transaction" if "start-editing-transaction" in available else "start_editing_transaction"
137
173
  started = _payload(client.call(start_name, {
138
174
  "design_id": design_id,
@@ -144,6 +180,12 @@ def run_direct_canva_thumbnail(
144
180
  page_id, pages = _first_page(started)
145
181
  element_id = _first_text_element(started) or _first_text_element(content)
146
182
  if not element_id:
183
+ cancel_name = "cancel-editing-transaction" if "cancel-editing-transaction" in available else "cancel_editing_transaction"
184
+ if cancel_name in available:
185
+ client.call(cancel_name, {
186
+ "transaction_id": transaction_id,
187
+ "user_intent": "Discard the Canva editing transaction because no editable text element was returned.",
188
+ })
147
189
  raise CanvaMCPError("O design Canva não devolveu um elemento de texto editável.")
148
190
  perform_name = "perform-editing-operations" if "perform-editing-operations" in available else "perform_editing_operations"
149
191
  operations = [{"type": "replace_text", "element_id": element_id, "text": title[:255]}]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",