@danhachuel/thunderbolt 0.5.18 → 0.5.20

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.
@@ -27,7 +27,7 @@ def _payload(result: Mapping[str, Any]) -> Any:
27
27
 
28
28
  def _items(value: Any) -> list[dict[str, Any]]:
29
29
  if isinstance(value, Mapping):
30
- for key in ("items", "designs", "results"):
30
+ for key in ("items", "designs", "results", "candidates"):
31
31
  if isinstance(value.get(key), list):
32
32
  return [dict(item) for item in value[key] if isinstance(item, Mapping)]
33
33
  for key in ("data", "result", "response"):
@@ -38,6 +38,25 @@ def _items(value: Any) -> list[dict[str, Any]]:
38
38
  return []
39
39
 
40
40
 
41
+ def _first_named_id(value: Any, keys: tuple[str, ...]) -> str:
42
+ """Find an ID by field name without treating unrelated nested IDs as matches."""
43
+ if isinstance(value, Mapping):
44
+ for key in keys:
45
+ candidate = str(value.get(key) or "").strip()
46
+ if candidate:
47
+ return candidate
48
+ for child in value.values():
49
+ found = _first_named_id(child, keys)
50
+ if found:
51
+ return found
52
+ elif isinstance(value, list):
53
+ for child in value:
54
+ found = _first_named_id(child, keys)
55
+ if found:
56
+ return found
57
+ return ""
58
+
59
+
41
60
  def _design_id(value: Any) -> str:
42
61
  """Extract a Canva design identifier from REST/MCP result variants."""
43
62
  if isinstance(value, Mapping):
@@ -99,6 +118,22 @@ def _first_text_element(value: Any, *, text_context: bool = False) -> str:
99
118
  return ""
100
119
 
101
120
 
121
+ def _has_richtext(value: Any) -> bool:
122
+ """Return whether a Canva payload contains at least one richtext item."""
123
+ if isinstance(value, Mapping):
124
+ for key, child in value.items():
125
+ if str(key).lower() in {"richtexts", "rich_texts", "text_elements", "textelements"}:
126
+ if isinstance(child, list) and any(isinstance(item, Mapping) for item in child):
127
+ return True
128
+ if isinstance(child, Mapping) and child:
129
+ return True
130
+ if _has_richtext(child):
131
+ return True
132
+ elif isinstance(value, list):
133
+ return any(_has_richtext(child) for child in value)
134
+ return False
135
+
136
+
102
137
  def _first_page(value: Any) -> tuple[str, list[dict[str, Any]]]:
103
138
  pages: list[dict[str, Any]] = []
104
139
  if isinstance(value, Mapping) and isinstance(value.get("pages"), list):
@@ -132,17 +167,61 @@ def run_direct_canva_thumbnail(
132
167
  candidates = _items(result)
133
168
  if not candidates:
134
169
  raise CanvaMCPError("Canva não encontrou designs para as palavras-chave da thumbnail.")
135
- selected = next((candidate for candidate in candidates if _design_id(candidate)), candidates[0])
136
- design_id = _design_id(selected)
137
- if not design_id:
170
+ candidates = [candidate for candidate in candidates if _design_id(candidate)]
171
+ if not candidates:
138
172
  raise CanvaMCPError("A pesquisa Canva devolveu um design sem ID.")
139
173
 
140
174
  get_name = "get-design-content" if "get-design-content" in available else "get_design_content"
141
- content = _payload(client.call(get_name, {
142
- "design_id": design_id,
143
- "content_types": ["richtexts"],
144
- "user_intent": "Inspect the selected Canva design before applying the thumbnail edit.",
145
- }))
175
+ selected = None
176
+ content: Any = {}
177
+ for candidate in candidates:
178
+ candidate_id = _design_id(candidate)
179
+ candidate_content = _payload(client.call(get_name, {
180
+ "design_id": candidate_id,
181
+ "content_types": ["richtexts"],
182
+ "user_intent": "Inspect the selected Canva design before applying the thumbnail edit.",
183
+ }))
184
+ if _has_richtext(candidate_content):
185
+ selected = candidate
186
+ content = candidate_content
187
+ break
188
+ if selected is None:
189
+ generate_name = "generate-design" if "generate-design" in available else "generate_design"
190
+ create_name = "create-design-from-candidate" if "create-design-from-candidate" in available else "create_design_from_candidate"
191
+ if generate_name not in available or create_name not in available:
192
+ raise CanvaMCPError("Os designs Canva encontrados não têm elementos de texto editáveis e esta sessão MCP não disponibiliza geração de design.")
193
+ blueprint_text = str(blueprint.get("content") or "")[:5000]
194
+ generated = _payload(client.call(generate_name, {
195
+ "design_type": "youtube_thumbnail",
196
+ "query": (
197
+ "Create one editable YouTube thumbnail in 16:9 format for this topic: "
198
+ f"{title}. Context: {topic}. Thumbnail prompt: {prompt[:1200]}. "
199
+ "Use clearly editable headline text boxes, a strong background, readable text "
200
+ "background shapes, relevant icons, depth/effects, and high-contrast lettering. "
201
+ "Follow these local Thumbnail Blueprint rules: " + blueprint_text
202
+ ),
203
+ "user_intent": "Create an editable YouTube thumbnail with text boxes and visual elements when searched designs are not editable.",
204
+ }))
205
+ job_id = _first_named_id(generated, ("job_id", "jobId"))
206
+ generated_candidates = _items(generated)
207
+ candidate_id = _first_named_id(generated_candidates[0] if generated_candidates else {}, ("candidate_id", "candidateId"))
208
+ if not job_id or not candidate_id:
209
+ raise CanvaMCPError("Canva não devolveu um candidato editável para a thumbnail.")
210
+ created = _payload(client.call(create_name, {
211
+ "job_id": job_id,
212
+ "candidate_id": candidate_id,
213
+ "user_intent": "Create the selected editable YouTube thumbnail candidate in the user's Canva account.",
214
+ }))
215
+ design_id = _design_id(created)
216
+ if not design_id:
217
+ raise CanvaMCPError("Canva não devolveu o ID do design gerado.")
218
+ selected = {"id": design_id}
219
+ content = _payload(client.call(get_name, {
220
+ "design_id": design_id,
221
+ "content_types": ["richtexts"],
222
+ "user_intent": "Inspect the generated editable thumbnail before applying the final lettering.",
223
+ }))
224
+ design_id = _design_id(selected)
146
225
  start_name = "start-editing-transaction" if "start-editing-transaction" in available else "start_editing_transaction"
147
226
  started = _payload(client.call(start_name, {
148
227
  "design_id": design_id,
@@ -154,6 +233,12 @@ def run_direct_canva_thumbnail(
154
233
  page_id, pages = _first_page(started)
155
234
  element_id = _first_text_element(started) or _first_text_element(content)
156
235
  if not element_id:
236
+ cancel_name = "cancel-editing-transaction" if "cancel-editing-transaction" in available else "cancel_editing_transaction"
237
+ if cancel_name in available:
238
+ client.call(cancel_name, {
239
+ "transaction_id": transaction_id,
240
+ "user_intent": "Discard the Canva editing transaction because no editable text element was returned.",
241
+ })
157
242
  raise CanvaMCPError("O design Canva não devolveu um elemento de texto editável.")
158
243
  perform_name = "perform-editing-operations" if "perform-editing-operations" in available else "perform_editing_operations"
159
244
  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.18",
3
+ "version": "0.5.20",
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",