@clawpump/claw-agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2177 -3162
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +215 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6190 -973
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +4 -1
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +300 -684
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
|
@@ -20,24 +20,40 @@ Required environment variables:
|
|
|
20
20
|
(default: ws://127.0.0.1:5225)
|
|
21
21
|
|
|
22
22
|
Optional environment variables:
|
|
23
|
-
SIMPLEX_ALLOWED_USERS Comma-separated
|
|
23
|
+
SIMPLEX_ALLOWED_USERS Comma-separated allowlist. Each entry may be
|
|
24
|
+
either a numeric contactId (stable across
|
|
25
|
+
renames; visible via `/contacts` in the CLI)
|
|
26
|
+
or a contact display name (what the SimpleX
|
|
27
|
+
UI shows). Both forms are accepted.
|
|
24
28
|
SIMPLEX_ALLOW_ALL_USERS Set 'true' to allow all contacts
|
|
29
|
+
SIMPLEX_AUTO_ACCEPT Set 'false' to disable contact-request auto-accept
|
|
30
|
+
(default: 'true')
|
|
31
|
+
SIMPLEX_GROUP_ALLOWED Comma-separated group IDs to monitor, or '*'
|
|
32
|
+
for any group. Omit to disable groups entirely.
|
|
25
33
|
SIMPLEX_HOME_CHANNEL Default contact/group ID for cron delivery
|
|
26
34
|
SIMPLEX_HOME_CHANNEL_NAME Human label for the home channel
|
|
35
|
+
HERMES_SIMPLEX_TEXT_BATCH_DELAY
|
|
36
|
+
Quiet-period seconds (default: 0.8) used to
|
|
37
|
+
concatenate rapid-fire inbound text messages
|
|
38
|
+
into a single MessageEvent — same pattern as
|
|
39
|
+
Telegram's text batching.
|
|
27
40
|
|
|
28
41
|
The ``websockets`` Python package is imported lazily — the plugin is
|
|
29
|
-
discoverable and
|
|
42
|
+
discoverable and ``hermes setup`` can describe it even when websockets is
|
|
30
43
|
not installed. ``check_requirements()`` returns False until the package
|
|
31
44
|
is present, so the gateway will not attempt to instantiate the adapter.
|
|
32
45
|
"""
|
|
33
46
|
|
|
34
47
|
import asyncio
|
|
48
|
+
import base64
|
|
35
49
|
import json
|
|
36
50
|
import logging
|
|
37
51
|
import os
|
|
38
52
|
import random
|
|
53
|
+
import re
|
|
39
54
|
import time
|
|
40
55
|
from datetime import datetime, timezone
|
|
56
|
+
from pathlib import Path
|
|
41
57
|
from typing import Any, Dict, List, Optional
|
|
42
58
|
|
|
43
59
|
# Lazy import: BasePlatformAdapter and friends live in the main repo.
|
|
@@ -49,9 +65,6 @@ from gateway.platforms.base import (
|
|
|
49
65
|
MessageEvent,
|
|
50
66
|
MessageType,
|
|
51
67
|
SendResult,
|
|
52
|
-
cache_image_from_bytes,
|
|
53
|
-
cache_audio_from_bytes,
|
|
54
|
-
cache_document_from_bytes,
|
|
55
68
|
)
|
|
56
69
|
|
|
57
70
|
logger = logging.getLogger(__name__)
|
|
@@ -59,12 +72,11 @@ logger = logging.getLogger(__name__)
|
|
|
59
72
|
# ---------------------------------------------------------------------------
|
|
60
73
|
# Constants
|
|
61
74
|
# ---------------------------------------------------------------------------
|
|
62
|
-
MAX_MESSAGE_LENGTH =
|
|
63
|
-
TYPING_INTERVAL = 10.0
|
|
75
|
+
MAX_MESSAGE_LENGTH = 8000 # SimpleX has no hard limit; chunk for sanity
|
|
64
76
|
WS_RETRY_DELAY_INITIAL = 2.0
|
|
65
77
|
WS_RETRY_DELAY_MAX = 60.0
|
|
66
78
|
HEALTH_CHECK_INTERVAL = 30.0
|
|
67
|
-
HEALTH_CHECK_STALE_THRESHOLD =
|
|
79
|
+
HEALTH_CHECK_STALE_THRESHOLD = 300.0
|
|
68
80
|
|
|
69
81
|
# Correlation ID prefix for requests we send so we can ignore our own echoes.
|
|
70
82
|
_CORR_PREFIX = "hermes-"
|
|
@@ -79,6 +91,16 @@ def _parse_comma_list(value: str) -> List[str]:
|
|
|
79
91
|
return [v.strip() for v in value.split(",") if v.strip()]
|
|
80
92
|
|
|
81
93
|
|
|
94
|
+
def _redact_id(contact_id: str) -> str:
|
|
95
|
+
"""Redact a contact/group ID for logging."""
|
|
96
|
+
if not contact_id:
|
|
97
|
+
return "<none>"
|
|
98
|
+
s = str(contact_id)
|
|
99
|
+
if len(s) <= 4:
|
|
100
|
+
return s
|
|
101
|
+
return s[:2] + "**" + s[-2:]
|
|
102
|
+
|
|
103
|
+
|
|
82
104
|
def _guess_extension(data: bytes) -> str:
|
|
83
105
|
"""Guess file extension from magic bytes."""
|
|
84
106
|
if data[:4] == b"\x89PNG":
|
|
@@ -105,7 +127,7 @@ def _is_image_ext(ext: str) -> bool:
|
|
|
105
127
|
|
|
106
128
|
|
|
107
129
|
def _is_audio_ext(ext: str) -> bool:
|
|
108
|
-
return ext.lower() in {".mp3", ".wav", ".ogg", ".m4a", ".aac"}
|
|
130
|
+
return ext.lower() in {".mp3", ".wav", ".ogg", ".m4a", ".aac", ".opus"}
|
|
109
131
|
|
|
110
132
|
|
|
111
133
|
# ---------------------------------------------------------------------------
|
|
@@ -119,6 +141,8 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
119
141
|
``ctx.register_platform()`` in :func:`register`.
|
|
120
142
|
"""
|
|
121
143
|
|
|
144
|
+
MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH
|
|
145
|
+
|
|
122
146
|
def __init__(self, config: PlatformConfig, **kwargs):
|
|
123
147
|
platform = Platform("simplex")
|
|
124
148
|
super().__init__(config=config, platform=platform)
|
|
@@ -126,11 +150,27 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
126
150
|
extra = getattr(config, "extra", {}) or {}
|
|
127
151
|
self.ws_url = extra.get("ws_url", "ws://127.0.0.1:5225").rstrip("/")
|
|
128
152
|
|
|
153
|
+
# Contact-request auto-accept (on by default — matches the way most
|
|
154
|
+
# bot deployments expect to behave). Read from env first, then fall
|
|
155
|
+
# back to the value seeded by ``_env_enablement``.
|
|
156
|
+
env_auto = os.getenv("SIMPLEX_AUTO_ACCEPT")
|
|
157
|
+
if env_auto is not None:
|
|
158
|
+
self.auto_accept = env_auto.strip().lower() not in {"0", "false", "no", ""}
|
|
159
|
+
else:
|
|
160
|
+
self.auto_accept = bool(extra.get("auto_accept", True))
|
|
161
|
+
|
|
162
|
+
# Group allowlist. Without ``SIMPLEX_GROUP_ALLOWED``, group messages
|
|
163
|
+
# are ignored entirely (safer default — a bot in a group otherwise
|
|
164
|
+
# processes every member's traffic). Use ``*`` to accept any group.
|
|
165
|
+
group_allowed_str = os.getenv("SIMPLEX_GROUP_ALLOWED", "") or extra.get(
|
|
166
|
+
"group_allowed", ""
|
|
167
|
+
)
|
|
168
|
+
self.group_allow_from = set(_parse_comma_list(group_allowed_str))
|
|
169
|
+
|
|
129
170
|
# Running state
|
|
130
171
|
self._ws = None # websockets connection
|
|
131
172
|
self._ws_task: Optional[asyncio.Task] = None
|
|
132
173
|
self._health_task: Optional[asyncio.Task] = None
|
|
133
|
-
self._typing_tasks: Dict[str, asyncio.Task] = {}
|
|
134
174
|
self._running = False
|
|
135
175
|
self._last_ws_activity = 0.0
|
|
136
176
|
|
|
@@ -138,7 +178,31 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
138
178
|
self._pending_corr_ids: set = set()
|
|
139
179
|
self._max_pending_corr = 200
|
|
140
180
|
|
|
141
|
-
|
|
181
|
+
# File transfers awaiting rcvFileComplete (keyed by fileId). Populated
|
|
182
|
+
# when a newChatItems event carries an unfinished rcvFileTransfer,
|
|
183
|
+
# consumed when the file finishes downloading.
|
|
184
|
+
self._pending_file_transfers: Dict[int, dict] = {}
|
|
185
|
+
|
|
186
|
+
# Correlation tracking for ``_send_command``. Separate from
|
|
187
|
+
# ``_pending_corr_ids`` (which is the upstream cosmetic echo filter)
|
|
188
|
+
# because we actually await responses to commands we send.
|
|
189
|
+
self._pending_responses: Dict[str, asyncio.Future] = {}
|
|
190
|
+
self._corr_counter = 0
|
|
191
|
+
|
|
192
|
+
# Text message batching — concatenate rapid-fire messages into one
|
|
193
|
+
# event before dispatching, mirroring Telegram's batching.
|
|
194
|
+
self._text_batch_delay = float(
|
|
195
|
+
os.getenv("HERMES_SIMPLEX_TEXT_BATCH_DELAY", "0.8")
|
|
196
|
+
)
|
|
197
|
+
self._pending_text_batches: Dict[str, MessageEvent] = {}
|
|
198
|
+
self._pending_text_batch_tasks: Dict[str, asyncio.Task] = {}
|
|
199
|
+
|
|
200
|
+
logger.info(
|
|
201
|
+
"SimpleX adapter initialized: url=%s auto_accept=%s groups=%s",
|
|
202
|
+
self.ws_url,
|
|
203
|
+
self.auto_accept,
|
|
204
|
+
"enabled" if self.group_allow_from else "disabled",
|
|
205
|
+
)
|
|
142
206
|
|
|
143
207
|
# ------------------------------------------------------------------
|
|
144
208
|
# Lifecycle
|
|
@@ -173,6 +237,8 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
173
237
|
self._ws_task = asyncio.create_task(self._ws_listener())
|
|
174
238
|
self._health_task = asyncio.create_task(self._health_monitor())
|
|
175
239
|
|
|
240
|
+
if hasattr(self, "_mark_connected"):
|
|
241
|
+
self._mark_connected()
|
|
176
242
|
logger.info("SimpleX: connected to %s", self.ws_url)
|
|
177
243
|
return True
|
|
178
244
|
|
|
@@ -194,10 +260,6 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
194
260
|
except asyncio.CancelledError:
|
|
195
261
|
pass
|
|
196
262
|
|
|
197
|
-
for task in self._typing_tasks.values():
|
|
198
|
-
task.cancel()
|
|
199
|
-
self._typing_tasks.clear()
|
|
200
|
-
|
|
201
263
|
if self._ws:
|
|
202
264
|
try:
|
|
203
265
|
await self._ws.close()
|
|
@@ -205,6 +267,21 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
205
267
|
pass
|
|
206
268
|
self._ws = None
|
|
207
269
|
|
|
270
|
+
# Cancel pending text-batch flush timers
|
|
271
|
+
for task in list(self._pending_text_batch_tasks.values()):
|
|
272
|
+
if not task.done():
|
|
273
|
+
task.cancel()
|
|
274
|
+
self._pending_text_batch_tasks.clear()
|
|
275
|
+
self._pending_text_batches.clear()
|
|
276
|
+
|
|
277
|
+
# Cancel pending command futures
|
|
278
|
+
for fut in self._pending_responses.values():
|
|
279
|
+
if not fut.done():
|
|
280
|
+
fut.cancel()
|
|
281
|
+
self._pending_responses.clear()
|
|
282
|
+
|
|
283
|
+
if hasattr(self, "_mark_disconnected"):
|
|
284
|
+
self._mark_disconnected()
|
|
208
285
|
logger.info("SimpleX: disconnected")
|
|
209
286
|
|
|
210
287
|
# ------------------------------------------------------------------
|
|
@@ -214,7 +291,7 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
214
291
|
async def _ws_listener(self) -> None:
|
|
215
292
|
"""Maintain a persistent WebSocket connection to the daemon."""
|
|
216
293
|
import websockets as _wsclient
|
|
217
|
-
|
|
294
|
+
from websockets.exceptions import ConnectionClosed
|
|
218
295
|
|
|
219
296
|
backoff = WS_RETRY_DELAY_INITIAL
|
|
220
297
|
|
|
@@ -225,6 +302,7 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
225
302
|
self.ws_url,
|
|
226
303
|
ping_interval=20,
|
|
227
304
|
ping_timeout=20,
|
|
305
|
+
close_timeout=10,
|
|
228
306
|
) as ws:
|
|
229
307
|
self._ws = ws
|
|
230
308
|
backoff = WS_RETRY_DELAY_INITIAL
|
|
@@ -245,10 +323,11 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
245
323
|
|
|
246
324
|
except asyncio.CancelledError:
|
|
247
325
|
break
|
|
248
|
-
except
|
|
326
|
+
except ConnectionClosed as e:
|
|
249
327
|
if self._running:
|
|
250
328
|
logger.warning(
|
|
251
|
-
"SimpleX WS:
|
|
329
|
+
"SimpleX WS: connection closed: %s (reconnecting in %.0fs)",
|
|
330
|
+
e, backoff,
|
|
252
331
|
)
|
|
253
332
|
except Exception as e:
|
|
254
333
|
if self._running:
|
|
@@ -280,7 +359,6 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
280
359
|
await asyncio.sleep(HEALTH_CHECK_INTERVAL)
|
|
281
360
|
if not self._running:
|
|
282
361
|
break
|
|
283
|
-
|
|
284
362
|
elapsed = time.time() - self._last_ws_activity
|
|
285
363
|
if elapsed > HEALTH_CHECK_STALE_THRESHOLD:
|
|
286
364
|
logger.debug("SimpleX: WS application-idle for %.0fs", elapsed)
|
|
@@ -296,121 +374,248 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
296
374
|
# Older/examples may put the response fields at top-level. Normalize
|
|
297
375
|
# both forms before dispatching, otherwise inbound chatItems are lost.
|
|
298
376
|
resp = event.get("resp") if isinstance(event.get("resp"), dict) else event
|
|
299
|
-
|
|
377
|
+
corr_id = event.get("corrId")
|
|
300
378
|
|
|
301
|
-
#
|
|
302
|
-
corr_id
|
|
303
|
-
|
|
379
|
+
# Handle correlated responses (replies to our own commands)
|
|
380
|
+
if corr_id and corr_id in self._pending_responses:
|
|
381
|
+
fut = self._pending_responses.pop(corr_id)
|
|
382
|
+
if not fut.done():
|
|
383
|
+
fut.set_result(resp)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
# Cosmetic echo filter: prefixed corrIds are ours but didn't make it
|
|
387
|
+
# into _pending_responses (e.g. fire-and-forget).
|
|
388
|
+
if corr_id and isinstance(corr_id, str) and corr_id.startswith(_CORR_PREFIX):
|
|
304
389
|
self._pending_corr_ids.discard(corr_id)
|
|
305
390
|
return
|
|
306
391
|
|
|
392
|
+
resp_type = resp.get("type") or event.get("type", "")
|
|
393
|
+
|
|
394
|
+
# Auto-accept contact requests
|
|
395
|
+
if resp_type == "contactRequest" and self.auto_accept:
|
|
396
|
+
contact_req = resp.get("contactRequest", {}) or {}
|
|
397
|
+
contact_req_id = contact_req.get("contactRequestId")
|
|
398
|
+
if contact_req_id is not None:
|
|
399
|
+
logger.info(
|
|
400
|
+
"SimpleX: auto-accepting contact request %s",
|
|
401
|
+
_redact_id(str(contact_req_id)),
|
|
402
|
+
)
|
|
403
|
+
await self._send_command(f"/accept {contact_req_id}")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
# Early file-descriptor ready: simplex fires this before newChatItems
|
|
407
|
+
# for some file types (especially large files and voice messages
|
|
408
|
+
# transferred via XFTP). Send /freceive immediately so the download
|
|
409
|
+
# starts; the chat item arrives in a subsequent newChatItems event.
|
|
410
|
+
if resp_type == "rcvFileDescrReady":
|
|
411
|
+
rcv_file = resp.get("rcvFileTransfer", {}) or {}
|
|
412
|
+
file_id = rcv_file.get("fileId") if isinstance(rcv_file, dict) else None
|
|
413
|
+
if file_id is not None:
|
|
414
|
+
logger.debug(
|
|
415
|
+
"SimpleX: rcvFileDescrReady for fileId=%s — sending /freceive",
|
|
416
|
+
file_id,
|
|
417
|
+
)
|
|
418
|
+
await self._send_fire_and_forget(f"/freceive {file_id}")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# New messages — simplex-chat sends "newChatItems" with an array
|
|
422
|
+
if resp_type == "newChatItems":
|
|
423
|
+
chat_items = resp.get("chatItems", []) or []
|
|
424
|
+
if not isinstance(chat_items, list):
|
|
425
|
+
chat_items = [chat_items]
|
|
426
|
+
for item in chat_items:
|
|
427
|
+
try:
|
|
428
|
+
await self._handle_chat_item(item)
|
|
429
|
+
except Exception:
|
|
430
|
+
logger.exception("SimpleX: error processing chat item")
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
# Singular variant — some daemon versions emit this
|
|
307
434
|
if resp_type == "newChatItem":
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for item_wrapper in items:
|
|
313
|
-
await self._handle_new_chat_item(item_wrapper)
|
|
314
|
-
# Ignore all other event types (delivery receipts, contact updates, etc.)
|
|
315
|
-
|
|
316
|
-
async def _handle_new_chat_item(self, wrapper: dict) -> None:
|
|
317
|
-
"""Process a single newChatItem event into a MessageEvent."""
|
|
318
|
-
# The daemon wraps the chat item differently depending on version;
|
|
319
|
-
# normalise both layouts.
|
|
320
|
-
chat_info = wrapper.get("chatInfo") or wrapper.get("chat") or {}
|
|
321
|
-
chat_item = wrapper.get("chatItem") or wrapper.get("item") or {}
|
|
322
|
-
|
|
323
|
-
# Only process messages (not calls, deleted items, etc.)
|
|
324
|
-
item_content = chat_item.get("content") or {}
|
|
325
|
-
msg_content = item_content.get("msgContent") or {}
|
|
326
|
-
if not msg_content:
|
|
435
|
+
try:
|
|
436
|
+
await self._handle_chat_item(resp)
|
|
437
|
+
except Exception:
|
|
438
|
+
logger.exception("SimpleX: error processing chat item")
|
|
327
439
|
return
|
|
328
440
|
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
441
|
+
# File transfer completion — deliver any deferred chat item
|
|
442
|
+
if resp_type == "rcvFileComplete":
|
|
443
|
+
chat_item = resp.get("chatItem", {}) or {}
|
|
444
|
+
chat_item_data = chat_item.get("chatItem", {}) or {}
|
|
445
|
+
file_info = chat_item_data.get("file", {}) or {}
|
|
446
|
+
file_id = file_info.get("fileId") if isinstance(file_info, dict) else None
|
|
447
|
+
if file_id is not None and file_id in self._pending_file_transfers:
|
|
448
|
+
pending = self._pending_file_transfers.pop(file_id)
|
|
449
|
+
file_source = file_info.get("fileSource", {}) or {}
|
|
450
|
+
file_path = (
|
|
451
|
+
file_source.get("filePath")
|
|
452
|
+
if isinstance(file_source, dict)
|
|
453
|
+
else None
|
|
454
|
+
)
|
|
455
|
+
if file_path:
|
|
456
|
+
pending_item_data = pending.get("chatItem", {}) or {}
|
|
457
|
+
pending_item_data.setdefault("file", {})["fileSource"] = {
|
|
458
|
+
"filePath": file_path
|
|
459
|
+
}
|
|
460
|
+
pending["chatItem"] = pending_item_data
|
|
461
|
+
try:
|
|
462
|
+
await self._handle_chat_item(pending)
|
|
463
|
+
except Exception:
|
|
464
|
+
logger.exception(
|
|
465
|
+
"SimpleX: error processing deferred file message"
|
|
466
|
+
)
|
|
333
467
|
return
|
|
334
468
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
is_group = chat_type_raw in {"group", "groupInfo"}
|
|
469
|
+
if resp_type:
|
|
470
|
+
logger.debug("SimpleX: unhandled event type: %s", resp_type)
|
|
338
471
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
contact_info.get("displayName")
|
|
350
|
-
or contact_info.get("localDisplayName")
|
|
351
|
-
or contact_id
|
|
352
|
-
)
|
|
353
|
-
# Replies must be routed by SimpleX CLI display name, while
|
|
354
|
-
# authorization should still use the stable numeric contactId.
|
|
355
|
-
chat_id = contact_name or contact_id
|
|
356
|
-
chat_name = contact_name
|
|
472
|
+
async def _handle_chat_item(self, chat_item: dict) -> None:
|
|
473
|
+
"""Process a single chat item from a newChatItems event."""
|
|
474
|
+
chat_info = chat_item.get("chatInfo", {}) or {}
|
|
475
|
+
chat_item_data = chat_item.get("chatItem", {}) or {}
|
|
476
|
+
|
|
477
|
+
chat_type = chat_info.get("type", "")
|
|
478
|
+
|
|
479
|
+
meta = chat_item_data.get("meta", {}) or {}
|
|
480
|
+
content = chat_item_data.get("content", {}) or {}
|
|
481
|
+
msg_content = content.get("msgContent", {}) or {}
|
|
357
482
|
|
|
358
|
-
|
|
359
|
-
|
|
483
|
+
# Filter out our own messages
|
|
484
|
+
item_direction = chat_item_data.get("chatDir", {}) or {}
|
|
485
|
+
direction_type = (
|
|
486
|
+
item_direction.get("type", "") if isinstance(item_direction, dict) else ""
|
|
487
|
+
)
|
|
488
|
+
if direction_type in ("directSnd", "groupSnd"):
|
|
360
489
|
return
|
|
361
490
|
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
if
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
)
|
|
491
|
+
# Only process received messages
|
|
492
|
+
content_type = content.get("type", "") if isinstance(content, dict) else ""
|
|
493
|
+
if content_type != "rcvMsgContent":
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# Text content
|
|
497
|
+
text = ""
|
|
498
|
+
msg_type_str = (
|
|
499
|
+
msg_content.get("type", "") if isinstance(msg_content, dict) else ""
|
|
500
|
+
)
|
|
501
|
+
if msg_type_str in ("text", "file", "image", "voice", "link", "video"):
|
|
502
|
+
text = msg_content.get("text", "")
|
|
503
|
+
|
|
504
|
+
if not text and msg_type_str not in ("image", "file", "voice"):
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
# Sender + chat IDs
|
|
508
|
+
sender_id = ""
|
|
509
|
+
sender_name = ""
|
|
510
|
+
chat_id = ""
|
|
511
|
+
is_group = False
|
|
512
|
+
|
|
513
|
+
if chat_type == "direct":
|
|
514
|
+
contact = chat_info.get("contact", {}) or {}
|
|
515
|
+
sender_id = str(contact.get("contactId", ""))
|
|
516
|
+
sender_name = contact.get("localDisplayName", "") or contact.get(
|
|
517
|
+
"profile", {}
|
|
518
|
+
).get("displayName", "")
|
|
519
|
+
chat_id = sender_id
|
|
520
|
+
elif chat_type == "group":
|
|
521
|
+
group_info = chat_info.get("groupInfo", {}) or {}
|
|
522
|
+
group_id = str(group_info.get("groupId", ""))
|
|
523
|
+
chat_id = f"group:{group_id}"
|
|
524
|
+
is_group = True
|
|
525
|
+
|
|
526
|
+
member = item_direction.get("groupMember", {}) or {}
|
|
527
|
+
sender_id = str(member.get("memberId", ""))
|
|
528
|
+
sender_name = member.get("localDisplayName", "") or member.get(
|
|
529
|
+
"memberProfile", {}
|
|
530
|
+
).get("displayName", "")
|
|
531
|
+
|
|
532
|
+
# Group allowlist
|
|
533
|
+
if self.group_allow_from:
|
|
534
|
+
if (
|
|
535
|
+
"*" not in self.group_allow_from
|
|
536
|
+
and group_id not in self.group_allow_from
|
|
537
|
+
):
|
|
538
|
+
logger.debug(
|
|
539
|
+
"SimpleX: group %s not in allowlist",
|
|
540
|
+
_redact_id(group_id),
|
|
541
|
+
)
|
|
542
|
+
return
|
|
543
|
+
else:
|
|
544
|
+
logger.debug(
|
|
545
|
+
"SimpleX: ignoring group message (no SIMPLEX_GROUP_ALLOWED)"
|
|
546
|
+
)
|
|
547
|
+
return
|
|
371
548
|
else:
|
|
372
|
-
|
|
373
|
-
|
|
549
|
+
logger.debug("SimpleX: unhandled chat type: %s", chat_type)
|
|
550
|
+
return
|
|
374
551
|
|
|
375
|
-
|
|
376
|
-
|
|
552
|
+
if not sender_id:
|
|
553
|
+
logger.debug("SimpleX: ignoring message with no sender")
|
|
554
|
+
return
|
|
377
555
|
|
|
378
|
-
#
|
|
556
|
+
# File / image / voice attachment handling. File info is at
|
|
557
|
+
# chatItem.chatItem.file (sibling of meta, content, chatDir).
|
|
379
558
|
media_urls: List[str] = []
|
|
380
559
|
media_types: List[str] = []
|
|
381
|
-
file_info =
|
|
382
|
-
|
|
560
|
+
file_info = chat_item_data.get("file")
|
|
561
|
+
|
|
562
|
+
if file_info and isinstance(file_info, dict):
|
|
563
|
+
file_source = file_info.get("fileSource", {}) or {}
|
|
564
|
+
file_path = (
|
|
565
|
+
file_source.get("filePath")
|
|
566
|
+
if isinstance(file_source, dict)
|
|
567
|
+
else None
|
|
568
|
+
)
|
|
569
|
+
file_name = file_info.get("fileName", "")
|
|
383
570
|
file_id = file_info.get("fileId")
|
|
384
|
-
file_name = file_info.get("fileName", "file")
|
|
385
|
-
if file_id:
|
|
386
|
-
try:
|
|
387
|
-
cached = await self._fetch_file(file_id, file_name)
|
|
388
|
-
if cached:
|
|
389
|
-
ext = cached.rsplit(".", 1)[-1]
|
|
390
|
-
if _is_image_ext("." + ext):
|
|
391
|
-
media_types.append("image/" + ext.replace("jpg", "jpeg"))
|
|
392
|
-
elif _is_audio_ext("." + ext):
|
|
393
|
-
media_types.append("audio/" + ext)
|
|
394
|
-
else:
|
|
395
|
-
media_types.append("application/octet-stream")
|
|
396
|
-
media_urls.append(cached)
|
|
397
|
-
except Exception:
|
|
398
|
-
logger.exception("SimpleX: failed to fetch file %s", file_id)
|
|
399
571
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
572
|
+
ext = ""
|
|
573
|
+
if file_path:
|
|
574
|
+
ext = Path(file_path).suffix.lower()
|
|
575
|
+
if not ext and file_name:
|
|
576
|
+
ext = Path(file_name).suffix.lower()
|
|
577
|
+
|
|
578
|
+
# Voice notes typically arrive before the file finishes
|
|
579
|
+
# downloading. Defer the message until rcvFileComplete fires.
|
|
580
|
+
if not file_path and _is_audio_ext(ext) and file_id is not None:
|
|
581
|
+
logger.info(
|
|
582
|
+
"SimpleX: voice file %d not yet received, accepting transfer",
|
|
583
|
+
file_id,
|
|
584
|
+
)
|
|
585
|
+
self._pending_file_transfers[file_id] = chat_item
|
|
586
|
+
# Fire-and-forget: simplex-chat does not return a corrId reply
|
|
587
|
+
# for /freceive, so awaiting one would block the event loop.
|
|
588
|
+
await self._send_fire_and_forget(f"/freceive {file_id}")
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
if file_path:
|
|
592
|
+
ext = Path(file_path).suffix.lower() or (
|
|
593
|
+
Path(file_name).suffix.lower() if file_name else ""
|
|
594
|
+
)
|
|
595
|
+
if _is_image_ext(ext):
|
|
596
|
+
media_urls.append(file_path)
|
|
597
|
+
media_types.append(f"image/{ext.lstrip('.')}")
|
|
598
|
+
elif _is_audio_ext(ext):
|
|
599
|
+
media_urls.append(file_path)
|
|
600
|
+
media_types.append(f"audio/{ext.lstrip('.')}")
|
|
601
|
+
else:
|
|
602
|
+
media_urls.append(file_path)
|
|
603
|
+
media_types.append("application/octet-stream")
|
|
604
|
+
|
|
605
|
+
# Source
|
|
606
|
+
chat_name = sender_name
|
|
607
|
+
if is_group:
|
|
608
|
+
group_info = chat_info.get("groupInfo", {}) or {}
|
|
609
|
+
chat_name = group_info.get("localDisplayName", "") or group_info.get(
|
|
610
|
+
"groupProfile", {}
|
|
611
|
+
).get("displayName", chat_id)
|
|
406
612
|
|
|
407
|
-
# Build source
|
|
408
613
|
source = self.build_source(
|
|
409
614
|
chat_id=chat_id,
|
|
410
615
|
chat_name=chat_name,
|
|
411
616
|
chat_type="group" if is_group else "dm",
|
|
412
617
|
user_id=sender_id,
|
|
413
|
-
user_name=sender_name,
|
|
618
|
+
user_name=sender_name or sender_id,
|
|
414
619
|
)
|
|
415
620
|
|
|
416
621
|
# Message type
|
|
@@ -420,85 +625,179 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
420
625
|
msg_type = MessageType.VOICE
|
|
421
626
|
elif any(mt.startswith("image/") for mt in media_types):
|
|
422
627
|
msg_type = MessageType.PHOTO
|
|
628
|
+
else:
|
|
629
|
+
# Catch-all: non-image/non-audio files (tagged
|
|
630
|
+
# application/octet-stream above) are documents so run.py's
|
|
631
|
+
# document-context injection surfaces the file to the agent.
|
|
632
|
+
msg_type = MessageType.DOCUMENT
|
|
423
633
|
|
|
424
|
-
|
|
634
|
+
# Timestamp
|
|
635
|
+
ts_str = meta.get("itemTs") or meta.get("createdAt", "")
|
|
636
|
+
try:
|
|
637
|
+
if ts_str:
|
|
638
|
+
timestamp = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
639
|
+
else:
|
|
640
|
+
timestamp = datetime.now(tz=timezone.utc)
|
|
641
|
+
except (ValueError, AttributeError):
|
|
642
|
+
timestamp = datetime.now(tz=timezone.utc)
|
|
643
|
+
|
|
644
|
+
msg_event = MessageEvent(
|
|
425
645
|
source=source,
|
|
426
|
-
text=text,
|
|
646
|
+
text=text or "",
|
|
427
647
|
message_type=msg_type,
|
|
428
648
|
media_urls=media_urls,
|
|
429
649
|
media_types=media_types,
|
|
430
650
|
timestamp=timestamp,
|
|
431
|
-
raw_message=
|
|
651
|
+
raw_message=chat_item,
|
|
432
652
|
)
|
|
433
653
|
|
|
434
|
-
|
|
654
|
+
logger.debug(
|
|
655
|
+
"SimpleX: message from %s in %s: %s",
|
|
656
|
+
_redact_id(sender_id),
|
|
657
|
+
chat_id[:20],
|
|
658
|
+
(text or "")[:50],
|
|
659
|
+
)
|
|
435
660
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
#
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
661
|
+
# Batch consecutive text messages so the agent sees one combined
|
|
662
|
+
# message instead of dropping earlier ones when the user pastes
|
|
663
|
+
# several lines in quick succession.
|
|
664
|
+
if msg_type == MessageType.TEXT and text:
|
|
665
|
+
self._enqueue_text_event(msg_event)
|
|
666
|
+
else:
|
|
667
|
+
await self.handle_message(msg_event)
|
|
668
|
+
|
|
669
|
+
# ------------------------------------------------------------------
|
|
670
|
+
# Text message batching
|
|
671
|
+
# ------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
def _text_batch_key(self, event: MessageEvent) -> str:
|
|
674
|
+
"""Session-scoped key for text message batching."""
|
|
675
|
+
return f"{event.source.platform.value}:{event.source.chat_id}"
|
|
676
|
+
|
|
677
|
+
def _enqueue_text_event(self, event: MessageEvent) -> None:
|
|
678
|
+
"""Buffer a text event and reset the flush timer."""
|
|
679
|
+
key = self._text_batch_key(event)
|
|
680
|
+
existing = self._pending_text_batches.get(key)
|
|
681
|
+
if existing is None:
|
|
682
|
+
self._pending_text_batches[key] = event
|
|
683
|
+
else:
|
|
684
|
+
if event.text:
|
|
685
|
+
existing.text = (
|
|
686
|
+
f"{existing.text}\n{event.text}" if existing.text else event.text
|
|
687
|
+
)
|
|
688
|
+
if event.media_urls:
|
|
689
|
+
existing.media_urls.extend(event.media_urls)
|
|
690
|
+
existing.media_types.extend(event.media_types)
|
|
691
|
+
|
|
692
|
+
prior_task = self._pending_text_batch_tasks.get(key)
|
|
693
|
+
if prior_task and not prior_task.done():
|
|
694
|
+
prior_task.cancel()
|
|
695
|
+
self._pending_text_batch_tasks[key] = asyncio.create_task(
|
|
696
|
+
self._flush_text_batch(key)
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
async def _flush_text_batch(self, key: str) -> None:
|
|
700
|
+
"""Wait for the quiet period then dispatch the aggregated text."""
|
|
701
|
+
current_task = asyncio.current_task()
|
|
702
|
+
try:
|
|
703
|
+
await asyncio.sleep(self._text_batch_delay)
|
|
704
|
+
event = self._pending_text_batches.pop(key, None)
|
|
705
|
+
if not event:
|
|
706
|
+
return
|
|
707
|
+
logger.info(
|
|
708
|
+
"[SimpleX] Flushing text batch %s (%d chars)",
|
|
709
|
+
key,
|
|
710
|
+
len(event.text or ""),
|
|
711
|
+
)
|
|
712
|
+
await self.handle_message(event)
|
|
713
|
+
finally:
|
|
714
|
+
if self._pending_text_batch_tasks.get(key) is current_task:
|
|
715
|
+
self._pending_text_batch_tasks.pop(key, None)
|
|
473
716
|
|
|
474
717
|
# ------------------------------------------------------------------
|
|
475
|
-
#
|
|
718
|
+
# Command interface
|
|
476
719
|
# ------------------------------------------------------------------
|
|
477
720
|
|
|
478
721
|
def _make_corr_id(self) -> str:
|
|
479
|
-
"""
|
|
480
|
-
|
|
722
|
+
"""Mint a new correlation ID and remember it for echo-filtering.
|
|
723
|
+
|
|
724
|
+
We add every minted id to ``_pending_corr_ids`` so the inbound
|
|
725
|
+
event loop can drop the daemon's echo of our own commands without
|
|
726
|
+
ever invoking ``_handle_chat_item``. The set is bounded — when
|
|
727
|
+
it grows past ``_max_pending_corr``, the oldest entries are
|
|
728
|
+
evicted in a single sweep.
|
|
729
|
+
"""
|
|
730
|
+
self._corr_counter += 1
|
|
731
|
+
corr_id = f"{_CORR_PREFIX}{self._corr_counter}-{int(time.time() * 1000)}"
|
|
481
732
|
self._pending_corr_ids.add(corr_id)
|
|
482
733
|
if len(self._pending_corr_ids) > self._max_pending_corr:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
734
|
+
overflow = len(self._pending_corr_ids) - self._max_pending_corr
|
|
735
|
+
for _ in range(overflow):
|
|
736
|
+
try:
|
|
737
|
+
self._pending_corr_ids.pop()
|
|
738
|
+
except KeyError:
|
|
739
|
+
break
|
|
486
740
|
return corr_id
|
|
487
741
|
|
|
488
742
|
async def _send_ws(self, payload: dict) -> None:
|
|
489
|
-
"""
|
|
490
|
-
|
|
743
|
+
"""Fire-and-forget JSON payload write.
|
|
744
|
+
|
|
745
|
+
Drops cleanly when the WebSocket is missing or already closed; the
|
|
746
|
+
caller never has to handle reconnection — the ``_ws_listener``
|
|
747
|
+
loop does that out of band.
|
|
748
|
+
"""
|
|
491
749
|
ws = self._ws
|
|
492
750
|
if not ws:
|
|
493
|
-
logger.debug("SimpleX: WS not connected
|
|
751
|
+
logger.debug("SimpleX: WS send dropped (not connected)")
|
|
494
752
|
return
|
|
495
753
|
try:
|
|
496
754
|
await ws.send(json.dumps(payload))
|
|
497
|
-
except _wsexc.ConnectionClosed:
|
|
498
|
-
logger.warning("SimpleX: WS closed while sending")
|
|
499
755
|
except Exception as e:
|
|
500
756
|
logger.warning("SimpleX: WS send error: %s", e)
|
|
501
757
|
|
|
758
|
+
async def _send_command(
|
|
759
|
+
self, command: str, timeout: float = 30.0
|
|
760
|
+
) -> Optional[dict]:
|
|
761
|
+
"""Send a command and await the correlated response."""
|
|
762
|
+
ws = self._ws
|
|
763
|
+
if not ws:
|
|
764
|
+
logger.warning("SimpleX: command sent but WebSocket not connected")
|
|
765
|
+
return None
|
|
766
|
+
|
|
767
|
+
corr_id = self._make_corr_id()
|
|
768
|
+
payload = json.dumps({"corrId": corr_id, "cmd": command})
|
|
769
|
+
|
|
770
|
+
loop = asyncio.get_event_loop()
|
|
771
|
+
fut: asyncio.Future = loop.create_future()
|
|
772
|
+
self._pending_responses[corr_id] = fut
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
await ws.send(payload)
|
|
776
|
+
result = await asyncio.wait_for(fut, timeout=timeout)
|
|
777
|
+
return result
|
|
778
|
+
except asyncio.TimeoutError:
|
|
779
|
+
logger.warning("SimpleX: command timed out: %s", command[:50])
|
|
780
|
+
self._pending_responses.pop(corr_id, None)
|
|
781
|
+
return None
|
|
782
|
+
except Exception as e:
|
|
783
|
+
logger.warning("SimpleX: command failed: %s — %s", command[:50], e)
|
|
784
|
+
self._pending_responses.pop(corr_id, None)
|
|
785
|
+
return None
|
|
786
|
+
|
|
787
|
+
async def _send_fire_and_forget(self, command: str) -> None:
|
|
788
|
+
"""Send a command without waiting for a correlated response.
|
|
789
|
+
|
|
790
|
+
Use this for commands the daemon never sends a corrId reply for,
|
|
791
|
+
such as ``/freceive``. Awaiting a corr-id reply on those would
|
|
792
|
+
stall the event loop for the full command timeout.
|
|
793
|
+
"""
|
|
794
|
+
corr_id = self._make_corr_id()
|
|
795
|
+
await self._send_ws({"corrId": corr_id, "cmd": command})
|
|
796
|
+
|
|
797
|
+
# ------------------------------------------------------------------
|
|
798
|
+
# Outbound — text
|
|
799
|
+
# ------------------------------------------------------------------
|
|
800
|
+
|
|
502
801
|
async def send(
|
|
503
802
|
self,
|
|
504
803
|
chat_id: str,
|
|
@@ -506,50 +805,283 @@ class SimplexAdapter(BasePlatformAdapter):
|
|
|
506
805
|
reply_to: Optional[str] = None,
|
|
507
806
|
metadata: Optional[Dict[str, Any]] = None,
|
|
508
807
|
) -> SendResult:
|
|
509
|
-
"""Send a text message
|
|
510
|
-
|
|
808
|
+
"""Send a text message.
|
|
809
|
+
|
|
810
|
+
If *content* contains ``MEDIA:<path>`` tags (embedded by TTS / audio
|
|
811
|
+
tools to signal file attachments), they are stripped from the text
|
|
812
|
+
body and sent as native voice notes or documents.
|
|
813
|
+
|
|
814
|
+
Groups use the structured ``/_send #<id> json [...]`` form
|
|
815
|
+
because the bracket chat-command syntax (``#[<id>] text``) is
|
|
816
|
+
parsed by the daemon as a display-name lookup, which silently
|
|
817
|
+
drops when the group's display name isn't the literal ID. DMs
|
|
818
|
+
use the simple ``@<id> text`` form which has always worked in
|
|
819
|
+
production.
|
|
820
|
+
|
|
821
|
+
The call is fire-and-forget at the WebSocket level: the daemon
|
|
822
|
+
doesn't always return a corrId reply for chat commands, and
|
|
823
|
+
waiting for one would serialise all outbound traffic behind a
|
|
824
|
+
30-second timeout.
|
|
825
|
+
"""
|
|
826
|
+
_voice_exts = {".ogg", ".mp3", ".wav", ".m4a", ".opus"}
|
|
827
|
+
media_paths = re.findall(r"MEDIA:(\S+)", content)
|
|
828
|
+
if media_paths:
|
|
829
|
+
content = re.sub(r"MEDIA:\S+", "", content).strip()
|
|
830
|
+
|
|
831
|
+
if content:
|
|
832
|
+
corr_id = self._make_corr_id()
|
|
833
|
+
if chat_id.startswith("group:"):
|
|
834
|
+
# Structured form: addresses by numeric ID, and json.dumps
|
|
835
|
+
# escapes newlines + special chars correctly.
|
|
836
|
+
composed = json.dumps(
|
|
837
|
+
[{"msgContent": {"type": "text", "text": content}}]
|
|
838
|
+
)
|
|
839
|
+
cmd_str = f"/_send #{chat_id[6:]} json {composed}"
|
|
840
|
+
else:
|
|
841
|
+
cmd_str = f"@{chat_id} {content}"
|
|
511
842
|
|
|
512
|
-
|
|
513
|
-
group_id = chat_id[6:]
|
|
514
|
-
cmd_str = f"#[{group_id}] {content}"
|
|
515
|
-
else:
|
|
516
|
-
# SimpleX CLI addresses direct contacts by display name, e.g.
|
|
517
|
-
# `@Alice hello`. `@[Alice]` is interpreted literally as a contact
|
|
518
|
-
# named "[Alice]" and `@[4]` as "[4]", so do not wrap direct
|
|
519
|
-
# chat IDs / display names in brackets.
|
|
520
|
-
cmd_str = f"@{chat_id} {content}"
|
|
843
|
+
await self._send_ws({"corrId": corr_id, "cmd": cmd_str})
|
|
521
844
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
845
|
+
for path in media_paths:
|
|
846
|
+
is_voice = os.path.splitext(path)[1].lower() in _voice_exts
|
|
847
|
+
if is_voice:
|
|
848
|
+
media_result = await self.send_voice(chat_id, path)
|
|
849
|
+
else:
|
|
850
|
+
media_result = await self.send_document(chat_id, path)
|
|
851
|
+
if not media_result.success:
|
|
852
|
+
return media_result
|
|
526
853
|
|
|
527
|
-
await self._send_ws(payload)
|
|
528
854
|
return SendResult(success=True)
|
|
529
855
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
856
|
+
# ------------------------------------------------------------------
|
|
857
|
+
# Outbound — media
|
|
858
|
+
# ------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
@staticmethod
|
|
861
|
+
def _prepare_image(file_path: str) -> tuple[str, str]:
|
|
862
|
+
"""Ensure *file_path* is a PNG and return ``(png_path, thumb_data_uri)``.
|
|
863
|
+
|
|
864
|
+
SimpleX clients can't display WebP and a few other formats inline.
|
|
865
|
+
This converts to PNG when needed and generates a small JPEG thumbnail
|
|
866
|
+
for the ``image`` field in the ``/_send`` payload so the chat shows
|
|
867
|
+
an inline preview. Uses Pillow when available, falls back to
|
|
868
|
+
ImageMagick ``convert``.
|
|
869
|
+
"""
|
|
870
|
+
import subprocess
|
|
871
|
+
import tempfile
|
|
872
|
+
|
|
873
|
+
p = Path(file_path)
|
|
874
|
+
png_path = file_path
|
|
875
|
+
thumb_uri = ""
|
|
876
|
+
|
|
877
|
+
try:
|
|
878
|
+
from PIL import Image
|
|
879
|
+
|
|
880
|
+
img = Image.open(file_path)
|
|
881
|
+
if p.suffix.lower() not in (".png", ".jpg", ".jpeg"):
|
|
882
|
+
png_path = str(p.with_suffix(".png"))
|
|
883
|
+
img.save(png_path, "PNG")
|
|
884
|
+
thumb = img.copy()
|
|
885
|
+
thumb.thumbnail((128, 128))
|
|
886
|
+
import io
|
|
887
|
+
|
|
888
|
+
buf = io.BytesIO()
|
|
889
|
+
thumb.save(buf, "JPEG", quality=70)
|
|
890
|
+
thumb_uri = (
|
|
891
|
+
"data:image/jpg;base64,"
|
|
892
|
+
+ base64.b64encode(buf.getvalue()).decode()
|
|
893
|
+
)
|
|
894
|
+
except ImportError:
|
|
895
|
+
try:
|
|
896
|
+
if p.suffix.lower() not in (".png", ".jpg", ".jpeg"):
|
|
897
|
+
png_path = str(p.with_suffix(".png"))
|
|
898
|
+
subprocess.run(
|
|
899
|
+
["convert", file_path, png_path],
|
|
900
|
+
check=True,
|
|
901
|
+
capture_output=True,
|
|
902
|
+
timeout=30,
|
|
903
|
+
)
|
|
904
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
|
905
|
+
tmp_path = tmp.name
|
|
906
|
+
subprocess.run(
|
|
907
|
+
[
|
|
908
|
+
"convert",
|
|
909
|
+
file_path,
|
|
910
|
+
"-resize",
|
|
911
|
+
"128x128",
|
|
912
|
+
"-quality",
|
|
913
|
+
"70",
|
|
914
|
+
tmp_path,
|
|
915
|
+
],
|
|
916
|
+
check=True,
|
|
917
|
+
capture_output=True,
|
|
918
|
+
timeout=30,
|
|
919
|
+
)
|
|
920
|
+
with open(tmp_path, "rb") as f:
|
|
921
|
+
thumb_uri = (
|
|
922
|
+
"data:image/jpg;base64," + base64.b64encode(f.read()).decode()
|
|
923
|
+
)
|
|
924
|
+
os.remove(tmp_path)
|
|
925
|
+
except (FileNotFoundError, subprocess.SubprocessError) as exc:
|
|
926
|
+
logger.warning("SimpleX: image conversion unavailable: %s", exc)
|
|
927
|
+
|
|
928
|
+
return png_path, thumb_uri
|
|
533
929
|
|
|
534
930
|
async def send_image(
|
|
535
931
|
self,
|
|
536
932
|
chat_id: str,
|
|
537
933
|
image_url: str,
|
|
538
934
|
caption: Optional[str] = None,
|
|
935
|
+
**kwargs,
|
|
936
|
+
) -> SendResult:
|
|
937
|
+
"""Send an image. Supports ``file://`` URLs and ``http(s)://`` URLs."""
|
|
938
|
+
from urllib.parse import unquote
|
|
939
|
+
|
|
940
|
+
if image_url.startswith("file://"):
|
|
941
|
+
file_path = unquote(image_url[7:])
|
|
942
|
+
else:
|
|
943
|
+
try:
|
|
944
|
+
from gateway.platforms.base import cache_image_from_url
|
|
945
|
+
|
|
946
|
+
file_path = await cache_image_from_url(image_url)
|
|
947
|
+
except Exception as e:
|
|
948
|
+
logger.warning("SimpleX: failed to download image: %s", e)
|
|
949
|
+
return SendResult(success=False, error=str(e))
|
|
950
|
+
|
|
951
|
+
if not file_path or not Path(file_path).exists():
|
|
952
|
+
return SendResult(success=False, error="Image file not found")
|
|
953
|
+
|
|
954
|
+
png_path, thumb_uri = self._prepare_image(file_path)
|
|
955
|
+
|
|
956
|
+
# /_send addresses by numeric ID; /f only accepts display names which
|
|
957
|
+
# breaks for group IDs.
|
|
958
|
+
composed = json.dumps(
|
|
959
|
+
[
|
|
960
|
+
{
|
|
961
|
+
"filePath": png_path,
|
|
962
|
+
"msgContent": {
|
|
963
|
+
"type": "image",
|
|
964
|
+
"image": thumb_uri,
|
|
965
|
+
"text": caption or "",
|
|
966
|
+
},
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
if chat_id.startswith("group:"):
|
|
972
|
+
group_id = chat_id[6:]
|
|
973
|
+
command = f"/_send #{group_id} json {composed}"
|
|
974
|
+
else:
|
|
975
|
+
command = f"/_send @{chat_id} json {composed}"
|
|
976
|
+
|
|
977
|
+
result = await self._send_command(command)
|
|
978
|
+
if result is not None:
|
|
979
|
+
return SendResult(success=True)
|
|
980
|
+
return SendResult(success=False, error="Failed to send image")
|
|
981
|
+
|
|
982
|
+
async def send_image_file(
|
|
983
|
+
self,
|
|
984
|
+
chat_id: str,
|
|
985
|
+
image_path: str,
|
|
986
|
+
caption: Optional[str] = None,
|
|
539
987
|
reply_to: Optional[str] = None,
|
|
540
|
-
|
|
988
|
+
**kwargs,
|
|
989
|
+
) -> SendResult:
|
|
990
|
+
"""Send a local image file via SimpleX."""
|
|
991
|
+
return await self.send_image(
|
|
992
|
+
chat_id, f"file://{image_path}", caption=caption, **kwargs
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
async def send_video(
|
|
996
|
+
self,
|
|
997
|
+
chat_id: str,
|
|
998
|
+
video_path: str,
|
|
999
|
+
caption: Optional[str] = None,
|
|
1000
|
+
reply_to: Optional[str] = None,
|
|
1001
|
+
**kwargs,
|
|
541
1002
|
) -> SendResult:
|
|
542
|
-
"""Send
|
|
1003
|
+
"""Send a video file via SimpleX (as a file attachment)."""
|
|
1004
|
+
return await self.send_document(chat_id, video_path, caption=caption)
|
|
543
1005
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1006
|
+
async def send_document(
|
|
1007
|
+
self,
|
|
1008
|
+
chat_id: str,
|
|
1009
|
+
file_path: str,
|
|
1010
|
+
caption: Optional[str] = None,
|
|
1011
|
+
filename: Optional[str] = None,
|
|
1012
|
+
**kwargs,
|
|
1013
|
+
) -> SendResult:
|
|
1014
|
+
"""Send a document/file attachment."""
|
|
1015
|
+
if not Path(file_path).exists():
|
|
1016
|
+
return SendResult(success=False, error="File not found")
|
|
1017
|
+
|
|
1018
|
+
composed = json.dumps(
|
|
1019
|
+
[
|
|
1020
|
+
{
|
|
1021
|
+
"filePath": file_path,
|
|
1022
|
+
"msgContent": {"type": "file", "text": caption or ""},
|
|
1023
|
+
}
|
|
1024
|
+
]
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
if chat_id.startswith("group:"):
|
|
1028
|
+
group_id = chat_id[6:]
|
|
1029
|
+
command = f"/_send #{group_id} json {composed}"
|
|
1030
|
+
else:
|
|
1031
|
+
command = f"/_send @{chat_id} json {composed}"
|
|
1032
|
+
|
|
1033
|
+
result = await self._send_command(command)
|
|
1034
|
+
if result is not None:
|
|
1035
|
+
return SendResult(success=True)
|
|
1036
|
+
return SendResult(success=False, error="Failed to send document")
|
|
1037
|
+
|
|
1038
|
+
async def send_voice(
|
|
1039
|
+
self,
|
|
1040
|
+
chat_id: str,
|
|
1041
|
+
audio_path: str,
|
|
1042
|
+
caption: Optional[str] = None,
|
|
1043
|
+
reply_to: Optional[str] = None,
|
|
1044
|
+
duration: int = 0,
|
|
1045
|
+
**kwargs,
|
|
1046
|
+
) -> SendResult:
|
|
1047
|
+
"""Send an audio file as a SimpleX voice note (plays inline).
|
|
1048
|
+
|
|
1049
|
+
SimpleX distinguishes a generic file attachment (``type: "file"``)
|
|
1050
|
+
from an inline voice note (``type: "voice"``). ``/f`` would deliver
|
|
1051
|
+
a downloadable file; the structured ``/_send`` form with
|
|
1052
|
+
``msgContent.type == "voice"`` produces the voice-note player.
|
|
548
1053
|
"""
|
|
549
|
-
|
|
550
|
-
|
|
1054
|
+
if not Path(audio_path).exists():
|
|
1055
|
+
return SendResult(success=False, error="Voice file not found")
|
|
1056
|
+
|
|
1057
|
+
composed = json.dumps(
|
|
1058
|
+
[
|
|
1059
|
+
{
|
|
1060
|
+
"msgContent": {
|
|
1061
|
+
"type": "voice",
|
|
1062
|
+
"text": caption or "",
|
|
1063
|
+
"duration": duration,
|
|
1064
|
+
},
|
|
1065
|
+
"fileSource": {"filePath": audio_path},
|
|
1066
|
+
}
|
|
1067
|
+
]
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
if chat_id.startswith("group:"):
|
|
1071
|
+
group_id = chat_id[6:]
|
|
1072
|
+
command = f"/_send #{group_id} json {composed}"
|
|
1073
|
+
else:
|
|
1074
|
+
command = f"/_send @{chat_id} json {composed}"
|
|
1075
|
+
|
|
1076
|
+
result = await self._send_command(command)
|
|
1077
|
+
if result is not None:
|
|
1078
|
+
return SendResult(success=True)
|
|
1079
|
+
return SendResult(success=False, error="Failed to send voice message")
|
|
1080
|
+
|
|
1081
|
+
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
|
1082
|
+
"""SimpleX has no typing-indicator API — no-op."""
|
|
551
1083
|
|
|
552
|
-
async def get_chat_info(self, chat_id: str) ->
|
|
1084
|
+
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
|
553
1085
|
"""Return basic chat info."""
|
|
554
1086
|
if chat_id.startswith("group:"):
|
|
555
1087
|
return {"chat_id": chat_id, "type": "group", "name": chat_id[6:]}
|
|
@@ -590,7 +1122,7 @@ def is_connected(config) -> bool:
|
|
|
590
1122
|
return bool(ws_url)
|
|
591
1123
|
|
|
592
1124
|
|
|
593
|
-
def _env_enablement() -> dict
|
|
1125
|
+
def _env_enablement() -> Optional[dict]:
|
|
594
1126
|
"""Seed ``PlatformConfig.extra`` from env vars during gateway config load.
|
|
595
1127
|
|
|
596
1128
|
Called by the platform registry's env-enablement hook BEFORE adapter
|
|
@@ -598,14 +1130,23 @@ def _env_enablement() -> dict | None:
|
|
|
598
1130
|
reflect env-only configuration without instantiating the WebSocket
|
|
599
1131
|
client. Returns ``None`` when SimpleX isn't minimally configured.
|
|
600
1132
|
|
|
601
|
-
The special ``home_channel`` key
|
|
602
|
-
|
|
603
|
-
|
|
1133
|
+
The special ``home_channel`` key is handled by the core hook — it
|
|
1134
|
+
becomes a proper ``HomeChannel`` dataclass on the ``PlatformConfig``
|
|
1135
|
+
rather than being merged into ``extra``.
|
|
604
1136
|
"""
|
|
605
1137
|
ws_url = os.getenv("SIMPLEX_WS_URL", "").strip()
|
|
606
1138
|
if not ws_url:
|
|
607
1139
|
return None
|
|
608
1140
|
seed: dict = {"ws_url": ws_url}
|
|
1141
|
+
|
|
1142
|
+
auto_accept = os.getenv("SIMPLEX_AUTO_ACCEPT", "").strip().lower()
|
|
1143
|
+
if auto_accept:
|
|
1144
|
+
seed["auto_accept"] = auto_accept not in {"0", "false", "no"}
|
|
1145
|
+
|
|
1146
|
+
group_allowed = os.getenv("SIMPLEX_GROUP_ALLOWED", "").strip()
|
|
1147
|
+
if group_allowed:
|
|
1148
|
+
seed["group_allowed"] = group_allowed
|
|
1149
|
+
|
|
609
1150
|
home = os.getenv("SIMPLEX_HOME_CHANNEL", "").strip()
|
|
610
1151
|
if home:
|
|
611
1152
|
seed["home_channel"] = {
|
|
@@ -633,9 +1174,9 @@ async def _standalone_send(
|
|
|
633
1174
|
|
|
634
1175
|
``thread_id`` and ``force_document`` are accepted for signature parity
|
|
635
1176
|
with other plugins but are not meaningful here. ``media_files`` is
|
|
636
|
-
accepted but only the text body is delivered — SimpleX
|
|
637
|
-
daemon's filesystem-backed
|
|
638
|
-
cannot drive safely.
|
|
1177
|
+
accepted but only the text body is delivered — SimpleX file transfers
|
|
1178
|
+
require the daemon's filesystem-backed flow, which an ephemeral
|
|
1179
|
+
connection cannot drive safely.
|
|
639
1180
|
"""
|
|
640
1181
|
try:
|
|
641
1182
|
import websockets as _wsclient
|
|
@@ -643,24 +1184,31 @@ async def _standalone_send(
|
|
|
643
1184
|
return {"error": "websockets not installed. Run: pip install websockets"}
|
|
644
1185
|
|
|
645
1186
|
extra = getattr(pconfig, "extra", {}) or {}
|
|
646
|
-
ws_url = os.getenv("SIMPLEX_WS_URL") or extra.get(
|
|
1187
|
+
ws_url = os.getenv("SIMPLEX_WS_URL") or extra.get(
|
|
1188
|
+
"ws_url", "ws://127.0.0.1:5225"
|
|
1189
|
+
)
|
|
647
1190
|
if not ws_url:
|
|
648
1191
|
return {"error": "SimpleX standalone send: SIMPLEX_WS_URL is required"}
|
|
649
1192
|
|
|
650
1193
|
try:
|
|
651
1194
|
if chat_id.startswith("group:"):
|
|
652
1195
|
group_id = chat_id[6:]
|
|
653
|
-
|
|
1196
|
+
composed = json.dumps(
|
|
1197
|
+
[{"msgContent": {"type": "text", "text": message}}]
|
|
1198
|
+
)
|
|
1199
|
+
cmd_str = f"/_send #{group_id} json {composed}"
|
|
654
1200
|
else:
|
|
655
1201
|
# Direct contacts are addressed by display name without brackets.
|
|
656
1202
|
cmd_str = f"@{chat_id} {message}"
|
|
657
1203
|
|
|
658
1204
|
payload = {
|
|
659
|
-
"corrId": f"
|
|
1205
|
+
"corrId": f"{_CORR_PREFIX}snd-{int(time.time() * 1000)}",
|
|
660
1206
|
"cmd": cmd_str,
|
|
661
1207
|
}
|
|
662
1208
|
|
|
663
|
-
async with _wsclient.connect(
|
|
1209
|
+
async with _wsclient.connect(
|
|
1210
|
+
ws_url, open_timeout=10, close_timeout=5
|
|
1211
|
+
) as ws:
|
|
664
1212
|
await ws.send(json.dumps(payload))
|
|
665
1213
|
# Give the daemon a moment to process the command before closing.
|
|
666
1214
|
await asyncio.sleep(0.5)
|
|
@@ -673,8 +1221,9 @@ async def _standalone_send(
|
|
|
673
1221
|
def interactive_setup() -> None:
|
|
674
1222
|
"""Minimal stdin wizard for ``hermes setup gateway`` → SimpleX.
|
|
675
1223
|
|
|
676
|
-
Prompts for the WebSocket URL and the optional allowlist /
|
|
677
|
-
Writes to ``~/.hermes/.env`` via
|
|
1224
|
+
Prompts for the WebSocket URL and the optional allowlist / groups /
|
|
1225
|
+
auto-accept / home channel. Writes to ``~/.hermes/.env`` via
|
|
1226
|
+
``hermes_cli.config``.
|
|
678
1227
|
"""
|
|
679
1228
|
print()
|
|
680
1229
|
print("SimpleX Chat setup")
|
|
@@ -687,7 +1236,10 @@ def interactive_setup() -> None:
|
|
|
687
1236
|
try:
|
|
688
1237
|
from hermes_cli.config import get_env_value, save_env_value
|
|
689
1238
|
except ImportError:
|
|
690
|
-
print(
|
|
1239
|
+
print(
|
|
1240
|
+
"hermes_cli.config not available; set SIMPLEX_* vars manually in "
|
|
1241
|
+
"~/.hermes/.env"
|
|
1242
|
+
)
|
|
691
1243
|
return
|
|
692
1244
|
|
|
693
1245
|
def _prompt(var: str, prompt: str, *, secret: bool = False) -> None:
|
|
@@ -706,9 +1258,20 @@ def interactive_setup() -> None:
|
|
|
706
1258
|
save_env_value(var, value)
|
|
707
1259
|
|
|
708
1260
|
_prompt("SIMPLEX_WS_URL", "Daemon WebSocket URL (default ws://127.0.0.1:5225)")
|
|
709
|
-
_prompt("SIMPLEX_ALLOWED_USERS", "Allowed
|
|
1261
|
+
_prompt("SIMPLEX_ALLOWED_USERS", "Allowed contactIds or display names (comma-separated; blank=skip)")
|
|
1262
|
+
_prompt(
|
|
1263
|
+
"SIMPLEX_GROUP_ALLOWED",
|
|
1264
|
+
"Allowed group IDs (comma-separated, or '*' for any; blank=disable groups)",
|
|
1265
|
+
)
|
|
1266
|
+
_prompt(
|
|
1267
|
+
"SIMPLEX_AUTO_ACCEPT",
|
|
1268
|
+
"Auto-accept incoming contact requests? (true/false, default true)",
|
|
1269
|
+
)
|
|
710
1270
|
_prompt("SIMPLEX_HOME_CHANNEL", "Home channel contact/group ID (or empty)")
|
|
711
|
-
print(
|
|
1271
|
+
print(
|
|
1272
|
+
"Done. Make sure the simplex-chat daemon is running before starting "
|
|
1273
|
+
"the gateway."
|
|
1274
|
+
)
|
|
712
1275
|
|
|
713
1276
|
|
|
714
1277
|
def register(ctx) -> None:
|
|
@@ -721,36 +1284,30 @@ def register(ctx) -> None:
|
|
|
721
1284
|
validate_config=validate_config,
|
|
722
1285
|
is_connected=is_connected,
|
|
723
1286
|
required_env=["SIMPLEX_WS_URL"],
|
|
724
|
-
install_hint=
|
|
1287
|
+
install_hint=(
|
|
1288
|
+
"pip install websockets # SimpleX adapter requires the "
|
|
1289
|
+
"websockets package"
|
|
1290
|
+
),
|
|
725
1291
|
setup_fn=interactive_setup,
|
|
726
|
-
# Env-driven auto-configuration: seeds PlatformConfig.extra so
|
|
727
|
-
# env-only setups show up in `hermes gateway status` without
|
|
728
|
-
# instantiating the adapter.
|
|
729
1292
|
env_enablement_fn=_env_enablement,
|
|
730
|
-
# Cron home-channel delivery support — `deliver=simplex` cron jobs
|
|
731
|
-
# route to SIMPLEX_HOME_CHANNEL when set.
|
|
732
1293
|
cron_deliver_env_var="SIMPLEX_HOME_CHANNEL",
|
|
733
|
-
# Out-of-process cron delivery. Without this hook, deliver=simplex
|
|
734
|
-
# cron jobs fail with "No live adapter" when cron runs separately
|
|
735
|
-
# from the gateway.
|
|
736
1294
|
standalone_sender_fn=_standalone_send,
|
|
737
|
-
# Auth env vars for _is_user_authorized() integration
|
|
738
1295
|
allowed_users_env="SIMPLEX_ALLOWED_USERS",
|
|
739
1296
|
allow_all_env="SIMPLEX_ALLOW_ALL_USERS",
|
|
740
|
-
# SimpleX has no hard line length; we still chunk for sanity.
|
|
741
1297
|
max_message_length=MAX_MESSAGE_LENGTH,
|
|
742
|
-
# Display
|
|
743
1298
|
emoji="🔒",
|
|
744
|
-
# SimpleX uses opaque contact IDs only — no phone numbers or
|
|
745
|
-
#
|
|
1299
|
+
# SimpleX uses opaque contact IDs only — no phone numbers or email
|
|
1300
|
+
# addresses to redact.
|
|
746
1301
|
pii_safe=True,
|
|
747
1302
|
allow_update_command=True,
|
|
748
|
-
# LLM guidance
|
|
749
1303
|
platform_hint=(
|
|
750
1304
|
"You are chatting via SimpleX Chat, a private decentralised "
|
|
751
1305
|
"messenger. Contacts are identified by opaque internal IDs, "
|
|
752
1306
|
"not phone numbers or usernames. SimpleX supports standard "
|
|
753
1307
|
"markdown formatting. There is no typing indicator and no "
|
|
754
|
-
"hard message length limit, but keep responses conversational."
|
|
1308
|
+
"hard message length limit, but keep responses conversational. "
|
|
1309
|
+
"You can attach native images, voice notes, and arbitrary "
|
|
1310
|
+
"files; the adapter handles MEDIA:<path> tags by sending them "
|
|
1311
|
+
"as inline voice notes (audio extensions) or documents."
|
|
755
1312
|
),
|
|
756
1313
|
)
|