dulus 3.10.2__tar.gz → 3.10.4__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 (347) hide show
  1. {dulus-3.10.2/dulus.egg-info → dulus-3.10.4}/PKG-INFO +1 -1
  2. {dulus-3.10.2 → dulus-3.10.4}/compaction.py +21 -0
  3. {dulus-3.10.2 → dulus-3.10.4/dulus.egg-info}/PKG-INFO +1 -1
  4. {dulus-3.10.2 → dulus-3.10.4}/dulus.py +194 -1
  5. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/client.py +19 -0
  6. {dulus-3.10.2 → dulus-3.10.4}/pyproject.toml +1 -1
  7. {dulus-3.10.2 → dulus-3.10.4}/skill/clawhub.py +200 -0
  8. {dulus-3.10.2 → dulus-3.10.4}/LICENSE +0 -0
  9. {dulus-3.10.2 → dulus-3.10.4}/MANIFEST.in +0 -0
  10. {dulus-3.10.2 → dulus-3.10.4}/README.md +0 -0
  11. {dulus-3.10.2 → dulus-3.10.4}/agent.py +0 -0
  12. {dulus-3.10.2 → dulus-3.10.4}/analytics.py +0 -0
  13. {dulus-3.10.2 → dulus-3.10.4}/backend/__init__.py +0 -0
  14. {dulus-3.10.2 → dulus-3.10.4}/backend/agents_bridge.py +0 -0
  15. {dulus-3.10.2 → dulus-3.10.4}/backend/compressor.py +0 -0
  16. {dulus-3.10.2 → dulus-3.10.4}/backend/context.py +0 -0
  17. {dulus-3.10.2 → dulus-3.10.4}/backend/githook.py +0 -0
  18. {dulus-3.10.2 → dulus-3.10.4}/backend/marketplace.py +0 -0
  19. {dulus-3.10.2 → dulus-3.10.4}/backend/mempalace_bridge.py +0 -0
  20. {dulus-3.10.2 → dulus-3.10.4}/backend/personas.py +0 -0
  21. {dulus-3.10.2 → dulus-3.10.4}/backend/plugins.py +0 -0
  22. {dulus-3.10.2 → dulus-3.10.4}/backend/server.py +0 -0
  23. {dulus-3.10.2 → dulus-3.10.4}/backend/tasks.py +0 -0
  24. {dulus-3.10.2 → dulus-3.10.4}/batch_api.py +0 -0
  25. {dulus-3.10.2 → dulus-3.10.4}/checkpoint/__init__.py +0 -0
  26. {dulus-3.10.2 → dulus-3.10.4}/checkpoint/hooks.py +0 -0
  27. {dulus-3.10.2 → dulus-3.10.4}/checkpoint/store.py +0 -0
  28. {dulus-3.10.2 → dulus-3.10.4}/checkpoint/types.py +0 -0
  29. {dulus-3.10.2 → dulus-3.10.4}/claude_code_watcher.py +0 -0
  30. {dulus-3.10.2 → dulus-3.10.4}/clipboard_utils.py +0 -0
  31. {dulus-3.10.2 → dulus-3.10.4}/cloudsave.py +0 -0
  32. {dulus-3.10.2 → dulus-3.10.4}/common.py +0 -0
  33. {dulus-3.10.2 → dulus-3.10.4}/config.py +0 -0
  34. {dulus-3.10.2 → dulus-3.10.4}/context.py +0 -0
  35. {dulus-3.10.2 → dulus-3.10.4}/data/__init__.py +0 -0
  36. {dulus-3.10.2 → dulus-3.10.4}/data/active_persona.json +0 -0
  37. {dulus-3.10.2 → dulus-3.10.4}/data/marketplace.json +0 -0
  38. {dulus-3.10.2 → dulus-3.10.4}/data/personas.json +0 -0
  39. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/__init__.py +0 -0
  40. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/__init__.py +0 -0
  41. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  42. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  43. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  44. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/plugin.json +0 -0
  45. {dulus-3.10.2 → dulus-3.10.4}/data/plugins/composio/plugin_tool.py +0 -0
  46. {dulus-3.10.2 → dulus-3.10.4}/data/tasks.json +0 -0
  47. {dulus-3.10.2 → dulus-3.10.4}/display_blocks.py +0 -0
  48. {dulus-3.10.2 → dulus-3.10.4}/docs/API.md +0 -0
  49. {dulus-3.10.2 → dulus-3.10.4}/docs/BRAND_GUIDE.md +0 -0
  50. {dulus-3.10.2 → dulus-3.10.4}/docs/CHANGELOG.md +0 -0
  51. {dulus-3.10.2 → dulus-3.10.4}/docs/CONTRIBUTING.md +0 -0
  52. {dulus-3.10.2 → dulus-3.10.4}/docs/DEPLOYMENT.md +0 -0
  53. {dulus-3.10.2 → dulus-3.10.4}/docs/Dulus_AI_Whitepaper_(v2.0).pdf +0 -0
  54. {dulus-3.10.2 → dulus-3.10.4}/docs/FAQ.md +0 -0
  55. {dulus-3.10.2 → dulus-3.10.4}/docs/GETTING_STARTED.md +0 -0
  56. {dulus-3.10.2 → dulus-3.10.4}/docs/INVESTOR_BRIEF.md +0 -0
  57. {dulus-3.10.2 → dulus-3.10.4}/docs/README.md +0 -0
  58. {dulus-3.10.2 → dulus-3.10.4}/docs/README_AR.md +0 -0
  59. {dulus-3.10.2 → dulus-3.10.4}/docs/README_EN.md +0 -0
  60. {dulus-3.10.2 → dulus-3.10.4}/docs/README_ES.md +0 -0
  61. {dulus-3.10.2 → dulus-3.10.4}/docs/README_FR.md +0 -0
  62. {dulus-3.10.2 → dulus-3.10.4}/docs/README_JA.md +0 -0
  63. {dulus-3.10.2 → dulus-3.10.4}/docs/README_KO.md +0 -0
  64. {dulus-3.10.2 → dulus-3.10.4}/docs/README_PT.md +0 -0
  65. {dulus-3.10.2 → dulus-3.10.4}/docs/README_RU.md +0 -0
  66. {dulus-3.10.2 → dulus-3.10.4}/docs/README_ZH.md +0 -0
  67. {dulus-3.10.2 → dulus-3.10.4}/docs/SECURITY.md +0 -0
  68. {dulus-3.10.2 → dulus-3.10.4}/docs/__init__.py +0 -0
  69. {dulus-3.10.2 → dulus-3.10.4}/docs/api.html +0 -0
  70. {dulus-3.10.2 → dulus-3.10.4}/docs/architecture.md +0 -0
  71. {dulus-3.10.2 → dulus-3.10.4}/docs/azure-speech-template.json +0 -0
  72. {dulus-3.10.2 → dulus-3.10.4}/docs/dashboard/index.html +0 -0
  73. {dulus-3.10.2 → dulus-3.10.4}/docs/divider.svg +0 -0
  74. {dulus-3.10.2 → dulus-3.10.4}/docs/dulus-bird.png +0 -0
  75. {dulus-3.10.2 → dulus-3.10.4}/docs/generate.py +0 -0
  76. {dulus-3.10.2 → dulus-3.10.4}/docs/hero.svg +0 -0
  77. {dulus-3.10.2 → dulus-3.10.4}/docs/index.html +0 -0
  78. {dulus-3.10.2 → dulus-3.10.4}/docs/news.md +0 -0
  79. {dulus-3.10.2 → dulus-3.10.4}/docs/nvidia-models.svg +0 -0
  80. {dulus-3.10.2 → dulus-3.10.4}/docs/particle-playground.html +0 -0
  81. {dulus-3.10.2 → dulus-3.10.4}/docs/personas/index.html +0 -0
  82. {dulus-3.10.2 → dulus-3.10.4}/docs/poetry-banner.png +0 -0
  83. {dulus-3.10.2 → dulus-3.10.4}/docs/preview.html +0 -0
  84. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-agents.svg +0 -0
  85. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-brainstorm.svg +0 -0
  86. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-bridges.svg +0 -0
  87. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-features.svg +0 -0
  88. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-freetier.svg +0 -0
  89. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-memory.svg +0 -0
  90. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-models.svg +0 -0
  91. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-perms.svg +0 -0
  92. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-plugins.svg +0 -0
  93. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-quickstart.svg +0 -0
  94. {dulus-3.10.2 → dulus-3.10.4}/docs/sec-ssj.svg +0 -0
  95. {dulus-3.10.2 → dulus-3.10.4}/docs/spinners.svg +0 -0
  96. {dulus-3.10.2 → dulus-3.10.4}/docs/split-pane.svg +0 -0
  97. {dulus-3.10.2 → dulus-3.10.4}/docs/terminal-boot.svg +0 -0
  98. {dulus-3.10.2 → dulus-3.10.4}/dulus.egg-info/SOURCES.txt +0 -0
  99. {dulus-3.10.2 → dulus-3.10.4}/dulus.egg-info/dependency_links.txt +0 -0
  100. {dulus-3.10.2 → dulus-3.10.4}/dulus.egg-info/entry_points.txt +0 -0
  101. {dulus-3.10.2 → dulus-3.10.4}/dulus.egg-info/requires.txt +0 -0
  102. {dulus-3.10.2 → dulus-3.10.4}/dulus.egg-info/top_level.txt +0 -0
  103. {dulus-3.10.2 → dulus-3.10.4}/dulus_gui.py +0 -0
  104. {dulus-3.10.2 → dulus-3.10.4}/dulus_gui_web.py +0 -0
  105. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/__init__.py +0 -0
  106. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/commands.py +0 -0
  107. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/config.py +0 -0
  108. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/hub.py +0 -0
  109. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/tools.py +0 -0
  110. {dulus-3.10.2 → dulus-3.10.4}/dulus_mcp/types.py +0 -0
  111. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/__init__.py +0 -0
  112. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/add_dir_manager.py +0 -0
  113. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/afk_mode.py +0 -0
  114. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/approval_runtime.py +0 -0
  115. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/background_tasks.py +0 -0
  116. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/clipboard_utils.py +0 -0
  117. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/diff_visualizer.py +0 -0
  118. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/export_import.py +0 -0
  119. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/hook_engine.py +0 -0
  120. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/notification_manager.py +0 -0
  121. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/session_fork.py +0 -0
  122. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/shell_mode.py +0 -0
  123. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/todo_visualizer.py +0 -0
  124. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/tree_ls.py +0 -0
  125. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/wire_events.py +0 -0
  126. {dulus-3.10.2 → dulus-3.10.4}/dulus_tools/yolo_mode.py +0 -0
  127. {dulus-3.10.2 → dulus-3.10.4}/file_filter.py +0 -0
  128. {dulus-3.10.2 → dulus-3.10.4}/governance.py +0 -0
  129. {dulus-3.10.2 → dulus-3.10.4}/gui/__init__.py +0 -0
  130. {dulus-3.10.2 → dulus-3.10.4}/gui/agent_bridge.py +0 -0
  131. {dulus-3.10.2 → dulus-3.10.4}/gui/chat_widget.py +0 -0
  132. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/App.tsx +0 -0
  133. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/THIRD_PARTY_NOTICES.md +0 -0
  134. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/dist/index.html +0 -0
  135. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/dist/standalone.html +0 -0
  136. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/index.html +0 -0
  137. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/index.tsx +0 -0
  138. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/metadata.json +0 -0
  139. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/package-lock.json +0 -0
  140. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/package.json +0 -0
  141. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/types.ts +0 -0
  142. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/utils.ts +0 -0
  143. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/vertex-ai-proxy-interceptor.js +0 -0
  144. {dulus-3.10.2 → dulus-3.10.4}/gui/dulus_orchestrator/vite.config.ts +0 -0
  145. {dulus-3.10.2 → dulus-3.10.4}/gui/main_window.py +0 -0
  146. {dulus-3.10.2 → dulus-3.10.4}/gui/personas.py +0 -0
  147. {dulus-3.10.2 → dulus-3.10.4}/gui/sandbox_server.py +0 -0
  148. {dulus-3.10.2 → dulus-3.10.4}/gui/session_utils.py +0 -0
  149. {dulus-3.10.2 → dulus-3.10.4}/gui/settings_dialog.py +0 -0
  150. {dulus-3.10.2 → dulus-3.10.4}/gui/sidebar.py +0 -0
  151. {dulus-3.10.2 → dulus-3.10.4}/gui/tasks_view.py +0 -0
  152. {dulus-3.10.2 → dulus-3.10.4}/gui/themes.py +0 -0
  153. {dulus-3.10.2 → dulus-3.10.4}/gui/tool_panel.py +0 -0
  154. {dulus-3.10.2 → dulus-3.10.4}/input.py +0 -0
  155. {dulus-3.10.2 → dulus-3.10.4}/license_manager.py +0 -0
  156. {dulus-3.10.2 → dulus-3.10.4}/memory/__init__.py +0 -0
  157. {dulus-3.10.2 → dulus-3.10.4}/memory/audit.py +0 -0
  158. {dulus-3.10.2 → dulus-3.10.4}/memory/consolidator.py +0 -0
  159. {dulus-3.10.2 → dulus-3.10.4}/memory/context.py +0 -0
  160. {dulus-3.10.2 → dulus-3.10.4}/memory/offload.py +0 -0
  161. {dulus-3.10.2 → dulus-3.10.4}/memory/palace.py +0 -0
  162. {dulus-3.10.2 → dulus-3.10.4}/memory/scan.py +0 -0
  163. {dulus-3.10.2 → dulus-3.10.4}/memory/sessions.py +0 -0
  164. {dulus-3.10.2 → dulus-3.10.4}/memory/store.py +0 -0
  165. {dulus-3.10.2 → dulus-3.10.4}/memory/tools.py +0 -0
  166. {dulus-3.10.2 → dulus-3.10.4}/memory/types.py +0 -0
  167. {dulus-3.10.2 → dulus-3.10.4}/memory/vector_search.py +0 -0
  168. {dulus-3.10.2 → dulus-3.10.4}/multi_agent/__init__.py +0 -0
  169. {dulus-3.10.2 → dulus-3.10.4}/multi_agent/subagent.py +0 -0
  170. {dulus-3.10.2 → dulus-3.10.4}/multi_agent/tools.py +0 -0
  171. {dulus-3.10.2 → dulus-3.10.4}/offload_helper.py +0 -0
  172. {dulus-3.10.2 → dulus-3.10.4}/paste_placeholders.py +0 -0
  173. {dulus-3.10.2 → dulus-3.10.4}/personality_config.py +0 -0
  174. {dulus-3.10.2 → dulus-3.10.4}/plugin/__init__.py +0 -0
  175. {dulus-3.10.2 → dulus-3.10.4}/plugin/autoadapter.py +0 -0
  176. {dulus-3.10.2 → dulus-3.10.4}/plugin/loader.py +0 -0
  177. {dulus-3.10.2 → dulus-3.10.4}/plugin/recommend.py +0 -0
  178. {dulus-3.10.2 → dulus-3.10.4}/plugin/store.py +0 -0
  179. {dulus-3.10.2 → dulus-3.10.4}/plugin/types.py +0 -0
  180. {dulus-3.10.2 → dulus-3.10.4}/profiles.py +0 -0
  181. {dulus-3.10.2 → dulus-3.10.4}/providers.py +0 -0
  182. {dulus-3.10.2 → dulus-3.10.4}/sandbox/README.md +0 -0
  183. {dulus-3.10.2 → dulus-3.10.4}/sandbox/__init__.py +0 -0
  184. {dulus-3.10.2 → dulus-3.10.4}/sandbox/components.json +0 -0
  185. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  186. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  187. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/index.html +0 -0
  188. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/wallpaper-default.jpg +0 -0
  189. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/wallpapers/default.jpeg +0 -0
  190. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/wallpapers/light.jpeg +0 -0
  191. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  192. {dulus-3.10.2 → dulus-3.10.4}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  193. {dulus-3.10.2 → dulus-3.10.4}/sandbox/eslint.config.js +0 -0
  194. {dulus-3.10.2 → dulus-3.10.4}/sandbox/index.html +0 -0
  195. {dulus-3.10.2 → dulus-3.10.4}/sandbox/info.md +0 -0
  196. {dulus-3.10.2 → dulus-3.10.4}/sandbox/package-lock.json +0 -0
  197. {dulus-3.10.2 → dulus-3.10.4}/sandbox/package.json +0 -0
  198. {dulus-3.10.2 → dulus-3.10.4}/sandbox/postcss.config.js +0 -0
  199. {dulus-3.10.2 → dulus-3.10.4}/sandbox/tailwind.config.js +0 -0
  200. {dulus-3.10.2 → dulus-3.10.4}/sandbox/tsconfig.app.json +0 -0
  201. {dulus-3.10.2 → dulus-3.10.4}/sandbox/tsconfig.json +0 -0
  202. {dulus-3.10.2 → dulus-3.10.4}/sandbox/tsconfig.node.json +0 -0
  203. {dulus-3.10.2 → dulus-3.10.4}/sandbox/vite.config.ts +0 -0
  204. {dulus-3.10.2 → dulus-3.10.4}/sandbox_bootstrap.py +0 -0
  205. {dulus-3.10.2 → dulus-3.10.4}/setup.cfg +0 -0
  206. {dulus-3.10.2 → dulus-3.10.4}/skill/__init__.py +0 -0
  207. {dulus-3.10.2 → dulus-3.10.4}/skill/builtin.py +0 -0
  208. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/LICENSE-obsidian-skills +0 -0
  209. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/README.md +0 -0
  210. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/defuddle/SKILL.md +0 -0
  211. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/json-canvas/SKILL.md +0 -0
  212. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
  213. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-bases/SKILL.md +0 -0
  214. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
  215. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-cli/SKILL.md +0 -0
  216. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
  217. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
  218. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
  219. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
  220. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/README.md +0 -0
  221. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/SKILL.md +0 -0
  222. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
  223. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
  224. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
  225. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
  226. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
  227. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
  228. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
  229. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
  230. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
  231. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
  232. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
  233. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
  234. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
  235. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
  236. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
  237. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
  238. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
  239. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
  240. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
  241. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
  242. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
  243. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
  244. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
  245. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/legal/drop.md +0 -0
  246. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
  247. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/methods.md +0 -0
  248. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
  249. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/references/state-machine.md +0 -0
  250. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
  251. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/badbool.py +0 -0
  252. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/brokers.py +0 -0
  253. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/cdp.py +0 -0
  254. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/config.py +0 -0
  255. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/crypto.py +0 -0
  256. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/dossier.py +0 -0
  257. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
  258. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/emailer.py +0 -0
  259. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/ledger.py +0 -0
  260. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/legal.py +0 -0
  261. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/paths.py +0 -0
  262. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/pdd.py +0 -0
  263. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/registry.py +0 -0
  264. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/report.py +0 -0
  265. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/scan.py +0 -0
  266. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/storage.py +0 -0
  267. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/tiers.py +0 -0
  268. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/scripts/vectors.py +0 -0
  269. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
  270. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
  271. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
  272. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
  273. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
  274. {dulus-3.10.2 → dulus-3.10.4}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
  275. {dulus-3.10.2 → dulus-3.10.4}/skill/executor.py +0 -0
  276. {dulus-3.10.2 → dulus-3.10.4}/skill/loader.py +0 -0
  277. {dulus-3.10.2 → dulus-3.10.4}/skill/tools.py +0 -0
  278. {dulus-3.10.2 → dulus-3.10.4}/skills.py +0 -0
  279. {dulus-3.10.2 → dulus-3.10.4}/soul.py +0 -0
  280. {dulus-3.10.2 → dulus-3.10.4}/spinner.py +0 -0
  281. {dulus-3.10.2 → dulus-3.10.4}/steer_input.py +0 -0
  282. {dulus-3.10.2 → dulus-3.10.4}/string_utils.py +0 -0
  283. {dulus-3.10.2 → dulus-3.10.4}/subagent.py +0 -0
  284. {dulus-3.10.2 → dulus-3.10.4}/task/__init__.py +0 -0
  285. {dulus-3.10.2 → dulus-3.10.4}/task/store.py +0 -0
  286. {dulus-3.10.2 → dulus-3.10.4}/task/tools.py +0 -0
  287. {dulus-3.10.2 → dulus-3.10.4}/task/types.py +0 -0
  288. {dulus-3.10.2 → dulus-3.10.4}/tests/test_afk_yolo.py +0 -0
  289. {dulus-3.10.2 → dulus-3.10.4}/tests/test_approval_runtime.py +0 -0
  290. {dulus-3.10.2 → dulus-3.10.4}/tests/test_background_task_tools.py +0 -0
  291. {dulus-3.10.2 → dulus-3.10.4}/tests/test_background_tasks.py +0 -0
  292. {dulus-3.10.2 → dulus-3.10.4}/tests/test_checkpoint.py +0 -0
  293. {dulus-3.10.2 → dulus-3.10.4}/tests/test_clipboard_utils.py +0 -0
  294. {dulus-3.10.2 → dulus-3.10.4}/tests/test_compaction.py +0 -0
  295. {dulus-3.10.2 → dulus-3.10.4}/tests/test_diff_view.py +0 -0
  296. {dulus-3.10.2 → dulus-3.10.4}/tests/test_diff_visualization.py +0 -0
  297. {dulus-3.10.2 → dulus-3.10.4}/tests/test_display_blocks.py +0 -0
  298. {dulus-3.10.2 → dulus-3.10.4}/tests/test_export_import.py +0 -0
  299. {dulus-3.10.2 → dulus-3.10.4}/tests/test_hook_engine.py +0 -0
  300. {dulus-3.10.2 → dulus-3.10.4}/tests/test_injection_fix.py +0 -0
  301. {dulus-3.10.2 → dulus-3.10.4}/tests/test_license.py +0 -0
  302. {dulus-3.10.2 → dulus-3.10.4}/tests/test_mcp.py +0 -0
  303. {dulus-3.10.2 → dulus-3.10.4}/tests/test_memory.py +0 -0
  304. {dulus-3.10.2 → dulus-3.10.4}/tests/test_notification_manager.py +0 -0
  305. {dulus-3.10.2 → dulus-3.10.4}/tests/test_plugin.py +0 -0
  306. {dulus-3.10.2 → dulus-3.10.4}/tests/test_session_fork.py +0 -0
  307. {dulus-3.10.2 → dulus-3.10.4}/tests/test_shell_mode.py +0 -0
  308. {dulus-3.10.2 → dulus-3.10.4}/tests/test_skills.py +0 -0
  309. {dulus-3.10.2 → dulus-3.10.4}/tests/test_steer_input.py +0 -0
  310. {dulus-3.10.2 → dulus-3.10.4}/tests/test_subagent.py +0 -0
  311. {dulus-3.10.2 → dulus-3.10.4}/tests/test_task.py +0 -0
  312. {dulus-3.10.2 → dulus-3.10.4}/tests/test_telegram_buffer.py +0 -0
  313. {dulus-3.10.2 → dulus-3.10.4}/tests/test_think_tool.py +0 -0
  314. {dulus-3.10.2 → dulus-3.10.4}/tests/test_todo_tool.py +0 -0
  315. {dulus-3.10.2 → dulus-3.10.4}/tests/test_todo_visualization.py +0 -0
  316. {dulus-3.10.2 → dulus-3.10.4}/tests/test_tool_registry.py +0 -0
  317. {dulus-3.10.2 → dulus-3.10.4}/tests/test_tree_ls.py +0 -0
  318. {dulus-3.10.2 → dulus-3.10.4}/tests/test_voice.py +0 -0
  319. {dulus-3.10.2 → dulus-3.10.4}/tests/test_wire_events.py +0 -0
  320. {dulus-3.10.2 → dulus-3.10.4}/tmux_offloader.py +0 -0
  321. {dulus-3.10.2 → dulus-3.10.4}/tmux_tools.py +0 -0
  322. {dulus-3.10.2 → dulus-3.10.4}/tool_registry.py +0 -0
  323. {dulus-3.10.2 → dulus-3.10.4}/tools.py +0 -0
  324. {dulus-3.10.2 → dulus-3.10.4}/tools_background.py +0 -0
  325. {dulus-3.10.2 → dulus-3.10.4}/tools_think.py +0 -0
  326. {dulus-3.10.2 → dulus-3.10.4}/tools_todo.py +0 -0
  327. {dulus-3.10.2 → dulus-3.10.4}/ui/__init__.py +0 -0
  328. {dulus-3.10.2 → dulus-3.10.4}/ui/input.py +0 -0
  329. {dulus-3.10.2 → dulus-3.10.4}/ui/render.py +0 -0
  330. {dulus-3.10.2 → dulus-3.10.4}/updater.py +0 -0
  331. {dulus-3.10.2 → dulus-3.10.4}/voice/__init__.py +0 -0
  332. {dulus-3.10.2 → dulus-3.10.4}/voice/audio_utils.py +0 -0
  333. {dulus-3.10.2 → dulus-3.10.4}/voice/keyterms.py +0 -0
  334. {dulus-3.10.2 → dulus-3.10.4}/voice/recorder.py +0 -0
  335. {dulus-3.10.2 → dulus-3.10.4}/voice/stt.py +0 -0
  336. {dulus-3.10.2 → dulus-3.10.4}/voice/tts.py +0 -0
  337. {dulus-3.10.2 → dulus-3.10.4}/voice/wake_word.py +0 -0
  338. {dulus-3.10.2 → dulus-3.10.4}/webbridge/__init__.py +0 -0
  339. {dulus-3.10.2 → dulus-3.10.4}/webbridge/core.py +0 -0
  340. {dulus-3.10.2 → dulus-3.10.4}/webbridge/tools.py +0 -0
  341. {dulus-3.10.2 → dulus-3.10.4}/webchat.py +0 -0
  342. {dulus-3.10.2 → dulus-3.10.4}/webchat_server.py +0 -0
  343. {dulus-3.10.2 → dulus-3.10.4}/webchat_ui/__init__.py +0 -0
  344. {dulus-3.10.2 → dulus-3.10.4}/webchat_ui/app.js +0 -0
  345. {dulus-3.10.2 → dulus-3.10.4}/webchat_ui/index.html +0 -0
  346. {dulus-3.10.2 → dulus-3.10.4}/webchat_ui/style.css +0 -0
  347. {dulus-3.10.2 → dulus-3.10.4}/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 3.10.2
3
+ Version: 3.10.4
4
4
  Summary: Use IA as an agent without an API key. $0. Harvest your browser session for Gemini (guest, no login), Claude.ai, Claude Code, Kimi.com, Qwen, DeepSeek — drive them like an API, no per-token billing. Plus LiteLLM (100+ paid providers), local models via Ollama, /lang in 34 languages, Mesa Redonda, voice, OCR, MemPalace, embedded sandbox OS.
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -78,6 +78,27 @@ def estimate_tokens(messages: list, model: str = "", config: dict | None = None,
78
78
  for v in tc.values():
79
79
  if isinstance(v, str):
80
80
  total_chars += len(v)
81
+ # ── Images / videos (2026-07-13 fix) ──────────────────────────────
82
+ # agent.py attaches /image and /video payloads as sibling keys
83
+ # user_msg["images"] / user_msg["videos"] (base64 strings), NOT
84
+ # inside "content". This loop used to never look at them, so a
85
+ # single video attachment (often 200-800k+ base64 chars) was
86
+ # completely invisible to the token estimate. Result: compaction
87
+ # thought the conversation was small and never fired auto-compact,
88
+ # while the model actually receiving that payload (e.g. Kimi's
89
+ # multimodal video support) silently ballooned past its real
90
+ # context window and degraded. Count them like any other content.
91
+ for key in ("images", "videos"):
92
+ items = m.get(key)
93
+ if not items:
94
+ continue
95
+ for item in items:
96
+ if isinstance(item, str):
97
+ total_chars += len(item)
98
+ elif isinstance(item, dict):
99
+ data = item.get("data", "")
100
+ if isinstance(data, str):
101
+ total_chars += len(data)
81
102
  content_tokens = int(total_chars / 2.8)
82
103
  framing_tokens = msg_count * 4 # role + delimiters overhead per msg
83
104
  return int((content_tokens + framing_tokens) * 1.1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 3.10.2
3
+ Version: 3.10.4
4
4
  Summary: Use IA as an agent without an API key. $0. Harvest your browser session for Gemini (guest, no login), Claude.ai, Claude Code, Kimi.com, Qwen, DeepSeek — drive them like an API, no per-token billing. Plus LiteLLM (100+ paid providers), local models via Ollama, /lang in 34 languages, Mesa Redonda, voice, OCR, MemPalace, embedded sandbox OS.
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -121,6 +121,7 @@ Slash commands in REPL:
121
121
  /roundtable Start a multi-model roundtable discussion
122
122
  /fork Fork session at a given turn
123
123
  /undo Undo last turn
124
+ /workspace [cmd] Manage Dulus workspaces (switch/list/default)
124
125
  /add-dir [path] Manage additional workspace directories
125
126
  /import <file> Import conversation from file or session
126
127
  /harvest Harvest Claude.ai cookies (alias: /harvest-claude)
@@ -854,6 +855,7 @@ _HELP_PAGES = [
854
855
  ("/cost", "Show API cost this session"),
855
856
  ("/fork", "Fork session at a given turn"),
856
857
  ("/undo", "Undo last turn"),
858
+ ("/workspace [cmd]", "Manage Dulus workspaces (switch/list/default)"),
857
859
  ("/import <file>","Import conversation from file/session"),
858
860
  ("/add-dir [path]","Manage additional workspace directories"),
859
861
  ("/batch", "Manage Kimi Batch tasks"),
@@ -4012,6 +4014,167 @@ def cmd_cwd(args: str, _state, config) -> bool:
4012
4014
  err(str(e))
4013
4015
  return True
4014
4016
 
4017
+
4018
+ # ── Workspace manager ───────────────────────────────────────────────────────
4019
+
4020
+ _WORKSPACES_DIR: Path = Path.home() / ".dulus" / "workspaces"
4021
+ _DEFAULT_WORKSPACE: str = "workspace1"
4022
+
4023
+
4024
+ def _workspace_path(name: str) -> Path:
4025
+ return _WORKSPACES_DIR / name
4026
+
4027
+
4028
+ def _ensure_workspace(name: str) -> Path:
4029
+ """Create workspace dir if missing and return its path."""
4030
+ ws = _workspace_path(name)
4031
+ ws.mkdir(parents=True, exist_ok=True)
4032
+ return ws
4033
+
4034
+
4035
+ def _list_workspaces() -> list[str]:
4036
+ if not _WORKSPACES_DIR.exists():
4037
+ return []
4038
+ return sorted(p.name for p in _WORKSPACES_DIR.iterdir() if p.is_dir())
4039
+
4040
+
4041
+ def _current_workspace_name() -> str | None:
4042
+ """Return the workspace name if cwd is inside ~/.dulus/workspaces/."""
4043
+ try:
4044
+ cwd = Path.cwd().resolve()
4045
+ root = _WORKSPACES_DIR.resolve()
4046
+ if root in cwd.parents or cwd == root:
4047
+ rel = cwd.relative_to(root)
4048
+ first = rel.parts[0] if rel.parts else None
4049
+ if first and _workspace_path(first).is_dir():
4050
+ return first
4051
+ except Exception:
4052
+ pass
4053
+ return None
4054
+
4055
+
4056
+ def _activate_workspace(name: str, config: dict) -> bool:
4057
+ """Change cwd into workspace, create it if missing, and persist as last used."""
4058
+ from config import save_config
4059
+ ws = _ensure_workspace(name)
4060
+ try:
4061
+ os.chdir(ws)
4062
+ config["workspace_last"] = name
4063
+ save_config(config)
4064
+ # Directory changed — git info is stale
4065
+ if _git_prompt is not None:
4066
+ _git_prompt.reset_git_cache()
4067
+ return True
4068
+ except Exception as e:
4069
+ err(f"No pude cambiar al workspace '{name}': {e}")
4070
+ return False
4071
+
4072
+
4073
+ def _apply_workspace(config: dict) -> None:
4074
+ """At boot, move cwd into the last-used workspace (or workspace1)."""
4075
+ last = config.get("workspace_last") or _DEFAULT_WORKSPACE
4076
+ ws = _workspace_path(last)
4077
+ if not ws.exists():
4078
+ _ensure_workspace(last)
4079
+ try:
4080
+ os.chdir(ws)
4081
+ if config.get("verbose", False):
4082
+ info(f"Workspace activo: {last}")
4083
+ except Exception as e:
4084
+ warn(f"No pude entrar al workspace '{last}': {e}")
4085
+
4086
+
4087
+ def cmd_workspace(args: str, _state, config) -> bool:
4088
+ """Manage Dulus workspaces under ~/.dulus/workspaces.
4089
+
4090
+ /workspace — show current workspace + cwd
4091
+ /workspace current — same as above
4092
+ /workspace list — list workspaces
4093
+ /workspace switch <name> — change to workspace (creates if missing)
4094
+ /workspace default [name] — show or set the startup workspace
4095
+ /workspace create <name> — create a workspace without switching
4096
+ /workspace delete <name> — delete a workspace (must be empty)
4097
+ """
4098
+ from config import save_config
4099
+ parts = args.strip().split(None, 1)
4100
+ subcmd = parts[0].lower() if parts else "current"
4101
+ rest = parts[1].strip() if len(parts) > 1 else ""
4102
+
4103
+ if subcmd in ("current", "cwd", ""):
4104
+ current = _current_workspace_name()
4105
+ if current:
4106
+ info(f"Workspace: {current}")
4107
+ else:
4108
+ info("No estás dentro de un workspace de Dulus.")
4109
+ info(f"Working directory: {os.getcwd()}")
4110
+ return True
4111
+
4112
+ if subcmd == "list":
4113
+ workspaces = _list_workspaces()
4114
+ current = _current_workspace_name()
4115
+ if not workspaces:
4116
+ info("No hay workspaces todavía. Usa /workspace create <nombre>.")
4117
+ return True
4118
+ info(f"Workspaces en {_WORKSPACES_DIR}:")
4119
+ for w in workspaces:
4120
+ mark = " → " if w == current else " "
4121
+ print(f"{mark}{w}")
4122
+ return True
4123
+
4124
+ if subcmd == "switch":
4125
+ if not rest:
4126
+ err("Uso: /workspace switch <nombre>")
4127
+ return True
4128
+ name = rest.split()[0]
4129
+ if _activate_workspace(name, config):
4130
+ ok(f"Workspace cambiado a: {name}")
4131
+ return True
4132
+
4133
+ if subcmd == "default":
4134
+ if not rest:
4135
+ current_default = config.get("workspace_last") or _DEFAULT_WORKSPACE
4136
+ info(f"Workspace por defecto: {current_default}")
4137
+ return True
4138
+ name = rest.split()[0]
4139
+ _ensure_workspace(name)
4140
+ config["workspace_last"] = name
4141
+ save_config(config)
4142
+ ok(f"Workspace por defecto ahora: {name}")
4143
+ return True
4144
+
4145
+ if subcmd == "create":
4146
+ if not rest:
4147
+ err("Uso: /workspace create <nombre>")
4148
+ return True
4149
+ name = rest.split()[0]
4150
+ _ensure_workspace(name)
4151
+ ok(f"Workspace creado: {name}")
4152
+ return True
4153
+
4154
+ if subcmd == "delete":
4155
+ if not rest:
4156
+ err("Uso: /workspace delete <nombre>")
4157
+ return True
4158
+ name = rest.split()[0]
4159
+ target = _workspace_path(name)
4160
+ if not target.exists():
4161
+ err(f"Workspace '{name}' no existe.")
4162
+ return True
4163
+ current = _current_workspace_name()
4164
+ if name == current:
4165
+ err("No puedes borrar el workspace en el que estás. Cambia primero con /workspace switch.")
4166
+ return True
4167
+ try:
4168
+ target.rmdir()
4169
+ ok(f"Workspace borrado: {name}")
4170
+ except OSError as e:
4171
+ err(f"No se pudo borrar '{name}': {e}. Asegúrate de que esté vacío.")
4172
+ return True
4173
+
4174
+ err(f"Subcomando desconocido: /workspace {subcmd}")
4175
+ return True
4176
+
4177
+
4015
4178
  def _build_session_data(state, session_id: str | None = None) -> dict:
4016
4179
  """Serialize current conversation state to a JSON-serializable dict."""
4017
4180
  import uuid
@@ -4922,6 +5085,7 @@ def cmd_skill(args: str, state, config) -> bool:
4922
5085
  list_awesome_remote, list_composio_toolkits,
4923
5086
  install_awesome_remote,
4924
5087
  list_dulus_remote, install_dulus_remote,
5088
+ search_skillssh, install_skillssh,
4925
5089
  )
4926
5090
  from pathlib import Path as _Path
4927
5091
 
@@ -4959,11 +5123,12 @@ def cmd_skill(args: str, state, config) -> bool:
4959
5123
  print(f" 4) {clr('local', 'yellow')} — Anthropic + awesome marketplaces on disk (~/.claude/...)")
4960
5124
  print(f" 5) {clr('installed', 'yellow')} — skills already in ~/.dulus/skills/")
4961
5125
  print(f" 6) {clr('all', 'yellow')} — combine dulus + awesome + composio + local")
5126
+ print(f" 7) {clr('skillssh', 'yellow')} — skills.sh open directory (100K+ skills, needs a search term)")
4962
5127
  try:
4963
5128
  choice = input(clr(" > ", "cyan")).strip().lower()
4964
5129
  except (EOFError, KeyboardInterrupt):
4965
5130
  return True
4966
- mapping = {"1": "dulus", "2": "awesome", "3": "composio", "4": "local", "5": "installed", "6": "all"}
5131
+ mapping = {"1": "dulus", "2": "awesome", "3": "composio", "4": "local", "5": "installed", "6": "all", "7": "skillssh"}
4967
5132
  rest = mapping.get(choice, choice)
4968
5133
 
4969
5134
  if rest.startswith("dulus"):
@@ -5072,6 +5237,23 @@ def cmd_skill(args: str, state, config) -> bool:
5072
5237
  print(f" {clr(r['slug'], 'cyan'):30s} {r.get('description','')[:60]}")
5073
5238
  return True
5074
5239
 
5240
+ if rest.startswith("skillssh"):
5241
+ q = rest[8:].strip()
5242
+ if not q:
5243
+ info("skills.sh has no browse endpoint — give me a search term: /skill list skillssh <query>")
5244
+ return True
5245
+ info(f"Searching skills.sh for '{q}'...")
5246
+ results = search_skillssh(q, limit=25)
5247
+ if not results:
5248
+ err("No skills found on skills.sh (network or empty result).")
5249
+ return True
5250
+ lines = [
5251
+ f" {clr(s['id'], 'cyan'):60s} {s.get('description', '')[:70]}"
5252
+ for s in results
5253
+ ]
5254
+ _pager(f"skills.sh results ({len(results)}) — /skill get <id> to install — n=next q=quit", lines)
5255
+ return True
5256
+
5075
5257
  if rest.startswith("installed"):
5076
5258
  query = rest[9:].strip()
5077
5259
  skills = list_installed(query)
@@ -5179,6 +5361,8 @@ def cmd_skill(args: str, state, config) -> bool:
5179
5361
  if rest.startswith("clawhub:"):
5180
5362
  slug = rest[8:]
5181
5363
  success, msg = install_clawhub(slug)
5364
+ elif rest.startswith("skillssh/"):
5365
+ success, msg = install_skillssh(rest)
5182
5366
  elif rest.startswith("dulus/"):
5183
5367
  success, msg = install_dulus_remote(rest)
5184
5368
  elif rest.startswith("awesome/"):
@@ -9935,6 +10119,7 @@ COMMANDS = {
9935
10119
  "export": cmd_export,
9936
10120
  "fork": cmd_fork,
9937
10121
  "undo": cmd_undo,
10122
+ "workspace": cmd_workspace,
9938
10123
  "add-dir": cmd_add_dir,
9939
10124
  "import": cmd_import,
9940
10125
  "copy": cmd_copy,
@@ -10068,6 +10253,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
10068
10253
  "export": ("Export conversation to file", []),
10069
10254
  "fork": ("Fork session at a turn", []),
10070
10255
  "undo": ("Undo last turn", []),
10256
+ "workspace": ("Manage Dulus workspaces", ["current", "list", "switch", "default", "create", "delete"]),
10071
10257
  "add-dir": ("Manage additional workspace dirs", ["list", "remove"]),
10072
10258
  "import": ("Import from file or session", []),
10073
10259
  "copy": ("Copy last response or file to clipboard", ["file"]),
@@ -10518,6 +10704,13 @@ def repl(config: dict, initial_prompt: str = None):
10518
10704
  except Exception:
10519
10705
  pass
10520
10706
 
10707
+ # ── Workspace: start in the last-used workspace (or workspace1) ──────────
10708
+ try:
10709
+ _apply_workspace(config)
10710
+ except Exception as _e:
10711
+ if config.get("verbose", False):
10712
+ warn(f"Workspace init skipped: {_e}")
10713
+
10521
10714
  # ── Auto-update check (on by default; /update off to disable) ─────────
10522
10715
  # Keep every Dulus in the world on the latest release: the organism
10523
10716
  # heals fastest when fixes propagate instantly. This is intentionally
@@ -36,6 +36,17 @@ class StdioTransport:
36
36
 
37
37
  def start(self) -> None:
38
38
  env = {**os.environ, **(self._config.env or {})}
39
+ # ── Guard: empty/blank command ────────────────────────────────────
40
+ # A corrupted or half-filled-out .mcp.json entry (e.g. {"command": ""})
41
+ # used to reach subprocess.Popen([""], ...) directly. On Windows that
42
+ # raises a bare OSError [WinError 87] "The parameter is incorrect" with
43
+ # zero context — impossible to debug from the GUI toast. Fail fast with
44
+ # a message that names the actual broken server instead.
45
+ if not (self._config.command or "").strip():
46
+ raise RuntimeError(
47
+ f"MCP server '{self._config.name}' has no command configured "
48
+ f"(empty 'command' field in mcp.json). Fix or remove this entry."
49
+ )
39
50
  # ── Windows launcher resolution ──────────────────────────────────
40
51
  # On Windows, node tooling ships as .cmd shims (npx.cmd, npm.cmd) and
41
52
  # Python's subprocess won't find a bare "npx" without the extension.
@@ -46,6 +57,14 @@ class StdioTransport:
46
57
  _resolved = _shutil.which(_launcher)
47
58
  if _resolved:
48
59
  _launcher = _resolved
60
+ elif os.name == "nt" and not _launcher.lower().endswith((".exe", ".cmd", ".bat")):
61
+ # Launcher wasn't found on PATH at all (typo, uninstalled runtime,
62
+ # etc). Raise a clean, actionable error instead of letting Popen
63
+ # attempt to launch a bare/garbled name and blow up with WinError87.
64
+ raise RuntimeError(
65
+ f"MCP server '{self._config.name}': launcher '{_launcher}' not found on PATH. "
66
+ f"Install it or fix the 'command' field in mcp.json."
67
+ )
49
68
  cmd = [_launcher] + list(self._config.args or [])
50
69
  # On Windows, .cmd/.bat shims must run through the shell layer.
51
70
  _use_shell = os.name == "nt" and str(_launcher).lower().endswith((".cmd", ".bat"))
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "3.10.2"
7
+ version = "3.10.4"
8
8
  description = "Use IA as an agent without an API key. $0. Harvest your browser session for Gemini (guest, no login), Claude.ai, Claude Code, Kimi.com, Qwen, DeepSeek — drive them like an API, no per-token billing. Plus LiteLLM (100+ paid providers), local models via Ollama, /lang in 34 languages, Mesa Redonda, voice, OCR, MemPalace, embedded sandbox OS."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -731,6 +731,206 @@ def install_clawhub(slug: str) -> tuple[bool, str]:
731
731
  return False, f"ClawHub API endpoint not yet mapped. Try: /skill get local/{slug}"
732
732
 
733
733
 
734
+ # ── SKILLS.SH (skills.sh — the open agent skills directory) ────────────────
735
+ # skills.sh indexes 100K+ agent skills across GitHub repos. Search hits its
736
+ # public JSON API; install downloads the skill folder straight from the source
737
+ # repo via raw.githubusercontent.com — no npx, no clone. Fallback catalog:
738
+ # skillsdirectory.com public registry.
739
+
740
+ _SKILLSSH_API = "https://skills.sh/api/search"
741
+ _SKILLSSH_FALLBACK_API = "https://skillsdirectory.com/api/registry"
742
+ _SKILLSSH_TIMEOUT = 15
743
+
744
+
745
+ def _skillssh_gh_headers() -> dict:
746
+ """GitHub API headers — uses GITHUB_TOKEN/GH_TOKEN if present for rate limits."""
747
+ import os as _os
748
+ headers = {"User-Agent": "dulus-skill-manager"}
749
+ token = _os.environ.get("GITHUB_TOKEN") or _os.environ.get("GH_TOKEN")
750
+ if token:
751
+ headers["Authorization"] = f"Bearer {token}"
752
+ return headers
753
+
754
+
755
+ def search_skillssh(query: Optional[str], limit: int = 10) -> list[dict]:
756
+ """Search skills.sh for skills matching query.
757
+
758
+ Returns entries shaped like the other providers:
759
+ {id: "skillssh/<owner>/<repo>/<skill>", skill, name, description, source}
760
+ An empty query returns [] (skills.sh has no browse endpoint; use a term).
761
+ """
762
+ if not query or not str(query).strip():
763
+ return []
764
+
765
+ url = f"{_SKILLSSH_API}?q={urllib.parse.quote(str(query).strip())}"
766
+ data = None
767
+ try:
768
+ req = urllib.request.Request(url, headers={"User-Agent": "dulus-skill-manager"})
769
+ with urllib.request.urlopen(req, timeout=_SKILLSSH_TIMEOUT) as resp:
770
+ data = json.loads(resp.read())
771
+ except Exception:
772
+ data = None
773
+
774
+ skills: list[dict] = []
775
+ if data and isinstance(data.get("skills"), list):
776
+ for it in data["skills"][: max(limit, 1)]:
777
+ sid = it.get("id", "") # "owner/repo/skillId"
778
+ repo = it.get("source", "") # "owner/repo"
779
+ name = it.get("skillId") or it.get("name", "")
780
+ installs = it.get("installs", 0)
781
+ if not sid or not name:
782
+ continue
783
+ skills.append({
784
+ "id": f"skillssh/{sid}",
785
+ "skill": name,
786
+ "name": name,
787
+ "description": f"{installs:,} installs · {repo} (skills.sh)",
788
+ "installs": installs,
789
+ "repo": repo,
790
+ "source": "skillssh",
791
+ })
792
+ return skills
793
+
794
+ # Fallback: skillsdirectory.com public registry (no auth, ~97K skills)
795
+ try:
796
+ q = urllib.parse.quote(str(query).strip())
797
+ req = urllib.request.Request(
798
+ f"{_SKILLSSH_FALLBACK_API}?search={q}",
799
+ headers={"User-Agent": "dulus-skill-manager"},
800
+ )
801
+ with urllib.request.urlopen(req, timeout=_SKILLSSH_TIMEOUT) as resp:
802
+ reg = json.loads(resp.read())
803
+ entries = reg if isinstance(reg, list) else reg.get("skills", reg.get("data", []))
804
+ for it in entries[: max(limit, 1)]:
805
+ repo = it.get("repository", "") or it.get("repo", "")
806
+ name = it.get("slug") or it.get("name", "")
807
+ if not repo or not name:
808
+ continue
809
+ skills.append({
810
+ "id": f"skillssh/{repo}/{name}",
811
+ "skill": name,
812
+ "name": name,
813
+ "description": (it.get("description") or "")[:160],
814
+ "repo": repo,
815
+ "source": "skillssh",
816
+ })
817
+ except Exception:
818
+ pass
819
+ return skills
820
+
821
+
822
+ def _skillssh_default_branch(owner_repo: str) -> str:
823
+ """Resolve a repo's default branch (main/master/…)."""
824
+ try:
825
+ req = urllib.request.Request(
826
+ f"https://api.github.com/repos/{owner_repo}", headers=_skillssh_gh_headers()
827
+ )
828
+ with urllib.request.urlopen(req, timeout=_SKILLSSH_TIMEOUT) as resp:
829
+ return json.loads(resp.read()).get("default_branch", "main")
830
+ except Exception:
831
+ return "main"
832
+
833
+
834
+ def install_skillssh(slug: str) -> tuple[bool, str]:
835
+ """Install a skills.sh skill into ~/.dulus/skills/.
836
+
837
+ Accepts: skillssh/<owner>/<repo>/<skill> · <owner>/<repo>/<skill>
838
+ <owner>/<repo> (repo whose root SKILL.md IS the skill)
839
+ Downloads the whole skill folder (SKILL.md + assets/scripts/references)
840
+ from the source repo and rewrites frontmatter for Dulus.
841
+ """
842
+ parts = slug.replace("skillssh/", "", 1).strip("/").split("/")
843
+ if len(parts) < 2:
844
+ return False, (
845
+ f"Invalid skills.sh slug: '{slug}'. "
846
+ f"Expected <owner>/<repo>/<skill> or <owner>/<repo>."
847
+ )
848
+ owner_repo = f"{parts[0]}/{parts[1]}"
849
+ skill_name = parts[-1] if len(parts) >= 3 else parts[1]
850
+
851
+ branch = _skillssh_default_branch(owner_repo)
852
+
853
+ # One tree call to locate the skill dir (mirrors the skills-CLI discovery:
854
+ # skills/<name>/, .claude/skills/<name>/, catalog layouts, or root SKILL.md).
855
+ tree_url = f"https://api.github.com/repos/{owner_repo}/git/trees/{branch}?recursive=1"
856
+ try:
857
+ req = urllib.request.Request(tree_url, headers=_skillssh_gh_headers())
858
+ with urllib.request.urlopen(req, timeout=_SKILLSSH_TIMEOUT) as resp:
859
+ tree = json.loads(resp.read())
860
+ except Exception as e:
861
+ return False, f"Could not read repo tree for '{owner_repo}': {e}"
862
+
863
+ blobs = [
864
+ it.get("path", "")
865
+ for it in tree.get("tree", [])
866
+ if it.get("type") == "blob"
867
+ ]
868
+
869
+ # Candidate SKILL.md paths whose parent dir matches the skill name.
870
+ want = skill_name.lower()
871
+ candidates = [
872
+ p for p in blobs
873
+ if p.endswith("/SKILL.md") and p.split("/")[-2].lower() == want
874
+ ]
875
+ rel_dir = ""
876
+ if candidates:
877
+ rel_dir = "/".join(sorted(candidates, key=lambda p: p.count("/"))[0].split("/")[:-1])
878
+ elif "SKILL.md" in blobs and len(parts) == 2:
879
+ rel_dir = "" # root-level skill: the repo itself is the skill
880
+ else:
881
+ near = sorted({p.split("/")[-2] for p in blobs if p.endswith("/SKILL.md")})[:8]
882
+ return False, (
883
+ f"No SKILL.md found for '{skill_name}' in {owner_repo}. "
884
+ f"Skills in that repo: {', '.join(near) if near else '(none)'}"
885
+ )
886
+
887
+ dest_dir = DULUS_SKILLS_DIR / skill_name
888
+ dest_dir.mkdir(parents=True, exist_ok=True)
889
+
890
+ prefix = rel_dir + "/" if rel_dir else ""
891
+ targets = (
892
+ [p for p in blobs if p.startswith(prefix)] if rel_dir
893
+ else ["SKILL.md"]
894
+ )
895
+
896
+ raw_base = f"https://raw.githubusercontent.com/{owner_repo}/{branch}"
897
+ downloaded: list[str] = []
898
+ for p in targets:
899
+ rel = p[len(prefix):] if prefix and p.startswith(prefix) else p.split("/")[-1]
900
+ dst = dest_dir / rel
901
+ dst.parent.mkdir(parents=True, exist_ok=True)
902
+ try:
903
+ with urllib.request.urlopen(f"{raw_base}/{p}", timeout=_SKILLSSH_TIMEOUT) as r:
904
+ dst.write_bytes(r.read())
905
+ downloaded.append(rel)
906
+ except Exception:
907
+ pass # skip files we can't fetch
908
+
909
+ skill_md = dest_dir / "SKILL.md"
910
+ if not skill_md.exists():
911
+ return False, f"Could not download SKILL.md for '{skill_name}' from {owner_repo}."
912
+
913
+ # Rewrite frontmatter for Dulus (keep original description if present)
914
+ raw = skill_md.read_text(encoding="utf-8")
915
+ orig = _parse_frontmatter(raw)
916
+ body = _strip_frontmatter(raw)
917
+ frontmatter = (
918
+ f"---\n"
919
+ f"name: {skill_name}\n"
920
+ f"description: {orig.get('description', f'Skill from skills.sh ({owner_repo})')}\n"
921
+ f"source: skillssh\n"
922
+ f"repo: {owner_repo}\n"
923
+ f"triggers: [/{skill_name}]\n"
924
+ f"---\n\n"
925
+ )
926
+ skill_md.write_text(frontmatter + body, encoding="utf-8")
927
+
928
+ return True, (
929
+ f"Installed '{skill_name}' from skills.sh ({owner_repo}@{branch}) -> {dest_dir} "
930
+ f"({len(downloaded)} files: {', '.join(downloaded[:5])}{'...' if len(downloaded) > 5 else ''})"
931
+ )
932
+
933
+
734
934
  # ── Installed skills ───────────────────────────────────────────────────────
735
935
 
736
936
  def list_installed(query: Optional[str] = None) -> list[dict]:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes