@danhachuel/thunderbolt 0.4.55 → 0.4.57

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.
@@ -825,3 +825,13 @@ Na interface, uma tarefa em execução mostra a última actividade do helper e o
825
825
  A partir da versão 0.3.78, o card **Backlog Vídeos** mostra a causa terminal e disponibiliza **Ver diagnóstico completo**. As mensagens `Pexels key validation completed`, `installing or verifying project dependencies with uv` e `TASK_DIR` são apenas progresso ou metadados; não são, por si só, uma falha.
826
826
 
827
827
  Se o `uv` falhar, o erro inclui o detalhe seguro do resolver. Se a CLI do MoneyPrinterTurbo falhar, o resumo inclui `MPT_ERROR` e mantém `Log completo` e `Manifesto`. A interface não exibe chaves, cookies ou tokens. O tempo limite da verificação de dependências é de 15 minutos; a etapa é encerrada em vez de ficar indefinidamente sem resposta.
828
+
829
+ ## 18. Upload via Composio
830
+
831
+ Em **Pipeline Vídeos > Upload > Upload via Composio**, o Thunderbolt permite descobrir ferramentas de upload disponíveis no projecto Composio Platform, seleccionar um toolkit e enviar um vídeo pronto da pipeline. Como cada provider expõe ferramentas e campos diferentes, o slug da ferramenta e o campo que recebe o vídeo são escolhidos explicitamente na interface; nenhum destino é assumido automaticamente.
832
+
833
+ Configure primeiro **Configurações > Configuração API > API Keys Upload > Composio**. Active a integração, introduza a API key de projecto Composio Platform e defina um `user ID` estável para associar as contas conectadas. A chave permanece no storage local e não é incluída no histórico, nos logs nem no repositório. O botão de teste faz apenas descoberta read-only.
834
+
835
+ Quando o provider ainda não estiver conectado, use **Autorizar toolkit no Composio**. O Thunderbolt apresenta o Connect Link devolvido pelo Composio; conclua a autorização nesse link e repita o envio. O Thunderbolt não implementa nem armazena um fluxo OAuth próprio.
836
+
837
+ Antes de clicar em **Enviar vídeo via Composio**, confirme o vídeo, o toolkit, o slug da ferramenta, o campo do ficheiro e os argumentos JSON. O resultado guarda apenas um registo local mínimo com o estado e, quando devolvido, o `log_id` do Composio. URLs presigned, tokens, headers e a API key não são guardados no histórico.
package/app/main.py CHANGED
@@ -98,6 +98,7 @@ from integrations.youtube_batch import account_key as youtube_batch_account_key,
98
98
  from integrations.local_runtime import MoneyPrinterRuntime
99
99
  from integrations.moneyprinter_config import sync_moneyprinter_config
100
100
  from integrations.openai_model_discovery import DEFAULT_NVIDIA_NIM_BASE_URL
101
+ from integrations.composio_upload import ComposioUploadError, authorize_toolkit, discover_tools, execute_upload, parse_arguments, test_configuration
101
102
 
102
103
  DEFAULT_UI_LANGUAGE = "en"
103
104
 
@@ -5302,7 +5303,7 @@ def render_music_upload() -> None:
5302
5303
 
5303
5304
  def render_upload():
5304
5305
  st.title("Upload")
5305
- upload_tab, direct_tab, postiz_tab, upload_post_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post"])
5306
+ upload_tab, direct_tab, postiz_tab, upload_post_tab, composio_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post", "Upload via Composio"])
5306
5307
  with direct_tab:
5307
5308
  render_upload_direct()
5308
5309
  with postiz_tab:
@@ -5311,6 +5312,81 @@ def render_upload():
5311
5312
  render_upload_post()
5312
5313
  with upload_tab:
5313
5314
  render_upload_conventional()
5315
+ with composio_tab:
5316
+ render_upload_composio()
5317
+
5318
+
5319
+ def render_upload_composio():
5320
+ st.subheader("Upload via Composio")
5321
+ st.caption("Descubra uma ferramenta de upload Composio, ligue a conta do provider quando necessário e envie um vídeo seleccionado. O slug e o campo do ficheiro são sempre escolhidos explicitamente.")
5322
+ settings = read_json("settings.json", {})
5323
+ api_key = str(settings.get("composio_api_key") or "").strip()
5324
+ user_id = str(settings.get("composio_user_id") or "").strip()
5325
+ if not settings.get("composio_enabled", False) or not api_key:
5326
+ st.warning("Composio está desactivado ou sem API key. Configure-o em Configuração API > API Keys Upload > Composio.")
5327
+ return
5328
+ query = st.text_input("Pesquisar ferramenta Composio", value="upload a video file", key="composio_upload_query")
5329
+ toolkit_filter = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="composio_upload_toolkit")
5330
+ if st.button("Descobrir ferramentas", key="composio_upload_discover", use_container_width=True):
5331
+ try:
5332
+ st.session_state["composio_upload_tools"] = discover_tools(api_key, user_id, query, toolkit_filter)
5333
+ if not st.session_state["composio_upload_tools"]:
5334
+ st.info("Nenhuma ferramenta encontrada. Tente uma pesquisa mais específica ou deixe o toolkit vazio.")
5335
+ else:
5336
+ st.success(f"{len(st.session_state['composio_upload_tools'])} ferramenta(s) encontrada(s).")
5337
+ except ComposioUploadError as exc:
5338
+ st.error(str(exc))
5339
+ tools = [item for item in st.session_state.get("composio_upload_tools", []) if isinstance(item, dict) and item.get("slug")]
5340
+ if not tools:
5341
+ st.info("Clique em Descobrir ferramentas para carregar os destinos disponíveis na sua conta Composio.")
5342
+ return
5343
+ tool_slugs = [str(item["slug"]) for item in tools]
5344
+ selected_slug = st.selectbox("Ferramenta de upload", tool_slugs, format_func=lambda slug: next((f"{item.get('name') or slug} — {item.get('toolkit') or 'toolkit desconhecido'}" for item in tools if item.get("slug") == slug), slug), key="composio_upload_slug")
5345
+ selected_tool = next((item for item in tools if item.get("slug") == selected_slug), tools[0])
5346
+ if selected_tool.get("description"):
5347
+ st.caption(str(selected_tool["description"]))
5348
+ schema = selected_tool.get("schema") or {}
5349
+ with st.expander("Schema da ferramenta", expanded=False):
5350
+ st.json(schema)
5351
+ file_field = st.text_input("Campo que recebe o vídeo", value=str(st.session_state.get("composio_upload_file_field") or "file"), key="composio_upload_file_field")
5352
+ arguments_json = st.text_area("Argumentos JSON adicionais", value=str(st.session_state.get("composio_upload_arguments") or "{}"), height=150, key="composio_upload_arguments")
5353
+ if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize", use_container_width=True):
5354
+ try:
5355
+ auth = authorize_toolkit(api_key, user_id, str(selected_tool.get("toolkit") or ""))
5356
+ if auth.get("redirect_url"):
5357
+ st.success("Connect Link criado. Abra o link para autorizar a conta e depois repita o envio.")
5358
+ st.markdown(f"[Abrir Connect Link do Composio]({auth['redirect_url']})")
5359
+ else:
5360
+ st.info("O Composio não devolveu um Connect Link; confirme o estado da conta no dashboard.")
5361
+ except ComposioUploadError as exc:
5362
+ st.error(str(exc))
5363
+ tasks = [task for task in read_json("tasks.json", []) if task.get("state") == "done" or task.get("artifacts", {}).get("video")]
5364
+ if not tasks:
5365
+ st.info("Não há vídeos prontos para enviar via Composio.")
5366
+ return
5367
+ task_options = {str(task.get("id")): task for task in tasks if task.get("id")}
5368
+ selected_task_id = st.selectbox("Vídeo", list(task_options), format_func=lambda task_id: f"{task_options[task_id].get('topic') or 'Vídeo Thunderbolt'} — {task_options[task_id].get('artifacts', {}).get('video') or 'sem ficheiro'}", key="composio_upload_task")
5369
+ task = task_options[selected_task_id]
5370
+ video_path = str((task.get("artifacts", {}) or {}).get("video") or "")
5371
+ st.caption(video_path or "Sem caminho de vídeo registado")
5372
+ st.info(f"Resumo: `{selected_slug}` · toolkit `{selected_tool.get('toolkit') or 'não indicado'}` · campo `{file_field or 'não indicado'}`")
5373
+ if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", use_container_width=True):
5374
+ try:
5375
+ parse_arguments(arguments_json)
5376
+ result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json)
5377
+ record = {"id": uuid.uuid4().hex, "task_id": task.get("id"), "destination": "Composio", "target": {"toolkit": selected_tool.get("toolkit"), "slug": selected_slug, "file_field": file_field}, "status": "published" if result.get("successful") else "failed", "message": result.get("error") or "Upload Composio concluído.", "data": result.get("data") or {}, "log_id": result.get("log_id") or "", "created_at": now()}
5378
+ uploads = read_json("uploads.json", [])
5379
+ uploads.append(record)
5380
+ write_json("uploads.json", uploads)
5381
+ reconcile_persisted_notifications()
5382
+ if result.get("successful"):
5383
+ st.success(record["message"])
5384
+ else:
5385
+ st.error(record["message"])
5386
+ if result.get("log_id"):
5387
+ st.caption(f"Composio log ID: {result['log_id']}")
5388
+ except ComposioUploadError as exc:
5389
+ st.error(str(exc))
5314
5390
 
5315
5391
 
5316
5392
  def render_upload_postiz():
@@ -7048,20 +7124,70 @@ def render_settings():
7048
7124
  with st.expander("Contas Google", expanded=False):
7049
7125
  render_google_accounts(include_innertube=False)
7050
7126
  with st.expander("API Innertube", expanded=False):
7051
- st.caption("INNERTUBE_API_KEY global usada pelo Upload directo para todas as contas Google/YouTube.")
7052
- upload_innertube = st.text_input("INNERTUBE_API_KEY", value=str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or ""), type="password", key="upload_api_innertube_key")
7053
- if st.button("Guardar INNERTUBE_API_KEY", type="primary", use_container_width=True, key="upload_save_innertube"):
7127
+ st.markdown("### INNERTUBE_API_KEY")
7128
+ st.caption("Esta é uma chave API global do YouTube: aplica-se a todas as contas Google/YouTube e a todo o sistema. Não é associada a uma conta específica, não faz parte dos documentos de cookies/credenciais e não é editada no separador API Keys.")
7129
+ upload_batch_accounts = [item for item in settings.get("youtube_batch_accounts", []) if isinstance(item, dict) and item.get("id")]
7130
+ current_upload_innertube = str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or "").strip()
7131
+ if not current_upload_innertube:
7132
+ for legacy_account in upload_batch_accounts:
7133
+ current_upload_innertube = str(legacy_account.get("innertube_api_key") or legacy_account.get("INNERTUBE_API_KEY") or "").strip()
7134
+ if current_upload_innertube:
7135
+ break
7136
+ if current_upload_innertube:
7137
+ settings["direct_innertube_api_key"] = current_upload_innertube
7138
+ settings.pop("INNERTUBE_API_KEY", None)
7139
+ for legacy_account in upload_batch_accounts:
7140
+ legacy_account.pop("innertube_api_key", None)
7141
+ legacy_account.pop("INNERTUBE_API_KEY", None)
7142
+ settings["youtube_batch_accounts"] = upload_batch_accounts
7143
+ write_json("settings.json", settings)
7144
+ innertube_upload_status = st.columns([3.2, 1.2])
7145
+ with innertube_upload_status[0]:
7146
+ st.caption("Estado da chave global")
7147
+ with innertube_upload_status[1]:
7148
+ _render_credential_status(current_upload_innertube)
7149
+ with st.form("upload_innertube_api_key_form"):
7150
+ upload_innertube = st.text_input("INNERTUBE_API_KEY", value=current_upload_innertube, type="password", key="upload_api_innertube_key", help="Chave global usada pelo Upload directo para todas as contas Google/YouTube. Guarde-a na configuração global, separada dos documentos de cookies.")
7151
+ _render_api_test_control(settings, "innertube_upload", lambda: test_innertube_api_key(upload_innertube), widget_key="api_test_innertube_upload")
7152
+ save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", use_container_width=True)
7153
+ if save_upload_innertube:
7054
7154
  settings["direct_innertube_api_key"] = upload_innertube.strip()
7055
7155
  settings.pop("INNERTUBE_API_KEY", None)
7156
+ for legacy_account in upload_batch_accounts:
7157
+ legacy_account.pop("innertube_api_key", None)
7158
+ legacy_account.pop("INNERTUBE_API_KEY", None)
7159
+ settings["youtube_batch_accounts"] = upload_batch_accounts
7056
7160
  write_json("settings.json", settings)
7057
- st.success("INNERTUBE_API_KEY guardada.")
7161
+ st.success("INNERTUBE_API_KEY global guardada para todas as contas Google/YouTube e para todo o sistema.")
7058
7162
  st.rerun()
7059
7163
  with st.expander("API Tiktok", expanded=False):
7060
7164
  render_tiktok_api_cards(settings)
7061
7165
  with st.expander("API Bilibili", expanded=False):
7062
7166
  render_bilibili_api_cards(settings)
7063
7167
  with st.expander("Composio", expanded=False):
7064
- st.info("Secção reservada para uma futura integração Composio.")
7168
+ st.caption("A API key é guardada apenas no storage local. O slug da ferramenta e o provider são descobertos no Upload via Composio; a autenticação da conta é feita pelo Connect Link do Composio.")
7169
+ composio_enabled = st.checkbox("Activar Composio", value=bool(settings.get("composio_enabled", False)), key="upload_composio_enabled")
7170
+ composio_api_key = st.text_input("Composio API key", value=str(settings.get("composio_api_key") or ""), type="password", key="upload_composio_api_key", help="Use a API key de projecto do Composio Platform. Nunca coloque esta chave no GitHub, em URLs ou em mensagens.")
7171
+ composio_user_id = st.text_input("Composio user ID", value=str(settings.get("composio_user_id") or "thunderbolt-local"), key="upload_composio_user_id", help="Identidade estável usada para associar as contas conectadas no Composio.")
7172
+ composio_toolkit = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="upload_composio_toolkit", help="Deixe vazio para descobrir ferramentas em todos os toolkits.")
7173
+ _render_credential_status(composio_api_key)
7174
+ composio_action_cols = st.columns(2)
7175
+ with composio_action_cols[0]:
7176
+ save_composio = st.button("Guardar Composio", type="primary", use_container_width=True, key="upload_composio_save")
7177
+ with composio_action_cols[1]:
7178
+ test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
7179
+ if save_composio:
7180
+ settings.update({"composio_enabled": bool(composio_enabled), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip()})
7181
+ write_json("settings.json", settings)
7182
+ st.success("Configuração Composio guardada.")
7183
+ st.rerun()
7184
+ if test_composio:
7185
+ try:
7186
+ result = test_configuration(composio_api_key, composio_user_id)
7187
+ found = len((result.get("data") or {}).get("tools", []))
7188
+ st.success(f"Configuração Composio válida; {found} ferramenta(s) de upload encontradas.")
7189
+ except ComposioUploadError as exc:
7190
+ st.error(str(exc))
7065
7191
  with st.expander("Upload-Post", expanded=False):
7066
7192
  upload_post_enabled_upload = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)), key="upload_tab_upload_post_enabled")
7067
7193
  upload_post_key_upload = st.text_input("Upload-Post API key", value=str(settings.get("upload_post_api_key") or ""), type="password", key="upload_tab_upload_post_key")
@@ -0,0 +1,174 @@
1
+ """Composio Platform adapter for user-selected video upload tools."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ class ComposioUploadError(ValueError):
10
+ """Safe, user-facing validation or integration error."""
11
+
12
+
13
+ def _safe_value(value: Any) -> Any:
14
+ if value is None or isinstance(value, (str, int, float, bool)):
15
+ return value
16
+ if isinstance(value, dict):
17
+ return {str(key): _safe_value(item) for key, item in value.items()}
18
+ if isinstance(value, (list, tuple)):
19
+ return [_safe_value(item) for item in value]
20
+ for method in ("model_dump", "to_dict", "dict"):
21
+ converter = getattr(value, method, None)
22
+ if callable(converter):
23
+ try:
24
+ return _safe_value(converter())
25
+ except Exception:
26
+ pass
27
+ attributes = getattr(value, "__dict__", None)
28
+ if isinstance(attributes, dict):
29
+ return {str(key): _safe_value(item) for key, item in attributes.items()}
30
+ return str(value)
31
+
32
+
33
+ def _tool_item(tool: Any) -> dict[str, Any]:
34
+ raw = _safe_value(tool)
35
+ if isinstance(raw, dict) and isinstance(raw.get("function"), dict):
36
+ function = raw["function"]
37
+ return {
38
+ "slug": str(function.get("name") or raw.get("name") or ""),
39
+ "name": str(function.get("name") or raw.get("name") or ""),
40
+ "description": str(function.get("description") or raw.get("description") or ""),
41
+ "toolkit": str(raw.get("toolkit") or ""),
42
+ "schema": function.get("parameters") or {},
43
+ }
44
+ if isinstance(raw, dict):
45
+ toolkit = raw.get("toolkit")
46
+ if isinstance(toolkit, dict):
47
+ toolkit = toolkit.get("slug") or toolkit.get("name") or ""
48
+ return {
49
+ "slug": str(raw.get("slug") or raw.get("name") or ""),
50
+ "name": str(raw.get("name") or raw.get("slug") or ""),
51
+ "description": str(raw.get("description") or ""),
52
+ "toolkit": str(toolkit or ""),
53
+ "schema": raw.get("input_parameters") or raw.get("parameters") or {},
54
+ }
55
+ return {"slug": "", "name": "", "description": "", "toolkit": "", "schema": {}}
56
+
57
+
58
+ def _response(result: Any) -> dict[str, Any]:
59
+ raw = _safe_value(result)
60
+ if not isinstance(raw, dict):
61
+ raw = {"data": raw}
62
+ error = raw.get("error")
63
+ return {
64
+ "successful": not bool(error),
65
+ "data": raw.get("data") or {},
66
+ "error": str(error) if error else "",
67
+ "log_id": str(raw.get("log_id") or raw.get("request_id") or ""),
68
+ }
69
+
70
+
71
+ def _require_api_key(api_key: str) -> str:
72
+ value = str(api_key or "").strip()
73
+ if len(value) < 10 or value.lower() in {"placeholder", "your_composio_api_key"}:
74
+ raise ComposioUploadError("Configure uma API key válida do Composio em Configuração API > API Keys Upload > Composio.")
75
+ return value
76
+
77
+
78
+ def _require_user_id(user_id: str) -> str:
79
+ value = str(user_id or "").strip()
80
+ if not value:
81
+ raise ComposioUploadError("Indique um Composio user ID estável antes de continuar.")
82
+ return value
83
+
84
+
85
+ def _client(api_key: str, *, upload_dir: Path | None = None):
86
+ try:
87
+ from composio import Composio
88
+ except ImportError as exc:
89
+ raise ComposioUploadError("A SDK Python composio não está instalada. Execute a instalação do Thunderbolt novamente.") from exc
90
+ kwargs: dict[str, Any] = {"api_key": _require_api_key(api_key), "allow_tracking": False}
91
+ if upload_dir is not None:
92
+ kwargs.update({
93
+ "dangerously_allow_auto_upload_download_files": True,
94
+ "file_upload_dirs": [str(upload_dir.resolve())],
95
+ })
96
+ return Composio(**kwargs)
97
+
98
+
99
+ def discover_tools(api_key: str, user_id: str, query: str, toolkit: str = "") -> list[dict[str, Any]]:
100
+ client = _client(api_key)
101
+ try:
102
+ tools = client.tools.get(
103
+ _require_user_id(user_id),
104
+ search=(query or "upload a video file").strip(),
105
+ toolkits=[toolkit.strip()] if toolkit.strip() else None,
106
+ limit=10,
107
+ )
108
+ return [item for item in (_tool_item(tool) for tool in tools) if item.get("slug")]
109
+ except ComposioUploadError:
110
+ raise
111
+ except Exception as exc:
112
+ raise ComposioUploadError(f"Não foi possível descobrir ferramentas Composio: {type(exc).__name__}: {exc}") from exc
113
+
114
+
115
+ def authorize_toolkit(api_key: str, user_id: str, toolkit: str) -> dict[str, Any]:
116
+ toolkit = str(toolkit or "").strip()
117
+ if not toolkit:
118
+ raise ComposioUploadError("Seleccione uma ferramenta descoberta para saber qual toolkit deve ser autorizado.")
119
+ client = _client(api_key)
120
+ try:
121
+ request = client.create(user_id=_require_user_id(user_id)).authorize(toolkit)
122
+ return {"connected_account_id": str(getattr(request, "id", "") or ""), "redirect_url": str(getattr(request, "redirect_url", "") or "")}
123
+ except ComposioUploadError:
124
+ raise
125
+ except Exception as exc:
126
+ raise ComposioUploadError(f"Não foi possível criar o Connect Link do Composio: {type(exc).__name__}: {exc}") from exc
127
+
128
+
129
+ def parse_arguments(arguments_json: str) -> dict[str, Any]:
130
+ try:
131
+ parsed = json.loads(arguments_json or "{}")
132
+ except json.JSONDecodeError as exc:
133
+ raise ComposioUploadError(f"Os argumentos da ferramenta não são JSON válido: {exc.msg}.") from exc
134
+ if not isinstance(parsed, dict):
135
+ raise ComposioUploadError("Os argumentos da ferramenta devem ser um objecto JSON.")
136
+ return parsed
137
+
138
+
139
+ def execute_upload(api_key: str, user_id: str, slug: str, video_path: str, file_field: str, arguments_json: str = "") -> dict[str, Any]:
140
+ path = Path(str(video_path or "").strip()).expanduser()
141
+ slug = str(slug or "").strip()
142
+ file_field = str(file_field or "").strip()
143
+ if not path.is_file():
144
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado não existe ou não é um ficheiro.")
145
+ if path.stat().st_size <= 0:
146
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado está vazio.")
147
+ if not slug:
148
+ raise ComposioUploadError("Seleccione ou indique o slug de uma ferramenta Composio.")
149
+ if not file_field:
150
+ raise ComposioUploadError("Indique o campo de argumentos que recebe o ficheiro.")
151
+ arguments = parse_arguments(arguments_json)
152
+ if file_field in arguments and arguments[file_field] not in (None, "", str(path)):
153
+ raise ComposioUploadError(f"O campo `{file_field}` já contém um valor. Remova-o antes de injectar o vídeo.")
154
+ arguments[file_field] = str(path.resolve())
155
+ client = _client(api_key, upload_dir=path.parent)
156
+ try:
157
+ result = client.tools.execute(
158
+ slug,
159
+ arguments=arguments,
160
+ user_id=_require_user_id(user_id),
161
+ version="latest",
162
+ dangerously_skip_version_check=True,
163
+ )
164
+ return _response(result)
165
+ except ComposioUploadError:
166
+ raise
167
+ except Exception as exc:
168
+ safe_message = str(exc).replace(str(api_key), "[REDACTED]")
169
+ raise ComposioUploadError(f"A ferramenta Composio falhou: {type(exc).__name__}: {safe_message}") from exc
170
+
171
+
172
+ def test_configuration(api_key: str, user_id: str) -> dict[str, Any]:
173
+ tools = discover_tools(api_key, user_id, "upload a video file")
174
+ return {"successful": True, "data": {"tools": tools}, "error": "", "log_id": ""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.55",
3
+ "version": "0.4.57",
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",
package/requirements.txt CHANGED
@@ -22,3 +22,4 @@ kagglehub>=0.3,<1
22
22
  supabase>=2.4,<3
23
23
  bilibili-api-python>=17.4,<18
24
24
  playwright>=1.45,<2
25
+ composio>=0.21,<1
package/scripts/cli.mjs CHANGED
@@ -116,7 +116,7 @@ function check() {
116
116
  console.error(`Python não encontrado. Execute: npx.cmd --yes @danhachuel/thunderbolt install`);
117
117
  process.exit(1);
118
118
  }
119
- const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub"];
119
+ const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub", "composio"];
120
120
  const missing = requiredModules.filter((moduleName) => !moduleAvailable(moduleName));
121
121
  const ffmpeg = moduleAvailable("imageio_ffmpeg");
122
122
  const mptPath = configuredMoneyPrinterPath();