dulus 3.10.76__tar.gz → 3.10.78__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 (390) hide show
  1. {dulus-3.10.76/dulus.egg-info → dulus-3.10.78}/PKG-INFO +9 -1
  2. {dulus-3.10.76 → dulus-3.10.78}/README.md +8 -0
  3. {dulus-3.10.76 → dulus-3.10.78}/checkpoint/__init__.py +3 -1
  4. {dulus-3.10.76 → dulus-3.10.78}/checkpoint/store.py +125 -4
  5. {dulus-3.10.76 → dulus-3.10.78}/docs/news.md +4 -0
  6. {dulus-3.10.76 → dulus-3.10.78/dulus.egg-info}/PKG-INFO +9 -1
  7. {dulus-3.10.76 → dulus-3.10.78}/dulus.py +15 -1
  8. {dulus-3.10.76 → dulus-3.10.78}/pyproject.toml +1 -1
  9. {dulus-3.10.76 → dulus-3.10.78}/tests/test_checkpoint.py +75 -0
  10. {dulus-3.10.76 → dulus-3.10.78}/LICENSE +0 -0
  11. {dulus-3.10.76 → dulus-3.10.78}/MANIFEST.in +0 -0
  12. {dulus-3.10.76 → dulus-3.10.78}/agent.py +0 -0
  13. {dulus-3.10.76 → dulus-3.10.78}/algolia_search.py +0 -0
  14. {dulus-3.10.76 → dulus-3.10.78}/analytics.py +0 -0
  15. {dulus-3.10.76 → dulus-3.10.78}/backend/__init__.py +0 -0
  16. {dulus-3.10.76 → dulus-3.10.78}/backend/agents_bridge.py +0 -0
  17. {dulus-3.10.76 → dulus-3.10.78}/backend/compressor.py +0 -0
  18. {dulus-3.10.76 → dulus-3.10.78}/backend/context.py +0 -0
  19. {dulus-3.10.76 → dulus-3.10.78}/backend/githook.py +0 -0
  20. {dulus-3.10.76 → dulus-3.10.78}/backend/marketplace.py +0 -0
  21. {dulus-3.10.76 → dulus-3.10.78}/backend/mempalace_bridge.py +0 -0
  22. {dulus-3.10.76 → dulus-3.10.78}/backend/paths.py +0 -0
  23. {dulus-3.10.76 → dulus-3.10.78}/backend/personas.py +0 -0
  24. {dulus-3.10.76 → dulus-3.10.78}/backend/plugins.py +0 -0
  25. {dulus-3.10.76 → dulus-3.10.78}/backend/server.py +0 -0
  26. {dulus-3.10.76 → dulus-3.10.78}/backend/tasks.py +0 -0
  27. {dulus-3.10.76 → dulus-3.10.78}/banners/__init__.py +0 -0
  28. {dulus-3.10.76 → dulus-3.10.78}/banners/banner_rotator.py +0 -0
  29. {dulus-3.10.76 → dulus-3.10.78}/banners/banners_dulus_ai.txt +0 -0
  30. {dulus-3.10.76 → dulus-3.10.78}/batch_api.py +0 -0
  31. {dulus-3.10.76 → dulus-3.10.78}/checkpoint/hooks.py +0 -0
  32. {dulus-3.10.76 → dulus-3.10.78}/checkpoint/types.py +0 -0
  33. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/__init__.py +0 -0
  34. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/__main__.py +0 -0
  35. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/ansi.py +0 -0
  36. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/assets/ascii/dulus_logo.txt +0 -0
  37. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/assets/ascii/eagle.txt +0 -0
  38. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/assets/ascii/terminal_art.txt +0 -0
  39. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/banners.py +0 -0
  40. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/demo.py +0 -0
  41. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/effects.py +0 -0
  42. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/effort_slider.py +0 -0
  43. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/progress.py +0 -0
  44. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/py.typed +0 -0
  45. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/spinners.py +0 -0
  46. {dulus-3.10.76 → dulus-3.10.78}/cli_animations/status.py +0 -0
  47. {dulus-3.10.76 → dulus-3.10.78}/clipboard_utils.py +0 -0
  48. {dulus-3.10.76 → dulus-3.10.78}/cloudsave.py +0 -0
  49. {dulus-3.10.76 → dulus-3.10.78}/common.py +0 -0
  50. {dulus-3.10.76 → dulus-3.10.78}/compaction.py +0 -0
  51. {dulus-3.10.76 → dulus-3.10.78}/config.py +0 -0
  52. {dulus-3.10.76 → dulus-3.10.78}/context.py +0 -0
  53. {dulus-3.10.76 → dulus-3.10.78}/data/__init__.py +0 -0
  54. {dulus-3.10.76 → dulus-3.10.78}/data/active_persona.json +0 -0
  55. {dulus-3.10.76 → dulus-3.10.78}/data/marketplace.json +0 -0
  56. {dulus-3.10.76 → dulus-3.10.78}/data/personas.json +0 -0
  57. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/__init__.py +0 -0
  58. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/__init__.py +0 -0
  59. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  60. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  61. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  62. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/plugin.json +0 -0
  63. {dulus-3.10.76 → dulus-3.10.78}/data/plugins/composio/plugin_tool.py +0 -0
  64. {dulus-3.10.76 → dulus-3.10.78}/data/tasks.json +0 -0
  65. {dulus-3.10.76 → dulus-3.10.78}/display_blocks.py +0 -0
  66. {dulus-3.10.76 → dulus-3.10.78}/docs/API.md +0 -0
  67. {dulus-3.10.76 → dulus-3.10.78}/docs/BRAND_GUIDE.md +0 -0
  68. {dulus-3.10.76 → dulus-3.10.78}/docs/CHANGELOG.md +0 -0
  69. {dulus-3.10.76 → dulus-3.10.78}/docs/CONTRIBUTING.md +0 -0
  70. {dulus-3.10.76 → dulus-3.10.78}/docs/DEPLOYMENT.md +0 -0
  71. {dulus-3.10.76 → dulus-3.10.78}/docs/Dulus_AI_Whitepaper_(v2.0).pdf +0 -0
  72. {dulus-3.10.76 → dulus-3.10.78}/docs/FAQ.md +0 -0
  73. {dulus-3.10.76 → dulus-3.10.78}/docs/GETTING_STARTED.md +0 -0
  74. {dulus-3.10.76 → dulus-3.10.78}/docs/README.md +0 -0
  75. {dulus-3.10.76 → dulus-3.10.78}/docs/README_AR.md +0 -0
  76. {dulus-3.10.76 → dulus-3.10.78}/docs/README_EN.md +0 -0
  77. {dulus-3.10.76 → dulus-3.10.78}/docs/README_ES.md +0 -0
  78. {dulus-3.10.76 → dulus-3.10.78}/docs/README_FR.md +0 -0
  79. {dulus-3.10.76 → dulus-3.10.78}/docs/README_JA.md +0 -0
  80. {dulus-3.10.76 → dulus-3.10.78}/docs/README_KO.md +0 -0
  81. {dulus-3.10.76 → dulus-3.10.78}/docs/README_PT.md +0 -0
  82. {dulus-3.10.76 → dulus-3.10.78}/docs/README_RU.md +0 -0
  83. {dulus-3.10.76 → dulus-3.10.78}/docs/README_ZH.md +0 -0
  84. {dulus-3.10.76 → dulus-3.10.78}/docs/SECURITY.md +0 -0
  85. {dulus-3.10.76 → dulus-3.10.78}/docs/__init__.py +0 -0
  86. {dulus-3.10.76 → dulus-3.10.78}/docs/api.html +0 -0
  87. {dulus-3.10.76 → dulus-3.10.78}/docs/architecture.md +0 -0
  88. {dulus-3.10.76 → dulus-3.10.78}/docs/azure-speech-template.json +0 -0
  89. {dulus-3.10.76 → dulus-3.10.78}/docs/dashboard/index.html +0 -0
  90. {dulus-3.10.76 → dulus-3.10.78}/docs/divider.svg +0 -0
  91. {dulus-3.10.76 → dulus-3.10.78}/docs/dulus-bird.png +0 -0
  92. {dulus-3.10.76 → dulus-3.10.78}/docs/generate.py +0 -0
  93. {dulus-3.10.76 → dulus-3.10.78}/docs/index.html +0 -0
  94. {dulus-3.10.76 → dulus-3.10.78}/docs/nvidia-models.svg +0 -0
  95. {dulus-3.10.76 → dulus-3.10.78}/docs/particle-playground.html +0 -0
  96. {dulus-3.10.76 → dulus-3.10.78}/docs/personas/index.html +0 -0
  97. {dulus-3.10.76 → dulus-3.10.78}/docs/poetry-banner.png +0 -0
  98. {dulus-3.10.76 → dulus-3.10.78}/docs/preview.html +0 -0
  99. {dulus-3.10.76 → dulus-3.10.78}/docs/python-console.md +0 -0
  100. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/auto-adapter.png +0 -0
  101. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/dulus-os.png +0 -0
  102. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/hero.png +0 -0
  103. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/lookback_two_windows.png +0 -0
  104. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/pyc_heap.svg +0 -0
  105. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/pyc_receipts.svg +0 -0
  106. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/pyc_trap.svg +0 -0
  107. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/roundtable.png +0 -0
  108. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/task-board.png +0 -0
  109. {dulus-3.10.76 → dulus-3.10.78}/docs/readme/webchat.png +0 -0
  110. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-agents.svg +0 -0
  111. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-brainstorm.svg +0 -0
  112. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-bridges.svg +0 -0
  113. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-features.svg +0 -0
  114. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-freetier.svg +0 -0
  115. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-memory.svg +0 -0
  116. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-models.svg +0 -0
  117. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-perms.svg +0 -0
  118. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-plugins.svg +0 -0
  119. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-quickstart.svg +0 -0
  120. {dulus-3.10.76 → dulus-3.10.78}/docs/sec-ssj.svg +0 -0
  121. {dulus-3.10.76 → dulus-3.10.78}/docs/spinners.svg +0 -0
  122. {dulus-3.10.76 → dulus-3.10.78}/docs/split-pane.svg +0 -0
  123. {dulus-3.10.76 → dulus-3.10.78}/docs/terminal-boot.svg +0 -0
  124. {dulus-3.10.76 → dulus-3.10.78}/docs/termux-edge.md +0 -0
  125. {dulus-3.10.76 → dulus-3.10.78}/dulus.egg-info/SOURCES.txt +0 -0
  126. {dulus-3.10.76 → dulus-3.10.78}/dulus.egg-info/dependency_links.txt +0 -0
  127. {dulus-3.10.76 → dulus-3.10.78}/dulus.egg-info/entry_points.txt +0 -0
  128. {dulus-3.10.76 → dulus-3.10.78}/dulus.egg-info/requires.txt +0 -0
  129. {dulus-3.10.76 → dulus-3.10.78}/dulus.egg-info/top_level.txt +0 -0
  130. {dulus-3.10.76 → dulus-3.10.78}/dulus_bar_client.py +0 -0
  131. {dulus-3.10.76 → dulus-3.10.78}/dulus_gui.py +0 -0
  132. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/__init__.py +0 -0
  133. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/client.py +0 -0
  134. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/commands.py +0 -0
  135. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/config.py +0 -0
  136. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/hub.py +0 -0
  137. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/tools.py +0 -0
  138. {dulus-3.10.76 → dulus-3.10.78}/dulus_mcp/types.py +0 -0
  139. {dulus-3.10.76 → dulus-3.10.78}/dulus_quick_menu.py +0 -0
  140. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/__init__.py +0 -0
  141. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/add_dir_manager.py +0 -0
  142. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/afk_mode.py +0 -0
  143. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/approval_runtime.py +0 -0
  144. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/background_tasks.py +0 -0
  145. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/clipboard_utils.py +0 -0
  146. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/diff_visualizer.py +0 -0
  147. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/export_import.py +0 -0
  148. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/hook_engine.py +0 -0
  149. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/notification_manager.py +0 -0
  150. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/session_fork.py +0 -0
  151. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/shell_mode.py +0 -0
  152. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/todo_visualizer.py +0 -0
  153. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/tree_ls.py +0 -0
  154. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/wire_events.py +0 -0
  155. {dulus-3.10.76 → dulus-3.10.78}/dulus_tools/yolo_mode.py +0 -0
  156. {dulus-3.10.76 → dulus-3.10.78}/file_filter.py +0 -0
  157. {dulus-3.10.76 → dulus-3.10.78}/fs_compass.py +0 -0
  158. {dulus-3.10.76 → dulus-3.10.78}/governance.py +0 -0
  159. {dulus-3.10.76 → dulus-3.10.78}/gui/__init__.py +0 -0
  160. {dulus-3.10.76 → dulus-3.10.78}/gui/agent_bridge.py +0 -0
  161. {dulus-3.10.76 → dulus-3.10.78}/gui/chat_widget.py +0 -0
  162. {dulus-3.10.76 → dulus-3.10.78}/gui/main_window.py +0 -0
  163. {dulus-3.10.76 → dulus-3.10.78}/gui/personas.py +0 -0
  164. {dulus-3.10.76 → dulus-3.10.78}/gui/sandbox_server.py +0 -0
  165. {dulus-3.10.76 → dulus-3.10.78}/gui/session_utils.py +0 -0
  166. {dulus-3.10.76 → dulus-3.10.78}/gui/settings_dialog.py +0 -0
  167. {dulus-3.10.76 → dulus-3.10.78}/gui/sidebar.py +0 -0
  168. {dulus-3.10.76 → dulus-3.10.78}/gui/tasks_view.py +0 -0
  169. {dulus-3.10.76 → dulus-3.10.78}/gui/themes.py +0 -0
  170. {dulus-3.10.76 → dulus-3.10.78}/gui/tool_panel.py +0 -0
  171. {dulus-3.10.76 → dulus-3.10.78}/input.py +0 -0
  172. {dulus-3.10.76 → dulus-3.10.78}/license_manager.py +0 -0
  173. {dulus-3.10.76 → dulus-3.10.78}/lookback.py +0 -0
  174. {dulus-3.10.76 → dulus-3.10.78}/memory/__init__.py +0 -0
  175. {dulus-3.10.76 → dulus-3.10.78}/memory/audit.py +0 -0
  176. {dulus-3.10.76 → dulus-3.10.78}/memory/consolidator.py +0 -0
  177. {dulus-3.10.76 → dulus-3.10.78}/memory/context.py +0 -0
  178. {dulus-3.10.76 → dulus-3.10.78}/memory/mempalace_bridge.py +0 -0
  179. {dulus-3.10.76 → dulus-3.10.78}/memory/offload.py +0 -0
  180. {dulus-3.10.76 → dulus-3.10.78}/memory/palace.py +0 -0
  181. {dulus-3.10.76 → dulus-3.10.78}/memory/scan.py +0 -0
  182. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/adaptation_guides.md +0 -0
  183. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/askuserquestion_after_numbered_list.md +0 -0
  184. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/background_jobs_no_extra_timers.md +0 -0
  185. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/cleverness_over_resources.md +0 -0
  186. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/corporate_tls_breaks_curl_cffi.md +0 -0
  187. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/hardened_rules.md +0 -0
  188. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/investigator_workflow_heuristic.md +0 -0
  189. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/native_code_over_sketchy_plugins.md +0 -0
  190. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/no_personal_info_in_code.md +0 -0
  191. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/searches_include_modified_date.md +0 -0
  192. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/tmux_send_keys_power_patterns.md +0 -0
  193. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/token_optimization_technique.md +0 -0
  194. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/tool_chaining_capability.md +0 -0
  195. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/insights/webbridge_evaluate_first_pattern.md +0 -0
  196. {dulus-3.10.76 → dulus-3.10.78}/memory/seeds/short_memory.md +0 -0
  197. {dulus-3.10.76 → dulus-3.10.78}/memory/sessions.py +0 -0
  198. {dulus-3.10.76 → dulus-3.10.78}/memory/store.py +0 -0
  199. {dulus-3.10.76 → dulus-3.10.78}/memory/tools.py +0 -0
  200. {dulus-3.10.76 → dulus-3.10.78}/memory/types.py +0 -0
  201. {dulus-3.10.76 → dulus-3.10.78}/memory/vector_search.py +0 -0
  202. {dulus-3.10.76 → dulus-3.10.78}/model_params.py +0 -0
  203. {dulus-3.10.76 → dulus-3.10.78}/multi_agent/__init__.py +0 -0
  204. {dulus-3.10.76 → dulus-3.10.78}/multi_agent/subagent.py +0 -0
  205. {dulus-3.10.76 → dulus-3.10.78}/multi_agent/tools.py +0 -0
  206. {dulus-3.10.76 → dulus-3.10.78}/paste_placeholders.py +0 -0
  207. {dulus-3.10.76 → dulus-3.10.78}/personality_config.py +0 -0
  208. {dulus-3.10.76 → dulus-3.10.78}/plugin/__init__.py +0 -0
  209. {dulus-3.10.76 → dulus-3.10.78}/plugin/autoadapter.py +0 -0
  210. {dulus-3.10.76 → dulus-3.10.78}/plugin/loader.py +0 -0
  211. {dulus-3.10.76 → dulus-3.10.78}/plugin/recommend.py +0 -0
  212. {dulus-3.10.76 → dulus-3.10.78}/plugin/store.py +0 -0
  213. {dulus-3.10.76 → dulus-3.10.78}/plugin/types.py +0 -0
  214. {dulus-3.10.76 → dulus-3.10.78}/profiles.py +0 -0
  215. {dulus-3.10.76 → dulus-3.10.78}/providers.py +0 -0
  216. {dulus-3.10.76 → dulus-3.10.78}/pyconsole.py +0 -0
  217. {dulus-3.10.76 → dulus-3.10.78}/sandbox/README.md +0 -0
  218. {dulus-3.10.76 → dulus-3.10.78}/sandbox/__init__.py +0 -0
  219. {dulus-3.10.76 → dulus-3.10.78}/sandbox/components.json +0 -0
  220. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  221. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  222. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/index.html +0 -0
  223. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/wallpaper-default.jpg +0 -0
  224. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/wallpapers/default.jpeg +0 -0
  225. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/wallpapers/light.jpeg +0 -0
  226. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  227. {dulus-3.10.76 → dulus-3.10.78}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  228. {dulus-3.10.76 → dulus-3.10.78}/sandbox/eslint.config.js +0 -0
  229. {dulus-3.10.76 → dulus-3.10.78}/sandbox/index.html +0 -0
  230. {dulus-3.10.76 → dulus-3.10.78}/sandbox/info.md +0 -0
  231. {dulus-3.10.76 → dulus-3.10.78}/sandbox/package-lock.json +0 -0
  232. {dulus-3.10.76 → dulus-3.10.78}/sandbox/package.json +0 -0
  233. {dulus-3.10.76 → dulus-3.10.78}/sandbox/postcss.config.js +0 -0
  234. {dulus-3.10.76 → dulus-3.10.78}/sandbox/tailwind.config.js +0 -0
  235. {dulus-3.10.76 → dulus-3.10.78}/sandbox/tsconfig.app.json +0 -0
  236. {dulus-3.10.76 → dulus-3.10.78}/sandbox/tsconfig.json +0 -0
  237. {dulus-3.10.76 → dulus-3.10.78}/sandbox/tsconfig.node.json +0 -0
  238. {dulus-3.10.76 → dulus-3.10.78}/sandbox/vite.config.ts +0 -0
  239. {dulus-3.10.76 → dulus-3.10.78}/sandbox_bootstrap.py +0 -0
  240. {dulus-3.10.76 → dulus-3.10.78}/setup.cfg +0 -0
  241. {dulus-3.10.76 → dulus-3.10.78}/skill/__init__.py +0 -0
  242. {dulus-3.10.76 → dulus-3.10.78}/skill/builtin.py +0 -0
  243. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/LICENSE-obsidian-skills +0 -0
  244. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/README.md +0 -0
  245. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/defuddle/SKILL.md +0 -0
  246. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/json-canvas/SKILL.md +0 -0
  247. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
  248. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-bases/SKILL.md +0 -0
  249. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
  250. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-cli/SKILL.md +0 -0
  251. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
  252. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
  253. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
  254. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
  255. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/README.md +0 -0
  256. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/SKILL.md +0 -0
  257. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
  258. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
  259. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
  260. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
  261. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
  262. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
  263. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
  264. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
  265. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
  266. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
  267. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
  268. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
  269. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
  270. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
  271. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
  272. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
  273. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
  274. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
  275. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
  276. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
  277. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
  278. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
  279. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
  280. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/legal/drop.md +0 -0
  281. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
  282. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/methods.md +0 -0
  283. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
  284. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/references/state-machine.md +0 -0
  285. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
  286. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/badbool.py +0 -0
  287. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/brokers.py +0 -0
  288. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/cdp.py +0 -0
  289. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/config.py +0 -0
  290. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/crypto.py +0 -0
  291. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/dossier.py +0 -0
  292. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
  293. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/emailer.py +0 -0
  294. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/ledger.py +0 -0
  295. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/legal.py +0 -0
  296. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/paths.py +0 -0
  297. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/pdd.py +0 -0
  298. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/registry.py +0 -0
  299. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/report.py +0 -0
  300. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/scan.py +0 -0
  301. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/storage.py +0 -0
  302. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/tiers.py +0 -0
  303. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/scripts/vectors.py +0 -0
  304. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
  305. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
  306. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
  307. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
  308. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
  309. {dulus-3.10.76 → dulus-3.10.78}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
  310. {dulus-3.10.76 → dulus-3.10.78}/skill/clawhub.py +0 -0
  311. {dulus-3.10.76 → dulus-3.10.78}/skill/executor.py +0 -0
  312. {dulus-3.10.76 → dulus-3.10.78}/skill/loader.py +0 -0
  313. {dulus-3.10.76 → dulus-3.10.78}/skill/tools.py +0 -0
  314. {dulus-3.10.76 → dulus-3.10.78}/skills.py +0 -0
  315. {dulus-3.10.76 → dulus-3.10.78}/soul.py +0 -0
  316. {dulus-3.10.76 → dulus-3.10.78}/spinner.py +0 -0
  317. {dulus-3.10.76 → dulus-3.10.78}/steer_input.py +0 -0
  318. {dulus-3.10.76 → dulus-3.10.78}/subagent.py +0 -0
  319. {dulus-3.10.76 → dulus-3.10.78}/task/__init__.py +0 -0
  320. {dulus-3.10.76 → dulus-3.10.78}/task/store.py +0 -0
  321. {dulus-3.10.76 → dulus-3.10.78}/task/tools.py +0 -0
  322. {dulus-3.10.76 → dulus-3.10.78}/task/types.py +0 -0
  323. {dulus-3.10.76 → dulus-3.10.78}/tests/test_afk_yolo.py +0 -0
  324. {dulus-3.10.76 → dulus-3.10.78}/tests/test_approval_runtime.py +0 -0
  325. {dulus-3.10.76 → dulus-3.10.78}/tests/test_background_task_tools.py +0 -0
  326. {dulus-3.10.76 → dulus-3.10.78}/tests/test_background_tasks.py +0 -0
  327. {dulus-3.10.76 → dulus-3.10.78}/tests/test_chatgpt_oauth.py +0 -0
  328. {dulus-3.10.76 → dulus-3.10.78}/tests/test_cli_animations_integration.py +0 -0
  329. {dulus-3.10.76 → dulus-3.10.78}/tests/test_clipboard_utils.py +0 -0
  330. {dulus-3.10.76 → dulus-3.10.78}/tests/test_compaction.py +0 -0
  331. {dulus-3.10.76 → dulus-3.10.78}/tests/test_composio_tool_generator.py +0 -0
  332. {dulus-3.10.76 → dulus-3.10.78}/tests/test_diff_view.py +0 -0
  333. {dulus-3.10.76 → dulus-3.10.78}/tests/test_diff_visualization.py +0 -0
  334. {dulus-3.10.76 → dulus-3.10.78}/tests/test_display_blocks.py +0 -0
  335. {dulus-3.10.76 → dulus-3.10.78}/tests/test_export_import.py +0 -0
  336. {dulus-3.10.76 → dulus-3.10.78}/tests/test_grok_usage.py +0 -0
  337. {dulus-3.10.76 → dulus-3.10.78}/tests/test_hook_engine.py +0 -0
  338. {dulus-3.10.76 → dulus-3.10.78}/tests/test_injection_fix.py +0 -0
  339. {dulus-3.10.76 → dulus-3.10.78}/tests/test_license.py +0 -0
  340. {dulus-3.10.76 → dulus-3.10.78}/tests/test_lookback_cache.py +0 -0
  341. {dulus-3.10.76 → dulus-3.10.78}/tests/test_mcp.py +0 -0
  342. {dulus-3.10.76 → dulus-3.10.78}/tests/test_mcp_algolia.py +0 -0
  343. {dulus-3.10.76 → dulus-3.10.78}/tests/test_memory.py +0 -0
  344. {dulus-3.10.76 → dulus-3.10.78}/tests/test_memory_home_resolution.py +0 -0
  345. {dulus-3.10.76 → dulus-3.10.78}/tests/test_notification_manager.py +0 -0
  346. {dulus-3.10.76 → dulus-3.10.78}/tests/test_plugin.py +0 -0
  347. {dulus-3.10.76 → dulus-3.10.78}/tests/test_provider_errors.py +0 -0
  348. {dulus-3.10.76 → dulus-3.10.78}/tests/test_session_fork.py +0 -0
  349. {dulus-3.10.76 → dulus-3.10.78}/tests/test_shell_mode.py +0 -0
  350. {dulus-3.10.76 → dulus-3.10.78}/tests/test_skills.py +0 -0
  351. {dulus-3.10.76 → dulus-3.10.78}/tests/test_skillssh_algolia.py +0 -0
  352. {dulus-3.10.76 → dulus-3.10.78}/tests/test_steer_input.py +0 -0
  353. {dulus-3.10.76 → dulus-3.10.78}/tests/test_subagent.py +0 -0
  354. {dulus-3.10.76 → dulus-3.10.78}/tests/test_task.py +0 -0
  355. {dulus-3.10.76 → dulus-3.10.78}/tests/test_telegram_buffer.py +0 -0
  356. {dulus-3.10.76 → dulus-3.10.78}/tests/test_think_tool.py +0 -0
  357. {dulus-3.10.76 → dulus-3.10.78}/tests/test_todo_tool.py +0 -0
  358. {dulus-3.10.76 → dulus-3.10.78}/tests/test_todo_visualization.py +0 -0
  359. {dulus-3.10.76 → dulus-3.10.78}/tests/test_tool_registry.py +0 -0
  360. {dulus-3.10.76 → dulus-3.10.78}/tests/test_tree_ls.py +0 -0
  361. {dulus-3.10.76 → dulus-3.10.78}/tests/test_voice.py +0 -0
  362. {dulus-3.10.76 → dulus-3.10.78}/tests/test_webbridge_tabs.py +0 -0
  363. {dulus-3.10.76 → dulus-3.10.78}/tests/test_wire_events.py +0 -0
  364. {dulus-3.10.76 → dulus-3.10.78}/tmux_tools.py +0 -0
  365. {dulus-3.10.76 → dulus-3.10.78}/tool_registry.py +0 -0
  366. {dulus-3.10.76 → dulus-3.10.78}/tools.py +0 -0
  367. {dulus-3.10.76 → dulus-3.10.78}/tools_background.py +0 -0
  368. {dulus-3.10.76 → dulus-3.10.78}/tools_think.py +0 -0
  369. {dulus-3.10.76 → dulus-3.10.78}/tools_todo.py +0 -0
  370. {dulus-3.10.76 → dulus-3.10.78}/ui/__init__.py +0 -0
  371. {dulus-3.10.76 → dulus-3.10.78}/ui/input.py +0 -0
  372. {dulus-3.10.76 → dulus-3.10.78}/ui/render.py +0 -0
  373. {dulus-3.10.76 → dulus-3.10.78}/updater.py +0 -0
  374. {dulus-3.10.76 → dulus-3.10.78}/voice/__init__.py +0 -0
  375. {dulus-3.10.76 → dulus-3.10.78}/voice/audio_utils.py +0 -0
  376. {dulus-3.10.76 → dulus-3.10.78}/voice/keyterms.py +0 -0
  377. {dulus-3.10.76 → dulus-3.10.78}/voice/recorder.py +0 -0
  378. {dulus-3.10.76 → dulus-3.10.78}/voice/stt.py +0 -0
  379. {dulus-3.10.76 → dulus-3.10.78}/voice/tts.py +0 -0
  380. {dulus-3.10.76 → dulus-3.10.78}/voice/wake_word.py +0 -0
  381. {dulus-3.10.76 → dulus-3.10.78}/webbridge/__init__.py +0 -0
  382. {dulus-3.10.76 → dulus-3.10.78}/webbridge/core.py +0 -0
  383. {dulus-3.10.76 → dulus-3.10.78}/webbridge/tools.py +0 -0
  384. {dulus-3.10.76 → dulus-3.10.78}/webchat.py +0 -0
  385. {dulus-3.10.76 → dulus-3.10.78}/webchat_server.py +0 -0
  386. {dulus-3.10.76 → dulus-3.10.78}/webchat_ui/__init__.py +0 -0
  387. {dulus-3.10.76 → dulus-3.10.78}/webchat_ui/app.js +0 -0
  388. {dulus-3.10.76 → dulus-3.10.78}/webchat_ui/index.html +0 -0
  389. {dulus-3.10.76 → dulus-3.10.78}/webchat_ui/style.css +0 -0
  390. {dulus-3.10.76 → dulus-3.10.78}/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dulus
3
- Version: 3.10.76
3
+ Version: 3.10.78
4
4
  Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
5
5
  Author: KevRojo
6
6
  License: GPL-3.0-only
@@ -254,6 +254,14 @@ iwr -useb https://raw.githubusercontent.com/KevRojo/Dulus/main/install.ps1 | iex
254
254
 
255
255
  Windows users who do not want Python or a terminal can download the self-contained MSI from [GitHub Releases](https://github.com/KevRojo/Dulus/releases).
256
256
 
257
+ ### Dulus for Windows — desktop app (alpha/beta trial)
258
+
259
+ The full **Dulus desktop app** for Windows is now in open trial. No Python, no terminal, no API key — download, install, sign in, and go.
260
+
261
+ **[⬇️ Dulus Windows Trial setup (alpha/beta)](https://drive.google.com/file/d/1Nxz_coVpXRwKQB7baI_0nG12-p1cLznZ/view)**
262
+
263
+ It is early — expect rough edges, and tell us about them in [Issues](https://github.com/KevRojo/Dulus/issues). The CLI in this repository stays free and open source; the desktop app is the same engine with a face.
264
+
257
265
  ### Docker
258
266
 
259
267
  ```bash
@@ -176,6 +176,14 @@ iwr -useb https://raw.githubusercontent.com/KevRojo/Dulus/main/install.ps1 | iex
176
176
 
177
177
  Windows users who do not want Python or a terminal can download the self-contained MSI from [GitHub Releases](https://github.com/KevRojo/Dulus/releases).
178
178
 
179
+ ### Dulus for Windows — desktop app (alpha/beta trial)
180
+
181
+ The full **Dulus desktop app** for Windows is now in open trial. No Python, no terminal, no API key — download, install, sign in, and go.
182
+
183
+ **[⬇️ Dulus Windows Trial setup (alpha/beta)](https://drive.google.com/file/d/1Nxz_coVpXRwKQB7baI_0nG12-p1cLznZ/view)**
184
+
185
+ It is early — expect rough edges, and tell us about them in [Issues](https://github.com/KevRojo/Dulus/issues). The CLI in this repository stays free and open source; the desktop app is the same engine with a face.
186
+
179
187
  ### Docker
180
188
 
181
189
  ```bash
@@ -9,6 +9,7 @@ from .store import (
9
9
  files_changed_since,
10
10
  delete_session_checkpoints,
11
11
  cleanup_old_sessions,
12
+ enforce_store_budget,
12
13
  reset_file_versions,
13
14
  )
14
15
  from .hooks import (
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "FileBackup", "Snapshot", "MAX_SNAPSHOTS",
23
24
  "track_file_edit", "make_snapshot", "list_snapshots", "get_snapshot",
24
25
  "rewind_files", "files_changed_since",
25
- "delete_session_checkpoints", "cleanup_old_sessions", "reset_file_versions",
26
+ "delete_session_checkpoints", "cleanup_old_sessions", "enforce_store_budget",
27
+ "reset_file_versions",
26
28
  "set_session", "get_tracked_edits", "reset_tracked", "install_hooks",
27
29
  ]
@@ -22,6 +22,18 @@ from .types import FileBackup, Snapshot, MAX_SNAPSHOTS
22
22
  # Max file size to back up (1 MB)
23
23
  _MAX_FILE_SIZE = 1 * 1024 * 1024
24
24
 
25
+ # Hard ceiling for the whole checkpoint store. Age-based cleanup alone cannot
26
+ # bound this: MAX_SNAPSHOTS caps snapshots *per session*, but nothing capped
27
+ # the number of sessions, so a busy machine could accumulate gigabytes long
28
+ # before the 30-day cutoff ever applied. Sentry caught exactly that in the
29
+ # wild -- ENOSPC from mkdir under ~/.dulus/checkpoints on a user's server.
30
+ _MAX_STORE_BYTES = 512 * 1024 * 1024
31
+
32
+ # Free space below which we stop writing new backups entirely. Checkpoints are
33
+ # a convenience feature; they must never be the reason a user's disk fills up
34
+ # or their session dies.
35
+ _MIN_FREE_BYTES = 256 * 1024 * 1024
36
+
25
37
  # Per-file version counters (reset per session)
26
38
  _file_versions: dict[str, int] = {}
27
39
 
@@ -34,15 +46,29 @@ def _session_dir(session_id: str) -> Path:
34
46
  return _checkpoints_root() / session_id
35
47
 
36
48
 
49
+ def _safe_mkdir(path: Path) -> bool:
50
+ """mkdir -p that never raises.
51
+
52
+ A full disk raised OSError(ENOSPC) straight out of pathlib.mkdir and took
53
+ the whole session down with it. Checkpointing is optional; failing to
54
+ create its directory must degrade silently, not crash the caller.
55
+ """
56
+ try:
57
+ path.mkdir(parents=True, exist_ok=True)
58
+ return True
59
+ except OSError:
60
+ return False
61
+
62
+
37
63
  def _backups_dir(session_id: str) -> Path:
38
64
  d = _session_dir(session_id) / "backups"
39
- d.mkdir(parents=True, exist_ok=True)
65
+ _safe_mkdir(d)
40
66
  return d
41
67
 
42
68
 
43
69
  def _snapshots_file(session_id: str) -> Path:
44
70
  d = _session_dir(session_id)
45
- d.mkdir(parents=True, exist_ok=True)
71
+ _safe_mkdir(d)
46
72
  return d / "snapshots.json"
47
73
 
48
74
 
@@ -51,6 +77,90 @@ def _path_hash(file_path: str) -> str:
51
77
  return hashlib.sha256(file_path.encode()).hexdigest()[:16]
52
78
 
53
79
 
80
+ def _dir_size(path: Path) -> int:
81
+ """Total bytes under path. Best-effort: unreadable entries are skipped."""
82
+ total = 0
83
+ try:
84
+ for entry in path.rglob("*"):
85
+ try:
86
+ if entry.is_file():
87
+ total += entry.stat().st_size
88
+ except OSError:
89
+ continue
90
+ except OSError:
91
+ pass
92
+ return total
93
+
94
+
95
+ def _free_bytes(path: Path) -> int | None:
96
+ """Free space on the filesystem holding path, or None if undeterminable."""
97
+ try:
98
+ return shutil.disk_usage(str(path)).free
99
+ except OSError:
100
+ return None
101
+
102
+
103
+ def _has_room_for_backups() -> bool:
104
+ """Whether it is safe to write another backup.
105
+
106
+ Returns False when the store has grown past its ceiling or the disk is
107
+ nearly full. Callers must degrade gracefully: losing a checkpoint is
108
+ acceptable, killing the user's session with ENOSPC is not.
109
+ """
110
+ root = _checkpoints_root()
111
+ free = _free_bytes(root if root.exists() else root.parent)
112
+ if free is not None and free < _MIN_FREE_BYTES:
113
+ return False
114
+ return _dir_size(root) < _MAX_STORE_BYTES
115
+
116
+
117
+ def enforce_store_budget() -> int:
118
+ """Evict oldest sessions until the store fits in _MAX_STORE_BYTES.
119
+
120
+ Age-based cleanup runs on a 30-day cutoff, which is far too slow for a
121
+ machine that opens many sessions a day. This is the size-based backstop.
122
+ Returns the number of sessions removed.
123
+ """
124
+ root = _checkpoints_root()
125
+ if not root.exists():
126
+ return 0
127
+
128
+ size = _dir_size(root)
129
+ free = _free_bytes(root)
130
+ over_budget = size > _MAX_STORE_BYTES
131
+ low_disk = free is not None and free < _MIN_FREE_BYTES
132
+ if not (over_budget or low_disk):
133
+ return 0
134
+
135
+ try:
136
+ sessions = [d for d in root.iterdir() if d.is_dir()]
137
+ except OSError:
138
+ return 0
139
+
140
+ # Oldest first, so recent (likely resumable) sessions survive.
141
+ def _mtime(d: Path) -> float:
142
+ try:
143
+ return d.stat().st_mtime
144
+ except OSError:
145
+ return 0.0
146
+
147
+ sessions.sort(key=_mtime)
148
+
149
+ # When the disk itself is tight, free space aggressively rather than
150
+ # stopping at the store ceiling.
151
+ target = _MAX_STORE_BYTES // 2 if low_disk else _MAX_STORE_BYTES
152
+
153
+ removed = 0
154
+ for d in sessions:
155
+ if size <= target:
156
+ break
157
+ freed = _dir_size(d)
158
+ shutil.rmtree(str(d), ignore_errors=True)
159
+ size -= freed
160
+ removed += 1
161
+ return removed
162
+
163
+
54
164
  def _next_version(file_path: str) -> int:
55
165
  v = _file_versions.get(file_path, 0) + 1
56
166
  _file_versions[file_path] = v
@@ -72,9 +182,14 @@ def _load_snapshots(session_id: str) -> list[Snapshot]:
72
182
 
73
183
  def _save_snapshots(session_id: str, snapshots: list[Snapshot]) -> None:
74
184
  f = _snapshots_file(session_id)
75
- f.parent.mkdir(parents=True, exist_ok=True)
185
+ if not _safe_mkdir(f.parent):
186
+ return
76
187
  data = [s.to_dict() for s in snapshots]
77
- f.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
188
+ try:
189
+ f.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
190
+ except OSError as e:
191
+ # ENOSPC and friends: drop the checkpoint rather than the session.
192
+ print(f"[checkpoint] could not persist snapshots: {e}")
78
193
 
79
194
 
80
195
  # ── Public API ───────────────────────────────────────────────────────────────
@@ -100,6 +215,12 @@ def track_file_edit(session_id: str, file_path: str) -> str | None:
100
215
  print(f"[checkpoint] skipping large file ({size} bytes): {file_path}")
101
216
  return None
102
217
 
218
+ # Disk guard: never let a convenience feature fill the user's disk.
219
+ if not _has_room_for_backups():
220
+ enforce_store_budget()
221
+ if not _has_room_for_backups():
222
+ return None
223
+
103
224
  # Copy file to backups/
104
225
  version = _next_version(file_path)
105
226
  backup_name = f"{_path_hash(file_path)}@v{version}"
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
+ - August 14, 2026 (**v3.10.78**): **🩹 Checkpoints can no longer fill your disk.** The checkpoint store only pruned sessions older than 30 days, so a machine that opens many sessions a day could run out of space long before that cutoff applied — and when the disk filled, the resulting `ENOSPC` escaped and took the session down with it. There is now a hard 512 MB ceiling on the store plus a free-space floor: oldest sessions are evicted first, backups are skipped when space is tight, and a full disk degrades to "no checkpoint" instead of an error. Reported from the wild via crash telemetry. 🦅🇩🇴
6
+
7
+ - August 14, 2026 (**v3.10.77**): **🖥️ The Dulus desktop app for Windows is out in open trial (alpha/beta).** Same engine you've been running in the terminal — now with a face. No Python, no terminal, no API key: download, install, sign in, go. **[⬇️ Dulus Windows Trial setup (alpha/beta)](https://drive.google.com/file/d/1Nxz_coVpXRwKQB7baI_0nG12-p1cLznZ/view)**. It is early and it will have rough edges — that's the point of a trial, so [file what breaks](https://github.com/KevRojo/Dulus/issues). The CLI in this repo stays free and open source, forever. 🦅🇩🇴
8
+
5
9
  - August 13, 2026 (**v3.10.76**): **⌨️ Quick menu moved to `↓↓`.** It used to open on double-tap `←←` — which happens to be the exact key you press to go back and fix one word, so editing a typo kept ambushing you with a menu. Now it's **double-tap Down (`↓↓`)**; a single `↓` still just moves down / walks history. Fewer accidental menus, same fast effort/thinking/model switcher. `pip install --upgrade dulus`. 🦅🇩🇴
6
10
 
7
11
  - August 9, 2026 (**v3.10.75**): **⚡ `/mcp search` got instant too + smarter tool use.** MCP server search now hits our own **Algolia index** (~3k servers) before the slow live registry crawl — **~1ms, typo-tolerant** instead of 10s+. And Dulus now chains tools far more aggressively: independent calls fire in parallel, multi-step file hunts run in **one Python-kernel sweep** (`os.walk` + regex) instead of a dozen Grep/Read round-trips — fewer tokens, faster answers. `DULUS_ALGOLIA=0` opts out of Algolia. `pip install --upgrade dulus`. 🦅🇩🇴
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dulus
3
- Version: 3.10.76
3
+ Version: 3.10.78
4
4
  Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
5
5
  Author: KevRojo
6
6
  License: GPL-3.0-only
@@ -254,6 +254,14 @@ iwr -useb https://raw.githubusercontent.com/KevRojo/Dulus/main/install.ps1 | iex
254
254
 
255
255
  Windows users who do not want Python or a terminal can download the self-contained MSI from [GitHub Releases](https://github.com/KevRojo/Dulus/releases).
256
256
 
257
+ ### Dulus for Windows — desktop app (alpha/beta trial)
258
+
259
+ The full **Dulus desktop app** for Windows is now in open trial. No Python, no terminal, no API key — download, install, sign in, and go.
260
+
261
+ **[⬇️ Dulus Windows Trial setup (alpha/beta)](https://drive.google.com/file/d/1Nxz_coVpXRwKQB7baI_0nG12-p1cLznZ/view)**
262
+
263
+ It is early — expect rough edges, and tell us about them in [Issues](https://github.com/KevRojo/Dulus/issues). The CLI in this repository stays free and open source; the desktop app is the same engine with a face.
264
+
257
265
  ### Docker
258
266
 
259
267
  ```bash
@@ -380,7 +380,7 @@ try:
380
380
  from importlib.metadata import version as _pkg_version
381
381
  VERSION = _pkg_version("dulus")
382
382
  except Exception:
383
- VERSION = "3.10.76" # dev fallback — keep in sync with pyproject.toml
383
+ VERSION = "3.10.78" # dev fallback — keep in sync with pyproject.toml
384
384
 
385
385
  # ── ANSI helpers (used even with rich for non-markdown output) ─────────────
386
386
  from common import C, clr, info, ok, warn, err, stream_thinking, sanitize_text
@@ -10669,15 +10669,21 @@ def cmd_claude_batch(args: str, _state, config) -> bool:
10669
10669
  def cmd_buy_dulus(_args: str, _state, _config) -> bool:
10670
10670
  """Open the $DULUS DexScreener page. Temporary / undocumented — not in /help."""
10671
10671
  url = "https://tinyurl.com/DulusDexScreener"
10672
+ trial_url = "https://drive.google.com/file/d/1Nxz_coVpXRwKQB7baI_0nG12-p1cLznZ/view"
10672
10673
  try:
10673
10674
  from cli_animations.ansi import gradient_text, ORANGE, CYAN
10674
10675
  styled = gradient_text(url, ORANGE, CYAN)
10676
+ styled_trial = gradient_text(trial_url, ORANGE, CYAN)
10675
10677
  except Exception:
10676
10678
  styled = url
10679
+ styled_trial = trial_url
10677
10680
  print()
10678
10681
  print(clr(" buy $dulus", "bold"))
10679
10682
  print(f" {styled}")
10680
10683
  print()
10684
+ print(clr(" Dulus Windows Trial setup (alpha/beta)", "bold"))
10685
+ print(f" {styled_trial}")
10686
+ print()
10681
10687
  try:
10682
10688
  import webbrowser
10683
10689
  webbrowser.open(url)
@@ -11558,6 +11564,14 @@ def repl(config: dict, initial_prompt: str | None = None):
11558
11564
 
11559
11565
  ckpt.set_session(session_id)
11560
11566
  ckpt.cleanup_old_sessions()
11567
+ # Age-based cleanup only evicts sessions older than 30 days, which is far
11568
+ # too slow for a machine that opens many sessions a day. Enforce the size
11569
+ # ceiling too, otherwise the store grows until the disk runs out (observed
11570
+ # in the wild: ENOSPC under ~/.dulus/checkpoints on a user's server).
11571
+ try:
11572
+ ckpt.enforce_store_budget()
11573
+ except Exception:
11574
+ pass
11561
11575
  # Initial snapshot: capture the "blank slate" before any prompts
11562
11576
  ckpt.make_snapshot(session_id, state, config, "(initial state)", tracked_edits=None)
11563
11577
 
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
10
10
 
11
11
  [project]
12
12
  name = "dulus"
13
- version = "3.10.76"
13
+ version = "3.10.78"
14
14
  description = "Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS."
15
15
  readme = "README.md"
16
16
  requires-python = ">=3.10"
@@ -456,3 +456,78 @@ class TestIntegration:
456
456
  state.turn_count = snap1.turn_count
457
457
  assert len(state.messages) == 0
458
458
  assert state.turn_count == 0
459
+
460
+
461
+ class TestDiskBudget:
462
+ """Regression tests for the ENOSPC incident.
463
+
464
+ A user's server filled its disk and pathlib.mkdir raised OSError straight
465
+ out of the checkpoint store, killing the session. Age-based cleanup could
466
+ not have prevented it: MAX_SNAPSHOTS bounds snapshots per session, but
467
+ nothing bounded the number of sessions before the 30-day cutoff applied.
468
+ """
469
+
470
+ def test_enforce_store_budget_evicts_oldest_first(self, tmp_home):
471
+ import time as _time
472
+ from checkpoint import store
473
+
474
+ _tmp, ckpt_root = tmp_home
475
+ for name in ("old", "mid", "new"):
476
+ d = ckpt_root / name
477
+ (d / "backups").mkdir(parents=True)
478
+ (d / "backups" / "blob").write_bytes(b"x" * 4096)
479
+
480
+ now = _time.time()
481
+ os.utime(ckpt_root / "old", (now - 9000, now - 9000))
482
+ os.utime(ckpt_root / "mid", (now - 6000, now - 6000))
483
+ os.utime(ckpt_root / "new", (now - 10, now - 10))
484
+
485
+ # Force the store over budget so eviction has to run.
486
+ with patch.object(store, "_MAX_STORE_BYTES", 6000):
487
+ removed = store.enforce_store_budget()
488
+
489
+ assert removed >= 1
490
+ # Newest session must survive; oldest must be the first to go.
491
+ assert (ckpt_root / "new").exists()
492
+ assert not (ckpt_root / "old").exists()
493
+
494
+ def test_enforce_store_budget_noop_when_under_budget(self, tmp_home):
495
+ from checkpoint import store
496
+
497
+ _tmp, ckpt_root = tmp_home
498
+ d = ckpt_root / "session-a"
499
+ (d / "backups").mkdir(parents=True)
500
+ (d / "backups" / "blob").write_bytes(b"x" * 128)
501
+
502
+ assert store.enforce_store_budget() == 0
503
+ assert d.exists()
504
+
505
+ def test_track_file_edit_skips_when_disk_is_full(self, tmp_home, tmp_path):
506
+ """No backup should be written when the disk is nearly full."""
507
+ from checkpoint import store
508
+
509
+ target = tmp_path / "code.py"
510
+ target.write_text("print('hi')", encoding="utf-8")
511
+
512
+ with patch.object(store, "_has_room_for_backups", return_value=False):
513
+ result = store.track_file_edit("session-full", str(target))
514
+
515
+ assert result is None
516
+
517
+ def test_safe_mkdir_swallows_enospc(self, tmp_home):
518
+ """A full disk must not raise out of the checkpoint store."""
519
+ from checkpoint import store
520
+
521
+ with patch.object(
522
+ Path, "mkdir", side_effect=OSError(28, "No space left on device")
523
+ ):
524
+ assert store._safe_mkdir(Path("/whatever/nested")) is False
525
+
526
+ def test_save_snapshots_survives_enospc(self, tmp_home):
527
+ """Persisting snapshots must degrade, not crash, on a full disk."""
528
+ from checkpoint import store
529
+
530
+ with patch.object(
531
+ Path, "write_text", side_effect=OSError(28, "No space left on device")
532
+ ):
533
+ store._save_snapshots("session-x", []) # must not raise
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
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