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
|
@@ -207,8 +207,8 @@ class RelayWebSocketClient:
|
|
|
207
207
|
async def _clear_stored_session(self):
|
|
208
208
|
"""Clear stored pairing session from database."""
|
|
209
209
|
try:
|
|
210
|
-
from
|
|
211
|
-
database =
|
|
210
|
+
from services.plugin.deps import get_database
|
|
211
|
+
database = get_database()
|
|
212
212
|
|
|
213
213
|
await database.clear_android_relay_session()
|
|
214
214
|
logger.debug("[Relay] Cleared stored pairing session")
|
|
@@ -368,8 +368,8 @@ class RelayWebSocketClient:
|
|
|
368
368
|
async def _save_pairing_session(self):
|
|
369
369
|
"""Save pairing session to database for auto-reconnect."""
|
|
370
370
|
try:
|
|
371
|
-
from
|
|
372
|
-
database =
|
|
371
|
+
from services.plugin.deps import get_database
|
|
372
|
+
database = get_database()
|
|
373
373
|
|
|
374
374
|
await database.save_android_relay_session(
|
|
375
375
|
relay_url=self.base_url,
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
"""Android System Services routes."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
6
|
from pydantic import BaseModel, Field
|
|
5
7
|
from typing import Dict, Any
|
|
6
8
|
|
|
7
|
-
from
|
|
8
|
-
from services.android_service import AndroidService
|
|
9
|
+
from ._dispatcher import AndroidService
|
|
9
10
|
from core.logging import get_logger
|
|
11
|
+
from services.plugin.deps import get_android_service
|
|
10
12
|
|
|
11
13
|
logger = get_logger(__name__)
|
|
12
14
|
router = APIRouter(prefix="/api/android", tags=["android"])
|
|
13
15
|
|
|
16
|
+
# ADB device IDs: USB serials (alphanumeric), TCP "host:port" (digits + dots
|
|
17
|
+
# + colon), or "emulator-NNNN". All are safe characters but we lock the
|
|
18
|
+
# accepted set explicitly so untrusted input can't slip a flag or path
|
|
19
|
+
# separator into the argv list we pass to subprocess.run. Anything outside
|
|
20
|
+
# `[A-Za-z0-9._:-]` (and longer than 64 chars) is rejected with 400.
|
|
21
|
+
_DEVICE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{1,64}$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_device_id(device_id: str) -> str:
|
|
25
|
+
"""Return device_id if it matches the ADB device-id shape, else 400."""
|
|
26
|
+
if not _DEVICE_ID_PATTERN.fullmatch(device_id):
|
|
27
|
+
raise HTTPException(status_code=400, detail="invalid device_id")
|
|
28
|
+
return device_id
|
|
29
|
+
|
|
14
30
|
|
|
15
31
|
class AndroidServiceRequest(BaseModel):
|
|
16
32
|
"""Request model for Android service execution."""
|
|
@@ -24,7 +40,7 @@ class AndroidServiceRequest(BaseModel):
|
|
|
24
40
|
@router.post("/execute")
|
|
25
41
|
async def execute_android_service(
|
|
26
42
|
request: AndroidServiceRequest,
|
|
27
|
-
android_service: AndroidService = Depends(
|
|
43
|
+
android_service: AndroidService = Depends(get_android_service)
|
|
28
44
|
):
|
|
29
45
|
"""Execute an Android system service action.
|
|
30
46
|
|
|
@@ -57,7 +73,7 @@ async def execute_android_service(
|
|
|
57
73
|
async def check_device_status(
|
|
58
74
|
android_host: str = "localhost",
|
|
59
75
|
android_port: int = 8888,
|
|
60
|
-
android_service: AndroidService = Depends(
|
|
76
|
+
android_service: AndroidService = Depends(get_android_service)
|
|
61
77
|
):
|
|
62
78
|
"""Check if Android device API is reachable."""
|
|
63
79
|
logger.info(
|
|
@@ -77,7 +93,7 @@ async def check_device_status(
|
|
|
77
93
|
@router.get("/services/{service_id}/actions")
|
|
78
94
|
async def get_service_actions(
|
|
79
95
|
service_id: str,
|
|
80
|
-
android_service: AndroidService = Depends(
|
|
96
|
+
android_service: AndroidService = Depends(get_android_service)
|
|
81
97
|
):
|
|
82
98
|
"""Get available actions for a specific Android service.
|
|
83
99
|
|
|
@@ -105,7 +121,7 @@ async def get_service_actions(
|
|
|
105
121
|
async def get_action_parameters(
|
|
106
122
|
service_id: str,
|
|
107
123
|
action: str,
|
|
108
|
-
android_service: AndroidService = Depends(
|
|
124
|
+
android_service: AndroidService = Depends(get_android_service)
|
|
109
125
|
):
|
|
110
126
|
"""Get default parameters for a specific service action.
|
|
111
127
|
|
|
@@ -195,9 +211,12 @@ async def setup_port_forwarding(
|
|
|
195
211
|
device_port: int = 8888
|
|
196
212
|
):
|
|
197
213
|
"""Setup ADB port forwarding for Android device communication."""
|
|
214
|
+
device_id = _validate_device_id(device_id)
|
|
198
215
|
import subprocess
|
|
199
216
|
try:
|
|
200
217
|
# Setup port forwarding: adb -s device_id forward tcp:local_port tcp:device_port
|
|
218
|
+
# device_id passes _DEVICE_ID_PATTERN above; subprocess.run is called
|
|
219
|
+
# with an argv list (no shell), so no further interpolation risk.
|
|
201
220
|
cmd = ["adb", "-s", device_id, "forward", f"tcp:{local_port}", f"tcp:{device_port}"]
|
|
202
221
|
|
|
203
222
|
result = subprocess.run(
|
|
@@ -258,7 +277,7 @@ async def get_relay_connection_status():
|
|
|
258
277
|
and the paired Android device.
|
|
259
278
|
"""
|
|
260
279
|
try:
|
|
261
|
-
from
|
|
280
|
+
from ._relay import get_current_relay_client
|
|
262
281
|
|
|
263
282
|
relay_client = get_current_relay_client()
|
|
264
283
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Interactive browser automation via the agent-browser CLI. The plugin
|
|
4
4
|
maps the high-level operation enum to CLI argv, resolves the browser
|
|
5
5
|
binary (system Chrome / Edge / Chromium / bundled), and delegates the
|
|
6
|
-
subprocess invocation to ``
|
|
6
|
+
subprocess invocation to ``_service`` (the plugin-private service).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -317,7 +317,7 @@ class BrowserNode(ActionNode):
|
|
|
317
317
|
|
|
318
318
|
@Operation("dispatch")
|
|
319
319
|
async def dispatch(self, ctx: NodeContext, params: BrowserParams) -> BrowserOutput:
|
|
320
|
-
from
|
|
320
|
+
from ._service import get_browser_service
|
|
321
321
|
|
|
322
322
|
svc = get_browser_service()
|
|
323
323
|
if not svc:
|
|
@@ -29,9 +29,13 @@ class CodeExecutorOutput(BaseModel):
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class CodeExecutorBase(ActionNode, abstract=True):
|
|
32
|
-
"""Subclass and set type / display_name /
|
|
32
|
+
"""Subclass and set type / display_name / handler import.
|
|
33
|
+
|
|
34
|
+
Visual metadata (icon + color) lives in ``server/nodes/visuals.json``
|
|
35
|
+
keyed by individual plugin type. The ``_visuals.py`` resolver picks
|
|
36
|
+
each entry up at NodeSpec emit time; no class-level ClassVars needed.
|
|
37
|
+
"""
|
|
33
38
|
|
|
34
|
-
color = "#ffb86c"
|
|
35
39
|
group = ("code", "tool")
|
|
36
40
|
component_kind = "square"
|
|
37
41
|
handles = (
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Legacy single-task Claude Code shim.
|
|
2
|
+
|
|
3
|
+
Kept for back-compat with any caller that still imports
|
|
4
|
+
`get_claude_code_service()`. New code should call
|
|
5
|
+
`services.cli_agent.AICliService.run_batch("claude", ...)` directly via
|
|
6
|
+
the `claude_code_agent` plugin.
|
|
7
|
+
|
|
8
|
+
This module:
|
|
9
|
+
- Builds a single ``ClaudeTaskSpec`` from kwargs
|
|
10
|
+
- Calls ``AICliService.run_batch("claude", ...)``
|
|
11
|
+
- Adapts the ``BatchResult`` back into the dict shape the legacy
|
|
12
|
+
callers expected
|
|
13
|
+
|
|
14
|
+
Eventually deletable once all imports point at ``cli_agent.service``.
|
|
15
|
+
The hardcoded 300s `wait_for` is gone — ``timeout_seconds`` is now per
|
|
16
|
+
task and configurable.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
24
|
+
|
|
25
|
+
from core.config import Settings
|
|
26
|
+
from core.logging import get_logger
|
|
27
|
+
|
|
28
|
+
from services.cli_agent import ClaudeTaskSpec
|
|
29
|
+
from services.cli_agent.service import get_ai_cli_service
|
|
30
|
+
from services.plugin.singleton import ServiceSingleton
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClaudeCodeService(ServiceSingleton):
|
|
36
|
+
"""Thin shim that adapts to the new AICliService."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._session_map: Dict[str, str] = {} # node_id -> session_id
|
|
40
|
+
|
|
41
|
+
async def execute(
|
|
42
|
+
self,
|
|
43
|
+
prompt: str,
|
|
44
|
+
node_id: str = "",
|
|
45
|
+
model: str = "claude-sonnet-4-6",
|
|
46
|
+
cwd: Optional[str] = None,
|
|
47
|
+
allowed_tools: str = "Read,Edit,Bash,Glob,Grep,Write",
|
|
48
|
+
max_turns: int = 10,
|
|
49
|
+
max_budget_usd: float = 5.0,
|
|
50
|
+
system_prompt: Optional[str] = None,
|
|
51
|
+
timeout_seconds: int = 600,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""Run a single Claude Code task. Returns legacy dict shape."""
|
|
54
|
+
if not cwd:
|
|
55
|
+
cwd = os.path.join(Settings().workspace_base_resolved, "default")
|
|
56
|
+
os.makedirs(cwd, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Resume the prior session for this node if we have one
|
|
59
|
+
resume_session_id = self._session_map.get(node_id) if node_id else None
|
|
60
|
+
|
|
61
|
+
task = ClaudeTaskSpec(
|
|
62
|
+
task_id=f"legacy_{node_id}" if node_id else "legacy",
|
|
63
|
+
prompt=prompt,
|
|
64
|
+
model=model,
|
|
65
|
+
max_turns=max_turns,
|
|
66
|
+
max_budget_usd=max_budget_usd,
|
|
67
|
+
allowed_tools=allowed_tools,
|
|
68
|
+
system_prompt=system_prompt,
|
|
69
|
+
timeout_seconds=timeout_seconds,
|
|
70
|
+
resume_session_id=resume_session_id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
svc = get_ai_cli_service()
|
|
74
|
+
workspace_dir = Path(cwd)
|
|
75
|
+
repo_root = self._find_git_repo(workspace_dir)
|
|
76
|
+
|
|
77
|
+
result = await svc.run_batch(
|
|
78
|
+
"claude",
|
|
79
|
+
tasks=[task],
|
|
80
|
+
node_id=node_id or "legacy_node",
|
|
81
|
+
workflow_id="legacy_workflow",
|
|
82
|
+
workspace_dir=workspace_dir,
|
|
83
|
+
broadcaster=None,
|
|
84
|
+
repo_root=repo_root,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not result.tasks:
|
|
88
|
+
raise RuntimeError("AICliService returned empty batch")
|
|
89
|
+
|
|
90
|
+
sr = result.tasks[0]
|
|
91
|
+
if not sr.success:
|
|
92
|
+
raise RuntimeError(sr.error or "claude_code_service: task failed")
|
|
93
|
+
|
|
94
|
+
# Persist session_id for future resume
|
|
95
|
+
if sr.session_id and node_id:
|
|
96
|
+
self._session_map[node_id] = sr.session_id
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"result": sr.response,
|
|
100
|
+
"session_id": sr.session_id or "",
|
|
101
|
+
"total_cost_usd": sr.cost_usd,
|
|
102
|
+
"duration_ms": sr.duration_ms,
|
|
103
|
+
"num_turns": sr.num_turns,
|
|
104
|
+
"usage": {
|
|
105
|
+
"input_tokens": sr.canonical_usage.input_tokens,
|
|
106
|
+
"output_tokens": sr.canonical_usage.output_tokens,
|
|
107
|
+
"cache_creation_input_tokens": sr.canonical_usage.cache_write,
|
|
108
|
+
"cache_read_input_tokens": sr.canonical_usage.cache_read,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def get_session_id(self, node_id: str) -> Optional[str]:
|
|
113
|
+
return self._session_map.get(node_id)
|
|
114
|
+
|
|
115
|
+
def clear_session(self, node_id: str) -> None:
|
|
116
|
+
self._session_map.pop(node_id, None)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _find_git_repo(start: Path) -> Optional[Path]:
|
|
120
|
+
"""Walk up from `start` looking for a `.git` directory."""
|
|
121
|
+
cur = start.resolve()
|
|
122
|
+
for _ in range(8):
|
|
123
|
+
if (cur / ".git").exists():
|
|
124
|
+
return cur
|
|
125
|
+
if cur.parent == cur:
|
|
126
|
+
return None
|
|
127
|
+
cur = cur.parent
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_claude_code_service() -> ClaudeCodeService:
|
|
132
|
+
"""Module-level accessor preserved for legacy callers; delegates to
|
|
133
|
+
the :class:`ServiceSingleton` mixin's ``instance()`` classmethod."""
|
|
134
|
+
return ClaudeCodeService.instance()
|
|
@@ -7,7 +7,7 @@ from typing import List, Literal, Optional
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
10
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
10
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EmbeddingGeneratorParams(BaseModel):
|
|
@@ -83,7 +83,7 @@ class EmbeddingGeneratorNode(ActionNode):
|
|
|
83
83
|
try:
|
|
84
84
|
from langchain_huggingface import HuggingFaceEmbeddings
|
|
85
85
|
except ImportError:
|
|
86
|
-
raise
|
|
86
|
+
raise NodeUserError(
|
|
87
87
|
"HuggingFace embeddings unavailable. "
|
|
88
88
|
"pip install langchain-huggingface sentence-transformers",
|
|
89
89
|
)
|
|
@@ -95,7 +95,7 @@ class EmbeddingGeneratorNode(ActionNode):
|
|
|
95
95
|
from langchain_ollama import OllamaEmbeddings
|
|
96
96
|
embedder = OllamaEmbeddings(model=model)
|
|
97
97
|
else:
|
|
98
|
-
raise
|
|
98
|
+
raise NodeUserError(f"Unknown provider: {provider}")
|
|
99
99
|
|
|
100
100
|
embeddings = await asyncio.to_thread(embedder.embed_documents, texts)
|
|
101
101
|
dimensions = len(embeddings[0]) if embeddings else 0
|
|
@@ -15,7 +15,7 @@ from bs4 import BeautifulSoup
|
|
|
15
15
|
from pydantic import BaseModel, ConfigDict, Field
|
|
16
16
|
|
|
17
17
|
from core.logging import get_logger
|
|
18
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
18
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
21
21
|
|
|
@@ -130,7 +130,7 @@ class HttpScraperNode(ActionNode):
|
|
|
130
130
|
async def scrape(self, ctx: NodeContext, params: HttpScraperParams) -> HttpScraperOutput:
|
|
131
131
|
url = params.url
|
|
132
132
|
if not url:
|
|
133
|
-
raise
|
|
133
|
+
raise NodeUserError("URL is required")
|
|
134
134
|
|
|
135
135
|
iteration_mode = params.iteration_mode
|
|
136
136
|
link_selector = params.link_selector or 'a[href$=".pdf"]'
|
|
@@ -140,7 +140,7 @@ class HttpScraperNode(ActionNode):
|
|
|
140
140
|
urls_to_fetch = []
|
|
141
141
|
if iteration_mode == 'date':
|
|
142
142
|
if not params.start_date or not params.end_date:
|
|
143
|
-
raise
|
|
143
|
+
raise NodeUserError("start_date/end_date required for date mode")
|
|
144
144
|
placeholder = params.date_placeholder or '{date}'
|
|
145
145
|
start = datetime.strptime(params.start_date, "%Y-%m-%d")
|
|
146
146
|
end = datetime.strptime(params.end_date, "%Y-%m-%d")
|
|
@@ -13,7 +13,7 @@ from typing import Any, Dict, Literal, Optional
|
|
|
13
13
|
|
|
14
14
|
from pydantic import BaseModel, ConfigDict, Field
|
|
15
15
|
|
|
16
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
16
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class VectorStoreParams(BaseModel):
|
|
@@ -70,7 +70,7 @@ async def _chroma_op(operation: str, params: Dict[str, Any], collection: str) ->
|
|
|
70
70
|
try:
|
|
71
71
|
import chromadb
|
|
72
72
|
except ImportError:
|
|
73
|
-
raise
|
|
73
|
+
raise NodeUserError("ChromaDB unavailable. pip install chromadb")
|
|
74
74
|
|
|
75
75
|
persist_dir = params.get('persist_dir', './data/vectors')
|
|
76
76
|
client = chromadb.PersistentClient(path=persist_dir)
|
|
@@ -126,7 +126,7 @@ async def _qdrant_op(operation: str, params: Dict[str, Any], collection: str) ->
|
|
|
126
126
|
from qdrant_client import QdrantClient
|
|
127
127
|
from qdrant_client.models import Distance, PointStruct, VectorParams
|
|
128
128
|
except ImportError:
|
|
129
|
-
raise
|
|
129
|
+
raise NodeUserError("Qdrant client unavailable. pip install qdrant-client")
|
|
130
130
|
|
|
131
131
|
url = params.get('qdrant_url', 'http://localhost:6333')
|
|
132
132
|
client = QdrantClient(url=url)
|
|
@@ -188,7 +188,7 @@ async def _pinecone_op(operation: str, params: Dict[str, Any], collection: str)
|
|
|
188
188
|
|
|
189
189
|
api_key = params.get('pinecone_api_key', '')
|
|
190
190
|
if not api_key:
|
|
191
|
-
raise
|
|
191
|
+
raise NodeUserError("Pinecone API key required")
|
|
192
192
|
|
|
193
193
|
pc = Pinecone(api_key=api_key)
|
|
194
194
|
index = pc.Index(collection)
|
|
@@ -271,7 +271,7 @@ class VectorStoreNode(ActionNode):
|
|
|
271
271
|
elif backend == 'pinecone':
|
|
272
272
|
result = await _pinecone_op(operation, p, collection)
|
|
273
273
|
else:
|
|
274
|
-
raise
|
|
274
|
+
raise NodeUserError(f"Unknown backend: {backend_raw}")
|
|
275
275
|
|
|
276
276
|
result['backend'] = backend
|
|
277
277
|
result['collection_name'] = collection
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
"""Plugins for the 'email' palette group.
|
|
1
|
+
"""Plugins for the 'email' palette group.
|
|
2
|
+
|
|
3
|
+
Self-registers the trigger filter builder for ``emailReceive`` so the
|
|
4
|
+
central ``services/event_waiter.py`` carries no plugin-specific code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from services.event_waiter import register_filter_builder
|
|
8
|
+
|
|
9
|
+
from ._filters import build_filter as build_email_filter
|
|
10
|
+
|
|
11
|
+
register_filter_builder("emailReceive", build_email_filter)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Email event-trigger filter builder (Wave 11.I, milestone K).
|
|
2
|
+
|
|
3
|
+
Moved verbatim from ``services/event_waiter.build_email_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 for email events (Himalaya IMAP polling)."""
|
|
13
|
+
folder_filter = params.get('folder', 'INBOX')
|
|
14
|
+
|
|
15
|
+
def matches(data: Dict) -> bool:
|
|
16
|
+
if folder_filter and folder_filter != 'all':
|
|
17
|
+
if data.get('folder', '') != folder_filter:
|
|
18
|
+
return False
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
return matches
|
|
@@ -14,24 +14,20 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
from core.logging import get_logger
|
|
17
|
+
from services.plugin.singleton import ServiceSingleton
|
|
17
18
|
|
|
18
19
|
logger = get_logger(__name__)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class HimalayaService:
|
|
22
|
-
"""Manages Himalaya CLI configuration and execution.
|
|
22
|
+
class HimalayaService(ServiceSingleton):
|
|
23
|
+
"""Manages Himalaya CLI configuration and execution.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Inherits ``instance`` / ``reset_instance`` from
|
|
26
|
+
:class:`ServiceSingleton`."""
|
|
25
27
|
|
|
26
28
|
def __init__(self):
|
|
27
29
|
self._binary_path: Optional[str] = None
|
|
28
30
|
|
|
29
|
-
@classmethod
|
|
30
|
-
def get_instance(cls) -> "HimalayaService":
|
|
31
|
-
if cls._instance is None:
|
|
32
|
-
cls._instance = cls()
|
|
33
|
-
return cls._instance
|
|
34
|
-
|
|
35
31
|
async def ensure_binary(self) -> str:
|
|
36
32
|
"""Detect himalaya binary in PATH. Returns path or raises."""
|
|
37
33
|
if self._binary_path:
|
|
@@ -265,4 +261,4 @@ class HimalayaService:
|
|
|
265
261
|
|
|
266
262
|
def get_himalaya_service() -> HimalayaService:
|
|
267
263
|
"""Get singleton instance."""
|
|
268
|
-
return HimalayaService.
|
|
264
|
+
return HimalayaService.instance()
|
|
@@ -8,11 +8,12 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, Dict, Optional, Set
|
|
9
9
|
|
|
10
10
|
from core.logging import get_logger
|
|
11
|
+
from services.plugin.singleton import ServiceSingleton
|
|
11
12
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
13
14
|
|
|
14
15
|
_CONFIG: Optional[Dict] = None
|
|
15
|
-
_CONFIG_PATH = Path(__file__).
|
|
16
|
+
_CONFIG_PATH = Path(__file__).resolve().parents[2] / "config" / "email_providers.json"
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _load_config() -> Dict:
|
|
@@ -23,14 +24,9 @@ def _load_config() -> Dict:
|
|
|
23
24
|
return _CONFIG
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
class EmailService:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@classmethod
|
|
30
|
-
def get_instance(cls) -> "EmailService":
|
|
31
|
-
if cls._instance is None:
|
|
32
|
-
cls._instance = cls()
|
|
33
|
-
return cls._instance
|
|
27
|
+
class EmailService(ServiceSingleton):
|
|
28
|
+
"""Plugin-owned email orchestrator. Inherits ``instance`` /
|
|
29
|
+
``reset_instance`` from :class:`ServiceSingleton`."""
|
|
34
30
|
|
|
35
31
|
@property
|
|
36
32
|
def config(self) -> Dict:
|
|
@@ -46,7 +42,7 @@ class EmailService:
|
|
|
46
42
|
|
|
47
43
|
@property
|
|
48
44
|
def himalaya(self):
|
|
49
|
-
from
|
|
45
|
+
from ._himalaya import get_himalaya_service
|
|
50
46
|
return get_himalaya_service()
|
|
51
47
|
|
|
52
48
|
def _provider_preset(self, name: str) -> Dict:
|
|
@@ -61,8 +57,8 @@ class EmailService:
|
|
|
61
57
|
Stored custom keys (email_imap_host, email_smtp_port, etc.) are used when
|
|
62
58
|
the provider is 'custom' or when a preset field is empty.
|
|
63
59
|
"""
|
|
64
|
-
from
|
|
65
|
-
auth =
|
|
60
|
+
from services.plugin.deps import get_auth_service
|
|
61
|
+
auth = get_auth_service()
|
|
66
62
|
|
|
67
63
|
provider = params.get("provider") or await auth.get_api_key("email_provider") or self.defaults.get("provider")
|
|
68
64
|
preset = self._provider_preset(provider)
|
|
@@ -184,4 +180,4 @@ class EmailService:
|
|
|
184
180
|
|
|
185
181
|
|
|
186
182
|
def get_email_service() -> EmailService:
|
|
187
|
-
return EmailService.
|
|
183
|
+
return EmailService.instance()
|
|
@@ -120,5 +120,5 @@ class EmailReadNode(ActionNode):
|
|
|
120
120
|
@Operation("query", cost={"service": "email", "action": "imap", "count": 1})
|
|
121
121
|
async def query(self, ctx: NodeContext, params: EmailReadParams) -> Any:
|
|
122
122
|
# Body inlined from handlers/email.py (Wave 11.D.1).
|
|
123
|
-
from
|
|
123
|
+
from ._service import get_email_service
|
|
124
124
|
return await get_email_service().read(params.model_dump())
|
|
@@ -6,11 +6,13 @@ IMAP polling for new mail via Himalaya CLI. Thin delegation to
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import Any, Dict, Literal, Optional
|
|
9
|
+
from typing import Any, Dict, Literal, Optional, Set
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, ConfigDict, Field
|
|
12
12
|
|
|
13
|
-
from services.plugin import
|
|
13
|
+
from services.plugin import (
|
|
14
|
+
NodeContext, Operation, PollingTriggerNode, TaskQueue,
|
|
15
|
+
)
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class EmailReceiveParams(BaseModel):
|
|
@@ -35,23 +37,70 @@ class EmailReceiveOutput(BaseModel):
|
|
|
35
37
|
model_config = ConfigDict(extra="allow")
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
class EmailReceiveNode(
|
|
40
|
+
class EmailReceiveNode(PollingTriggerNode):
|
|
39
41
|
type = "emailReceive"
|
|
40
42
|
display_name = "Email Receive"
|
|
41
43
|
subtitle = "IMAP Polling"
|
|
42
44
|
group = ("email", "trigger")
|
|
43
45
|
description = "Polling trigger for new emails via IMAP"
|
|
44
46
|
component_kind = "trigger"
|
|
47
|
+
# Wave 11.I, milestone K: ``event_type`` ClassVar lets
|
|
48
|
+
# ``event_waiter._auto_populate_from_plugins`` backfill
|
|
49
|
+
# TRIGGER_REGISTRY without a hardcoded entry in event_waiter.
|
|
50
|
+
event_type = "email_received"
|
|
45
51
|
handles = (
|
|
46
52
|
{"name": "output-main", "kind": "output", "position": "right",
|
|
47
53
|
"label": "Output", "role": "main"},
|
|
48
54
|
)
|
|
49
55
|
task_queue = TaskQueue.TRIGGERS_POLL
|
|
50
|
-
|
|
56
|
+
# Email keeps its 30s lower bound (legacy floor in
|
|
57
|
+
# config/email_providers.json). Gmail uses the default (10, 3600).
|
|
58
|
+
poll_interval_clamp = (30, 3600)
|
|
51
59
|
|
|
52
60
|
Params = EmailReceiveParams
|
|
53
61
|
Output = EmailReceiveOutput
|
|
54
62
|
|
|
63
|
+
# ---- PollingTriggerNode hooks (deployment-mode loop) -------------
|
|
64
|
+
#
|
|
65
|
+
# The Run-button path lives in ``execute()`` below and stays
|
|
66
|
+
# bespoke: it broadcasts ``waiting`` status, dispatches via
|
|
67
|
+
# event_waiter, and returns after the first new email. The
|
|
68
|
+
# deployment loop owned by ``PollingTriggerNode`` drains
|
|
69
|
+
# continuously via the deployment manager's queue and uses the
|
|
70
|
+
# four hooks below.
|
|
71
|
+
|
|
72
|
+
async def setup_service(self, params: Dict[str, Any]) -> Any:
|
|
73
|
+
from ._service import get_email_service
|
|
74
|
+
|
|
75
|
+
svc = get_email_service()
|
|
76
|
+
creds = await svc.resolve_credentials(params)
|
|
77
|
+
cfg = svc.resolve_poll_params(params)
|
|
78
|
+
return svc, creds, cfg["folder"], cfg.get("mark_as_read", False)
|
|
79
|
+
|
|
80
|
+
async def fetch_ids(self, service: Any, params: Dict[str, Any]) -> Set[str]:
|
|
81
|
+
svc, creds, folder, _mark = service
|
|
82
|
+
return await svc.poll_ids(creds, folder)
|
|
83
|
+
|
|
84
|
+
async def fetch_detail(
|
|
85
|
+
self, service: Any, msg_id: str, params: Dict[str, Any]
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
svc, creds, folder, _mark = service
|
|
88
|
+
return await svc.fetch_detail(creds, msg_id, folder)
|
|
89
|
+
|
|
90
|
+
async def post_emit(
|
|
91
|
+
self, service: Any, msg_id: str, params: Dict[str, Any]
|
|
92
|
+
) -> None:
|
|
93
|
+
svc, creds, folder, mark = service
|
|
94
|
+
if not mark:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
d = svc.defaults
|
|
98
|
+
await svc.himalaya.flag_message(
|
|
99
|
+
creds, msg_id, d.get("flag"), d.get("flag_action"), folder,
|
|
100
|
+
)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
55
104
|
async def execute(
|
|
56
105
|
self,
|
|
57
106
|
node_id: str,
|
|
@@ -68,7 +117,7 @@ class EmailReceiveNode(TriggerNode):
|
|
|
68
117
|
import asyncio
|
|
69
118
|
import time
|
|
70
119
|
from datetime import datetime
|
|
71
|
-
from
|
|
120
|
+
from ._service import get_email_service
|
|
72
121
|
from services.status_broadcaster import get_status_broadcaster
|
|
73
122
|
from services import event_waiter
|
|
74
123
|
|
|
@@ -54,5 +54,5 @@ class EmailSendNode(ActionNode):
|
|
|
54
54
|
@Operation("send", cost={"service": "email", "action": "send", "count": 1})
|
|
55
55
|
async def send(self, ctx: NodeContext, params: EmailSendParams) -> Any:
|
|
56
56
|
# Body inlined from handlers/email.py (Wave 11.D.1).
|
|
57
|
-
from
|
|
57
|
+
from ._service import get_email_service
|
|
58
58
|
return await get_email_service().send(params.model_dump(by_alias=False))
|
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from typing import Any, Optional
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
9
|
|
|
9
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
10
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Bash chain-operators (` && ` / ` || `) are explicitly rejected by the
|
|
14
|
+
# Nushell parser. Detect them up-front so the LLM gets a corrective
|
|
15
|
+
# message ("use `;` or `try { … }`") instead of nu's
|
|
16
|
+
# ``shell_andand`` / ``shell_oror`` parse error two layers down.
|
|
17
|
+
# Surrounding spaces ensure we don't flag valid nu syntax accidentally
|
|
18
|
+
# (closure params `|x|` never carry spaces around the pipe pair).
|
|
19
|
+
_BASH_CHAIN_RE = re.compile(r"\s(\&\&|\|\|)\s")
|
|
10
20
|
|
|
11
21
|
|
|
12
22
|
class ShellParams(BaseModel):
|
|
@@ -51,6 +61,19 @@ class ShellNode(ActionNode):
|
|
|
51
61
|
from ._backend import get_backend
|
|
52
62
|
|
|
53
63
|
log = get_logger(__name__)
|
|
64
|
+
|
|
65
|
+
# Pre-flight: catch the most common bash-style chain mistake before
|
|
66
|
+
# Nushell's parser does, so the LLM sees an actionable hint instead
|
|
67
|
+
# of ``nu::parser::shell_andand``. Documented in
|
|
68
|
+
# ``server/skills/terminal/shell-skill/SKILL.md``.
|
|
69
|
+
if (m := _BASH_CHAIN_RE.search(params.command)):
|
|
70
|
+
op = m.group(1)
|
|
71
|
+
replacement = "; (sequential)" if op == "&&" else "try { … } catch { … }"
|
|
72
|
+
raise NodeUserError(
|
|
73
|
+
f"Nushell does not support `{op}`. Use `{replacement}` instead. "
|
|
74
|
+
"See shell-skill: https://www.nushell.sh/book/control_flow.html"
|
|
75
|
+
)
|
|
76
|
+
|
|
54
77
|
backend = get_backend(params.model_dump(), ctx.raw)
|
|
55
78
|
# "non-blocking" here only meant the asyncio event loop isn't
|
|
56
79
|
# blocked (the call is offloaded via ``to_thread``). The
|