dulus 0.2.52__tar.gz → 0.2.54__tar.gz
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.
- {dulus-0.2.52/dulus.egg-info → dulus-0.2.54}/PKG-INFO +1 -1
- {dulus-0.2.52 → dulus-0.2.54/dulus.egg-info}/PKG-INFO +1 -1
- {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/SOURCES.txt +3 -1
- {dulus-0.2.52 → dulus-0.2.54}/dulus.py +425 -23
- {dulus-0.2.52 → dulus-0.2.54}/input.py +73 -19
- {dulus-0.2.52 → dulus-0.2.54}/pyproject.toml +1 -1
- {dulus-0.2.52 → dulus-0.2.54}/skill/clawhub.py +89 -1
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_voice.py +56 -0
- {dulus-0.2.52 → dulus-0.2.54}/voice/__init__.py +8 -0
- dulus-0.2.54/voice/audio_utils.py +37 -0
- {dulus-0.2.52 → dulus-0.2.54}/voice/recorder.py +11 -7
- {dulus-0.2.52 → dulus-0.2.54}/voice/stt.py +20 -0
- {dulus-0.2.52 → dulus-0.2.54}/voice/tts.py +109 -36
- dulus-0.2.54/voice/wake_word.py +371 -0
- {dulus-0.2.52 → dulus-0.2.54}/LICENSE +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/MANIFEST.in +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/README.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/agent.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/agents_bridge.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/compressor.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/context.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/githook.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/marketplace.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/personas.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/plugins.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/server.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/backend/tasks.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/batch_api.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/checkpoint/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/checkpoint/hooks.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/checkpoint/store.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/checkpoint/types.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/claude_code_watcher.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/clipboard_utils.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/cloudsave.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/common.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/compaction.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/config.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/context.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/active_persona.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/context.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/marketplace.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/personas.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/data/tasks.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/README.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/api.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/architecture.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/dashboard/index.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/divider.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/generate.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/hero.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/index.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/news.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/particle-playground.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/personas/index.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/poetry-banner.png +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/preview.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-agents.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-features.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-memory.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-models.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-perms.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/spinners.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/split-pane.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_gui.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/client.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/config.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/types.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/agent_bridge.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/chat_widget.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/main_window.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/personas.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/session_utils.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/settings_dialog.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/sidebar.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/tasks_view.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/themes.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/gui/tool_panel.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/license_manager.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/audit.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/consolidator.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/context.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/offload.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/palace.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/scan.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/sessions.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/store.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/types.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/memory/vector_search.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/multi_agent/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/multi_agent/subagent.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/multi_agent/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/offload_helper.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/autoadapter.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/loader.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/recommend.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/store.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/plugin/types.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/providers.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/README.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/components.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/index.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpaper-default.jpg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/default.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/light.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/eslint.config.js +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/index.html +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/info.md +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/package-lock.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/package.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/postcss.config.js +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpaper-default.jpg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/default.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/light.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/App.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AgentMonitor.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ApiTester.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AppRouter.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ArchiveManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AsciiArt.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Base64Tool.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Browser.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Calculator.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Calendar.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Chat.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Chess.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Clock.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/CodeEditor.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ColorPalette.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ColorPicker.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Contacts.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/DocumentViewer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Drawing.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Email.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FileManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FlappyBird.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FtpClient.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Game2048.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/GitClient.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ImageGallery.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ImageViewer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/JsonFormatter.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MatrixRain.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MediaConverter.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Memory.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MemoryManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Minesweeper.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MusicPlayer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/NetworkTools.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Notes.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/PasswordManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/PhotoEditor.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Pong.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/RegexTester.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Reminders.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/RssReader.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Settings.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Snake.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Solitaire.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Spreadsheet.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Sudoku.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/SystemMonitor.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TaskManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Terminal.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Tetris.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TextEditor.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TicTacToe.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Todo.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/VideoPlayer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Weather.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Whiteboard.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/registry.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/AppContainer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/AppLauncher.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/BootSequence.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ContextMenu.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/Desktop.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/Dock.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/LoginScreen.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotImplemented.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotificationCenter.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotificationSystem.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/TopPanel.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/WindowFrame.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/WindowManager.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/accordion.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/alert.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/avatar.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/badge.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/button-group.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/button.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/calendar.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/card.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/carousel.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/chart.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/checkbox.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/collapsible.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/command.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/context-menu.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/dialog.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/drawer.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/empty.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/field.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/form.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/hover-card.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input-group.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input-otp.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/item.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/kbd.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/label.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/menubar.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/pagination.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/popover.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/progress.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/radio-group.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/resizable.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/scroll-area.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/select.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/separator.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sheet.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sidebar.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/skeleton.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/slider.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sonner.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/spinner.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/switch.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/table.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/tabs.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/textarea.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/toggle-group.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/toggle.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/tooltip.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/index.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/use-mobile.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusAgents.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusChat.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusEvents.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusHealth.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusMemory.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusSkills.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusTasks.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useFileSystem.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useOSStore.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSkillBridge.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemBattery.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemVolume.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/index.css +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/lib/dulus-api.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/lib/utils.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/main.tsx +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/types/index.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/utils/assets.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/tailwind.config.js +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.app.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.node.json +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/sandbox/vite.config.ts +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/setup.cfg +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skill/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skill/builtin.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skill/executor.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skill/loader.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skill/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/skills.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/spinner.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/string_utils.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/subagent.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/task/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/task/store.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/task/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/task/types.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_afk_yolo.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_approval_runtime.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_background_task_tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_background_tasks.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_clipboard_utils.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_compaction.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_diff_view.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_diff_visualization.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_display_blocks.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_export_import.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_hook_engine.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_license.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_mcp.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_memory.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_notification_manager.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_plugin.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_session_fork.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_shell_mode.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_skills.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_steer_input.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_subagent.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_task.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_think_tool.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_todo_tool.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_todo_visualization.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tests/test_wire_events.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tmux_offloader.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tmux_tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tool_registry.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/tools.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/ui/__init__.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/ui/input.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/ui/render.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/voice/keyterms.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/webchat.py +0 -0
- {dulus-0.2.52 → dulus-0.2.54}/webchat_server.py +0 -0
|
@@ -90,6 +90,12 @@ Slash commands in REPL:
|
|
|
90
90
|
/voice Record voice input, transcribe, and submit
|
|
91
91
|
/voice status Show available recording and STT backends
|
|
92
92
|
/voice lang <code> Set STT language (e.g. zh, en, ja — default: auto)
|
|
93
|
+
/wake on|off Toggle wake-word (hotword) detection — say "Hey Dulus"
|
|
94
|
+
/wake status Show wake-word listener state
|
|
95
|
+
/wake phrases List recognised wake phrases
|
|
96
|
+
/wake calibrate Measure your mic levels for 5s and suggest a threshold
|
|
97
|
+
/wake test Debug mode — shows RMS + STT output for 10 seconds
|
|
98
|
+
/wake threshold <n> Tune mic sensitivity (0.001-1.0, default 0.020)
|
|
93
99
|
/proactive [dur] Background sentinel polling (e.g. /proactive 5m)
|
|
94
100
|
/proactive off Disable proactive polling
|
|
95
101
|
/cloudsave setup <token> Configure GitHub token for cloud sync
|
|
@@ -193,7 +199,7 @@ import time
|
|
|
193
199
|
import uuid
|
|
194
200
|
from datetime import datetime
|
|
195
201
|
from pathlib import Path
|
|
196
|
-
from typing import Optional, Union
|
|
202
|
+
from typing import Optional, Union, Any, Callable
|
|
197
203
|
|
|
198
204
|
if sys.platform == "win32":
|
|
199
205
|
os.system("") # Enable ANSI escape codes on Windows CMD
|
|
@@ -736,6 +742,7 @@ def cmd_help(_args: str, _state, config) -> bool:
|
|
|
736
742
|
("lite_mode", False, "/lite", "Lite mode (smaller system prompt)"),
|
|
737
743
|
("brave_search_enabled", False, "/brave", "Brave Search API integration"),
|
|
738
744
|
("tts_enabled", False, "/tts", "Automatic Text-to-Speech"),
|
|
745
|
+
("wake_enabled", False, "/wake", "Wake-word hotword detection (Hey Dulus)"),
|
|
739
746
|
("daemon", False, "/daemon", "Allow external triggers when no REPL is open"),
|
|
740
747
|
("afk_mode", False, "/afk", "AFK mode (auto-dismiss, auto-approve tools)"),
|
|
741
748
|
("yolo_mode", False, "/yolo", "YOLO mode (auto-approve ALL actions)"),
|
|
@@ -4326,6 +4333,7 @@ def cmd_skill(args: str, state, config) -> bool:
|
|
|
4326
4333
|
list_local, list_installed, install_local, install_clawhub,
|
|
4327
4334
|
search_clawhub, read_skill,
|
|
4328
4335
|
list_awesome_remote, list_composio_toolkits,
|
|
4336
|
+
install_awesome_remote,
|
|
4329
4337
|
)
|
|
4330
4338
|
from pathlib import Path as _Path
|
|
4331
4339
|
|
|
@@ -4371,17 +4379,16 @@ def cmd_skill(args: str, state, config) -> bool:
|
|
|
4371
4379
|
|
|
4372
4380
|
if rest.startswith("awesome"):
|
|
4373
4381
|
query = rest[7:].strip()
|
|
4374
|
-
# `--
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
info("Fetching awesome skills + descriptions from GitHub (parallel, ~5s)...")
|
|
4382
|
+
# `--fast` skips descriptions for an instant list; default pulls them in parallel.
|
|
4383
|
+
fast = False
|
|
4384
|
+
if "--fast" in query.split():
|
|
4385
|
+
fast = True
|
|
4386
|
+
query = " ".join(t for t in query.split() if t != "--fast").strip()
|
|
4387
|
+
if fast:
|
|
4388
|
+
info("Fetching awesome skill list from GitHub (instant, no descriptions)...")
|
|
4382
4389
|
else:
|
|
4383
|
-
info("Fetching awesome
|
|
4384
|
-
skills = list_awesome_remote(query, with_descriptions=
|
|
4390
|
+
info("Fetching awesome skills + descriptions from GitHub (parallel, ~5s)...")
|
|
4391
|
+
skills = list_awesome_remote(query, with_descriptions=not fast)
|
|
4385
4392
|
if not skills:
|
|
4386
4393
|
err("Could not fetch awesome skills (network or rate-limit).")
|
|
4387
4394
|
return True
|
|
@@ -4390,7 +4397,7 @@ def cmd_skill(args: str, state, config) -> bool:
|
|
|
4390
4397
|
for s in skills
|
|
4391
4398
|
]
|
|
4392
4399
|
header = f"Awesome skills ({len(skills)})" + (f" matching '{query}'" if query else "")
|
|
4393
|
-
hint = "" if
|
|
4400
|
+
hint = "" if not fast else " — remove `--fast` for descriptions"
|
|
4394
4401
|
_pager(f"{header}{hint} — n=next q=quit", lines)
|
|
4395
4402
|
return True
|
|
4396
4403
|
|
|
@@ -4455,6 +4462,21 @@ def cmd_skill(args: str, state, config) -> bool:
|
|
|
4455
4462
|
print(f" {clr(r['slug'], 'cyan'):30s} {r.get('description','')[:60]}")
|
|
4456
4463
|
return True
|
|
4457
4464
|
|
|
4465
|
+
if rest.startswith("installed"):
|
|
4466
|
+
query = rest[9:].strip()
|
|
4467
|
+
skills = list_installed(query)
|
|
4468
|
+
if not skills:
|
|
4469
|
+
if query:
|
|
4470
|
+
info(f"No installed skills matching '{query}'.")
|
|
4471
|
+
else:
|
|
4472
|
+
info("No skills installed yet.")
|
|
4473
|
+
return True
|
|
4474
|
+
header = f"Installed skills ({len(skills)})" + (f" matching '{query}'" if query else "")
|
|
4475
|
+
info(header + ":")
|
|
4476
|
+
for s in skills:
|
|
4477
|
+
print(f" • {clr(s['name'], 'cyan'):22s} [{s['source']}] {s['description']}")
|
|
4478
|
+
return True
|
|
4479
|
+
|
|
4458
4480
|
# /skill info <name>
|
|
4459
4481
|
if subcmd == "info":
|
|
4460
4482
|
if not rest:
|
|
@@ -4494,27 +4516,83 @@ def cmd_skill(args: str, state, config) -> bool:
|
|
|
4494
4516
|
if rest.startswith("clawhub:"):
|
|
4495
4517
|
slug = rest[8:]
|
|
4496
4518
|
success, msg = install_clawhub(slug)
|
|
4519
|
+
elif rest.startswith("awesome/"):
|
|
4520
|
+
success, msg = install_awesome_remote(rest)
|
|
4497
4521
|
else:
|
|
4498
4522
|
success, msg = install_local(rest)
|
|
4523
|
+
# Fallback to awesome-remote if not found locally
|
|
4524
|
+
if not success:
|
|
4525
|
+
success, msg = install_awesome_remote(rest)
|
|
4499
4526
|
(ok if success else err)(msg)
|
|
4500
4527
|
return True
|
|
4501
4528
|
|
|
4502
4529
|
# ── /skill use ─────────────────────────────────────────────────────────
|
|
4503
4530
|
if subcmd == "use":
|
|
4531
|
+
from skill.clawhub import DULUS_SKILLS_DIR
|
|
4532
|
+
installed = list_installed()
|
|
4533
|
+
if not installed:
|
|
4534
|
+
err("No skills installed yet. Run /skill list to browse and install skills.")
|
|
4535
|
+
return True
|
|
4536
|
+
|
|
4537
|
+
# Interactive picker when no target is given
|
|
4504
4538
|
if not rest:
|
|
4505
|
-
|
|
4539
|
+
print(clr(" Select skill(s) to inject (active for this turn):", "cyan", "bold"))
|
|
4540
|
+
menu_buf = clr(" Select skill(s) to inject:", "cyan", "bold")
|
|
4541
|
+
for i, s in enumerate(installed):
|
|
4542
|
+
line = f" {clr(f'[{i+1:2d}]', 'yellow')} {clr(s['name'], 'white', 'bold'):<24} [{clr(s['source'], 'dim')}] {s['description'][:55]}"
|
|
4543
|
+
print(line)
|
|
4544
|
+
menu_buf += "\n" + line
|
|
4545
|
+
print()
|
|
4546
|
+
ans = ask_input_interactive(
|
|
4547
|
+
clr(" Enter number(s) (e.g. 1 or 1,2,3), name, or Enter to cancel > ", "cyan"),
|
|
4548
|
+
config, menu_buf,
|
|
4549
|
+
).strip()
|
|
4550
|
+
if not ans:
|
|
4551
|
+
info(" Cancelled.")
|
|
4552
|
+
return True
|
|
4553
|
+
rest = ans
|
|
4554
|
+
|
|
4555
|
+
# Resolve rest → list of skill names
|
|
4556
|
+
selected_names: list[str] = []
|
|
4557
|
+
tokens = [t.strip() for t in rest.replace(",", " ").split() if t.strip()]
|
|
4558
|
+
for tok in tokens:
|
|
4559
|
+
if tok.isdigit():
|
|
4560
|
+
idx = int(tok) - 1
|
|
4561
|
+
if 0 <= idx < len(installed):
|
|
4562
|
+
selected_names.append(installed[idx]["name"])
|
|
4563
|
+
else:
|
|
4564
|
+
warn(f"Index {tok} out of range (1-{len(installed)}). Skipping.")
|
|
4565
|
+
else:
|
|
4566
|
+
match = next((s["name"] for s in installed if s["name"].lower() == tok.lower()), None)
|
|
4567
|
+
if match is None:
|
|
4568
|
+
warn(f"No skill named '{tok}'. Skipping.")
|
|
4569
|
+
else:
|
|
4570
|
+
selected_names.append(match)
|
|
4571
|
+
|
|
4572
|
+
if not selected_names:
|
|
4573
|
+
err("No valid skill selected.")
|
|
4506
4574
|
return True
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4575
|
+
|
|
4576
|
+
# Inject selected skills into context
|
|
4577
|
+
blocks = []
|
|
4578
|
+
for name in selected_names:
|
|
4579
|
+
body = read_skill(name)
|
|
4580
|
+
if not body:
|
|
4581
|
+
warn(f"Skill '{name}' could not be read. Skipping.")
|
|
4582
|
+
continue
|
|
4583
|
+
skill_dir = DULUS_SKILLS_DIR / name
|
|
4584
|
+
path_hint = f"\n\n# NOTE: Skill '{name}' files are located at: {skill_dir}" if skill_dir.exists() else ""
|
|
4585
|
+
blocks.append(f"## Skill: {name}\n\n{body}{path_hint}")
|
|
4586
|
+
|
|
4587
|
+
if not blocks:
|
|
4588
|
+
err("No skills could be injected.")
|
|
4511
4589
|
return True
|
|
4512
|
-
|
|
4513
|
-
skill_dir = DULUS_SKILLS_DIR / rest
|
|
4514
|
-
path_hint = f"\n\n# NOTE: Skill '{rest}' files are located at: {skill_dir}" if skill_dir.exists() else ""
|
|
4590
|
+
|
|
4515
4591
|
existing = config.get("_skill_inject", "")
|
|
4516
|
-
|
|
4517
|
-
|
|
4592
|
+
new_inject = "\n\n---\n\n".join(blocks)
|
|
4593
|
+
config["_skill_inject"] = (existing + "\n\n" + new_inject).strip() if existing else new_inject
|
|
4594
|
+
names_txt = ", ".join(f"'{n}'" for n in selected_names)
|
|
4595
|
+
ok(f"Injected {len(blocks)} skill(s) — active for this turn: {names_txt}")
|
|
4518
4596
|
return True
|
|
4519
4597
|
|
|
4520
4598
|
# ── /skill remove ──────────────────────────────────────────────────────
|
|
@@ -6355,6 +6433,230 @@ def cmd_voice(args: str, state, config) -> bool:
|
|
|
6355
6433
|
return ("__voice__", text)
|
|
6356
6434
|
|
|
6357
6435
|
|
|
6436
|
+
# Global handle to the running wake-word listener (managed by repl)
|
|
6437
|
+
_wake_listener: Any = None
|
|
6438
|
+
|
|
6439
|
+
|
|
6440
|
+
def cmd_wake(args: str, state, config) -> bool:
|
|
6441
|
+
"""Wake-word (hotword) detection — hands-free voice activation.
|
|
6442
|
+
|
|
6443
|
+
/wake on — start listening for "Hey Dulus" in background
|
|
6444
|
+
/wake off — stop the background listener
|
|
6445
|
+
/wake status — show whether listener is active
|
|
6446
|
+
/wake phrases — list recognised wake phrases
|
|
6447
|
+
/wake threshold <0.01-0.20> — tune mic sensitivity (default 0.035)
|
|
6448
|
+
"""
|
|
6449
|
+
global _wake_listener
|
|
6450
|
+
|
|
6451
|
+
subcmd = args.strip().lower().split()[0] if args.strip() else ""
|
|
6452
|
+
rest = args.strip()[len(subcmd):].strip()
|
|
6453
|
+
|
|
6454
|
+
# ── /wake threshold ──
|
|
6455
|
+
if subcmd == "threshold":
|
|
6456
|
+
if not rest:
|
|
6457
|
+
current = config.get("wake_threshold", 0.020)
|
|
6458
|
+
info(f"Current wake threshold: {current} (higher = less sensitive)")
|
|
6459
|
+
return True
|
|
6460
|
+
try:
|
|
6461
|
+
val = float(rest)
|
|
6462
|
+
if not 0.001 <= val <= 1.0:
|
|
6463
|
+
raise ValueError
|
|
6464
|
+
config["wake_threshold"] = val
|
|
6465
|
+
ok(f"Wake threshold set to {val}")
|
|
6466
|
+
try:
|
|
6467
|
+
from config import save_config
|
|
6468
|
+
save_config(config)
|
|
6469
|
+
except Exception as e:
|
|
6470
|
+
warn(f"Could not save config: {e}")
|
|
6471
|
+
except ValueError:
|
|
6472
|
+
err("Threshold must be a number between 0.001 and 1.0")
|
|
6473
|
+
return True
|
|
6474
|
+
|
|
6475
|
+
# ── /wake phrases ──
|
|
6476
|
+
if subcmd == "phrases":
|
|
6477
|
+
try:
|
|
6478
|
+
from voice.wake_word import WAKE_PHRASES
|
|
6479
|
+
except ImportError:
|
|
6480
|
+
err("voice/wake_word.py not found")
|
|
6481
|
+
return True
|
|
6482
|
+
print(clr(" Recognised wake phrases:", "cyan", "bold"))
|
|
6483
|
+
for p in WAKE_PHRASES:
|
|
6484
|
+
print(f" • {p}")
|
|
6485
|
+
return True
|
|
6486
|
+
|
|
6487
|
+
# ── /wake calibrate ──
|
|
6488
|
+
if subcmd == "calibrate":
|
|
6489
|
+
try:
|
|
6490
|
+
from voice import check_voice_deps
|
|
6491
|
+
except ImportError:
|
|
6492
|
+
err("voice package not available")
|
|
6493
|
+
return True
|
|
6494
|
+
available, reason = check_voice_deps()
|
|
6495
|
+
if not available:
|
|
6496
|
+
err(f"Voice deps missing: {reason}")
|
|
6497
|
+
return True
|
|
6498
|
+
print(clr(" 🎙 Calibrating mic — speak normally for 5 seconds…", "cyan"))
|
|
6499
|
+
print(clr(" Press Ctrl+C when done.\n", "dim"))
|
|
6500
|
+
try:
|
|
6501
|
+
import sounddevice as sd
|
|
6502
|
+
import numpy as np
|
|
6503
|
+
_chunk = int(16000 * 0.3)
|
|
6504
|
+
_bars = " ▁▂▃▄▅▆▇█"
|
|
6505
|
+
_max_rms = 0.0
|
|
6506
|
+
with sd.InputStream(
|
|
6507
|
+
samplerate=16000, channels=1, dtype="int16",
|
|
6508
|
+
blocksize=_chunk,
|
|
6509
|
+
device=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6510
|
+
) as stream:
|
|
6511
|
+
import time as _time
|
|
6512
|
+
_t0 = _time.monotonic()
|
|
6513
|
+
while _time.monotonic() - _t0 < 5.0:
|
|
6514
|
+
pcm, _ = stream.read(_chunk)
|
|
6515
|
+
arr = np.frombuffer(pcm.tobytes(), dtype=np.int16).astype(np.float32)
|
|
6516
|
+
if arr.size:
|
|
6517
|
+
rms = float(np.sqrt(np.mean(arr ** 2))) / 32768.0
|
|
6518
|
+
_max_rms = max(_max_rms, rms)
|
|
6519
|
+
lvl = min(int(rms * 8 / 0.08), 8)
|
|
6520
|
+
bar = _bars[lvl]
|
|
6521
|
+
print(f"\r RMS: {rms:.4f} {bar} (max {_max_rms:.4f})", end="", flush=True)
|
|
6522
|
+
_time.sleep(0.05)
|
|
6523
|
+
print()
|
|
6524
|
+
print(clr(f"\n Max RMS detected: {_max_rms:.4f}", "cyan", "bold"))
|
|
6525
|
+
rec = _max_rms * 0.7
|
|
6526
|
+
if rec < 0.005:
|
|
6527
|
+
rec = 0.010
|
|
6528
|
+
info(f" Recommended threshold: ~{rec:.3f}")
|
|
6529
|
+
info(f" Current threshold: {config.get('wake_threshold', 0.020)}")
|
|
6530
|
+
info(" Use '/wake threshold <n>' to adjust.")
|
|
6531
|
+
except KeyboardInterrupt:
|
|
6532
|
+
print()
|
|
6533
|
+
except Exception as e:
|
|
6534
|
+
err(f"Calibration failed: {e}")
|
|
6535
|
+
return True
|
|
6536
|
+
|
|
6537
|
+
# ── /wake test ──
|
|
6538
|
+
if subcmd == "test":
|
|
6539
|
+
try:
|
|
6540
|
+
from voice import check_voice_deps
|
|
6541
|
+
from voice.wake_word import WakeWordListener
|
|
6542
|
+
except ImportError as e:
|
|
6543
|
+
err(f"Wake-word module not available: {e}")
|
|
6544
|
+
return True
|
|
6545
|
+
available, reason = check_voice_deps()
|
|
6546
|
+
if not available:
|
|
6547
|
+
err(f"Voice input not available:\n{reason}")
|
|
6548
|
+
return True
|
|
6549
|
+
print(clr(" 🎙 Wake-word TEST mode — debug output ON for 10 seconds", "cyan", "bold"))
|
|
6550
|
+
print(clr(" Say 'Hey Dulus' now. Press Ctrl+C to stop early.\n", "dim"))
|
|
6551
|
+
_test_listener = WakeWordListener(
|
|
6552
|
+
rms_threshold=config.get("wake_threshold", 0.020),
|
|
6553
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6554
|
+
language=_voice_language,
|
|
6555
|
+
debug=True,
|
|
6556
|
+
)
|
|
6557
|
+
_found: list[str] = []
|
|
6558
|
+
|
|
6559
|
+
def _test_wake(phrase: str) -> None:
|
|
6560
|
+
print(clr(f"\n ✅ WAKE DETECTED: '{phrase}'", "green", "bold"))
|
|
6561
|
+
|
|
6562
|
+
def _test_cmd(text: str) -> None:
|
|
6563
|
+
_found.append(text)
|
|
6564
|
+
print(clr(f'\n 🎙️ COMMAND: "{text}"', "green", "bold"))
|
|
6565
|
+
|
|
6566
|
+
_test_listener.start(on_wake=_test_wake, on_command=_test_cmd)
|
|
6567
|
+
try:
|
|
6568
|
+
import time as _time
|
|
6569
|
+
_time.sleep(10)
|
|
6570
|
+
except KeyboardInterrupt:
|
|
6571
|
+
print()
|
|
6572
|
+
finally:
|
|
6573
|
+
_test_listener.stop()
|
|
6574
|
+
if not _found:
|
|
6575
|
+
warn(" No wake word detected in 10s. Try '/wake calibrate' to check your mic levels.")
|
|
6576
|
+
return True
|
|
6577
|
+
|
|
6578
|
+
# ── /wake status ──
|
|
6579
|
+
if subcmd == "status":
|
|
6580
|
+
try:
|
|
6581
|
+
from voice import check_voice_deps
|
|
6582
|
+
except ImportError:
|
|
6583
|
+
err("voice package not available")
|
|
6584
|
+
return True
|
|
6585
|
+
available, reason = check_voice_deps()
|
|
6586
|
+
if not available:
|
|
6587
|
+
err(f"Voice deps missing: {reason}")
|
|
6588
|
+
return True
|
|
6589
|
+
active = _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)()
|
|
6590
|
+
state_str = clr("ACTIVE", "green", "bold") if active else clr("OFF", "gray")
|
|
6591
|
+
info(f"Wake-word listener: {state_str}")
|
|
6592
|
+
info(f"Threshold: {config.get('wake_threshold', 0.020)}")
|
|
6593
|
+
if active:
|
|
6594
|
+
info("Say 'Hey Dulus' followed by your command.")
|
|
6595
|
+
else:
|
|
6596
|
+
info("Use '/wake on' to start listening.")
|
|
6597
|
+
return True
|
|
6598
|
+
|
|
6599
|
+
# ── /wake off ──
|
|
6600
|
+
if subcmd in ["off", "false", "disable", "stop"]:
|
|
6601
|
+
config["wake_enabled"] = False
|
|
6602
|
+
try:
|
|
6603
|
+
from config import save_config
|
|
6604
|
+
save_config(config)
|
|
6605
|
+
except Exception:
|
|
6606
|
+
pass
|
|
6607
|
+
if _wake_listener is not None:
|
|
6608
|
+
try:
|
|
6609
|
+
_wake_listener.stop()
|
|
6610
|
+
except Exception as e:
|
|
6611
|
+
warn(f"Error stopping wake listener: {e}")
|
|
6612
|
+
_wake_listener = None
|
|
6613
|
+
ok("Wake-word listener stopped.")
|
|
6614
|
+
else:
|
|
6615
|
+
info("Wake-word listener was not running.")
|
|
6616
|
+
return True
|
|
6617
|
+
|
|
6618
|
+
# ── /wake on ──
|
|
6619
|
+
if subcmd in ["on", "true", "enable", "start"]:
|
|
6620
|
+
if _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)():
|
|
6621
|
+
info("Wake-word listener is already active.")
|
|
6622
|
+
return True
|
|
6623
|
+
|
|
6624
|
+
try:
|
|
6625
|
+
from voice import check_voice_deps
|
|
6626
|
+
from voice.wake_word import WakeWordListener
|
|
6627
|
+
except ImportError as e:
|
|
6628
|
+
err(f"Wake-word module not available: {e}")
|
|
6629
|
+
return True
|
|
6630
|
+
|
|
6631
|
+
available, reason = check_voice_deps()
|
|
6632
|
+
if not available:
|
|
6633
|
+
err(f"Voice input not available:\n{reason}")
|
|
6634
|
+
return True
|
|
6635
|
+
|
|
6636
|
+
# The actual on_wake / on_command callbacks are injected by repl()
|
|
6637
|
+
# via the _wake_listener global handle. Here we just create the
|
|
6638
|
+
# object; repl() wires the queue when it sees the handle change.
|
|
6639
|
+
_wake_listener = WakeWordListener(
|
|
6640
|
+
rms_threshold=config.get("wake_threshold", 0.035),
|
|
6641
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6642
|
+
language=_voice_language,
|
|
6643
|
+
)
|
|
6644
|
+
config["wake_enabled"] = True
|
|
6645
|
+
try:
|
|
6646
|
+
from config import save_config
|
|
6647
|
+
save_config(config)
|
|
6648
|
+
except Exception:
|
|
6649
|
+
pass
|
|
6650
|
+
ok("Wake-word listener starting… Say 'Hey Dulus' to activate.")
|
|
6651
|
+
return True
|
|
6652
|
+
|
|
6653
|
+
# ── Toggle ──
|
|
6654
|
+
if _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)():
|
|
6655
|
+
return cmd_wake("off", state, config)
|
|
6656
|
+
else:
|
|
6657
|
+
return cmd_wake("on", state, config)
|
|
6658
|
+
|
|
6659
|
+
|
|
6358
6660
|
def cmd_image(args: str, state, config) -> Union[bool, tuple]:
|
|
6359
6661
|
"""Grab image from clipboard and send to vision model with optional prompt."""
|
|
6360
6662
|
import sys as _sys
|
|
@@ -7468,6 +7770,7 @@ COMMANDS = {
|
|
|
7468
7770
|
"lite": cmd_lite,
|
|
7469
7771
|
"cloudsave": cmd_cloudsave,
|
|
7470
7772
|
"voice": cmd_voice,
|
|
7773
|
+
"wake": cmd_wake,
|
|
7471
7774
|
"git": cmd_git,
|
|
7472
7775
|
"webchat": cmd_webchat,
|
|
7473
7776
|
"sandbox": cmd_sandbox,
|
|
@@ -7579,6 +7882,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
|
|
|
7579
7882
|
"cloudsave": ("Cloud-sync sessions to GitHub Gist", ["setup", "auto", "list", "load", "push"]),
|
|
7580
7883
|
"tts": ("Toggle automatic TTS + lang/provider/auto", ["lang", "provider", "voice", "auto"]),
|
|
7581
7884
|
"voice": ("Voice input (record → STT)", ["lang", "status", "device"]),
|
|
7885
|
+
"wake": ("Wake-word hotword detection", ["on", "off", "status", "phrases", "calibrate", "test", "threshold"]),
|
|
7582
7886
|
"image": ("Send clipboard image to model", []),
|
|
7583
7887
|
"img": ("Send clipboard image (alias)", []),
|
|
7584
7888
|
"batch": ("Manage Kimi Batch tasks", ["status", "list", "fetch"]),
|
|
@@ -7710,6 +8014,10 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7710
8014
|
verbose = config.get("verbose", False)
|
|
7711
8015
|
config["_tg_send_callback"] = _tg_send
|
|
7712
8016
|
|
|
8017
|
+
# ── Wake-word queue ──
|
|
8018
|
+
import queue as _queue
|
|
8019
|
+
_wake_queue: "_queue.Queue[str]" = _queue.Queue()
|
|
8020
|
+
|
|
7713
8021
|
def _render_toolbar() -> str:
|
|
7714
8022
|
"""Return ANSI toolbar string for prompt_toolkit bottom bar.
|
|
7715
8023
|
|
|
@@ -8805,6 +9113,88 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8805
9113
|
"Please review the results and report back to the user.")
|
|
8806
9114
|
except Exception:
|
|
8807
9115
|
pass
|
|
9116
|
+
|
|
9117
|
+
# ── Wake-word listener lifecycle ──
|
|
9118
|
+
global _wake_listener
|
|
9119
|
+
|
|
9120
|
+
# [Autostart] If enabled in config but listener not created yet
|
|
9121
|
+
if _wake_listener is None and config.get("wake_enabled"):
|
|
9122
|
+
try:
|
|
9123
|
+
from voice.wake_word import WakeWordListener
|
|
9124
|
+
_wake_listener = WakeWordListener(
|
|
9125
|
+
rms_threshold=config.get("wake_threshold", 0.035),
|
|
9126
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
9127
|
+
language=_voice_language,
|
|
9128
|
+
)
|
|
9129
|
+
# Pre-load Whisper in background so detection is fast
|
|
9130
|
+
def _preload():
|
|
9131
|
+
try:
|
|
9132
|
+
from voice.stt import _get_faster_whisper_model
|
|
9133
|
+
_get_faster_whisper_model()
|
|
9134
|
+
except Exception:
|
|
9135
|
+
pass
|
|
9136
|
+
threading.Thread(target=_preload, daemon=True).start()
|
|
9137
|
+
except ImportError:
|
|
9138
|
+
pass
|
|
9139
|
+
|
|
9140
|
+
if _wake_listener is not None and not _wake_listener.is_running():
|
|
9141
|
+
def _on_wake(phrase: str) -> None:
|
|
9142
|
+
# Use safe_print_notification to avoid ghosting/re-printing in sticky mode
|
|
9143
|
+
import input as _dulus_input
|
|
9144
|
+
_dulus_input.safe_print_notification(clr(f"\x1b[32m ✅ WAKE DETECTED: '{phrase}'\x1b[0m", "bold"))
|
|
9145
|
+
_dulus_input.set_toolbar_status(clr("Waking up...", "cyan"))
|
|
9146
|
+
|
|
9147
|
+
# Immediate audible feedback — universal beep
|
|
9148
|
+
try:
|
|
9149
|
+
from voice import beep
|
|
9150
|
+
beep(880, 150)
|
|
9151
|
+
except Exception:
|
|
9152
|
+
pass
|
|
9153
|
+
# TTS feedback
|
|
9154
|
+
# NOTE: say() is blocking, which correctly delays the command recording
|
|
9155
|
+
# in WakeWordListener until after the response is finished.
|
|
9156
|
+
try:
|
|
9157
|
+
from voice import say
|
|
9158
|
+
_resp = config.get("wake_response", "¿Dime, papi?")
|
|
9159
|
+
say(_resp, provider=config.get("tts_provider", "auto"))
|
|
9160
|
+
except Exception:
|
|
9161
|
+
pass
|
|
9162
|
+
|
|
9163
|
+
def _on_command(text: str) -> None:
|
|
9164
|
+
# Filter common Whisper hallucinations on silence/noise
|
|
9165
|
+
# NOTE: We allow "gracias" as it's a valid thing a user might say.
|
|
9166
|
+
_HALLUC = {
|
|
9167
|
+
"thank you.", "thank you", "thanks for watching.",
|
|
9168
|
+
"thanks for watching!", "thanks.", ".", "you",
|
|
9169
|
+
"subtitles by the amara.org community",
|
|
9170
|
+
}
|
|
9171
|
+
_norm = text.strip().lower()
|
|
9172
|
+
if not _norm or _norm in _HALLUC:
|
|
9173
|
+
# Ignore hallucinations silently
|
|
9174
|
+
import input as _dulus_input
|
|
9175
|
+
_dulus_input.set_toolbar_status("")
|
|
9176
|
+
return
|
|
9177
|
+
|
|
9178
|
+
# Always put in queue so the main loop can pick it up
|
|
9179
|
+
_wake_queue.put(text)
|
|
9180
|
+
|
|
9181
|
+
# Signal the active prompt to exit (unblocks dulus_input.read_line_split)
|
|
9182
|
+
import input as _dulus_input
|
|
9183
|
+
_dulus_input.request_exit()
|
|
9184
|
+
|
|
9185
|
+
_dulus_input.set_toolbar_status("") # Clear toolbar on success
|
|
9186
|
+
_dulus_input.safe_print_notification(clr(f"\n 🎙️ COMMAND: \"{text}\"", "cyan", "bold"))
|
|
9187
|
+
|
|
9188
|
+
_wake_listener.start(on_wake=_on_wake, on_command=_on_command)
|
|
9189
|
+
|
|
9190
|
+
# ── Check for wake-word command before blocking on keyboard ──
|
|
9191
|
+
user_input = ""
|
|
9192
|
+
_wake_cmd: str | None = None
|
|
9193
|
+
try:
|
|
9194
|
+
_wake_cmd = _wake_queue.get_nowait()
|
|
9195
|
+
except _queue.Empty:
|
|
9196
|
+
_wake_cmd = None
|
|
9197
|
+
|
|
8808
9198
|
try:
|
|
8809
9199
|
cwd_short = Path.cwd().name
|
|
8810
9200
|
# Live context-usage indicator: "[73%]" — green<60, yellow<85, red otherwise.
|
|
@@ -8829,7 +9219,12 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8829
9219
|
prompt = _rl_safe(clr(f"\n[{cwd_short}] ", "dim") + ctx_tag + clr("» ", "cyan", "bold"))
|
|
8830
9220
|
if in_batch_mode:
|
|
8831
9221
|
prompt = _rl_safe(clr(f" batch[{len(batch_buffer)}] » ", "yellow", "bold"))
|
|
8832
|
-
|
|
9222
|
+
|
|
9223
|
+
if _wake_cmd is not None:
|
|
9224
|
+
user_input = _wake_cmd
|
|
9225
|
+
import input as _dulus_input
|
|
9226
|
+
_dulus_input.safe_print_notification(clr(f"\n 🦅 [Wake] » {user_input}\n", "green", "bold"))
|
|
9227
|
+
elif config.pop("_auto_voice_next", False) and not in_batch_mode:
|
|
8833
9228
|
print(clr(" 🎙 [auto-voice] Listening… (Ctrl+C to type instead)", "cyan"))
|
|
8834
9229
|
try:
|
|
8835
9230
|
from voice import voice_input as _av_voice_input
|
|
@@ -8864,6 +9259,13 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8864
9259
|
user_input = _read_input(prompt)
|
|
8865
9260
|
except (EOFError, KeyboardInterrupt):
|
|
8866
9261
|
print()
|
|
9262
|
+
# ── Stop wake-word listener on exit ──
|
|
9263
|
+
try:
|
|
9264
|
+
if _wake_listener is not None:
|
|
9265
|
+
_wake_listener.stop()
|
|
9266
|
+
globals()["_wake_listener"] = None
|
|
9267
|
+
except Exception:
|
|
9268
|
+
pass
|
|
8867
9269
|
# ── Sleep Trigger: Ask to consolidate before final exit ─────────
|
|
8868
9270
|
try:
|
|
8869
9271
|
# Only ask if there's actually a session worth saving
|