@danhachuel/thunderbolt 0.6.6 → 0.6.7

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.
@@ -10,6 +10,14 @@ class ComposioUploadError(ValueError):
10
10
  """Safe, user-facing validation or integration error."""
11
11
 
12
12
 
13
+ COMPOSIO_OPERATION_SEARCH = {
14
+ "upload_video": {"query": "Upload Video", "toolkit": "YOUTUBE"},
15
+ "update_video": {"query": "Update Video", "toolkit": "YOUTUBE"},
16
+ "upload_tiktok_video": {"query": "Upload Video", "toolkit": "TIKTOK"},
17
+ "upload_instagram_media": {"query": "Upload Video Reel Photo", "toolkit": "INSTAGRAM"},
18
+ }
19
+
20
+
13
21
  def _safe_value(value: Any) -> Any:
14
22
  if value is None or isinstance(value, (str, int, float, bool)):
15
23
  return value
@@ -119,6 +127,38 @@ def discover_tools(api_key: str, user_id: str, query: str, toolkit: str = "") ->
119
127
  raise ComposioUploadError(f"Não foi possível descobrir ferramentas Composio: {type(exc).__name__}: {exc}") from exc
120
128
 
121
129
 
130
+ def resolve_tool_slug(api_key: str, user_id: str, configured_slug: str, toolkit: str = "") -> str:
131
+ """Resolve a Thunderbolt operation alias to a real Composio tool slug."""
132
+ slug = str(configured_slug or "").strip()
133
+ operation = COMPOSIO_OPERATION_SEARCH.get(slug)
134
+ if operation is None:
135
+ return slug
136
+ search_toolkit = str(operation["toolkit"] or toolkit or "").strip()
137
+ tools = discover_tools(api_key, user_id, str(operation["query"]), search_toolkit)
138
+ if not tools:
139
+ raise ComposioUploadError(
140
+ f"Não foi encontrada uma ferramenta Composio para `{slug}`. "
141
+ f"Ligue o toolkit {search_toolkit or 'correspondente'} e use Descobrir ferramentas."
142
+ )
143
+
144
+ def score(item: dict[str, Any]) -> tuple[int, str]:
145
+ candidate = str(item.get("slug") or "").strip()
146
+ normalized = candidate.upper().replace("-", "_")
147
+ if slug == "upload_video":
148
+ priority = {
149
+ "YOUTUBE_UPLOAD_VIDEO": 0,
150
+ "YOUTUBE_MULTIPART_UPLOAD_VIDEO": 1,
151
+ "YOUTUBE_UPLOAD": 2,
152
+ }.get(normalized, 3)
153
+ elif slug == "update_video":
154
+ priority = 0 if normalized == "YOUTUBE_UPDATE_VIDEO" else 1
155
+ else:
156
+ priority = 0 if "VIDEO" in normalized and "UPLOAD" in normalized else 1
157
+ return priority, candidate
158
+
159
+ return min((item for item in tools if item.get("slug")), key=score)["slug"]
160
+
161
+
122
162
  def authorize_toolkit(api_key: str, user_id: str, toolkit: str) -> dict[str, Any]:
123
163
  toolkit = str(toolkit or "").strip()
124
164
  if not toolkit:
@@ -19,7 +19,7 @@ from integrations.session_info_health import check_account_session_info_health,
19
19
  from integrations.youtube_direct_credentials import document_status
20
20
  from integrations.youtube_direct_upload import YouTubeDirectUploader
21
21
  from integrations.youtube_session_manager import renew_account_session
22
- from integrations.composio_upload import ComposioUploadError, execute_upload
22
+ from integrations.composio_upload import ComposioUploadError, execute_upload, resolve_tool_slug
23
23
  from hermes_ui.storage import read_json, write_json
24
24
  from hermes_ui.languages import language_locale
25
25
 
@@ -239,7 +239,16 @@ def upload_with_default_route(
239
239
 
240
240
 
241
241
  def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwargs: Any) -> IntegrationResult:
242
- slug = str(settings.get("composio_tool_slug") or "").strip()
242
+ configured_slug = str(settings.get("composio_tool_slug") or "").strip()
243
+ try:
244
+ slug = resolve_tool_slug(
245
+ str(settings.get("composio_api_key") or ""),
246
+ str(settings.get("composio_user_id") or ""),
247
+ configured_slug,
248
+ str(settings.get("composio_toolkit") or ""),
249
+ )
250
+ except ComposioUploadError as exc:
251
+ return IntegrationResult(False, str(exc), {"status": "tool_resolution_failed", "configured_slug": configured_slug})
243
252
  normalized_slug = slug.upper().replace("-", "_")
244
253
  connected_account_tool = normalized_slug in {"UPLOAD_VIDEO", "UPDATE_VIDEO", "UPLOAD_TIKTOK_VIDEO", "UPLOAD_INSTAGRAM_MEDIA"}
245
254
  youtube_upload_tool = normalized_slug in {"UPLOAD_VIDEO", "YOUTUBE_UPLOAD_VIDEO", "YOUTUBE_MULTIPART_UPLOAD_VIDEO"} or ("YOUTUBE" in normalized_slug and "UPLOAD" in normalized_slug and normalized_slug != "YOUTUBE_UPLOAD")
@@ -294,7 +303,7 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
294
303
  result = execute_upload(
295
304
  str(settings.get("composio_api_key") or ""),
296
305
  str(settings.get("composio_user_id") or ""),
297
- str(settings.get("composio_tool_slug") or ""),
306
+ slug,
298
307
  str(kwargs.get("video_path") or ""),
299
308
  file_field,
300
309
  json.dumps(parsed_arguments, ensure_ascii=False),
@@ -304,5 +313,5 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
304
313
  return IntegrationResult(
305
314
  bool(result.get("successful")),
306
315
  str(result.get("error") or "Upload via Composio concluído."),
307
- {"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or "", "tool_slug": result.get("tool_slug") or slug},
316
+ {"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or "", "tool_slug": result.get("tool_slug") or slug, "configured_tool_slug": configured_slug},
308
317
  )
@@ -11,13 +11,9 @@ from integrations.platforms import IntegrationResult
11
11
  from integrations.youtube_batch import loopback_host, loopback_port, loopback_redirect_uri, token_path as batch_token_path
12
12
 
13
13
 
14
- # Mantemos os mesmos escopos usados pelo youtube-automation-agent.
15
- AGENT_SCOPES = [
16
- "https://www.googleapis.com/auth/youtube.upload",
17
- "https://www.googleapis.com/auth/youtube",
18
- "https://www.googleapis.com/auth/youtube.readonly",
19
- "https://www.googleapis.com/auth/yt-analytics.readonly",
20
- ]
14
+ # O fluxo de upload precisa apenas do scope de publicação; Analytics e leitura
15
+ # são autorizados separadamente e não devem ser enviados neste refresh OAuth.
16
+ AGENT_SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
21
17
  FALLBACK_SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
22
18
 
23
19
 
@@ -137,7 +133,9 @@ class _GoogleYouTubeBase:
137
133
  raw = json.loads(token_source.read_text(encoding="utf-8"))
138
134
  if isinstance(raw, dict) and isinstance(raw.get("youtube"), dict):
139
135
  raw = raw["youtube"]
140
- credentials = Credentials.from_authorized_user_info(raw, self.scopes)
136
+ # Não reenviar self.scopes durante o refresh: o Google deve reutilizar
137
+ # os scopes efectivamente concedidos no token guardado.
138
+ credentials = Credentials.from_authorized_user_info(raw, None)
141
139
  granted_scopes = {str(scope).strip() for scope in (credentials.scopes or [])}
142
140
  if "https://www.googleapis.com/auth/youtube.upload" in self.scopes and granted_scopes and "https://www.googleapis.com/auth/youtube.upload" not in granted_scopes:
143
141
  email = _safe_text((self.account or {}).get("email")) or "a conta seleccionada"
@@ -316,7 +314,9 @@ class DirectYouTubeOAuthUploader(_GoogleYouTubeBase):
316
314
  settings,
317
315
  storage_root / "state" / "youtube_oauth_fallback_token.json",
318
316
  FALLBACK_SCOPES,
319
- alternate_token_path=storage_root / "state" / "youtube_agent_tokens.json",
317
+ # Não reutilizar o token do agente: pode conter scopes de leitura/Analytics
318
+ # que provocam invalid_scope no refresh do fallback de upload.
319
+ alternate_token_path=None,
320
320
  account=account,
321
321
  )
322
322
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.06",
3
+ "version": "0.6.07",
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",