machinaos 0.0.76 → 0.0.78
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.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
"""Task Manager Tool — Wave 11.C migration.
|
|
1
|
+
"""Task Manager Tool — Wave 11.C migration; body inlined Wave 11.I (M.O).
|
|
2
|
+
|
|
3
|
+
Inspects the in-memory delegation registry that lives on
|
|
4
|
+
``services.handlers.tools`` (``_delegated_tasks`` for in-flight asyncio
|
|
5
|
+
tasks, ``_delegation_results`` for completed-but-still-cached results,
|
|
6
|
+
plus ``get_delegated_task_status`` which adds a DB-fallback layer for
|
|
7
|
+
older runs). The plugin doesn't own that state — the delegation
|
|
8
|
+
lifecycle in ``tools.py`` does — so we read through to it rather than
|
|
9
|
+
duplicating registry plumbing here.
|
|
10
|
+
"""
|
|
2
11
|
|
|
3
12
|
from __future__ import annotations
|
|
4
13
|
|
|
@@ -6,8 +15,11 @@ from typing import Any, Dict, Literal, Optional
|
|
|
6
15
|
|
|
7
16
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
17
|
|
|
18
|
+
from core.logging import get_logger
|
|
9
19
|
from services.plugin import NodeContext, Operation, TaskQueue, ToolNode
|
|
10
20
|
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
11
23
|
|
|
12
24
|
class TaskManagerParams(BaseModel):
|
|
13
25
|
action: Literal["create", "list", "complete", "delete", "update"] = "create"
|
|
@@ -49,7 +61,137 @@ class TaskManagerNode(ToolNode):
|
|
|
49
61
|
|
|
50
62
|
@Operation("manage")
|
|
51
63
|
async def manage(self, ctx: NodeContext, params: TaskManagerParams) -> Any:
|
|
52
|
-
from services.handlers.tools import _execute_task_manager
|
|
53
64
|
return await _execute_task_manager(
|
|
54
65
|
params.model_dump(), {"node_id": ctx.node_id, "workspace_dir": ctx.workspace_dir or ""},
|
|
55
66
|
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _execute_task_manager(
|
|
70
|
+
tool_args: Dict[str, Any],
|
|
71
|
+
config: Dict[str, Any],
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
"""Execute task manager operations.
|
|
74
|
+
|
|
75
|
+
Dual-purpose: works as AI tool (LLM fills args) or workflow node
|
|
76
|
+
(uses params).
|
|
77
|
+
|
|
78
|
+
Operations:
|
|
79
|
+
- ``list_tasks``: list all active and completed delegated tasks
|
|
80
|
+
- ``get_task``: detailed status for one task
|
|
81
|
+
- ``mark_done``: drop a task from active tracking
|
|
82
|
+
"""
|
|
83
|
+
# Read-through to the delegation registry owned by tools.py. Keeping
|
|
84
|
+
# the singletons there is intentional -- the entire delegation
|
|
85
|
+
# lifecycle (spawn / cleanup / refcount / DB persistence) lives in
|
|
86
|
+
# tools.py and is genuine cross-cutting framework state.
|
|
87
|
+
from services.handlers.tools import (
|
|
88
|
+
_delegated_tasks,
|
|
89
|
+
_delegation_results,
|
|
90
|
+
get_delegated_task_status,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
params = config.get("parameters", {})
|
|
94
|
+
operation = tool_args.get("operation") or params.get("operation", "list_tasks")
|
|
95
|
+
task_id = tool_args.get("task_id") or params.get("task_id")
|
|
96
|
+
status_filter = tool_args.get("status_filter") or params.get("status_filter")
|
|
97
|
+
database = config.get("database")
|
|
98
|
+
|
|
99
|
+
logger.debug(
|
|
100
|
+
f"[TaskManager] Operation: {operation}, task_id: {task_id}, filter: {status_filter}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if operation == "list_tasks":
|
|
104
|
+
tasks = []
|
|
105
|
+
|
|
106
|
+
# Active tasks from asyncio.Task tracking.
|
|
107
|
+
for tid, task in _delegated_tasks.items():
|
|
108
|
+
if task.done():
|
|
109
|
+
try:
|
|
110
|
+
if task.cancelled():
|
|
111
|
+
status = "cancelled"
|
|
112
|
+
elif task.exception():
|
|
113
|
+
status = "error"
|
|
114
|
+
else:
|
|
115
|
+
status = "completed"
|
|
116
|
+
except Exception:
|
|
117
|
+
status = "completed"
|
|
118
|
+
else:
|
|
119
|
+
status = "running"
|
|
120
|
+
|
|
121
|
+
tasks.append({"task_id": tid, "status": status, "active": True})
|
|
122
|
+
|
|
123
|
+
# Completed tasks from in-memory cache.
|
|
124
|
+
for tid, result in _delegation_results.items():
|
|
125
|
+
if tid not in [t["task_id"] for t in tasks]:
|
|
126
|
+
tasks.append({
|
|
127
|
+
"task_id": tid,
|
|
128
|
+
"status": result.get("status", "completed"),
|
|
129
|
+
"agent_name": result.get("agent_name"),
|
|
130
|
+
"result_summary": str(result.get("result", ""))[:200],
|
|
131
|
+
"active": False,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if status_filter:
|
|
135
|
+
tasks = [t for t in tasks if t.get("status") == status_filter]
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"success": True,
|
|
139
|
+
"operation": "list_tasks",
|
|
140
|
+
"tasks": tasks,
|
|
141
|
+
"count": len(tasks),
|
|
142
|
+
"running": sum(1 for t in tasks if t.get("status") == "running"),
|
|
143
|
+
"completed": sum(1 for t in tasks if t.get("status") == "completed"),
|
|
144
|
+
"errors": sum(1 for t in tasks if t.get("status") == "error"),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if operation == "get_task":
|
|
148
|
+
if not task_id:
|
|
149
|
+
return {"success": False, "error": "task_id is required for get_task operation"}
|
|
150
|
+
|
|
151
|
+
# 3-layer lookup (live tasks -> memory cache -> DB).
|
|
152
|
+
result = await get_delegated_task_status(task_ids=[task_id], database=database)
|
|
153
|
+
tasks = result.get("tasks", [])
|
|
154
|
+
|
|
155
|
+
if not tasks:
|
|
156
|
+
return {
|
|
157
|
+
"success": False,
|
|
158
|
+
"error": f"Task {task_id} not found",
|
|
159
|
+
"task_id": task_id,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
task_info = tasks[0]
|
|
163
|
+
return {
|
|
164
|
+
"success": True,
|
|
165
|
+
"operation": "get_task",
|
|
166
|
+
"task_id": task_id,
|
|
167
|
+
"status": task_info.get("status"),
|
|
168
|
+
"agent_name": task_info.get("agent_name"),
|
|
169
|
+
"result": task_info.get("result"),
|
|
170
|
+
"error": task_info.get("error"),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if operation == "mark_done":
|
|
174
|
+
if not task_id:
|
|
175
|
+
return {"success": False, "error": "task_id is required for mark_done operation"}
|
|
176
|
+
|
|
177
|
+
removed = False
|
|
178
|
+
if task_id in _delegated_tasks:
|
|
179
|
+
del _delegated_tasks[task_id]
|
|
180
|
+
removed = True
|
|
181
|
+
if task_id in _delegation_results:
|
|
182
|
+
del _delegation_results[task_id]
|
|
183
|
+
removed = True
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"success": True,
|
|
187
|
+
"operation": "mark_done",
|
|
188
|
+
"task_id": task_id,
|
|
189
|
+
"removed": removed,
|
|
190
|
+
"message": (
|
|
191
|
+
f"Task {task_id} marked as done and removed from tracking"
|
|
192
|
+
if removed
|
|
193
|
+
else f"Task {task_id} was not in active tracking"
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {"success": False, "error": f"Unknown operation: {operation}"}
|
|
@@ -1 +1,38 @@
|
|
|
1
|
-
"""Plugins for the 'twitter' palette group.
|
|
1
|
+
"""Plugins for the 'twitter' palette group.
|
|
2
|
+
|
|
3
|
+
Self-contained plugin folder (Wave 11.H pattern). Owns:
|
|
4
|
+
|
|
5
|
+
- Action / trigger nodes — ``twitter_send.py``, ``twitter_search.py``,
|
|
6
|
+
``twitter_user.py``, ``twitter_receive.py`` (auto-registered via
|
|
7
|
+
``BaseNode.__init_subclass__``).
|
|
8
|
+
- ``_credentials.py`` — :class:`TwitterCredential` (OAuth2).
|
|
9
|
+
- ``_oauth.py`` — OAuth 2.0 PKCE flow client (formerly
|
|
10
|
+
``services/twitter_oauth.py``).
|
|
11
|
+
- ``_handlers.py`` — 3 WebSocket handlers
|
|
12
|
+
(``twitter_oauth_login`` / ``twitter_oauth_status`` /
|
|
13
|
+
``twitter_logout``).
|
|
14
|
+
- ``_router.py`` — HTTP OAuth callback (``/api/twitter/callback``).
|
|
15
|
+
|
|
16
|
+
Two self-registration calls below — the central WS dispatcher and the
|
|
17
|
+
FastAPI app pick up the plugin's surface without ever importing this
|
|
18
|
+
module by name.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from services.event_waiter import register_filter_builder
|
|
22
|
+
from services.status_broadcaster import register_service_refresh
|
|
23
|
+
from services.ws_handler_registry import register_router, register_ws_handlers
|
|
24
|
+
|
|
25
|
+
from . import _router
|
|
26
|
+
from ._filters import build_filter as build_twitter_filter
|
|
27
|
+
from ._handlers import WS_HANDLERS
|
|
28
|
+
from ._refresh import refresh_twitter_status
|
|
29
|
+
|
|
30
|
+
# WebSocket handlers (3 message types) and the OAuth-callback HTTP
|
|
31
|
+
# router self-register so ``routers/websocket.py`` and ``main.py``
|
|
32
|
+
# stay free of per-plugin imports. Plus the service-status refresh
|
|
33
|
+
# callback (Wave 11.I, milestone J) and the trigger filter builder
|
|
34
|
+
# (milestone K).
|
|
35
|
+
register_ws_handlers(WS_HANDLERS)
|
|
36
|
+
register_router(_router.router, name="twitter")
|
|
37
|
+
register_service_refresh(refresh_twitter_status)
|
|
38
|
+
register_filter_builder("twitterReceive", build_twitter_filter)
|
|
@@ -28,12 +28,12 @@ async def track_twitter_usage(
|
|
|
28
28
|
node_id: str, action: str, resource_count: int, context: Dict[str, Any],
|
|
29
29
|
) -> Dict[str, float]:
|
|
30
30
|
"""Record a Twitter API call in ``api_usage_metrics``."""
|
|
31
|
-
from
|
|
31
|
+
from services.plugin.deps import get_database
|
|
32
32
|
|
|
33
33
|
pricing = get_pricing_service()
|
|
34
34
|
cost_data = pricing.calculate_api_cost('twitter', action, resource_count)
|
|
35
35
|
|
|
36
|
-
db =
|
|
36
|
+
db = get_database()
|
|
37
37
|
await db.save_api_usage_metric({
|
|
38
38
|
'session_id': context.get('session_id', 'default'),
|
|
39
39
|
'node_id': node_id,
|
|
@@ -49,10 +49,10 @@ async def track_twitter_usage(
|
|
|
49
49
|
|
|
50
50
|
async def get_twitter_client():
|
|
51
51
|
"""Build an XDK Client from the stored OAuth2 access token."""
|
|
52
|
-
from
|
|
52
|
+
from services.plugin.deps import get_auth_service
|
|
53
53
|
from xdk import Client
|
|
54
54
|
|
|
55
|
-
auth_service =
|
|
55
|
+
auth_service = get_auth_service()
|
|
56
56
|
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
57
57
|
if not tokens or not tokens.get("access_token"):
|
|
58
58
|
raise RuntimeError("Twitter not connected. Please authenticate via Credentials.")
|
|
@@ -61,11 +61,11 @@ async def get_twitter_client():
|
|
|
61
61
|
|
|
62
62
|
async def refresh_and_get_client():
|
|
63
63
|
"""Refresh the OAuth2 token and return a new client."""
|
|
64
|
-
from
|
|
65
|
-
from services.
|
|
64
|
+
from nodes.twitter._oauth import TwitterOAuth
|
|
65
|
+
from services.plugin.deps import get_auth_service
|
|
66
66
|
from xdk import Client
|
|
67
67
|
|
|
68
|
-
auth_service =
|
|
68
|
+
auth_service = get_auth_service()
|
|
69
69
|
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
70
70
|
refresh_token = tokens.get("refresh_token", "") if tokens else ""
|
|
71
71
|
if not refresh_token:
|
|
@@ -4,7 +4,7 @@ Used by the four twitter plugins in this folder (twitter_send, twitter_search,
|
|
|
4
4
|
twitter_user, twitter_receive).
|
|
5
5
|
|
|
6
6
|
OAuth 2.0 with PKCE. The refresh flow is non-trivial (custom code exchange
|
|
7
|
-
in :mod:`
|
|
7
|
+
in :mod:`nodes.twitter._oauth`), so :meth:`build_client` returns an
|
|
8
8
|
authenticated XDK ``Client`` — that's what the four twitter plugins
|
|
9
9
|
actually hand to the XDK API methods. Use :func:`nodes.twitter._base.call_with_retry`
|
|
10
10
|
on top; it transparently refreshes on 401/403 via this class.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Twitter event-trigger filter builder (Wave 11.I, milestone K).
|
|
2
|
+
|
|
3
|
+
Moved verbatim from ``services/event_waiter.build_twitter_filter``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Callable, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
12
|
+
"""Build filter function for Twitter events.
|
|
13
|
+
|
|
14
|
+
Filters by:
|
|
15
|
+
|
|
16
|
+
- ``trigger_type`` -- ``mentions`` / ``search`` / ``user_timeline``
|
|
17
|
+
- ``search_query`` -- search query for ``search``
|
|
18
|
+
- ``user_id`` -- user ID for ``user_timeline``
|
|
19
|
+
"""
|
|
20
|
+
trigger_type = params.get('trigger_type', 'mentions')
|
|
21
|
+
search_query = params.get('search_query', '')
|
|
22
|
+
user_id = params.get('user_id', '')
|
|
23
|
+
|
|
24
|
+
def matches(data: Dict) -> bool:
|
|
25
|
+
event_type = data.get('trigger_type', '')
|
|
26
|
+
if trigger_type != 'all' and event_type != trigger_type:
|
|
27
|
+
return False
|
|
28
|
+
if trigger_type == 'search' and search_query:
|
|
29
|
+
event_query = data.get('query', '')
|
|
30
|
+
if search_query.lower() not in event_query.lower():
|
|
31
|
+
return False
|
|
32
|
+
if trigger_type == 'user_timeline' and user_id:
|
|
33
|
+
if data.get('user_id') != user_id:
|
|
34
|
+
return False
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
return matches
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Twitter / X WebSocket handlers — factory-built (Wave 11.I, S).
|
|
2
|
+
|
|
3
|
+
The 3 handlers (``twitter_oauth_login`` / ``twitter_oauth_status`` /
|
|
4
|
+
``twitter_logout``) come from
|
|
5
|
+
:func:`services.events.oauth_lifecycle.make_oauth_lifecycle_handlers`.
|
|
6
|
+
The factory takes care of credential loading, redirect-URI derivation,
|
|
7
|
+
silent token refresh on status, revoke + remove + broadcast on
|
|
8
|
+
logout. Plugin-specific bits supplied via kwargs:
|
|
9
|
+
|
|
10
|
+
* ``oauth_factory`` -- async builder that constructs
|
|
11
|
+
:class:`TwitterOAuth` per call, pulling stored client_id /
|
|
12
|
+
client_secret from ``auth_service``.
|
|
13
|
+
* ``user_info_to_subject`` -- ``f"@{username}"`` (X has no email).
|
|
14
|
+
* ``extra_logout`` -- drops legacy API-key entries left behind by the
|
|
15
|
+
pre-OAuth layout.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, Optional
|
|
21
|
+
|
|
22
|
+
from services.events.oauth_lifecycle import make_oauth_lifecycle_handlers
|
|
23
|
+
|
|
24
|
+
from ._oauth import TwitterOAuth
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _twitter_oauth_factory(
|
|
28
|
+
*, redirect_uri: Optional[str] = None, **_kwargs,
|
|
29
|
+
) -> TwitterOAuth:
|
|
30
|
+
"""Build a :class:`TwitterOAuth` from stored client credentials."""
|
|
31
|
+
from services.plugin.deps import get_auth_service
|
|
32
|
+
|
|
33
|
+
auth_service = get_auth_service()
|
|
34
|
+
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
35
|
+
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
36
|
+
return TwitterOAuth(
|
|
37
|
+
client_id=client_id,
|
|
38
|
+
client_secret=client_secret,
|
|
39
|
+
redirect_uri=redirect_uri or "",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _user_info_to_handle(info: Dict[str, Any]) -> str:
|
|
44
|
+
return f"@{info.get('username', 'unknown')}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _drop_legacy_api_key_entries() -> None:
|
|
48
|
+
"""Remove stale API-key entries from the pre-OAuth layout.
|
|
49
|
+
|
|
50
|
+
Pre-Wave-11.I some flows mistakenly stored access / refresh tokens
|
|
51
|
+
as API keys. The OAuth tokens table is the canonical home now;
|
|
52
|
+
these orphans are cleaned on every logout for safety.
|
|
53
|
+
"""
|
|
54
|
+
from services.plugin.deps import get_auth_service
|
|
55
|
+
|
|
56
|
+
auth_service = get_auth_service()
|
|
57
|
+
for key in ("twitter_access_token", "twitter_refresh_token", "twitter_user_info"):
|
|
58
|
+
try:
|
|
59
|
+
await auth_service.remove_api_key(key)
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
WS_HANDLERS = make_oauth_lifecycle_handlers(
|
|
65
|
+
provider="twitter",
|
|
66
|
+
oauth_factory=_twitter_oauth_factory,
|
|
67
|
+
user_info_to_subject=_user_info_to_handle,
|
|
68
|
+
extra_logout=_drop_legacy_api_key_entries,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Module-level aliases so the contract tests in
|
|
72
|
+
# ``tests/credentials/test_websocket_handlers.py`` can import the
|
|
73
|
+
# handlers by name. The dispatch table itself is what
|
|
74
|
+
# ``register_ws_handlers`` consumes.
|
|
75
|
+
handle_twitter_oauth_login = WS_HANDLERS["twitter_oauth_login"]
|
|
76
|
+
handle_twitter_oauth_status = WS_HANDLERS["twitter_oauth_status"]
|
|
77
|
+
handle_twitter_logout = WS_HANDLERS["twitter_logout"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Twitter / X OAuth 2.0 PKCE client.
|
|
2
|
+
|
|
3
|
+
Wave 11.I, milestone S: subclasses :class:`OAuth2PKCEClient` from
|
|
4
|
+
:mod:`services.plugin.oauth`. The base class owns the PKCE state
|
|
5
|
+
store, code-verifier generation, code exchange, token refresh, and
|
|
6
|
+
revocation. This file declares X-specific endpoints + scopes +
|
|
7
|
+
``fetch_user_info`` translation.
|
|
8
|
+
|
|
9
|
+
Pre-S the file hand-rolled all of that (~410 LOC). The only Twitter-
|
|
10
|
+
specific behaviour now lives in this small subclass.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from core.logging import get_logger
|
|
20
|
+
from services.plugin.oauth import OAuth2PKCEClient, OAuthStateStore
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
# X API OAuth 2.0 endpoints. Updated URLs per latest docs. Public so
|
|
25
|
+
# the contract tests in tests/credentials/test_twitter_oauth.py can
|
|
26
|
+
# pin them via respx mocks.
|
|
27
|
+
AUTHORIZATION_URL = "https://x.com/i/oauth2/authorize"
|
|
28
|
+
TOKEN_URL = "https://api.x.com/2/oauth2/token"
|
|
29
|
+
REVOKE_URL = "https://api.x.com/2/oauth2/revoke"
|
|
30
|
+
USER_INFO_URL = "https://api.x.com/2/users/me"
|
|
31
|
+
|
|
32
|
+
# Required scopes for full Twitter integration.
|
|
33
|
+
# See: https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
|
|
34
|
+
_DEFAULT_SCOPES = [
|
|
35
|
+
"tweet.read",
|
|
36
|
+
"tweet.write",
|
|
37
|
+
"users.read",
|
|
38
|
+
"follows.read",
|
|
39
|
+
"like.read",
|
|
40
|
+
"like.write",
|
|
41
|
+
"offline.access", # enables refresh tokens
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TwitterOAuth(OAuth2PKCEClient):
|
|
46
|
+
"""X (Twitter) OAuth 2.0 PKCE client."""
|
|
47
|
+
|
|
48
|
+
provider = "twitter"
|
|
49
|
+
authorization_endpoint = AUTHORIZATION_URL
|
|
50
|
+
token_endpoint = TOKEN_URL
|
|
51
|
+
revocation_endpoint = REVOKE_URL
|
|
52
|
+
|
|
53
|
+
# Plugin-scoped state store -- isolated from Google's instance.
|
|
54
|
+
state_store = OAuthStateStore()
|
|
55
|
+
|
|
56
|
+
DEFAULT_SCOPES = _DEFAULT_SCOPES
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
client_id: str,
|
|
61
|
+
redirect_uri: str,
|
|
62
|
+
client_secret: Optional[str] = None,
|
|
63
|
+
scopes: Optional[List[str]] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(
|
|
66
|
+
client_id=client_id,
|
|
67
|
+
redirect_uri=redirect_uri,
|
|
68
|
+
client_secret=client_secret,
|
|
69
|
+
scopes=scopes,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Back-compat alias for the contract tests in
|
|
73
|
+
# tests/credentials/test_twitter_oauth.py -- the unified protocol
|
|
74
|
+
# method is :meth:`fetch_user_info`.
|
|
75
|
+
async def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
76
|
+
return await self.fetch_user_info(access_token)
|
|
77
|
+
|
|
78
|
+
async def fetch_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
79
|
+
"""Translate X's ``/users/me`` response into the unified shape."""
|
|
80
|
+
try:
|
|
81
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
82
|
+
response = await client.get(
|
|
83
|
+
USER_INFO_URL,
|
|
84
|
+
params={
|
|
85
|
+
"user.fields": "id,name,username,profile_image_url,verified",
|
|
86
|
+
},
|
|
87
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
88
|
+
)
|
|
89
|
+
except httpx.HTTPError as exc:
|
|
90
|
+
logger.error(f"[twitter] HTTP error getting user info: {exc}")
|
|
91
|
+
return {"success": False, "error": str(exc)}
|
|
92
|
+
|
|
93
|
+
if response.status_code != 200:
|
|
94
|
+
error_data = response.json() if response.text else {}
|
|
95
|
+
return {
|
|
96
|
+
"success": False,
|
|
97
|
+
"error": error_data.get("detail")
|
|
98
|
+
or error_data.get("title", "Failed to get user info"),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
user = response.json().get("data", {})
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"id": user.get("id"),
|
|
105
|
+
"username": user.get("username"),
|
|
106
|
+
"name": user.get("name"),
|
|
107
|
+
"profile_image_url": user.get("profile_image_url"),
|
|
108
|
+
"verified": user.get("verified", False),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Module-level alias for the contract tests' ``_oauth_states.clear()``
|
|
113
|
+
# pattern -- this is the same dict the class-level state store wraps,
|
|
114
|
+
# so clearing one clears the other.
|
|
115
|
+
_oauth_states = TwitterOAuth.state_store._states
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = [
|
|
119
|
+
"TwitterOAuth",
|
|
120
|
+
"AUTHORIZATION_URL",
|
|
121
|
+
"TOKEN_URL",
|
|
122
|
+
"REVOKE_URL",
|
|
123
|
+
"USER_INFO_URL",
|
|
124
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Twitter service-status refresh callback (Wave 11.I, milestone J).
|
|
2
|
+
|
|
3
|
+
Moved from ``services/status_broadcaster._refresh_twitter_status``.
|
|
4
|
+
Plugin packages register their own callback via
|
|
5
|
+
``status_broadcaster.register_service_refresh``; the broadcaster no
|
|
6
|
+
longer hardcodes a per-service refresh.
|
|
7
|
+
|
|
8
|
+
Reads OAuth tokens via ``auth_service.get_oauth_tokens("twitter")`` and
|
|
9
|
+
mirrors the result into the broadcaster cache. No plugin internals
|
|
10
|
+
are reached -- pure auth-service read + broadcast. The function still
|
|
11
|
+
runs in the broadcaster's TaskGroup so OTel spans aggregate the same
|
|
12
|
+
way they did pre-migration.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from opentelemetry import trace
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
tracer = trace.get_tracer(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def refresh_twitter_status(broadcaster: "StatusBroadcaster") -> None:
|
|
30
|
+
"""Refresh Twitter cache + broadcast. One pass per
|
|
31
|
+
``_refresh_all_services`` cycle.
|
|
32
|
+
"""
|
|
33
|
+
with tracer.start_as_current_span("broadcaster.refresh_twitter") as span:
|
|
34
|
+
try:
|
|
35
|
+
from services.plugin.deps import get_auth_service
|
|
36
|
+
|
|
37
|
+
auth_service = get_auth_service()
|
|
38
|
+
tokens = await auth_service.get_oauth_tokens(
|
|
39
|
+
"twitter", customer_id="owner"
|
|
40
|
+
)
|
|
41
|
+
if not tokens or not tokens.get("access_token"):
|
|
42
|
+
broadcaster._status["twitter"] = {
|
|
43
|
+
"connected": False,
|
|
44
|
+
"username": None,
|
|
45
|
+
"user_id": None,
|
|
46
|
+
"name": None,
|
|
47
|
+
"profile_image_url": None,
|
|
48
|
+
}
|
|
49
|
+
else:
|
|
50
|
+
# User info is stored in the OAuth token record.
|
|
51
|
+
# email field carries the ``@username`` form.
|
|
52
|
+
email = tokens.get("email", "")
|
|
53
|
+
name = tokens.get("name", "")
|
|
54
|
+
username = email.lstrip("@") if email.startswith("@") else email
|
|
55
|
+
broadcaster._status["twitter"] = {
|
|
56
|
+
"connected": True,
|
|
57
|
+
"username": username or None,
|
|
58
|
+
"user_id": None,
|
|
59
|
+
"name": name or None,
|
|
60
|
+
"profile_image_url": None,
|
|
61
|
+
}
|
|
62
|
+
logger.debug(
|
|
63
|
+
"[StatusBroadcaster] Twitter status: connected as @%s",
|
|
64
|
+
username,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await broadcaster.broadcast({
|
|
68
|
+
"type": "twitter_status",
|
|
69
|
+
"data": broadcaster._status["twitter"],
|
|
70
|
+
})
|
|
71
|
+
span.set_attribute(
|
|
72
|
+
"connected", bool(broadcaster._status["twitter"]["connected"])
|
|
73
|
+
)
|
|
74
|
+
except Exception as exc: # noqa: BLE001 -- mirror pre-migration behaviour
|
|
75
|
+
span.record_exception(exc)
|
|
76
|
+
logger.debug(
|
|
77
|
+
"[StatusBroadcaster] Could not refresh Twitter status: %s", exc
|
|
78
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Twitter / X OAuth callback router — factory-built (Wave 11.I, S).
|
|
2
|
+
|
|
3
|
+
The single ``GET /api/twitter/callback`` route comes from
|
|
4
|
+
:func:`services.events.oauth_lifecycle.make_oauth_callback_router`.
|
|
5
|
+
Pre-S the file hand-rolled the callback + status + logout REST routes
|
|
6
|
+
(~370 LOC). Status + logout were duplicates of the WS handlers in
|
|
7
|
+
``_handlers.py`` and have been retired -- the WS path is canonical.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
from services.events.oauth_lifecycle import make_oauth_callback_router
|
|
15
|
+
|
|
16
|
+
from ._handlers import _twitter_oauth_factory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _user_info_to_email(info: Dict[str, Any]) -> str:
|
|
20
|
+
"""X has no email field -- use ``@username`` as the identifier."""
|
|
21
|
+
return f"@{info.get('username', 'unknown')}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
router = make_oauth_callback_router(
|
|
25
|
+
provider="twitter",
|
|
26
|
+
oauth_factory=_twitter_oauth_factory,
|
|
27
|
+
user_info_to_email=_user_info_to_email,
|
|
28
|
+
color_hex="#00ba7c", # X brand green for the success page
|
|
29
|
+
)
|
|
@@ -39,6 +39,10 @@ class TwitterReceiveNode(TriggerNode):
|
|
|
39
39
|
group = ("social", "trigger")
|
|
40
40
|
description = "Trigger workflow on Twitter mentions, search results, or timeline updates (polling-based)"
|
|
41
41
|
component_kind = "trigger"
|
|
42
|
+
# Wave 11.I, milestone K: ``event_type`` ClassVar lets
|
|
43
|
+
# ``event_waiter._auto_populate_from_plugins`` backfill
|
|
44
|
+
# TRIGGER_REGISTRY without a hardcoded entry in event_waiter.
|
|
45
|
+
event_type = "twitter_event_received"
|
|
42
46
|
handles = (
|
|
43
47
|
{"name": "output-main", "kind": "output", "position": "right",
|
|
44
48
|
"label": "Output", "role": "main"},
|