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.
Files changed (361) hide show
  1. {dulus-0.2.52/dulus.egg-info → dulus-0.2.54}/PKG-INFO +1 -1
  2. {dulus-0.2.52 → dulus-0.2.54/dulus.egg-info}/PKG-INFO +1 -1
  3. {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/SOURCES.txt +3 -1
  4. {dulus-0.2.52 → dulus-0.2.54}/dulus.py +425 -23
  5. {dulus-0.2.52 → dulus-0.2.54}/input.py +73 -19
  6. {dulus-0.2.52 → dulus-0.2.54}/pyproject.toml +1 -1
  7. {dulus-0.2.52 → dulus-0.2.54}/skill/clawhub.py +89 -1
  8. {dulus-0.2.52 → dulus-0.2.54}/tests/test_voice.py +56 -0
  9. {dulus-0.2.52 → dulus-0.2.54}/voice/__init__.py +8 -0
  10. dulus-0.2.54/voice/audio_utils.py +37 -0
  11. {dulus-0.2.52 → dulus-0.2.54}/voice/recorder.py +11 -7
  12. {dulus-0.2.52 → dulus-0.2.54}/voice/stt.py +20 -0
  13. {dulus-0.2.52 → dulus-0.2.54}/voice/tts.py +109 -36
  14. dulus-0.2.54/voice/wake_word.py +371 -0
  15. {dulus-0.2.52 → dulus-0.2.54}/LICENSE +0 -0
  16. {dulus-0.2.52 → dulus-0.2.54}/MANIFEST.in +0 -0
  17. {dulus-0.2.52 → dulus-0.2.54}/README.md +0 -0
  18. {dulus-0.2.52 → dulus-0.2.54}/agent.py +0 -0
  19. {dulus-0.2.52 → dulus-0.2.54}/backend/__init__.py +0 -0
  20. {dulus-0.2.52 → dulus-0.2.54}/backend/agents_bridge.py +0 -0
  21. {dulus-0.2.52 → dulus-0.2.54}/backend/compressor.py +0 -0
  22. {dulus-0.2.52 → dulus-0.2.54}/backend/context.py +0 -0
  23. {dulus-0.2.52 → dulus-0.2.54}/backend/githook.py +0 -0
  24. {dulus-0.2.52 → dulus-0.2.54}/backend/marketplace.py +0 -0
  25. {dulus-0.2.52 → dulus-0.2.54}/backend/mempalace_bridge.py +0 -0
  26. {dulus-0.2.52 → dulus-0.2.54}/backend/personas.py +0 -0
  27. {dulus-0.2.52 → dulus-0.2.54}/backend/plugins.py +0 -0
  28. {dulus-0.2.52 → dulus-0.2.54}/backend/server.py +0 -0
  29. {dulus-0.2.52 → dulus-0.2.54}/backend/tasks.py +0 -0
  30. {dulus-0.2.52 → dulus-0.2.54}/batch_api.py +0 -0
  31. {dulus-0.2.52 → dulus-0.2.54}/checkpoint/__init__.py +0 -0
  32. {dulus-0.2.52 → dulus-0.2.54}/checkpoint/hooks.py +0 -0
  33. {dulus-0.2.52 → dulus-0.2.54}/checkpoint/store.py +0 -0
  34. {dulus-0.2.52 → dulus-0.2.54}/checkpoint/types.py +0 -0
  35. {dulus-0.2.52 → dulus-0.2.54}/claude_code_watcher.py +0 -0
  36. {dulus-0.2.52 → dulus-0.2.54}/clipboard_utils.py +0 -0
  37. {dulus-0.2.52 → dulus-0.2.54}/cloudsave.py +0 -0
  38. {dulus-0.2.52 → dulus-0.2.54}/common.py +0 -0
  39. {dulus-0.2.52 → dulus-0.2.54}/compaction.py +0 -0
  40. {dulus-0.2.52 → dulus-0.2.54}/config.py +0 -0
  41. {dulus-0.2.52 → dulus-0.2.54}/context.py +0 -0
  42. {dulus-0.2.52 → dulus-0.2.54}/data/__init__.py +0 -0
  43. {dulus-0.2.52 → dulus-0.2.54}/data/active_persona.json +0 -0
  44. {dulus-0.2.52 → dulus-0.2.54}/data/context.json +0 -0
  45. {dulus-0.2.52 → dulus-0.2.54}/data/marketplace.json +0 -0
  46. {dulus-0.2.52 → dulus-0.2.54}/data/personas.json +0 -0
  47. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/__init__.py +0 -0
  48. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/__init__.py +0 -0
  49. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  50. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  51. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  52. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/plugin.json +0 -0
  53. {dulus-0.2.52 → dulus-0.2.54}/data/plugins/composio/plugin_tool.py +0 -0
  54. {dulus-0.2.52 → dulus-0.2.54}/data/tasks.json +0 -0
  55. {dulus-0.2.52 → dulus-0.2.54}/docs/README.md +0 -0
  56. {dulus-0.2.52 → dulus-0.2.54}/docs/__init__.py +0 -0
  57. {dulus-0.2.52 → dulus-0.2.54}/docs/api.html +0 -0
  58. {dulus-0.2.52 → dulus-0.2.54}/docs/architecture.md +0 -0
  59. {dulus-0.2.52 → dulus-0.2.54}/docs/azure-speech-template.json +0 -0
  60. {dulus-0.2.52 → dulus-0.2.54}/docs/dashboard/index.html +0 -0
  61. {dulus-0.2.52 → dulus-0.2.54}/docs/divider.svg +0 -0
  62. {dulus-0.2.52 → dulus-0.2.54}/docs/generate.py +0 -0
  63. {dulus-0.2.52 → dulus-0.2.54}/docs/hero.svg +0 -0
  64. {dulus-0.2.52 → dulus-0.2.54}/docs/index.html +0 -0
  65. {dulus-0.2.52 → dulus-0.2.54}/docs/news.md +0 -0
  66. {dulus-0.2.52 → dulus-0.2.54}/docs/nvidia-models.svg +0 -0
  67. {dulus-0.2.52 → dulus-0.2.54}/docs/particle-playground.html +0 -0
  68. {dulus-0.2.52 → dulus-0.2.54}/docs/personas/index.html +0 -0
  69. {dulus-0.2.52 → dulus-0.2.54}/docs/poetry-banner.png +0 -0
  70. {dulus-0.2.52 → dulus-0.2.54}/docs/preview.html +0 -0
  71. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-agents.svg +0 -0
  72. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-brainstorm.svg +0 -0
  73. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-bridges.svg +0 -0
  74. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-features.svg +0 -0
  75. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-freetier.svg +0 -0
  76. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-memory.svg +0 -0
  77. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-models.svg +0 -0
  78. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-perms.svg +0 -0
  79. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-plugins.svg +0 -0
  80. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-quickstart.svg +0 -0
  81. {dulus-0.2.52 → dulus-0.2.54}/docs/sec-ssj.svg +0 -0
  82. {dulus-0.2.52 → dulus-0.2.54}/docs/spinners.svg +0 -0
  83. {dulus-0.2.52 → dulus-0.2.54}/docs/split-pane.svg +0 -0
  84. {dulus-0.2.52 → dulus-0.2.54}/docs/terminal-boot.svg +0 -0
  85. {dulus-0.2.52 → dulus-0.2.54}/docs/uploads/particle-playground.html +0 -0
  86. {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/dependency_links.txt +0 -0
  87. {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/entry_points.txt +0 -0
  88. {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/requires.txt +0 -0
  89. {dulus-0.2.52 → dulus-0.2.54}/dulus.egg-info/top_level.txt +0 -0
  90. {dulus-0.2.52 → dulus-0.2.54}/dulus_gui.py +0 -0
  91. {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/__init__.py +0 -0
  92. {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/client.py +0 -0
  93. {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/config.py +0 -0
  94. {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/tools.py +0 -0
  95. {dulus-0.2.52 → dulus-0.2.54}/dulus_mcp/types.py +0 -0
  96. {dulus-0.2.52 → dulus-0.2.54}/gui/__init__.py +0 -0
  97. {dulus-0.2.52 → dulus-0.2.54}/gui/agent_bridge.py +0 -0
  98. {dulus-0.2.52 → dulus-0.2.54}/gui/chat_widget.py +0 -0
  99. {dulus-0.2.52 → dulus-0.2.54}/gui/main_window.py +0 -0
  100. {dulus-0.2.52 → dulus-0.2.54}/gui/personas.py +0 -0
  101. {dulus-0.2.52 → dulus-0.2.54}/gui/session_utils.py +0 -0
  102. {dulus-0.2.52 → dulus-0.2.54}/gui/settings_dialog.py +0 -0
  103. {dulus-0.2.52 → dulus-0.2.54}/gui/sidebar.py +0 -0
  104. {dulus-0.2.52 → dulus-0.2.54}/gui/tasks_view.py +0 -0
  105. {dulus-0.2.52 → dulus-0.2.54}/gui/themes.py +0 -0
  106. {dulus-0.2.52 → dulus-0.2.54}/gui/tool_panel.py +0 -0
  107. {dulus-0.2.52 → dulus-0.2.54}/license_manager.py +0 -0
  108. {dulus-0.2.52 → dulus-0.2.54}/memory/__init__.py +0 -0
  109. {dulus-0.2.52 → dulus-0.2.54}/memory/audit.py +0 -0
  110. {dulus-0.2.52 → dulus-0.2.54}/memory/consolidator.py +0 -0
  111. {dulus-0.2.52 → dulus-0.2.54}/memory/context.py +0 -0
  112. {dulus-0.2.52 → dulus-0.2.54}/memory/offload.py +0 -0
  113. {dulus-0.2.52 → dulus-0.2.54}/memory/palace.py +0 -0
  114. {dulus-0.2.52 → dulus-0.2.54}/memory/scan.py +0 -0
  115. {dulus-0.2.52 → dulus-0.2.54}/memory/sessions.py +0 -0
  116. {dulus-0.2.52 → dulus-0.2.54}/memory/store.py +0 -0
  117. {dulus-0.2.52 → dulus-0.2.54}/memory/tools.py +0 -0
  118. {dulus-0.2.52 → dulus-0.2.54}/memory/types.py +0 -0
  119. {dulus-0.2.52 → dulus-0.2.54}/memory/vector_search.py +0 -0
  120. {dulus-0.2.52 → dulus-0.2.54}/multi_agent/__init__.py +0 -0
  121. {dulus-0.2.52 → dulus-0.2.54}/multi_agent/subagent.py +0 -0
  122. {dulus-0.2.52 → dulus-0.2.54}/multi_agent/tools.py +0 -0
  123. {dulus-0.2.52 → dulus-0.2.54}/offload_helper.py +0 -0
  124. {dulus-0.2.52 → dulus-0.2.54}/plugin/__init__.py +0 -0
  125. {dulus-0.2.52 → dulus-0.2.54}/plugin/autoadapter.py +0 -0
  126. {dulus-0.2.52 → dulus-0.2.54}/plugin/loader.py +0 -0
  127. {dulus-0.2.52 → dulus-0.2.54}/plugin/recommend.py +0 -0
  128. {dulus-0.2.52 → dulus-0.2.54}/plugin/store.py +0 -0
  129. {dulus-0.2.52 → dulus-0.2.54}/plugin/types.py +0 -0
  130. {dulus-0.2.52 → dulus-0.2.54}/providers.py +0 -0
  131. {dulus-0.2.52 → dulus-0.2.54}/sandbox/README.md +0 -0
  132. {dulus-0.2.52 → dulus-0.2.54}/sandbox/__init__.py +0 -0
  133. {dulus-0.2.52 → dulus-0.2.54}/sandbox/components.json +0 -0
  134. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  135. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  136. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/index.html +0 -0
  137. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpaper-default.jpg +0 -0
  138. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/default.jpeg +0 -0
  139. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/light.jpeg +0 -0
  140. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  141. {dulus-0.2.52 → dulus-0.2.54}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  142. {dulus-0.2.52 → dulus-0.2.54}/sandbox/eslint.config.js +0 -0
  143. {dulus-0.2.52 → dulus-0.2.54}/sandbox/index.html +0 -0
  144. {dulus-0.2.52 → dulus-0.2.54}/sandbox/info.md +0 -0
  145. {dulus-0.2.52 → dulus-0.2.54}/sandbox/package-lock.json +0 -0
  146. {dulus-0.2.52 → dulus-0.2.54}/sandbox/package.json +0 -0
  147. {dulus-0.2.52 → dulus-0.2.54}/sandbox/postcss.config.js +0 -0
  148. {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpaper-default.jpg +0 -0
  149. {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/default.jpeg +0 -0
  150. {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/light.jpeg +0 -0
  151. {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/nature.jpeg +0 -0
  152. {dulus-0.2.52 → dulus-0.2.54}/sandbox/public/wallpapers/tech.jpeg +0 -0
  153. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/App.tsx +0 -0
  154. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AgentMonitor.tsx +0 -0
  155. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ApiTester.tsx +0 -0
  156. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AppRouter.tsx +0 -0
  157. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ArchiveManager.tsx +0 -0
  158. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/AsciiArt.tsx +0 -0
  159. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Base64Tool.tsx +0 -0
  160. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Browser.tsx +0 -0
  161. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Calculator.tsx +0 -0
  162. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Calendar.tsx +0 -0
  163. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Chat.tsx +0 -0
  164. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Chess.tsx +0 -0
  165. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Clock.tsx +0 -0
  166. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/CodeEditor.tsx +0 -0
  167. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ColorPalette.tsx +0 -0
  168. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ColorPicker.tsx +0 -0
  169. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Contacts.tsx +0 -0
  170. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/DocumentViewer.tsx +0 -0
  171. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Drawing.tsx +0 -0
  172. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Email.tsx +0 -0
  173. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FileManager.tsx +0 -0
  174. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FlappyBird.tsx +0 -0
  175. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/FtpClient.tsx +0 -0
  176. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Game2048.tsx +0 -0
  177. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/GitClient.tsx +0 -0
  178. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ImageGallery.tsx +0 -0
  179. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ImageViewer.tsx +0 -0
  180. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/JsonFormatter.tsx +0 -0
  181. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
  182. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MatrixRain.tsx +0 -0
  183. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MediaConverter.tsx +0 -0
  184. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Memory.tsx +0 -0
  185. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MemoryManager.tsx +0 -0
  186. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Minesweeper.tsx +0 -0
  187. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/MusicPlayer.tsx +0 -0
  188. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/NetworkTools.tsx +0 -0
  189. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Notes.tsx +0 -0
  190. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/PasswordManager.tsx +0 -0
  191. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/PhotoEditor.tsx +0 -0
  192. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Pong.tsx +0 -0
  193. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/RegexTester.tsx +0 -0
  194. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Reminders.tsx +0 -0
  195. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/RssReader.tsx +0 -0
  196. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
  197. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Settings.tsx +0 -0
  198. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
  199. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Snake.tsx +0 -0
  200. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Solitaire.tsx +0 -0
  201. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Spreadsheet.tsx +0 -0
  202. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Sudoku.tsx +0 -0
  203. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/SystemMonitor.tsx +0 -0
  204. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TaskManager.tsx +0 -0
  205. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Terminal.tsx +0 -0
  206. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Tetris.tsx +0 -0
  207. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TextEditor.tsx +0 -0
  208. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/TicTacToe.tsx +0 -0
  209. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Todo.tsx +0 -0
  210. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/VideoPlayer.tsx +0 -0
  211. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
  212. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Weather.tsx +0 -0
  213. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/Whiteboard.tsx +0 -0
  214. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/apps/registry.ts +0 -0
  215. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/AppContainer.tsx +0 -0
  216. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/AppLauncher.tsx +0 -0
  217. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/BootSequence.tsx +0 -0
  218. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ContextMenu.tsx +0 -0
  219. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/Desktop.tsx +0 -0
  220. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/Dock.tsx +0 -0
  221. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/LoginScreen.tsx +0 -0
  222. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotImplemented.tsx +0 -0
  223. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotificationCenter.tsx +0 -0
  224. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/NotificationSystem.tsx +0 -0
  225. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/TopPanel.tsx +0 -0
  226. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/WindowFrame.tsx +0 -0
  227. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/WindowManager.tsx +0 -0
  228. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/accordion.tsx +0 -0
  229. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
  230. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/alert.tsx +0 -0
  231. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
  232. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/avatar.tsx +0 -0
  233. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/badge.tsx +0 -0
  234. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
  235. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/button-group.tsx +0 -0
  236. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/button.tsx +0 -0
  237. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/calendar.tsx +0 -0
  238. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/card.tsx +0 -0
  239. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/carousel.tsx +0 -0
  240. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/chart.tsx +0 -0
  241. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/checkbox.tsx +0 -0
  242. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/collapsible.tsx +0 -0
  243. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/command.tsx +0 -0
  244. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/context-menu.tsx +0 -0
  245. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/dialog.tsx +0 -0
  246. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/drawer.tsx +0 -0
  247. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
  248. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/empty.tsx +0 -0
  249. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/field.tsx +0 -0
  250. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/form.tsx +0 -0
  251. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/hover-card.tsx +0 -0
  252. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input-group.tsx +0 -0
  253. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input-otp.tsx +0 -0
  254. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/input.tsx +0 -0
  255. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/item.tsx +0 -0
  256. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/kbd.tsx +0 -0
  257. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/label.tsx +0 -0
  258. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/menubar.tsx +0 -0
  259. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
  260. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/pagination.tsx +0 -0
  261. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/popover.tsx +0 -0
  262. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/progress.tsx +0 -0
  263. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/radio-group.tsx +0 -0
  264. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/resizable.tsx +0 -0
  265. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/scroll-area.tsx +0 -0
  266. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/select.tsx +0 -0
  267. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/separator.tsx +0 -0
  268. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sheet.tsx +0 -0
  269. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sidebar.tsx +0 -0
  270. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/skeleton.tsx +0 -0
  271. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/slider.tsx +0 -0
  272. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/sonner.tsx +0 -0
  273. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/spinner.tsx +0 -0
  274. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/switch.tsx +0 -0
  275. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/table.tsx +0 -0
  276. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/tabs.tsx +0 -0
  277. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/textarea.tsx +0 -0
  278. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/toggle-group.tsx +0 -0
  279. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/toggle.tsx +0 -0
  280. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/components/ui/tooltip.tsx +0 -0
  281. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/index.ts +0 -0
  282. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/use-mobile.ts +0 -0
  283. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
  284. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusAgents.ts +0 -0
  285. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusChat.ts +0 -0
  286. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusEvents.ts +0 -0
  287. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusHealth.ts +0 -0
  288. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusMemory.ts +0 -0
  289. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusSkills.ts +0 -0
  290. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useDulusTasks.ts +0 -0
  291. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useFileSystem.ts +0 -0
  292. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useOSStore.tsx +0 -0
  293. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSkillBridge.ts +0 -0
  294. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemBattery.ts +0 -0
  295. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
  296. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/hooks/useSystemVolume.ts +0 -0
  297. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/index.css +0 -0
  298. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/lib/dulus-api.ts +0 -0
  299. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/lib/utils.ts +0 -0
  300. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/main.tsx +0 -0
  301. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/types/index.ts +0 -0
  302. {dulus-0.2.52 → dulus-0.2.54}/sandbox/src/utils/assets.ts +0 -0
  303. {dulus-0.2.52 → dulus-0.2.54}/sandbox/tailwind.config.js +0 -0
  304. {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.app.json +0 -0
  305. {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.json +0 -0
  306. {dulus-0.2.52 → dulus-0.2.54}/sandbox/tsconfig.node.json +0 -0
  307. {dulus-0.2.52 → dulus-0.2.54}/sandbox/vite.config.ts +0 -0
  308. {dulus-0.2.52 → dulus-0.2.54}/setup.cfg +0 -0
  309. {dulus-0.2.52 → dulus-0.2.54}/skill/__init__.py +0 -0
  310. {dulus-0.2.52 → dulus-0.2.54}/skill/builtin.py +0 -0
  311. {dulus-0.2.52 → dulus-0.2.54}/skill/executor.py +0 -0
  312. {dulus-0.2.52 → dulus-0.2.54}/skill/loader.py +0 -0
  313. {dulus-0.2.52 → dulus-0.2.54}/skill/tools.py +0 -0
  314. {dulus-0.2.52 → dulus-0.2.54}/skills.py +0 -0
  315. {dulus-0.2.52 → dulus-0.2.54}/spinner.py +0 -0
  316. {dulus-0.2.52 → dulus-0.2.54}/string_utils.py +0 -0
  317. {dulus-0.2.52 → dulus-0.2.54}/subagent.py +0 -0
  318. {dulus-0.2.52 → dulus-0.2.54}/task/__init__.py +0 -0
  319. {dulus-0.2.52 → dulus-0.2.54}/task/store.py +0 -0
  320. {dulus-0.2.52 → dulus-0.2.54}/task/tools.py +0 -0
  321. {dulus-0.2.52 → dulus-0.2.54}/task/types.py +0 -0
  322. {dulus-0.2.52 → dulus-0.2.54}/tests/test_afk_yolo.py +0 -0
  323. {dulus-0.2.52 → dulus-0.2.54}/tests/test_approval_runtime.py +0 -0
  324. {dulus-0.2.52 → dulus-0.2.54}/tests/test_background_task_tools.py +0 -0
  325. {dulus-0.2.52 → dulus-0.2.54}/tests/test_background_tasks.py +0 -0
  326. {dulus-0.2.52 → dulus-0.2.54}/tests/test_checkpoint.py +0 -0
  327. {dulus-0.2.52 → dulus-0.2.54}/tests/test_clipboard_utils.py +0 -0
  328. {dulus-0.2.52 → dulus-0.2.54}/tests/test_compaction.py +0 -0
  329. {dulus-0.2.52 → dulus-0.2.54}/tests/test_diff_view.py +0 -0
  330. {dulus-0.2.52 → dulus-0.2.54}/tests/test_diff_visualization.py +0 -0
  331. {dulus-0.2.52 → dulus-0.2.54}/tests/test_display_blocks.py +0 -0
  332. {dulus-0.2.52 → dulus-0.2.54}/tests/test_export_import.py +0 -0
  333. {dulus-0.2.52 → dulus-0.2.54}/tests/test_hook_engine.py +0 -0
  334. {dulus-0.2.52 → dulus-0.2.54}/tests/test_injection_fix.py +0 -0
  335. {dulus-0.2.52 → dulus-0.2.54}/tests/test_license.py +0 -0
  336. {dulus-0.2.52 → dulus-0.2.54}/tests/test_mcp.py +0 -0
  337. {dulus-0.2.52 → dulus-0.2.54}/tests/test_memory.py +0 -0
  338. {dulus-0.2.52 → dulus-0.2.54}/tests/test_notification_manager.py +0 -0
  339. {dulus-0.2.52 → dulus-0.2.54}/tests/test_plugin.py +0 -0
  340. {dulus-0.2.52 → dulus-0.2.54}/tests/test_session_fork.py +0 -0
  341. {dulus-0.2.52 → dulus-0.2.54}/tests/test_shell_mode.py +0 -0
  342. {dulus-0.2.52 → dulus-0.2.54}/tests/test_skills.py +0 -0
  343. {dulus-0.2.52 → dulus-0.2.54}/tests/test_steer_input.py +0 -0
  344. {dulus-0.2.52 → dulus-0.2.54}/tests/test_subagent.py +0 -0
  345. {dulus-0.2.52 → dulus-0.2.54}/tests/test_task.py +0 -0
  346. {dulus-0.2.52 → dulus-0.2.54}/tests/test_telegram_buffer.py +0 -0
  347. {dulus-0.2.52 → dulus-0.2.54}/tests/test_think_tool.py +0 -0
  348. {dulus-0.2.52 → dulus-0.2.54}/tests/test_todo_tool.py +0 -0
  349. {dulus-0.2.52 → dulus-0.2.54}/tests/test_todo_visualization.py +0 -0
  350. {dulus-0.2.52 → dulus-0.2.54}/tests/test_tool_registry.py +0 -0
  351. {dulus-0.2.52 → dulus-0.2.54}/tests/test_wire_events.py +0 -0
  352. {dulus-0.2.52 → dulus-0.2.54}/tmux_offloader.py +0 -0
  353. {dulus-0.2.52 → dulus-0.2.54}/tmux_tools.py +0 -0
  354. {dulus-0.2.52 → dulus-0.2.54}/tool_registry.py +0 -0
  355. {dulus-0.2.52 → dulus-0.2.54}/tools.py +0 -0
  356. {dulus-0.2.52 → dulus-0.2.54}/ui/__init__.py +0 -0
  357. {dulus-0.2.52 → dulus-0.2.54}/ui/input.py +0 -0
  358. {dulus-0.2.52 → dulus-0.2.54}/ui/render.py +0 -0
  359. {dulus-0.2.52 → dulus-0.2.54}/voice/keyterms.py +0 -0
  360. {dulus-0.2.52 → dulus-0.2.54}/webchat.py +0 -0
  361. {dulus-0.2.52 → dulus-0.2.54}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.52
3
+ Version: 0.2.54
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.52
3
+ Version: 0.2.54
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -351,7 +351,9 @@ ui/__init__.py
351
351
  ui/input.py
352
352
  ui/render.py
353
353
  voice/__init__.py
354
+ voice/audio_utils.py
354
355
  voice/keyterms.py
355
356
  voice/recorder.py
356
357
  voice/stt.py
357
- voice/tts.py
358
+ voice/tts.py
359
+ voice/wake_word.py
@@ -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
- # `--full` flag pulls per-skill descriptions in parallel (slower but
4375
- # informative). Default lists names only — instant.
4376
- full = False
4377
- if "--full" in query.split():
4378
- full = True
4379
- query = " ".join(t for t in query.split() if t != "--full").strip()
4380
- if full:
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 skill list from GitHub (instant)...")
4384
- skills = list_awesome_remote(query, with_descriptions=full)
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 full else " — add `--full` for descriptions"
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
- err("Usage: /skill use <name>")
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
- from skill.clawhub import DULUS_SKILLS_DIR
4508
- body = read_skill(rest)
4509
- if not body:
4510
- err(f"Skill '{rest}' not found. Run /skill list to see installed skills.")
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
- # Inject as a user-side system message for this turn
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
- config["_skill_inject"] = (existing + "\n\n" + body + path_hint).strip()
4517
- ok(f"Skill '{rest}' injected active for this turn.")
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
- if config.pop("_auto_voice_next", False) and not in_batch_mode:
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