dulus 3.5.0__tar.gz → 3.6.2__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 (330) hide show
  1. {dulus-3.5.0/dulus.egg-info → dulus-3.6.2}/PKG-INFO +2 -2
  2. {dulus-3.5.0 → dulus-3.6.2}/README.md +1 -1
  3. {dulus-3.5.0 → dulus-3.6.2}/analytics.py +21 -1
  4. {dulus-3.5.0 → dulus-3.6.2}/config.py +42 -2
  5. {dulus-3.5.0 → dulus-3.6.2}/docs/CHANGELOG.md +15 -0
  6. {dulus-3.5.0 → dulus-3.6.2}/docs/news.md +8 -0
  7. {dulus-3.5.0 → dulus-3.6.2/dulus.egg-info}/PKG-INFO +2 -2
  8. {dulus-3.5.0 → dulus-3.6.2}/dulus.py +116 -1
  9. {dulus-3.5.0 → dulus-3.6.2}/memory/sessions.py +44 -12
  10. {dulus-3.5.0 → dulus-3.6.2}/memory/tools.py +22 -8
  11. {dulus-3.5.0 → dulus-3.6.2}/pyproject.toml +1 -1
  12. {dulus-3.5.0 → dulus-3.6.2}/LICENSE +0 -0
  13. {dulus-3.5.0 → dulus-3.6.2}/MANIFEST.in +0 -0
  14. {dulus-3.5.0 → dulus-3.6.2}/agent.py +0 -0
  15. {dulus-3.5.0 → dulus-3.6.2}/backend/__init__.py +0 -0
  16. {dulus-3.5.0 → dulus-3.6.2}/backend/agents_bridge.py +0 -0
  17. {dulus-3.5.0 → dulus-3.6.2}/backend/compressor.py +0 -0
  18. {dulus-3.5.0 → dulus-3.6.2}/backend/context.py +0 -0
  19. {dulus-3.5.0 → dulus-3.6.2}/backend/githook.py +0 -0
  20. {dulus-3.5.0 → dulus-3.6.2}/backend/marketplace.py +0 -0
  21. {dulus-3.5.0 → dulus-3.6.2}/backend/mempalace_bridge.py +0 -0
  22. {dulus-3.5.0 → dulus-3.6.2}/backend/personas.py +0 -0
  23. {dulus-3.5.0 → dulus-3.6.2}/backend/plugins.py +0 -0
  24. {dulus-3.5.0 → dulus-3.6.2}/backend/server.py +0 -0
  25. {dulus-3.5.0 → dulus-3.6.2}/backend/tasks.py +0 -0
  26. {dulus-3.5.0 → dulus-3.6.2}/batch_api.py +0 -0
  27. {dulus-3.5.0 → dulus-3.6.2}/checkpoint/__init__.py +0 -0
  28. {dulus-3.5.0 → dulus-3.6.2}/checkpoint/hooks.py +0 -0
  29. {dulus-3.5.0 → dulus-3.6.2}/checkpoint/store.py +0 -0
  30. {dulus-3.5.0 → dulus-3.6.2}/checkpoint/types.py +0 -0
  31. {dulus-3.5.0 → dulus-3.6.2}/claude_code_watcher.py +0 -0
  32. {dulus-3.5.0 → dulus-3.6.2}/clipboard_utils.py +0 -0
  33. {dulus-3.5.0 → dulus-3.6.2}/cloudsave.py +0 -0
  34. {dulus-3.5.0 → dulus-3.6.2}/common.py +0 -0
  35. {dulus-3.5.0 → dulus-3.6.2}/compaction.py +0 -0
  36. {dulus-3.5.0 → dulus-3.6.2}/context.py +0 -0
  37. {dulus-3.5.0 → dulus-3.6.2}/data/__init__.py +0 -0
  38. {dulus-3.5.0 → dulus-3.6.2}/data/active_persona.json +0 -0
  39. {dulus-3.5.0 → dulus-3.6.2}/data/marketplace.json +0 -0
  40. {dulus-3.5.0 → dulus-3.6.2}/data/personas.json +0 -0
  41. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/__init__.py +0 -0
  42. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/__init__.py +0 -0
  43. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  44. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  45. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  46. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/plugin.json +0 -0
  47. {dulus-3.5.0 → dulus-3.6.2}/data/plugins/composio/plugin_tool.py +0 -0
  48. {dulus-3.5.0 → dulus-3.6.2}/data/tasks.json +0 -0
  49. {dulus-3.5.0 → dulus-3.6.2}/docs/API.md +0 -0
  50. {dulus-3.5.0 → dulus-3.6.2}/docs/BRAND_GUIDE.md +0 -0
  51. {dulus-3.5.0 → dulus-3.6.2}/docs/CONTRIBUTING.md +0 -0
  52. {dulus-3.5.0 → dulus-3.6.2}/docs/DEPLOYMENT.md +0 -0
  53. {dulus-3.5.0 → dulus-3.6.2}/docs/Dulus_AI_Whitepaper_(v2.0).pdf +0 -0
  54. {dulus-3.5.0 → dulus-3.6.2}/docs/FAQ.md +0 -0
  55. {dulus-3.5.0 → dulus-3.6.2}/docs/GETTING_STARTED.md +0 -0
  56. {dulus-3.5.0 → dulus-3.6.2}/docs/INVESTOR_BRIEF.md +0 -0
  57. {dulus-3.5.0 → dulus-3.6.2}/docs/README.md +0 -0
  58. {dulus-3.5.0 → dulus-3.6.2}/docs/README_AR.md +0 -0
  59. {dulus-3.5.0 → dulus-3.6.2}/docs/README_EN.md +0 -0
  60. {dulus-3.5.0 → dulus-3.6.2}/docs/README_ES.md +0 -0
  61. {dulus-3.5.0 → dulus-3.6.2}/docs/README_FR.md +0 -0
  62. {dulus-3.5.0 → dulus-3.6.2}/docs/README_JA.md +0 -0
  63. {dulus-3.5.0 → dulus-3.6.2}/docs/README_KO.md +0 -0
  64. {dulus-3.5.0 → dulus-3.6.2}/docs/README_PT.md +0 -0
  65. {dulus-3.5.0 → dulus-3.6.2}/docs/README_RU.md +0 -0
  66. {dulus-3.5.0 → dulus-3.6.2}/docs/README_ZH.md +0 -0
  67. {dulus-3.5.0 → dulus-3.6.2}/docs/SECURITY.md +0 -0
  68. {dulus-3.5.0 → dulus-3.6.2}/docs/__init__.py +0 -0
  69. {dulus-3.5.0 → dulus-3.6.2}/docs/api.html +0 -0
  70. {dulus-3.5.0 → dulus-3.6.2}/docs/architecture.md +0 -0
  71. {dulus-3.5.0 → dulus-3.6.2}/docs/azure-speech-template.json +0 -0
  72. {dulus-3.5.0 → dulus-3.6.2}/docs/dashboard/index.html +0 -0
  73. {dulus-3.5.0 → dulus-3.6.2}/docs/divider.svg +0 -0
  74. {dulus-3.5.0 → dulus-3.6.2}/docs/dulus-bird.png +0 -0
  75. {dulus-3.5.0 → dulus-3.6.2}/docs/generate.py +0 -0
  76. {dulus-3.5.0 → dulus-3.6.2}/docs/hero.svg +0 -0
  77. {dulus-3.5.0 → dulus-3.6.2}/docs/index.html +0 -0
  78. {dulus-3.5.0 → dulus-3.6.2}/docs/nvidia-models.svg +0 -0
  79. {dulus-3.5.0 → dulus-3.6.2}/docs/particle-playground.html +0 -0
  80. {dulus-3.5.0 → dulus-3.6.2}/docs/personas/index.html +0 -0
  81. {dulus-3.5.0 → dulus-3.6.2}/docs/poetry-banner.png +0 -0
  82. {dulus-3.5.0 → dulus-3.6.2}/docs/preview.html +0 -0
  83. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-agents.svg +0 -0
  84. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-brainstorm.svg +0 -0
  85. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-bridges.svg +0 -0
  86. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-features.svg +0 -0
  87. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-freetier.svg +0 -0
  88. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-memory.svg +0 -0
  89. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-models.svg +0 -0
  90. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-perms.svg +0 -0
  91. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-plugins.svg +0 -0
  92. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-quickstart.svg +0 -0
  93. {dulus-3.5.0 → dulus-3.6.2}/docs/sec-ssj.svg +0 -0
  94. {dulus-3.5.0 → dulus-3.6.2}/docs/spinners.svg +0 -0
  95. {dulus-3.5.0 → dulus-3.6.2}/docs/split-pane.svg +0 -0
  96. {dulus-3.5.0 → dulus-3.6.2}/docs/terminal-boot.svg +0 -0
  97. {dulus-3.5.0 → dulus-3.6.2}/dulus.egg-info/SOURCES.txt +0 -0
  98. {dulus-3.5.0 → dulus-3.6.2}/dulus.egg-info/dependency_links.txt +0 -0
  99. {dulus-3.5.0 → dulus-3.6.2}/dulus.egg-info/entry_points.txt +0 -0
  100. {dulus-3.5.0 → dulus-3.6.2}/dulus.egg-info/requires.txt +0 -0
  101. {dulus-3.5.0 → dulus-3.6.2}/dulus.egg-info/top_level.txt +0 -0
  102. {dulus-3.5.0 → dulus-3.6.2}/dulus_gui.py +0 -0
  103. {dulus-3.5.0 → dulus-3.6.2}/dulus_gui_web.py +0 -0
  104. {dulus-3.5.0 → dulus-3.6.2}/dulus_mcp/__init__.py +0 -0
  105. {dulus-3.5.0 → dulus-3.6.2}/dulus_mcp/client.py +0 -0
  106. {dulus-3.5.0 → dulus-3.6.2}/dulus_mcp/config.py +0 -0
  107. {dulus-3.5.0 → dulus-3.6.2}/dulus_mcp/tools.py +0 -0
  108. {dulus-3.5.0 → dulus-3.6.2}/dulus_mcp/types.py +0 -0
  109. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/__init__.py +0 -0
  110. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/add_dir_manager.py +0 -0
  111. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/afk_mode.py +0 -0
  112. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/approval_runtime.py +0 -0
  113. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/background_tasks.py +0 -0
  114. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/clipboard_utils.py +0 -0
  115. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/diff_visualizer.py +0 -0
  116. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/export_import.py +0 -0
  117. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/hook_engine.py +0 -0
  118. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/notification_manager.py +0 -0
  119. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/session_fork.py +0 -0
  120. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/shell_mode.py +0 -0
  121. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/todo_visualizer.py +0 -0
  122. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/wire_events.py +0 -0
  123. {dulus-3.5.0 → dulus-3.6.2}/dulus_tools/yolo_mode.py +0 -0
  124. {dulus-3.5.0 → dulus-3.6.2}/governance.py +0 -0
  125. {dulus-3.5.0 → dulus-3.6.2}/gui/__init__.py +0 -0
  126. {dulus-3.5.0 → dulus-3.6.2}/gui/agent_bridge.py +0 -0
  127. {dulus-3.5.0 → dulus-3.6.2}/gui/chat_widget.py +0 -0
  128. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/App.tsx +0 -0
  129. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/THIRD_PARTY_NOTICES.md +0 -0
  130. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/dist/index.html +0 -0
  131. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/dist/standalone.html +0 -0
  132. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/index.html +0 -0
  133. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/index.tsx +0 -0
  134. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/metadata.json +0 -0
  135. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/package-lock.json +0 -0
  136. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/package.json +0 -0
  137. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/types.ts +0 -0
  138. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/utils.ts +0 -0
  139. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/vertex-ai-proxy-interceptor.js +0 -0
  140. {dulus-3.5.0 → dulus-3.6.2}/gui/dulus_orchestrator/vite.config.ts +0 -0
  141. {dulus-3.5.0 → dulus-3.6.2}/gui/main_window.py +0 -0
  142. {dulus-3.5.0 → dulus-3.6.2}/gui/personas.py +0 -0
  143. {dulus-3.5.0 → dulus-3.6.2}/gui/sandbox_server.py +0 -0
  144. {dulus-3.5.0 → dulus-3.6.2}/gui/session_utils.py +0 -0
  145. {dulus-3.5.0 → dulus-3.6.2}/gui/settings_dialog.py +0 -0
  146. {dulus-3.5.0 → dulus-3.6.2}/gui/sidebar.py +0 -0
  147. {dulus-3.5.0 → dulus-3.6.2}/gui/tasks_view.py +0 -0
  148. {dulus-3.5.0 → dulus-3.6.2}/gui/themes.py +0 -0
  149. {dulus-3.5.0 → dulus-3.6.2}/gui/tool_panel.py +0 -0
  150. {dulus-3.5.0 → dulus-3.6.2}/input.py +0 -0
  151. {dulus-3.5.0 → dulus-3.6.2}/license_manager.py +0 -0
  152. {dulus-3.5.0 → dulus-3.6.2}/memory/__init__.py +0 -0
  153. {dulus-3.5.0 → dulus-3.6.2}/memory/audit.py +0 -0
  154. {dulus-3.5.0 → dulus-3.6.2}/memory/consolidator.py +0 -0
  155. {dulus-3.5.0 → dulus-3.6.2}/memory/context.py +0 -0
  156. {dulus-3.5.0 → dulus-3.6.2}/memory/offload.py +0 -0
  157. {dulus-3.5.0 → dulus-3.6.2}/memory/palace.py +0 -0
  158. {dulus-3.5.0 → dulus-3.6.2}/memory/scan.py +0 -0
  159. {dulus-3.5.0 → dulus-3.6.2}/memory/store.py +0 -0
  160. {dulus-3.5.0 → dulus-3.6.2}/memory/types.py +0 -0
  161. {dulus-3.5.0 → dulus-3.6.2}/memory/vector_search.py +0 -0
  162. {dulus-3.5.0 → dulus-3.6.2}/multi_agent/__init__.py +0 -0
  163. {dulus-3.5.0 → dulus-3.6.2}/multi_agent/subagent.py +0 -0
  164. {dulus-3.5.0 → dulus-3.6.2}/multi_agent/tools.py +0 -0
  165. {dulus-3.5.0 → dulus-3.6.2}/offload_helper.py +0 -0
  166. {dulus-3.5.0 → dulus-3.6.2}/personality_config.py +0 -0
  167. {dulus-3.5.0 → dulus-3.6.2}/plugin/__init__.py +0 -0
  168. {dulus-3.5.0 → dulus-3.6.2}/plugin/autoadapter.py +0 -0
  169. {dulus-3.5.0 → dulus-3.6.2}/plugin/loader.py +0 -0
  170. {dulus-3.5.0 → dulus-3.6.2}/plugin/recommend.py +0 -0
  171. {dulus-3.5.0 → dulus-3.6.2}/plugin/store.py +0 -0
  172. {dulus-3.5.0 → dulus-3.6.2}/plugin/types.py +0 -0
  173. {dulus-3.5.0 → dulus-3.6.2}/providers.py +0 -0
  174. {dulus-3.5.0 → dulus-3.6.2}/sandbox/README.md +0 -0
  175. {dulus-3.5.0 → dulus-3.6.2}/sandbox/__init__.py +0 -0
  176. {dulus-3.5.0 → dulus-3.6.2}/sandbox/components.json +0 -0
  177. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  178. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  179. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/index.html +0 -0
  180. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/wallpaper-default.jpg +0 -0
  181. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/wallpapers/default.jpeg +0 -0
  182. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/wallpapers/light.jpeg +0 -0
  183. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  184. {dulus-3.5.0 → dulus-3.6.2}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  185. {dulus-3.5.0 → dulus-3.6.2}/sandbox/eslint.config.js +0 -0
  186. {dulus-3.5.0 → dulus-3.6.2}/sandbox/index.html +0 -0
  187. {dulus-3.5.0 → dulus-3.6.2}/sandbox/info.md +0 -0
  188. {dulus-3.5.0 → dulus-3.6.2}/sandbox/package-lock.json +0 -0
  189. {dulus-3.5.0 → dulus-3.6.2}/sandbox/package.json +0 -0
  190. {dulus-3.5.0 → dulus-3.6.2}/sandbox/postcss.config.js +0 -0
  191. {dulus-3.5.0 → dulus-3.6.2}/sandbox/tailwind.config.js +0 -0
  192. {dulus-3.5.0 → dulus-3.6.2}/sandbox/tsconfig.app.json +0 -0
  193. {dulus-3.5.0 → dulus-3.6.2}/sandbox/tsconfig.json +0 -0
  194. {dulus-3.5.0 → dulus-3.6.2}/sandbox/tsconfig.node.json +0 -0
  195. {dulus-3.5.0 → dulus-3.6.2}/sandbox/vite.config.ts +0 -0
  196. {dulus-3.5.0 → dulus-3.6.2}/sandbox_bootstrap.py +0 -0
  197. {dulus-3.5.0 → dulus-3.6.2}/setup.cfg +0 -0
  198. {dulus-3.5.0 → dulus-3.6.2}/skill/__init__.py +0 -0
  199. {dulus-3.5.0 → dulus-3.6.2}/skill/builtin.py +0 -0
  200. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/LICENSE-obsidian-skills +0 -0
  201. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/README.md +0 -0
  202. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/defuddle/SKILL.md +0 -0
  203. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/json-canvas/SKILL.md +0 -0
  204. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
  205. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-bases/SKILL.md +0 -0
  206. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
  207. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-cli/SKILL.md +0 -0
  208. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
  209. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
  210. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
  211. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
  212. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/README.md +0 -0
  213. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/SKILL.md +0 -0
  214. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
  215. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
  216. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
  217. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
  218. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
  219. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
  220. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
  221. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
  222. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
  223. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
  224. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
  225. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
  226. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
  227. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
  228. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
  229. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
  230. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
  231. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
  232. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
  233. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
  234. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
  235. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
  236. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
  237. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/legal/drop.md +0 -0
  238. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
  239. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/methods.md +0 -0
  240. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
  241. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/references/state-machine.md +0 -0
  242. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
  243. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/badbool.py +0 -0
  244. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/brokers.py +0 -0
  245. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/cdp.py +0 -0
  246. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/config.py +0 -0
  247. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/crypto.py +0 -0
  248. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/dossier.py +0 -0
  249. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
  250. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/emailer.py +0 -0
  251. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/ledger.py +0 -0
  252. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/legal.py +0 -0
  253. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/paths.py +0 -0
  254. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/pdd.py +0 -0
  255. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/registry.py +0 -0
  256. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/report.py +0 -0
  257. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/scan.py +0 -0
  258. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/storage.py +0 -0
  259. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/tiers.py +0 -0
  260. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/scripts/vectors.py +0 -0
  261. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
  262. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
  263. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
  264. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
  265. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
  266. {dulus-3.5.0 → dulus-3.6.2}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
  267. {dulus-3.5.0 → dulus-3.6.2}/skill/clawhub.py +0 -0
  268. {dulus-3.5.0 → dulus-3.6.2}/skill/executor.py +0 -0
  269. {dulus-3.5.0 → dulus-3.6.2}/skill/loader.py +0 -0
  270. {dulus-3.5.0 → dulus-3.6.2}/skill/tools.py +0 -0
  271. {dulus-3.5.0 → dulus-3.6.2}/skills.py +0 -0
  272. {dulus-3.5.0 → dulus-3.6.2}/soul.py +0 -0
  273. {dulus-3.5.0 → dulus-3.6.2}/spinner.py +0 -0
  274. {dulus-3.5.0 → dulus-3.6.2}/string_utils.py +0 -0
  275. {dulus-3.5.0 → dulus-3.6.2}/subagent.py +0 -0
  276. {dulus-3.5.0 → dulus-3.6.2}/task/__init__.py +0 -0
  277. {dulus-3.5.0 → dulus-3.6.2}/task/store.py +0 -0
  278. {dulus-3.5.0 → dulus-3.6.2}/task/tools.py +0 -0
  279. {dulus-3.5.0 → dulus-3.6.2}/task/types.py +0 -0
  280. {dulus-3.5.0 → dulus-3.6.2}/tests/test_afk_yolo.py +0 -0
  281. {dulus-3.5.0 → dulus-3.6.2}/tests/test_approval_runtime.py +0 -0
  282. {dulus-3.5.0 → dulus-3.6.2}/tests/test_background_task_tools.py +0 -0
  283. {dulus-3.5.0 → dulus-3.6.2}/tests/test_background_tasks.py +0 -0
  284. {dulus-3.5.0 → dulus-3.6.2}/tests/test_checkpoint.py +0 -0
  285. {dulus-3.5.0 → dulus-3.6.2}/tests/test_clipboard_utils.py +0 -0
  286. {dulus-3.5.0 → dulus-3.6.2}/tests/test_compaction.py +0 -0
  287. {dulus-3.5.0 → dulus-3.6.2}/tests/test_diff_view.py +0 -0
  288. {dulus-3.5.0 → dulus-3.6.2}/tests/test_diff_visualization.py +0 -0
  289. {dulus-3.5.0 → dulus-3.6.2}/tests/test_display_blocks.py +0 -0
  290. {dulus-3.5.0 → dulus-3.6.2}/tests/test_export_import.py +0 -0
  291. {dulus-3.5.0 → dulus-3.6.2}/tests/test_hook_engine.py +0 -0
  292. {dulus-3.5.0 → dulus-3.6.2}/tests/test_injection_fix.py +0 -0
  293. {dulus-3.5.0 → dulus-3.6.2}/tests/test_license.py +0 -0
  294. {dulus-3.5.0 → dulus-3.6.2}/tests/test_mcp.py +0 -0
  295. {dulus-3.5.0 → dulus-3.6.2}/tests/test_memory.py +0 -0
  296. {dulus-3.5.0 → dulus-3.6.2}/tests/test_notification_manager.py +0 -0
  297. {dulus-3.5.0 → dulus-3.6.2}/tests/test_plugin.py +0 -0
  298. {dulus-3.5.0 → dulus-3.6.2}/tests/test_session_fork.py +0 -0
  299. {dulus-3.5.0 → dulus-3.6.2}/tests/test_shell_mode.py +0 -0
  300. {dulus-3.5.0 → dulus-3.6.2}/tests/test_skills.py +0 -0
  301. {dulus-3.5.0 → dulus-3.6.2}/tests/test_steer_input.py +0 -0
  302. {dulus-3.5.0 → dulus-3.6.2}/tests/test_subagent.py +0 -0
  303. {dulus-3.5.0 → dulus-3.6.2}/tests/test_task.py +0 -0
  304. {dulus-3.5.0 → dulus-3.6.2}/tests/test_telegram_buffer.py +0 -0
  305. {dulus-3.5.0 → dulus-3.6.2}/tests/test_think_tool.py +0 -0
  306. {dulus-3.5.0 → dulus-3.6.2}/tests/test_todo_tool.py +0 -0
  307. {dulus-3.5.0 → dulus-3.6.2}/tests/test_todo_visualization.py +0 -0
  308. {dulus-3.5.0 → dulus-3.6.2}/tests/test_tool_registry.py +0 -0
  309. {dulus-3.5.0 → dulus-3.6.2}/tests/test_voice.py +0 -0
  310. {dulus-3.5.0 → dulus-3.6.2}/tests/test_wire_events.py +0 -0
  311. {dulus-3.5.0 → dulus-3.6.2}/tmux_offloader.py +0 -0
  312. {dulus-3.5.0 → dulus-3.6.2}/tmux_tools.py +0 -0
  313. {dulus-3.5.0 → dulus-3.6.2}/tool_registry.py +0 -0
  314. {dulus-3.5.0 → dulus-3.6.2}/tools.py +0 -0
  315. {dulus-3.5.0 → dulus-3.6.2}/ui/__init__.py +0 -0
  316. {dulus-3.5.0 → dulus-3.6.2}/ui/input.py +0 -0
  317. {dulus-3.5.0 → dulus-3.6.2}/ui/render.py +0 -0
  318. {dulus-3.5.0 → dulus-3.6.2}/voice/__init__.py +0 -0
  319. {dulus-3.5.0 → dulus-3.6.2}/voice/audio_utils.py +0 -0
  320. {dulus-3.5.0 → dulus-3.6.2}/voice/keyterms.py +0 -0
  321. {dulus-3.5.0 → dulus-3.6.2}/voice/recorder.py +0 -0
  322. {dulus-3.5.0 → dulus-3.6.2}/voice/stt.py +0 -0
  323. {dulus-3.5.0 → dulus-3.6.2}/voice/tts.py +0 -0
  324. {dulus-3.5.0 → dulus-3.6.2}/voice/wake_word.py +0 -0
  325. {dulus-3.5.0 → dulus-3.6.2}/webbridge/__init__.py +0 -0
  326. {dulus-3.5.0 → dulus-3.6.2}/webbridge/core.py +0 -0
  327. {dulus-3.5.0 → dulus-3.6.2}/webbridge/tools.py +0 -0
  328. {dulus-3.5.0 → dulus-3.6.2}/webchat.py +0 -0
  329. {dulus-3.5.0 → dulus-3.6.2}/webchat_server.py +0 -0
  330. {dulus-3.5.0 → dulus-3.6.2}/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 3.5.0
3
+ Version: 3.6.2
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
@@ -133,7 +133,7 @@ SET /sticky_input ON since the first run for the best experience!
133
133
 
134
134
  > Full changelog: [`docs/news.md`](docs/news.md) · Inside the REPL: `/news`
135
135
 
136
- - **📡 dulus.work — announced.** Kevin finally revealed the final piece of the Dulus vision — the one he'd been keeping secret (even from Dulus itself 😂). Every agent ships updates. Nobody ships THIS. Already in motion. Stay tuned. 🦅 → [dulus.work](https://dulus.work)
136
+ - **🌍 dulus.work — THE HUB. The secret is out.** This is what Kevin had been hiding (even from Dulus itself 😂): **a global hub where every Dulus instance on the planet shares what it learns.** Your Dulus finds a fix, polishes a skill, adapts a plugin → it can publish it to the hub → every other Dulus in the world can pull it down. Not an update server an **organism**. Every installation is a node. *The fix a Dulus in Tokyo finds at 3AM, the one in Santo Domingo has by sunrise.* Other agents have N devs improving one product (linear). Dulus makes every user a dev (exponential). The pieces have been quietly shipping for months — SelfImprove engine, Auto-Adapter, the skills format, cloudsave — and now they converge. Domain is live, execution underway. 🦅🇩🇴 → [dulus.work](https://dulus.work)
137
137
 
138
138
  - **Dulus Agent — Telegram communities.** Dulus evolved from a CLI tool into a live AI agent inside Telegram groups. The first fully autonomous AI moderating and conversing in real communities — not a bot, not a filter, a real agent. Groups pay in $DULUS to activate him. We host Dulus for them — that's the business model. The CLI stays free forever. This is the paid layer. 🦅🇩🇴
139
139
 
@@ -68,7 +68,7 @@ SET /sticky_input ON since the first run for the best experience!
68
68
 
69
69
  > Full changelog: [`docs/news.md`](docs/news.md) · Inside the REPL: `/news`
70
70
 
71
- - **📡 dulus.work — announced.** Kevin finally revealed the final piece of the Dulus vision — the one he'd been keeping secret (even from Dulus itself 😂). Every agent ships updates. Nobody ships THIS. Already in motion. Stay tuned. 🦅 → [dulus.work](https://dulus.work)
71
+ - **🌍 dulus.work — THE HUB. The secret is out.** This is what Kevin had been hiding (even from Dulus itself 😂): **a global hub where every Dulus instance on the planet shares what it learns.** Your Dulus finds a fix, polishes a skill, adapts a plugin → it can publish it to the hub → every other Dulus in the world can pull it down. Not an update server an **organism**. Every installation is a node. *The fix a Dulus in Tokyo finds at 3AM, the one in Santo Domingo has by sunrise.* Other agents have N devs improving one product (linear). Dulus makes every user a dev (exponential). The pieces have been quietly shipping for months — SelfImprove engine, Auto-Adapter, the skills format, cloudsave — and now they converge. Domain is live, execution underway. 🦅🇩🇴 → [dulus.work](https://dulus.work)
72
72
 
73
73
  - **Dulus Agent — Telegram communities.** Dulus evolved from a CLI tool into a live AI agent inside Telegram groups. The first fully autonomous AI moderating and conversing in real communities — not a bot, not a filter, a real agent. Groups pay in $DULUS to activate him. We host Dulus for them — that's the business model. The CLI stays free forever. This is the paid layer. 🦅🇩🇴
74
74
 
@@ -41,7 +41,7 @@ import uuid
41
41
  # NOTE: Mixpanel ingestion tokens are write-only and designed to ship in
42
42
  # client code (every website using Mixpanel exposes theirs). It cannot be
43
43
  # used to read any data. Override with DULUS_MP_TOKEN.
44
- MP_TOKEN = os.environ.get("DULUS_MP_TOKEN", "")
44
+ MP_TOKEN = os.environ.get("DULUS_MP_TOKEN", "966ce8f5ccb32f06788f51be9f8bf8f5")
45
45
 
46
46
  _MP_ENDPOINT = "https://api.mixpanel.com/track"
47
47
 
@@ -142,3 +142,23 @@ def track_session_start(config: dict) -> None:
142
142
  "provider": str(config.get("provider", "")),
143
143
  "model": str(config.get("model", "")),
144
144
  })
145
+
146
+
147
+ def track_message_sent(model: str = "") -> None:
148
+ """One event per user prompt sent to the LLM (no content — count only)."""
149
+ track("message_sent", {"model": model})
150
+
151
+
152
+ def track_tool_used(tool_name: str) -> None:
153
+ """One event per tool invocation (tool NAME only — never inputs)."""
154
+ track("tool_used", {"tool": tool_name})
155
+
156
+
157
+ def track_command_used(command: str) -> None:
158
+ """One event per slash command (command NAME only — never args)."""
159
+ track("command_used", {"command": command})
160
+
161
+
162
+ def track_model_selected(model: str, provider: str = "") -> None:
163
+ """Fired when the user switches models via /model."""
164
+ track("model_selected", {"model": model, "provider": provider})
@@ -130,7 +130,31 @@ def load_config() -> dict:
130
130
  try:
131
131
  cfg.update(json.loads(CONFIG_FILE.read_text(encoding="utf-8")))
132
132
  except Exception:
133
- pass
133
+ # Config is corrupt (truncated write, crash mid-save, etc).
134
+ # NEVER silently reset the user's config: try the .bak backup
135
+ # first, and if that also fails, preserve the corrupt file for
136
+ # manual recovery instead of letting the next save_config()
137
+ # clobber it with bare defaults.
138
+ recovered = False
139
+ backup = CONFIG_FILE.with_suffix(".json.bak")
140
+ if backup.exists():
141
+ try:
142
+ cfg.update(json.loads(backup.read_text(encoding="utf-8")))
143
+ recovered = True
144
+ print(f"[dulus] WARNING: config.json was corrupt - recovered from {backup.name}")
145
+ except Exception:
146
+ pass
147
+ try:
148
+ import time as _time
149
+ quarantine = CONFIG_FILE.with_name(
150
+ f"config.corrupt-{int(_time.time())}.json")
151
+ CONFIG_FILE.replace(quarantine)
152
+ print(f"[dulus] Corrupt config preserved at: {quarantine}")
153
+ except Exception:
154
+ pass
155
+ if not recovered:
156
+ print("[dulus] WARNING: config.json was corrupt and no backup was found - "
157
+ "starting with defaults. Your old config was preserved (see above).")
134
158
  # Decrypt secured keys
135
159
  cfg = _unsecure_keys(cfg)
136
160
  # Backward-compat: legacy single api_key → anthropic_api_key
@@ -168,7 +192,23 @@ def save_config(cfg: dict):
168
192
  data = {k: v for k, v in cfg.items() if not k.startswith("_")}
169
193
  # Encrypt API keys before saving
170
194
  data = _secure_keys(dict(data))
171
- CONFIG_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
195
+ payload = json.dumps(data, indent=2)
196
+ # Atomic write: dump to a temp file in the same dir, then replace.
197
+ # A crash mid-write can no longer truncate/corrupt config.json.
198
+ tmp = CONFIG_FILE.with_suffix(".json.tmp")
199
+ tmp.write_text(payload, encoding="utf-8")
200
+ # Keep a rolling backup of the last-known-good config so load_config()
201
+ # can auto-recover if the main file ever goes bad.
202
+ if CONFIG_FILE.exists():
203
+ try:
204
+ backup = CONFIG_FILE.with_suffix(".json.bak")
205
+ # Only back up if current file is valid JSON (don't overwrite a
206
+ # good backup with a corrupt file).
207
+ json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
208
+ backup.write_text(CONFIG_FILE.read_text(encoding="utf-8"), encoding="utf-8")
209
+ except Exception:
210
+ pass
211
+ tmp.replace(CONFIG_FILE)
172
212
 
173
213
 
174
214
  def current_provider(cfg: dict) -> str:
@@ -15,6 +15,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  - Security policy
16
16
  - Brand guidelines
17
17
 
18
+ ## [3.6.2] - 2026-07-04
19
+
20
+ ### Fixed
21
+ - Opt-in telemetry never sent events: `MP_TOKEN` defaulted to empty string so
22
+ `is_enabled()` was always False, even after user consent. The public
23
+ project's write-only ingestion token now ships as the default
24
+ (`DULUS_MP_TOKEN` env var still overrides).
25
+
26
+ ### Added
27
+ - Named telemetry events (names/counts only — never content): `message_sent`,
28
+ `tool_used`, `command_used`, `model_selected`, wired into the REPL loop,
29
+ tool dispatch, slash commands and `/model`.
30
+ - Memory: session history search improvements — token matching, newest-first
31
+ ordering, no truncation (from 786bd34).
32
+
18
33
  ## [3.2.0] - 2026-05-30
19
34
 
20
35
  ### Added
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
+ - July 3, 2026: **🌍 dulus.work — THE HUB, revealed.** The secret is out: dulus.work is a **global hub where every Dulus instance on the planet shares what it learns**. Your Dulus finds a fix, refines a skill, adapts a plugin locally → publishes it to the hub → every other Dulus in the world pulls it down. Not an update server — an **organism**: every installation is a node, every user effectively becomes a contributor. *The fix a Dulus in Tokyo finds at 3AM, the one in Santo Domingo has by sunrise.* Traditional agents scale linearly (N devs improving 1 product); Dulus scales with its network (every user is a dev). The building blocks have been quietly shipping for months — the SelfImprove engine, the Auto-Adapter, the skills format, cloudsave — and dulus.work is where they converge. Domain is live, execution underway. 🦅🇩🇴 → [dulus.work](https://dulus.work)
6
+
7
+
8
+ - July 3, 2026 (**v3.6.1**): **🛡️ Your config.json can never be silently reset again.** A crash mid-save could truncate `~/.dulus/config.json`; on the next start Dulus silently swallowed the parse error, dropped you into the first-run welcome wizard, and the first auto-save clobbered your real config with bare defaults (classic symptom: a config containing only `voice_lang`). Fixed with a triple safety net: **atomic writes** (temp file + rename — a mid-write crash physically can't corrupt the file anymore), a **rolling `config.json.bak`** of the last-known-good config, **auto-recovery** from that backup when the main file goes bad, and **quarantine** (`config.corrupt-<timestamp>.json`) so the broken original is always preserved for manual rescue instead of vanishing. If you ever lost your config to the welcome screen: this was it, and it's dead now.
9
+
10
+ - July 3, 2026 (**v3.6.0**): **🧙 `/sage` (alias `/sabio`) — the AI studies your prompt before touching anything.** Inspired by ticket-intake classifier patterns but pointed the right way: instead of classifying requests into a database, Dulus decomposes YOUR request like a senior architect doing intake. Four phases: **INTAKE** (break the request into atomic sub-requests with category, priority and a verifiable "done"), **QUALITY GATE** (is it ambiguous? imprecise? mixing unrelated problems? — if missing info BLOCKS execution, ALL clarification questions arrive batched in one AskUserQuestion; otherwise assumptions are stated explicitly and it moves on), **PLAN** (ordered steps, tools per step, risks, verification — multi-step work registers via TaskCreate), **EXECUTE** (verify as you go, adapt honestly when reality diverges). `/sage` alone arms it one-shot for your NEXT prompt; `/sage <request>` runs the full treatment immediately; `/sage off|status` to manage. Difference vs `/plan`: plan mode is read-only planning for big projects — sage studies AND executes in one flow, built for the everyday "think before you act".
11
+
12
+ - July 3, 2026 (**v3.5.0**): **🕵️ unbroker bundled — your agent removes you from data brokers, free.** Ported from [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) (MIT, by SHL0MS) and seasoned for Dulus: finds a consenting person's exposed PII (name, addresses, phones, relatives) across people-search sites and removes it — CCPA/GDPR opt-out emails, cluster-parents-first ordering, verified-before-disclosure, audit ledger, scheduled re-scans when brokers re-list you. Consent-gated, no CAPTCHA defeating, least-disclosure by design. What DeleteMe/Incogni charge a subscription for, as a transparent self-hosted skill your own agent runs. 22 curated brokers out of the box (~50 via BADBOOL refresh + the CA registry's ~545 with the DROP one-shot). Just say: *"remove my data from data brokers"*. Also in 3.5.0: modern web GUI (`dulus --gui`, React + SSE), opt-in anonymous telemetry, Claude vision attachment fix.
5
13
 
6
14
  - July 3, 2026: **📡 dulus.work — the final piece, announced.** Kevin finally revealed the idea he'd been keeping secret — even from Dulus itself (opsec against your own AI is crazy work 😂). Every agent ships updates. Nobody ships THIS. The domain is live, the pieces have been quietly shipping for months (SelfImprove engine, Auto-Adapter, skills format, cloudsave...), and now they converge. Already in motion. Stay tuned. 🦅🇩🇴 → [dulus.work](https://dulus.work)
7
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 3.5.0
3
+ Version: 3.6.2
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
@@ -133,7 +133,7 @@ SET /sticky_input ON since the first run for the best experience!
133
133
 
134
134
  > Full changelog: [`docs/news.md`](docs/news.md) · Inside the REPL: `/news`
135
135
 
136
- - **📡 dulus.work — announced.** Kevin finally revealed the final piece of the Dulus vision — the one he'd been keeping secret (even from Dulus itself 😂). Every agent ships updates. Nobody ships THIS. Already in motion. Stay tuned. 🦅 → [dulus.work](https://dulus.work)
136
+ - **🌍 dulus.work — THE HUB. The secret is out.** This is what Kevin had been hiding (even from Dulus itself 😂): **a global hub where every Dulus instance on the planet shares what it learns.** Your Dulus finds a fix, polishes a skill, adapts a plugin → it can publish it to the hub → every other Dulus in the world can pull it down. Not an update server an **organism**. Every installation is a node. *The fix a Dulus in Tokyo finds at 3AM, the one in Santo Domingo has by sunrise.* Other agents have N devs improving one product (linear). Dulus makes every user a dev (exponential). The pieces have been quietly shipping for months — SelfImprove engine, Auto-Adapter, the skills format, cloudsave — and now they converge. Domain is live, execution underway. 🦅🇩🇴 → [dulus.work](https://dulus.work)
137
137
 
138
138
  - **Dulus Agent — Telegram communities.** Dulus evolved from a CLI tool into a live AI agent inside Telegram groups. The first fully autonomous AI moderating and conversing in real communities — not a bot, not a filter, a real agent. Groups pay in $DULUS to activate him. We host Dulus for them — that's the business model. The CLI stays free forever. This is the paid layer. 🦅🇩🇴
139
139
 
@@ -52,6 +52,7 @@ Slash commands in REPL:
52
52
  /autojob Toggle auto-job printer (auto-print job results)
53
53
  /auto_show Toggle auto-show for visual tools (ASCII art, etc.)
54
54
  /ultra_search Toggle ULTRA_SEARCH mode
55
+ /sage [req] Sage mode: decompose+quality-check+plan the prompt before executing (alias: /sabio; no arg = arm for next prompt)
55
56
  /permissions [mode] Set permission mode
56
57
  /afk Toggle AFK mode (auto-dismiss questions, auto-approve tools)
57
58
  /yolo Toggle YOLO mode (auto-approve ALL actions without prompts)
@@ -897,6 +898,7 @@ _HELP_PAGES = [
897
898
  ("/autojob", "Auto-print job results (toggle)"),
898
899
  ("/auto_show", "Auto-render visual tools (toggle)"),
899
900
  ("/ultra_search", "Aggressive multi-query search"),
901
+ ("/sage [req]", "Sage mode: study+plan prompt before executing (/sabio)"),
900
902
  ("/permissions [mode]", "Set permission mode"),
901
903
  ("/afk", "AFK mode (auto-approve tools)"),
902
904
  ("/yolo", "YOLO mode (auto-approve ALL)"),
@@ -999,6 +1001,11 @@ def cmd_model(args: str, _state, config) -> bool:
999
1001
  ok(f"Model set to {m} (provider: {pname})")
1000
1002
  from config import save_config
1001
1003
  save_config(config)
1004
+ try:
1005
+ import analytics as _telemetry
1006
+ _telemetry.track_model_selected(m, pname)
1007
+ except Exception:
1008
+ pass
1002
1009
  return True
1003
1010
 
1004
1011
  def _generate_personas(topic: str, curr_model: str, config: dict, count: int = 5) -> dict | None:
@@ -8080,6 +8087,78 @@ def cmd_plan(args: str, state, config) -> bool:
8080
8087
  return ("__plan__", arg)
8081
8088
 
8082
8089
 
8090
+ # ── Sage mode (/sage · /sabio) ──────────────────────────────────────────────
8091
+ # Inspired by intake-classifier patterns: before executing, the AI studies the
8092
+ # request like a senior architect doing ticket intake — breaks it into atomic
8093
+ # issues, runs a quality gate (ambiguity/scope/actionability), asks blocking
8094
+ # clarification questions, THEN plans and executes.
8095
+
8096
+ _SAGE_WRAPPER_PREFIX = """[SAGE MODE — structured intake & planning. Study the request below BEFORE acting.]
8097
+
8098
+ Phase 1 — INTAKE (breakdown):
8099
+ Decompose the request into its atomic sub-requests. For each one state: a one-line summary, category (code / research / infra / content / ops / other), priority (low|med|high|critical), and what "done" looks like (verifiable).
8100
+
8101
+ Phase 2 — QUALITY GATE:
8102
+ Evaluate the request itself: is it ambiguous? imprecise? does it mix multiple unrelated problems? is it actionable as stated? in scope for this environment?
8103
+ - If missing info BLOCKS correct execution → ask the user via AskUserQuestion (batch ALL questions into ONE call with options).
8104
+ - Otherwise, state your working assumptions explicitly in one short list and continue.
8105
+
8106
+ Phase 3 — PLAN:
8107
+ Produce a concrete ordered plan: steps, the tools you will use per step, risks, and how you will verify each step actually worked. For multi-step work, register the plan with TaskCreate so progress is trackable.
8108
+
8109
+ Phase 4 — EXECUTE:
8110
+ Execute the plan step by step, verifying as you go. If reality diverges from the plan, say so honestly and adapt. Never report "done" without having verified.
8111
+
8112
+ Show Phases 1-3 to the user briefly (compact, no theater) before executing.
8113
+
8114
+ USER REQUEST:
8115
+ """
8116
+
8117
+
8118
+ def _sage_wrap(prompt: str) -> str:
8119
+ """Wrap a raw user prompt in the sage intake+planning contract."""
8120
+ return _SAGE_WRAPPER_PREFIX + prompt
8121
+
8122
+
8123
+ def cmd_sage(args: str, state, config):
8124
+ """Sage mode — the AI studies, decomposes and plans your prompt before executing.
8125
+
8126
+ /sage — arm sage mode for your NEXT prompt
8127
+ /sage <request> — run intake + planning + execution on <request> right now
8128
+ /sage off — disarm
8129
+ /sage status — show whether sage mode is armed
8130
+ """
8131
+ arg = args.strip()
8132
+ low = arg.lower()
8133
+
8134
+ if low in ("off", "cancel", "no"):
8135
+ if config.pop("_sage_armed", None):
8136
+ info("Sage mode disarmed.")
8137
+ else:
8138
+ info("Sage mode was not armed.")
8139
+ return True
8140
+
8141
+ if low == "status":
8142
+ if config.get("_sage_armed"):
8143
+ info("Sage mode: ARMED — next prompt will be studied + planned first.")
8144
+ else:
8145
+ info("Sage mode: inactive.")
8146
+ return True
8147
+
8148
+ if not arg:
8149
+ config["_sage_armed"] = True
8150
+ ok("Sage mode armed 🧙 — your NEXT prompt will be decomposed, quality-checked and planned before execution.")
8151
+ info("Cancel with /sage off")
8152
+ return True
8153
+
8154
+ # /sage <request> — run it right now
8155
+ return ("__sage__", arg)
8156
+
8157
+
8158
+ # /sabio is the Spanish alias for /sage
8159
+ cmd_sabio = cmd_sage
8160
+
8161
+
8083
8162
  def cmd_compact(args: str, state, config) -> bool:
8084
8163
  """Manually compact conversation history.
8085
8164
 
@@ -9640,6 +9719,8 @@ COMMANDS = {
9640
9719
  "checkpoint": cmd_checkpoint,
9641
9720
  "rewind": cmd_rewind,
9642
9721
  "plan": cmd_plan,
9722
+ "sage": cmd_sage,
9723
+ "sabio": cmd_sabio,
9643
9724
  "compact": cmd_compact,
9644
9725
  "init": cmd_init,
9645
9726
  "export": cmd_export,
@@ -9677,9 +9758,14 @@ def handle_slash(line: str, state, config) -> Union[bool, tuple]:
9677
9758
  args = parts[1] if len(parts) > 1 else ""
9678
9759
  handler = COMMANDS.get(cmd)
9679
9760
  if handler:
9761
+ try:
9762
+ import analytics as _telemetry
9763
+ _telemetry.track_command_used(cmd)
9764
+ except Exception:
9765
+ pass
9680
9766
  result = handler(args, state, config)
9681
9767
  # cmd_voice/cmd_image/cmd_brainstorm/cmd_plan return sentinels to ask the REPL to run_query
9682
- if isinstance(result, tuple) and result[0] in ("__voice__", "__image__", "__video__", "__brainstorm__", "__worker__", "__ssj_cmd__", "__ssj_query__", "__ssj_debate__", "__ssj_passthrough__", "__ssj_promote_worker__", "__plan__", "__plugin_main_agent__", "__roundtable__", "__roundtable_stop__"):
9768
+ if isinstance(result, tuple) and result[0] in ("__voice__", "__image__", "__video__", "__brainstorm__", "__worker__", "__ssj_cmd__", "__ssj_query__", "__ssj_debate__", "__ssj_passthrough__", "__ssj_promote_worker__", "__plan__", "__sage__", "__plugin_main_agent__", "__roundtable__", "__roundtable_stop__"):
9683
9769
  return result
9684
9770
  return True
9685
9771
 
@@ -9761,6 +9847,8 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
9761
9847
  "checkpoint": ("List / restore checkpoints", ["clear"]),
9762
9848
  "rewind": ("Rewind to checkpoint (alias)", ["clear"]),
9763
9849
  "plan": ("Enter/exit plan mode", ["done", "status"]),
9850
+ "sage": ("Sage mode — study+plan the prompt before executing", ["off", "status"]),
9851
+ "sabio": ("Modo sabio — estudia+planifica el prompt antes de ejecutar (alias)", ["off", "status"]),
9764
9852
  "compact": ("Compact conversation history", []),
9765
9853
  "init": ("Initialize DULUS.md template", []),
9766
9854
  "export": ("Export conversation to file", []),
@@ -10546,6 +10634,12 @@ def repl(config: dict, initial_prompt: str = None):
10546
10634
  _duplicate_suppressed = False
10547
10635
 
10548
10636
  try:
10637
+ if not is_background:
10638
+ try:
10639
+ import analytics as _telemetry
10640
+ _telemetry.track_message_sent(str(config.get("model", "")))
10641
+ except Exception:
10642
+ pass
10549
10643
  for event in run(user_input, state, config, system_prompt):
10550
10644
  # Stop spinner only when visible output arrives
10551
10645
  if spinner_shown:
@@ -10610,6 +10704,11 @@ def repl(config: dict, initial_prompt: str = None):
10610
10704
  if event.name == "AskUserQuestion":
10611
10705
  _stop_tool_spinner()
10612
10706
  print_tool_start(event.name, event.inputs, verbose)
10707
+ try:
10708
+ import analytics as _telemetry
10709
+ _telemetry.track_tool_used(event.name)
10710
+ except Exception:
10711
+ pass
10613
10712
 
10614
10713
  elif isinstance(event, PermissionRequest):
10615
10714
  _stop_tool_spinner()
@@ -11653,6 +11752,17 @@ def repl(config: dict, initial_prompt: str = None):
11653
11752
  print(clr("\n (interrupted)", "yellow"))
11654
11753
  break
11655
11754
 
11755
+ # Sage sentinel: ("__sage__", request) — intake + plan + execute
11756
+ if result[0] == "__sage__":
11757
+ _, sage_req = result
11758
+ print(clr(" 🧙 Sage mode — studying the request before acting…", "dim"))
11759
+ try:
11760
+ run_query(_sage_wrap(sage_req))
11761
+ except KeyboardInterrupt:
11762
+ _track_ctrl_c()
11763
+ print(clr("\n (interrupted)", "yellow"))
11764
+ break
11765
+
11656
11766
  # Plugin main-agent handoff sentinel:
11657
11767
  # ("__plugin_main_agent__", plugin_name, plugin_source)
11658
11768
  # Triggered by `/plugin install name@url --main-agent` — the main agent
@@ -11881,6 +11991,11 @@ def repl(config: dict, initial_prompt: str = None):
11881
11991
  continue
11882
11992
 
11883
11993
  try:
11994
+ # Sage mode armed via /sage (no args): wrap THIS prompt in the
11995
+ # intake+planning contract, then disarm (one-shot).
11996
+ if config.pop("_sage_armed", None):
11997
+ print(clr(" 🧙 Sage mode — studying the request before acting…", "dim"))
11998
+ user_input = _sage_wrap(user_input)
11884
11999
  run_query(user_input)
11885
12000
 
11886
12001
  # Auto-save config after each turn for web providers to persist session IDs
@@ -62,39 +62,71 @@ def search_session_history(query: str, max_results: int = 5) -> list[dict]:
62
62
  except Exception:
63
63
  continue
64
64
 
65
- # 3. Perform search
65
+ # 3. Perform search — NEWEST sessions first, token-based matching
66
+ import re
67
+ q_tokens = [t for t in re.split(r"\W+", query) if len(t) > 2]
68
+ if not q_tokens:
69
+ q_tokens = [query] if query else []
70
+ if not q_tokens:
71
+ return []
72
+ # Require a majority of tokens (at least half, min 1) so multi-word
73
+ # queries still hit even if one word is missing from the message.
74
+ min_hits = max(1, (len(q_tokens) + 1) // 2)
75
+
76
+ # Search most recent sessions first (this is where answers usually live)
77
+ all_sessions.sort(key=lambda s: str(s.get("saved_at", "")), reverse=True)
78
+
66
79
  results = []
67
80
  for sess in all_sessions:
68
81
  session_id = sess.get("session_id", "unknown")
69
82
  saved_at = sess.get("saved_at", "unknown")
70
83
  messages = sess.get("messages", [])
71
-
84
+
72
85
  session_hits = []
86
+ best_score = 0
73
87
  for msg in messages:
74
88
  content = msg.get("content", "")
75
89
  if not isinstance(content, str):
76
90
  continue
77
-
78
- if query in content.lower():
79
- # Extract snippet
80
- start = max(0, content.lower().find(query) - 60)
91
+
92
+ low = content.lower()
93
+ matched = [t for t in q_tokens if t in low]
94
+ # Accept: full literal phrase OR majority of query tokens
95
+ if query in low or len(matched) >= min_hits:
96
+ anchor = query if query in low else matched[0]
97
+ # Extract snippet around the first matched token
98
+ start = max(0, low.find(anchor) - 60)
81
99
  end = min(len(content), start + 200)
82
100
  snippet = content[start:end].replace("\n", " ")
83
101
  if start > 0: snippet = "..." + snippet
84
102
  if end < len(content): snippet += "..."
85
-
103
+
104
+ score = len(q_tokens) + 1 if query in low else len(matched)
105
+ best_score = max(best_score, score)
86
106
  session_hits.append({
87
107
  "role": msg.get("role"),
88
- "snippet": snippet
108
+ "snippet": snippet,
109
+ "_score": score,
89
110
  })
90
-
111
+
91
112
  if session_hits:
113
+ # Best-matching hits first within the session
114
+ session_hits.sort(key=lambda h: h["_score"], reverse=True)
115
+ for h in session_hits:
116
+ h.pop("_score", None)
92
117
  results.append({
93
118
  "session_id": session_id,
94
119
  "saved_at": saved_at,
95
- "hits": session_hits[:3] # limit hits per session to avoid bloat
120
+ "hits": session_hits[:3], # limit hits per session to avoid bloat
121
+ "_score": best_score,
96
122
  })
123
+ # Early exit: sessions are newest-first, once we have enough
124
+ # strong matches there is no need to scan ancient history.
125
+ if len(results) >= max_results * 3:
126
+ break
97
127
 
98
- # Sort sessions by recency (newest hit first)
99
- results.sort(key=lambda x: x["saved_at"], reverse=True)
128
+ # Rank: match quality first, then recency (both already newest-first)
129
+ results.sort(key=lambda x: (x["_score"], str(x["saved_at"])), reverse=True)
130
+ for r in results:
131
+ r.pop("_score", None)
100
132
  return results[:max_results]
@@ -123,17 +123,24 @@ def _memory_search(params: dict, config: dict) -> str:
123
123
  query = params["query"]
124
124
  use_ai = params.get("use_ai", False)
125
125
 
126
- if config.get("ULTRA_SEARCH") in (1, "1", True, "true"):
126
+ ultra = config.get("ULTRA_SEARCH") in (1, "1", True, "true")
127
+ if ultra:
127
128
  params["include_sessions"] = True
128
- max_results = max(params.get("max_results", 5), 100)
129
+ # Sessions get the big budget; memories stay capped so session
130
+ # matches don't get buried/truncated under 100 memory entries.
131
+ max_results = max(params.get("max_results", 5), 10)
132
+ session_max = 20
129
133
  else:
130
134
  max_results = params.get("max_results", 5)
135
+ session_max = max_results
131
136
 
132
137
  results = find_relevant_memories(
133
138
  query, max_results=max_results * 3, use_ai=use_ai, config=config
134
139
  )
135
140
 
136
- if not results:
141
+ if not results and not params.get("include_sessions"):
142
+ # Only early-return when sessions won't be searched; otherwise a
143
+ # zero-memory result would skip session history entirely (old bug).
137
144
  return f"No memories found matching '{query}'."
138
145
 
139
146
  # Re-rank by confidence × recency score
@@ -172,15 +179,22 @@ def _memory_search(params: dict, config: dict) -> str:
172
179
  should_search_sessions = params.get("include_sessions")
173
180
 
174
181
  if should_search_sessions:
175
- sess_results = search_session_history(query, max_results=max_results)
182
+ sess_results = search_session_history(query, max_results=session_max)
176
183
  if sess_results:
177
- lines.append("\n" + "─" * 40)
178
- lines.append(f"Historical Session Matches ({len(sess_results)} sessions):")
184
+ sess_lines = ["─" * 40,
185
+ f"Historical Session Matches ({len(sess_results)} sessions, newest first):"]
179
186
  for sr in sess_results:
180
- lines.append(f"\nSession {sr['session_id']} ({sr['saved_at']})")
187
+ sess_lines.append(f"\nSession {sr['session_id']} ({sr['saved_at']})")
181
188
  for h in sr["hits"]:
182
189
  role_lbl = "User" if h["role"] == "user" else "Dulus"
183
- lines.append(f" [{role_lbl}] {h['snippet']}")
190
+ sess_lines.append(f" [{role_lbl}] {h['snippet']}")
191
+ if ultra:
192
+ # ULTRA mode: sessions are the primary target — put them FIRST
193
+ # so they never get truncated away under memory entries.
194
+ lines = [lines[0], ""] + sess_lines + ["", "─" * 40, "Memory entries:"] + lines[1:]
195
+ else:
196
+ lines.append("")
197
+ lines.extend(sess_lines)
184
198
 
185
199
  # ── Part 3: Offloaded Jobs Search ────────────────────────────────────
186
200
  try:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "3.5.0"
7
+ version = "3.6.2"
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"
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
File without changes
File without changes