dulus 3.14.18__tar.gz → 3.15.0__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 (349) hide show
  1. {dulus-3.14.18/dulus.egg-info → dulus-3.15.0}/PKG-INFO +3 -1
  2. {dulus-3.14.18 → dulus-3.15.0}/README.md +2 -0
  3. {dulus-3.14.18 → dulus-3.15.0}/docs/CHANGELOG.md +1 -1
  4. {dulus-3.14.18 → dulus-3.15.0}/docs/news.md +5 -1
  5. {dulus-3.14.18 → dulus-3.15.0/dulus.egg-info}/PKG-INFO +3 -1
  6. {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/SOURCES.txt +2 -0
  7. {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/top_level.txt +1 -0
  8. {dulus-3.14.18 → dulus-3.15.0}/dulus.py +110 -3
  9. {dulus-3.14.18 → dulus-3.15.0}/dulus_account.py +67 -10
  10. {dulus-3.14.18 → dulus-3.15.0}/providers.py +11 -3
  11. {dulus-3.14.18 → dulus-3.15.0}/pyproject.toml +1 -1
  12. dulus-3.15.0/tests/test_empty_assistant_turn.py +38 -0
  13. dulus-3.15.0/tests/test_login_gate.py +278 -0
  14. {dulus-3.14.18 → dulus-3.15.0}/LICENSE +0 -0
  15. {dulus-3.14.18 → dulus-3.15.0}/MANIFEST.in +0 -0
  16. {dulus-3.14.18 → dulus-3.15.0}/agent.py +0 -0
  17. {dulus-3.14.18 → dulus-3.15.0}/algolia_search.py +0 -0
  18. {dulus-3.14.18 → dulus-3.15.0}/analytics.py +0 -0
  19. {dulus-3.14.18 → dulus-3.15.0}/backend/__init__.py +0 -0
  20. {dulus-3.14.18 → dulus-3.15.0}/backend/agents_bridge.py +0 -0
  21. {dulus-3.14.18 → dulus-3.15.0}/backend/compressor.py +0 -0
  22. {dulus-3.14.18 → dulus-3.15.0}/backend/context.py +0 -0
  23. {dulus-3.14.18 → dulus-3.15.0}/backend/githook.py +0 -0
  24. {dulus-3.14.18 → dulus-3.15.0}/backend/marketplace.py +0 -0
  25. {dulus-3.14.18 → dulus-3.15.0}/backend/mempalace_bridge.py +0 -0
  26. {dulus-3.14.18 → dulus-3.15.0}/backend/paths.py +0 -0
  27. {dulus-3.14.18 → dulus-3.15.0}/backend/personas.py +0 -0
  28. {dulus-3.14.18 → dulus-3.15.0}/backend/plugins.py +0 -0
  29. {dulus-3.14.18 → dulus-3.15.0}/backend/server.py +0 -0
  30. {dulus-3.14.18 → dulus-3.15.0}/backend/session_utils.py +0 -0
  31. {dulus-3.14.18 → dulus-3.15.0}/backend/tasks.py +0 -0
  32. {dulus-3.14.18 → dulus-3.15.0}/backend/theme_pack.py +0 -0
  33. {dulus-3.14.18 → dulus-3.15.0}/backend/themes.py +0 -0
  34. {dulus-3.14.18 → dulus-3.15.0}/banners/__init__.py +0 -0
  35. {dulus-3.14.18 → dulus-3.15.0}/banners/banner_rotator.py +0 -0
  36. {dulus-3.14.18 → dulus-3.15.0}/banners/banners_dulus_ai.txt +0 -0
  37. {dulus-3.14.18 → dulus-3.15.0}/batch_api.py +0 -0
  38. {dulus-3.14.18 → dulus-3.15.0}/checkpoint/__init__.py +0 -0
  39. {dulus-3.14.18 → dulus-3.15.0}/checkpoint/hooks.py +0 -0
  40. {dulus-3.14.18 → dulus-3.15.0}/checkpoint/store.py +0 -0
  41. {dulus-3.14.18 → dulus-3.15.0}/checkpoint/types.py +0 -0
  42. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/__init__.py +0 -0
  43. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/__main__.py +0 -0
  44. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/ansi.py +0 -0
  45. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/dulus_logo.txt +0 -0
  46. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/eagle.txt +0 -0
  47. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/terminal_art.txt +0 -0
  48. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/banners.py +0 -0
  49. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/demo.py +0 -0
  50. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/effects.py +0 -0
  51. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/effort_slider.py +0 -0
  52. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/progress.py +0 -0
  53. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/py.typed +0 -0
  54. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/spinners.py +0 -0
  55. {dulus-3.14.18 → dulus-3.15.0}/cli_animations/status.py +0 -0
  56. {dulus-3.14.18 → dulus-3.15.0}/clipboard_utils.py +0 -0
  57. {dulus-3.14.18 → dulus-3.15.0}/cloudsave.py +0 -0
  58. {dulus-3.14.18 → dulus-3.15.0}/common.py +0 -0
  59. {dulus-3.14.18 → dulus-3.15.0}/compaction.py +0 -0
  60. {dulus-3.14.18 → dulus-3.15.0}/config.py +0 -0
  61. {dulus-3.14.18 → dulus-3.15.0}/context.py +0 -0
  62. {dulus-3.14.18 → dulus-3.15.0}/data/__init__.py +0 -0
  63. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/__init__.py +0 -0
  64. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/__init__.py +0 -0
  65. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  66. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  67. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  68. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/plugin.json +0 -0
  69. {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/plugin_tool.py +0 -0
  70. {dulus-3.14.18 → dulus-3.15.0}/display_blocks.py +0 -0
  71. {dulus-3.14.18 → dulus-3.15.0}/docs/API.md +0 -0
  72. {dulus-3.14.18 → dulus-3.15.0}/docs/BRAND_GUIDE.md +0 -0
  73. {dulus-3.14.18 → dulus-3.15.0}/docs/CONTRIBUTING.md +0 -0
  74. {dulus-3.14.18 → dulus-3.15.0}/docs/DEPLOYMENT.md +0 -0
  75. {dulus-3.14.18 → dulus-3.15.0}/docs/FAQ.md +0 -0
  76. {dulus-3.14.18 → dulus-3.15.0}/docs/GETTING_STARTED.md +0 -0
  77. {dulus-3.14.18 → dulus-3.15.0}/docs/SECURITY.md +0 -0
  78. {dulus-3.14.18 → dulus-3.15.0}/docs/__init__.py +0 -0
  79. {dulus-3.14.18 → dulus-3.15.0}/docs/architecture.md +0 -0
  80. {dulus-3.14.18 → dulus-3.15.0}/docs/dashboard/index.html +0 -0
  81. {dulus-3.14.18 → dulus-3.15.0}/docs/divider.svg +0 -0
  82. {dulus-3.14.18 → dulus-3.15.0}/docs/dulus-bird.png +0 -0
  83. {dulus-3.14.18 → dulus-3.15.0}/docs/generate.py +0 -0
  84. {dulus-3.14.18 → dulus-3.15.0}/docs/nvidia-models.svg +0 -0
  85. {dulus-3.14.18 → dulus-3.15.0}/docs/particle-playground.html +0 -0
  86. {dulus-3.14.18 → dulus-3.15.0}/docs/personas/index.html +0 -0
  87. {dulus-3.14.18 → dulus-3.15.0}/docs/preview.html +0 -0
  88. {dulus-3.14.18 → dulus-3.15.0}/docs/python-console.md +0 -0
  89. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/dulus-os.png +0 -0
  90. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/hero.png +0 -0
  91. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_heap.svg +0 -0
  92. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_receipts.svg +0 -0
  93. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_trap.svg +0 -0
  94. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/task-board.png +0 -0
  95. {dulus-3.14.18 → dulus-3.15.0}/docs/readme/webchat.png +0 -0
  96. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-agents.svg +0 -0
  97. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-brainstorm.svg +0 -0
  98. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-bridges.svg +0 -0
  99. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-features.svg +0 -0
  100. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-freetier.svg +0 -0
  101. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-memory.svg +0 -0
  102. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-models.svg +0 -0
  103. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-perms.svg +0 -0
  104. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-plugins.svg +0 -0
  105. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-quickstart.svg +0 -0
  106. {dulus-3.14.18 → dulus-3.15.0}/docs/sec-ssj.svg +0 -0
  107. {dulus-3.14.18 → dulus-3.15.0}/docs/spinners.svg +0 -0
  108. {dulus-3.14.18 → dulus-3.15.0}/docs/split-pane.svg +0 -0
  109. {dulus-3.14.18 → dulus-3.15.0}/docs/terminal-boot.svg +0 -0
  110. {dulus-3.14.18 → dulus-3.15.0}/docs/termux-edge.md +0 -0
  111. {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/dependency_links.txt +0 -0
  112. {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/entry_points.txt +0 -0
  113. {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/requires.txt +0 -0
  114. {dulus-3.14.18 → dulus-3.15.0}/dulus_bar_client.py +0 -0
  115. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/__init__.py +0 -0
  116. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/client.py +0 -0
  117. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/commands.py +0 -0
  118. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/config.py +0 -0
  119. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/hub.py +0 -0
  120. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/tools.py +0 -0
  121. {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/types.py +0 -0
  122. {dulus-3.14.18 → dulus-3.15.0}/dulus_quick_menu.py +0 -0
  123. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/__init__.py +0 -0
  124. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/add_dir_manager.py +0 -0
  125. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/afk_mode.py +0 -0
  126. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/approval_runtime.py +0 -0
  127. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/background_tasks.py +0 -0
  128. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/clipboard_utils.py +0 -0
  129. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/diff_visualizer.py +0 -0
  130. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/export_import.py +0 -0
  131. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/hook_engine.py +0 -0
  132. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/isolate.py +0 -0
  133. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/notification_manager.py +0 -0
  134. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/session_fork.py +0 -0
  135. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/shell_mode.py +0 -0
  136. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/todo_visualizer.py +0 -0
  137. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/tree_ls.py +0 -0
  138. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/wire_events.py +0 -0
  139. {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/yolo_mode.py +0 -0
  140. {dulus-3.14.18 → dulus-3.15.0}/file_filter.py +0 -0
  141. {dulus-3.14.18 → dulus-3.15.0}/fs_compass.py +0 -0
  142. {dulus-3.14.18 → dulus-3.15.0}/governance.py +0 -0
  143. {dulus-3.14.18 → dulus-3.15.0}/input.py +0 -0
  144. {dulus-3.14.18 → dulus-3.15.0}/license_manager.py +0 -0
  145. {dulus-3.14.18 → dulus-3.15.0}/memory/__init__.py +0 -0
  146. {dulus-3.14.18 → dulus-3.15.0}/memory/audit.py +0 -0
  147. {dulus-3.14.18 → dulus-3.15.0}/memory/consolidator.py +0 -0
  148. {dulus-3.14.18 → dulus-3.15.0}/memory/context.py +0 -0
  149. {dulus-3.14.18 → dulus-3.15.0}/memory/mempalace_bridge.py +0 -0
  150. {dulus-3.14.18 → dulus-3.15.0}/memory/offload.py +0 -0
  151. {dulus-3.14.18 → dulus-3.15.0}/memory/palace.py +0 -0
  152. {dulus-3.14.18 → dulus-3.15.0}/memory/scan.py +0 -0
  153. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/askuserquestion_after_numbered_list.md +0 -0
  154. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/background_jobs_no_extra_timers.md +0 -0
  155. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/cleverness_over_resources.md +0 -0
  156. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/corporate_tls_breaks_curl_cffi.md +0 -0
  157. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/hardened_rules.md +0 -0
  158. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/investigator_workflow_heuristic.md +0 -0
  159. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/native_code_over_sketchy_plugins.md +0 -0
  160. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/no_personal_info_in_code.md +0 -0
  161. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/searches_include_modified_date.md +0 -0
  162. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/tmux_send_keys_power_patterns.md +0 -0
  163. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/token_optimization_technique.md +0 -0
  164. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/tool_chaining_capability.md +0 -0
  165. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/webbridge_evaluate_first_pattern.md +0 -0
  166. {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/short_memory.md +0 -0
  167. {dulus-3.14.18 → dulus-3.15.0}/memory/sessions.py +0 -0
  168. {dulus-3.14.18 → dulus-3.15.0}/memory/store.py +0 -0
  169. {dulus-3.14.18 → dulus-3.15.0}/memory/tools.py +0 -0
  170. {dulus-3.14.18 → dulus-3.15.0}/memory/types.py +0 -0
  171. {dulus-3.14.18 → dulus-3.15.0}/memory/vector_search.py +0 -0
  172. {dulus-3.14.18 → dulus-3.15.0}/model_params.py +0 -0
  173. {dulus-3.14.18 → dulus-3.15.0}/multi_agent/__init__.py +0 -0
  174. {dulus-3.14.18 → dulus-3.15.0}/multi_agent/subagent.py +0 -0
  175. {dulus-3.14.18 → dulus-3.15.0}/multi_agent/tools.py +0 -0
  176. {dulus-3.14.18 → dulus-3.15.0}/paste_placeholders.py +0 -0
  177. {dulus-3.14.18 → dulus-3.15.0}/personality_config.py +0 -0
  178. {dulus-3.14.18 → dulus-3.15.0}/plugin/__init__.py +0 -0
  179. {dulus-3.14.18 → dulus-3.15.0}/plugin/loader.py +0 -0
  180. {dulus-3.14.18 → dulus-3.15.0}/plugin/recommend.py +0 -0
  181. {dulus-3.14.18 → dulus-3.15.0}/plugin/store.py +0 -0
  182. {dulus-3.14.18 → dulus-3.15.0}/plugin/types.py +0 -0
  183. {dulus-3.14.18 → dulus-3.15.0}/profiles.py +0 -0
  184. {dulus-3.14.18 → dulus-3.15.0}/pyconsole.py +0 -0
  185. {dulus-3.14.18 → dulus-3.15.0}/setup.cfg +0 -0
  186. {dulus-3.14.18 → dulus-3.15.0}/skill/__init__.py +0 -0
  187. {dulus-3.14.18 → dulus-3.15.0}/skill/builtin.py +0 -0
  188. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/LICENSE-obsidian-skills +0 -0
  189. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/README.md +0 -0
  190. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/defuddle/SKILL.md +0 -0
  191. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/json-canvas/SKILL.md +0 -0
  192. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
  193. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-bases/SKILL.md +0 -0
  194. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
  195. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-cli/SKILL.md +0 -0
  196. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
  197. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
  198. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
  199. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
  200. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/README.md +0 -0
  201. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/SKILL.md +0 -0
  202. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
  203. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
  204. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
  205. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
  206. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
  207. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
  208. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
  209. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
  210. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
  211. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
  212. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
  213. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
  214. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
  215. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
  216. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
  217. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
  218. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
  219. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
  220. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
  221. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
  222. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
  223. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
  224. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
  225. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/drop.md +0 -0
  226. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
  227. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/methods.md +0 -0
  228. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
  229. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/state-machine.md +0 -0
  230. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
  231. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/badbool.py +0 -0
  232. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/brokers.py +0 -0
  233. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/cdp.py +0 -0
  234. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/config.py +0 -0
  235. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/crypto.py +0 -0
  236. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/dossier.py +0 -0
  237. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
  238. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/emailer.py +0 -0
  239. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/ledger.py +0 -0
  240. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/legal.py +0 -0
  241. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/paths.py +0 -0
  242. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/pdd.py +0 -0
  243. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/registry.py +0 -0
  244. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/report.py +0 -0
  245. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/scan.py +0 -0
  246. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/storage.py +0 -0
  247. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/tiers.py +0 -0
  248. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/vectors.py +0 -0
  249. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
  250. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
  251. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
  252. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
  253. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
  254. {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
  255. {dulus-3.14.18 → dulus-3.15.0}/skill/clawhub.py +0 -0
  256. {dulus-3.14.18 → dulus-3.15.0}/skill/executor.py +0 -0
  257. {dulus-3.14.18 → dulus-3.15.0}/skill/loader.py +0 -0
  258. {dulus-3.14.18 → dulus-3.15.0}/skill/tools.py +0 -0
  259. {dulus-3.14.18 → dulus-3.15.0}/skills.py +0 -0
  260. {dulus-3.14.18 → dulus-3.15.0}/soul.py +0 -0
  261. {dulus-3.14.18 → dulus-3.15.0}/spinner.py +0 -0
  262. {dulus-3.14.18 → dulus-3.15.0}/steer_input.py +0 -0
  263. {dulus-3.14.18 → dulus-3.15.0}/subagent.py +0 -0
  264. {dulus-3.14.18 → dulus-3.15.0}/task/__init__.py +0 -0
  265. {dulus-3.14.18 → dulus-3.15.0}/task/store.py +0 -0
  266. {dulus-3.14.18 → dulus-3.15.0}/task/tools.py +0 -0
  267. {dulus-3.14.18 → dulus-3.15.0}/task/types.py +0 -0
  268. {dulus-3.14.18 → dulus-3.15.0}/tests/test_afk_yolo.py +0 -0
  269. {dulus-3.14.18 → dulus-3.15.0}/tests/test_agent_tool_inputs.py +0 -0
  270. {dulus-3.14.18 → dulus-3.15.0}/tests/test_approval_runtime.py +0 -0
  271. {dulus-3.14.18 → dulus-3.15.0}/tests/test_background_task_tools.py +0 -0
  272. {dulus-3.14.18 → dulus-3.15.0}/tests/test_background_tasks.py +0 -0
  273. {dulus-3.14.18 → dulus-3.15.0}/tests/test_chatgpt_oauth.py +0 -0
  274. {dulus-3.14.18 → dulus-3.15.0}/tests/test_checkpoint.py +0 -0
  275. {dulus-3.14.18 → dulus-3.15.0}/tests/test_cli_animations_integration.py +0 -0
  276. {dulus-3.14.18 → dulus-3.15.0}/tests/test_clipboard_utils.py +0 -0
  277. {dulus-3.14.18 → dulus-3.15.0}/tests/test_compaction.py +0 -0
  278. {dulus-3.14.18 → dulus-3.15.0}/tests/test_compaction_failures.py +0 -0
  279. {dulus-3.14.18 → dulus-3.15.0}/tests/test_composio_tool_generator.py +0 -0
  280. {dulus-3.14.18 → dulus-3.15.0}/tests/test_diff_view.py +0 -0
  281. {dulus-3.14.18 → dulus-3.15.0}/tests/test_diff_visualization.py +0 -0
  282. {dulus-3.14.18 → dulus-3.15.0}/tests/test_display_blocks.py +0 -0
  283. {dulus-3.14.18 → dulus-3.15.0}/tests/test_dulus_account.py +0 -0
  284. {dulus-3.14.18 → dulus-3.15.0}/tests/test_export_import.py +0 -0
  285. {dulus-3.14.18 → dulus-3.15.0}/tests/test_grok_usage.py +0 -0
  286. {dulus-3.14.18 → dulus-3.15.0}/tests/test_hook_engine.py +0 -0
  287. {dulus-3.14.18 → dulus-3.15.0}/tests/test_injection_fix.py +0 -0
  288. {dulus-3.14.18 → dulus-3.15.0}/tests/test_interruptible_stream.py +0 -0
  289. {dulus-3.14.18 → dulus-3.15.0}/tests/test_json_protocol.py +0 -0
  290. {dulus-3.14.18 → dulus-3.15.0}/tests/test_license.py +0 -0
  291. {dulus-3.14.18 → dulus-3.15.0}/tests/test_mcp.py +0 -0
  292. {dulus-3.14.18 → dulus-3.15.0}/tests/test_mcp_algolia.py +0 -0
  293. {dulus-3.14.18 → dulus-3.15.0}/tests/test_memory.py +0 -0
  294. {dulus-3.14.18 → dulus-3.15.0}/tests/test_memory_home_resolution.py +0 -0
  295. {dulus-3.14.18 → dulus-3.15.0}/tests/test_notification_manager.py +0 -0
  296. {dulus-3.14.18 → dulus-3.15.0}/tests/test_persistence_safety.py +0 -0
  297. {dulus-3.14.18 → dulus-3.15.0}/tests/test_plugin.py +0 -0
  298. {dulus-3.14.18 → dulus-3.15.0}/tests/test_profile_isolation.py +0 -0
  299. {dulus-3.14.18 → dulus-3.15.0}/tests/test_profile_plugin_scope.py +0 -0
  300. {dulus-3.14.18 → dulus-3.15.0}/tests/test_provider_errors.py +0 -0
  301. {dulus-3.14.18 → dulus-3.15.0}/tests/test_provider_stability.py +0 -0
  302. {dulus-3.14.18 → dulus-3.15.0}/tests/test_public_surface.py +0 -0
  303. {dulus-3.14.18 → dulus-3.15.0}/tests/test_repl_resilience.py +0 -0
  304. {dulus-3.14.18 → dulus-3.15.0}/tests/test_session_fork.py +0 -0
  305. {dulus-3.14.18 → dulus-3.15.0}/tests/test_shell_detection.py +0 -0
  306. {dulus-3.14.18 → dulus-3.15.0}/tests/test_shell_mode.py +0 -0
  307. {dulus-3.14.18 → dulus-3.15.0}/tests/test_skills.py +0 -0
  308. {dulus-3.14.18 → dulus-3.15.0}/tests/test_skillssh_algolia.py +0 -0
  309. {dulus-3.14.18 → dulus-3.15.0}/tests/test_steer_input.py +0 -0
  310. {dulus-3.14.18 → dulus-3.15.0}/tests/test_subagent.py +0 -0
  311. {dulus-3.14.18 → dulus-3.15.0}/tests/test_task.py +0 -0
  312. {dulus-3.14.18 → dulus-3.15.0}/tests/test_telegram_buffer.py +0 -0
  313. {dulus-3.14.18 → dulus-3.15.0}/tests/test_think_tool.py +0 -0
  314. {dulus-3.14.18 → dulus-3.15.0}/tests/test_todo_tool.py +0 -0
  315. {dulus-3.14.18 → dulus-3.15.0}/tests/test_todo_visualization.py +0 -0
  316. {dulus-3.14.18 → dulus-3.15.0}/tests/test_tool_registry.py +0 -0
  317. {dulus-3.14.18 → dulus-3.15.0}/tests/test_tool_safety.py +0 -0
  318. {dulus-3.14.18 → dulus-3.15.0}/tests/test_tree_ls.py +0 -0
  319. {dulus-3.14.18 → dulus-3.15.0}/tests/test_voice.py +0 -0
  320. {dulus-3.14.18 → dulus-3.15.0}/tests/test_webbridge_tabs.py +0 -0
  321. {dulus-3.14.18 → dulus-3.15.0}/tests/test_wire_events.py +0 -0
  322. {dulus-3.14.18 → dulus-3.15.0}/tmux_tools.py +0 -0
  323. {dulus-3.14.18 → dulus-3.15.0}/token_budget.py +0 -0
  324. {dulus-3.14.18 → dulus-3.15.0}/tool_registry.py +0 -0
  325. {dulus-3.14.18 → dulus-3.15.0}/tools.py +0 -0
  326. {dulus-3.14.18 → dulus-3.15.0}/tools_background.py +0 -0
  327. {dulus-3.14.18 → dulus-3.15.0}/tools_think.py +0 -0
  328. {dulus-3.14.18 → dulus-3.15.0}/tools_todo.py +0 -0
  329. {dulus-3.14.18 → dulus-3.15.0}/ui/__init__.py +0 -0
  330. {dulus-3.14.18 → dulus-3.15.0}/ui/input.py +0 -0
  331. {dulus-3.14.18 → dulus-3.15.0}/ui/render.py +0 -0
  332. {dulus-3.14.18 → dulus-3.15.0}/updater.py +0 -0
  333. {dulus-3.14.18 → dulus-3.15.0}/voice/__init__.py +0 -0
  334. {dulus-3.14.18 → dulus-3.15.0}/voice/audio_utils.py +0 -0
  335. {dulus-3.14.18 → dulus-3.15.0}/voice/keyterms.py +0 -0
  336. {dulus-3.14.18 → dulus-3.15.0}/voice/recorder.py +0 -0
  337. {dulus-3.14.18 → dulus-3.15.0}/voice/stt.py +0 -0
  338. {dulus-3.14.18 → dulus-3.15.0}/voice/tts.py +0 -0
  339. {dulus-3.14.18 → dulus-3.15.0}/voice/wake_word.py +0 -0
  340. {dulus-3.14.18 → dulus-3.15.0}/webbridge/__init__.py +0 -0
  341. {dulus-3.14.18 → dulus-3.15.0}/webbridge/core.py +0 -0
  342. {dulus-3.14.18 → dulus-3.15.0}/webbridge/tools.py +0 -0
  343. {dulus-3.14.18 → dulus-3.15.0}/webchat.py +0 -0
  344. {dulus-3.14.18 → dulus-3.15.0}/webchat_server.py +0 -0
  345. {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/__init__.py +0 -0
  346. {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/app.js +0 -0
  347. {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/index.html +0 -0
  348. {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/style.css +0 -0
  349. {dulus-3.14.18 → dulus-3.15.0}/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dulus
3
- Version: 3.14.18
3
+ Version: 3.15.0
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-email: KevRojo <dev@dulus.ai>
6
6
  Maintainer-email: KevRojo <dev@dulus.ai>
@@ -464,6 +464,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
464
464
 
465
465
  ## Install in 30 seconds
466
466
 
467
+ > **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
468
+
467
469
  If Python 3.11 or newer is already installed:
468
470
 
469
471
  ```bash
@@ -359,6 +359,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
359
359
 
360
360
  ## Install in 30 seconds
361
361
 
362
+ > **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
363
+
362
364
  If Python 3.11 or newer is already installed:
363
365
 
364
366
  ```bash
@@ -88,7 +88,7 @@ set a provider key, or `/model ollama/<name>` to pick a supported route.
88
88
  - **First-turn amnesia**: agent + lookback strip GUI/REPL gold/soul/welcome *display* blobs before the provider call, so web-history consolidate no longer treats gold as “last assistant reply” and drops real context
89
89
  - **MemPalace dual-path noise**: per-turn inject (CLI + WebChat) skips `short_memory` / soul — already in the system prompt
90
90
  - **Second `/compact` archive wipe**: `save_loopback_archive` refuses to shrink; Loopback keeps the full pre-compact history across N compacts
91
- - **WebChat gold preload**: session seeds gold *display* copies for the transcript; model source of truth is still the system prompt (public GUI stays as-is — this is server/engine, not an Interant GUI port)
91
+ - **WebChat gold preload**: session seeds gold *display* copies for the transcript; model source of truth is still the system prompt (public GUI stays as-is — this is server/engine, not a port of the private GUI)
92
92
 
93
93
  ### Tests
94
94
  - `test_gold_memory_preload` + `test_lookback_compact` (11 passed)
@@ -2,7 +2,11 @@
2
2
 
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
- - November 3, 2026 (**📡 v3.14.18 — Dulus Radio gets a station ident**): **The first screen you see now says who is talking.** Dulus Radio — the dispatch board that prints on every interactive boot — gained a station ident: *Interant · Santo Domingo 🇩🇴 · what shipped while you were away*, so a fresh install learns who builds this and what the board is for instead of reading a bare "ON AIR". The sign-off states the honest bit too: **this repo is a mirror, and the signed desktop build runs ahead of it.** **Long dispatch headlines no longer wreck the layout** — a headline is capped at 68 characters and the overflow moves into the body, so the board stays readable on an 80-column terminal and no detail is lost; the cut prefers a sentence or clause boundary over splitting a word. The README also now documents the real distance between this mirror and the signed line: **2.2× the commits, 4.3× the code, 13.3× the 30-day velocity.** `pip install -U dulus`. 🦅🇩🇴
5
+ - November 9, 2026 (**🔐 v3.15.0 — Dulus now runs on a Dulus account**): **Sign in once, then it's all yours.** From this release on, the first launch asks you to sign in with a free Dulus account — your browser opens, you approve, and you're back in the terminal. That's the whole ritual: the session is remembered and renewed quietly, and when Dulus can't be reached for a renewal you keep working offline. CI, scripts and servers skip the browser: set `DULUS_API_KEY` to a `dulus_sk_*` key minted with `/login dulus key`. `--version` and `--help` stay open to everyone, and your own provider keys (OpenAI, Anthropic, Ollama…) work exactly as before — they just ride on a signed-in Dulus now. `pip install -U dulus`. 🦅🇩🇴
6
+
7
+ - November 3, 2026 (**📡 v3.14.19 — the boot ident signs with our own name**): **One small, public-facing fix.** The Dulus Radio station ident that prints on every interactive boot now signs with the product's own name. Same board, same dispatch, same sign-off about this repo being a mirror — only the signature changed. `pip install -U dulus`. 🦅🇩🇴
8
+
9
+ - November 3, 2026 (**📡 v3.14.18 — Dulus Radio gets a station ident**): **The first screen you see now says who is talking.** Dulus Radio — the dispatch board that prints on every interactive boot — gained a station ident: *Dulus · Santo Domingo 🇩🇴 · what shipped while you were away*, so a fresh install learns who builds this and what the board is for instead of reading a bare "ON AIR". The sign-off states the honest bit too: **this repo is a mirror, and the signed desktop build runs ahead of it.** **Long dispatch headlines no longer wreck the layout** — a headline is capped at 68 characters and the overflow moves into the body, so the board stays readable on an 80-column terminal and no detail is lost; the cut prefers a sentence or clause boundary over splitting a word. The README also now documents the real distance between this mirror and the signed line: **2.2× the commits, 4.3× the code, 13.3× the 30-day velocity.** `pip install -U dulus`. 🦅🇩🇴
6
10
 
7
11
  - October 29, 2026 (**🧠 v6.4.1 — memory that stays with you**): **Memory belongs to the profile now.** Recall was missing things it had already saved; both sides of the memory path are scoped to the profile you are actually running, so what you save is what comes back. **Context limits finally add up** — the per-source budgets are one real ceiling instead of separate numbers that never summed. **Bash commands no longer cut themselves off mid-run**, so a long build returns its whole output instead of a truncated tail. **Native installers are back on both platforms**: a signed `.exe` for Windows and a `.dmg` for macOS ARM64, no Python setup required. A **silent nudge in the taskbar** tells you a reply is waiting instead of leaving you staring at a dead window. And **Dulus Business** — the deployable stack and the owners' dashboard — is back in the repo. Grab the signed build from [dulus-updates](https://github.com/Dulus-Ai/dulus-updates/releases/tag/v6.4.1), or `pip install -U dulus` for the open runtime. 🦅🇩🇴
8
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dulus
3
- Version: 3.14.18
3
+ Version: 3.15.0
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-email: KevRojo <dev@dulus.ai>
6
6
  Maintainer-email: KevRojo <dev@dulus.ai>
@@ -464,6 +464,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
464
464
 
465
465
  ## Install in 30 seconds
466
466
 
467
+ > **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
468
+
467
469
  If Python 3.11 or newer is already installed:
468
470
 
469
471
  ```bash
@@ -288,6 +288,7 @@ tests/test_diff_view.py
288
288
  tests/test_diff_visualization.py
289
289
  tests/test_display_blocks.py
290
290
  tests/test_dulus_account.py
291
+ tests/test_empty_assistant_turn.py
291
292
  tests/test_export_import.py
292
293
  tests/test_grok_usage.py
293
294
  tests/test_hook_engine.py
@@ -295,6 +296,7 @@ tests/test_injection_fix.py
295
296
  tests/test_interruptible_stream.py
296
297
  tests/test_json_protocol.py
297
298
  tests/test_license.py
299
+ tests/test_login_gate.py
298
300
  tests/test_mcp.py
299
301
  tests/test_mcp_algolia.py
300
302
  tests/test_memory.py
@@ -32,6 +32,7 @@ multi_agent
32
32
  paste_placeholders
33
33
  personality_config
34
34
  plugin
35
+ plugins
35
36
  profiles
36
37
  providers
37
38
  pyconsole
@@ -375,7 +375,7 @@ try:
375
375
  from importlib.metadata import version as _pkg_version
376
376
  VERSION = _pkg_version("dulus")
377
377
  except Exception:
378
- VERSION = "3.14.18" # dev fallback — keep in sync with pyproject.toml
378
+ VERSION = "3.15.0" # dev fallback — keep in sync with pyproject.toml
379
379
 
380
380
  # ── Machine-readable protocol output (`--output json`) ─────────────────────
381
381
  # Dulus is increasingly run as a child process by another agent runtime (an
@@ -9042,7 +9042,7 @@ def _print_news_broadcast(
9042
9042
  # Station ident. This is the first thing a new install ever reads, so it
9043
9043
  # says who is broadcasting and what the board is actually for.
9044
9044
  print(
9045
- clr(" Interant", "white", "bold")
9045
+ clr(" Dulus", "white", "bold")
9046
9046
  + clr(" · Santo Domingo 🇩🇴 · ", "dim")
9047
9047
  + clr("what shipped while you were away", "cyan", "dim")
9048
9048
  )
@@ -11241,7 +11241,7 @@ def repl(config: dict, initial_prompt: str | None = None):
11241
11241
  "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⢾⣯⣧⣬⣭⣤⡶⠖⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
11242
11242
  ]
11243
11243
  _DULUS_LOGO.append(" " + clr("v" + VERSION, "green", "bold"))
11244
- _DULUS_LOGO.append(" " + clr("📡 Dulus Radio · by Interant · ON AIR at boot", "cyan", "dim"))
11244
+ _DULUS_LOGO.append(" " + clr("📡 Dulus Radio · ON AIR at boot", "cyan", "dim"))
11245
11245
  _DULUS_LOGO.append(" ")
11246
11246
 
11247
11247
  # Shared Dulus animation language for the short boot beat. Redirected
@@ -13075,6 +13075,110 @@ def repl(config: dict, initial_prompt: str | None = None):
13075
13075
 
13076
13076
  # ── Entry point ────────────────────────────────────────────────────────────
13077
13077
 
13078
+ def _attached_to_msys_pty(stream) -> bool:
13079
+ """Is this stream a mintty terminal (Git Bash, MSYS2, Cygwin)?
13080
+
13081
+ mintty hands Windows programs a named pipe instead of a console, so
13082
+ isatty() says nobody is there even with a person typing. The pipe's name
13083
+ gives the terminal away, and ordinary pipes (`| tail`, a subprocess
13084
+ capture) never carry it. MSYSTEM can't be trusted for this: every child
13085
+ of such a shell inherits it, scripts and scheduled jobs included.
13086
+ """
13087
+ if sys.platform != "win32":
13088
+ return False
13089
+ try:
13090
+ import ctypes
13091
+ import msvcrt
13092
+ from ctypes import wintypes
13093
+
13094
+ class _FileNameInfo(ctypes.Structure):
13095
+ _fields_ = [("FileNameLength", wintypes.DWORD), ("FileName", wintypes.WCHAR * 260)]
13096
+
13097
+ info = _FileNameInfo()
13098
+ handle = wintypes.HANDLE(msvcrt.get_osfhandle(stream.fileno()))
13099
+ file_name_info = 2 # FILE_INFO_BY_HANDLE_CLASS.FileNameInfo
13100
+ if not ctypes.windll.kernel32.GetFileInformationByHandleEx(
13101
+ handle, file_name_info, ctypes.byref(info), ctypes.sizeof(info)):
13102
+ return False
13103
+ name = info.FileName[: info.FileNameLength // 2]
13104
+ except Exception:
13105
+ return False
13106
+ return re.match(r"\\(cygwin|msys)-[0-9a-fA-F]+-pty\d+-(from|to)-master", name) is not None
13107
+
13108
+
13109
+ def _login_gate_can_prompt() -> bool:
13110
+ """Is a person there to see a sign-in link? CI never counts."""
13111
+ if os.environ.get("CI"):
13112
+ return False
13113
+ for stream in (sys.stdout, sys.stderr):
13114
+ if stream is None:
13115
+ continue
13116
+ try:
13117
+ if stream.isatty() or _attached_to_msys_pty(stream):
13118
+ return True
13119
+ except Exception:
13120
+ pass
13121
+ return False
13122
+
13123
+
13124
+ def _require_dulus_login(args) -> None:
13125
+ """Dulus runs on a free Dulus account: no account, no Dulus.
13126
+
13127
+ Checked once per launch, after --version/--help (those stay open to
13128
+ everyone). Background job runners are spawned by a session that already
13129
+ passed here, and `dulus -c "login dulus"` / `-c logout` stay reachable so
13130
+ a broken sign-in can always be fixed from the command line. Renewal only
13131
+ happens when a person launched Dulus; background children and `-c` runs
13132
+ take the stored session at its word so parallel launches never race to
13133
+ rotate the same refresh token.
13134
+ """
13135
+ if args.run_tool or args.job_id or args.job_path:
13136
+ return
13137
+ if args.exec_cmd:
13138
+ verb = " ".join(args.exec_cmd).strip().strip("\"'").lstrip("/").split(" ", 1)[0].lower()
13139
+ if verb in ("login", "logout"):
13140
+ return
13141
+
13142
+ import dulus_account
13143
+
13144
+ protocol = _protocol_enabled()
13145
+ can_prompt = not protocol and _login_gate_can_prompt()
13146
+ status = dulus_account.session_status(renew=can_prompt and not args.exec_cmd)
13147
+ if status == "active":
13148
+ return
13149
+ if status == "offline":
13150
+ print(clr("[dulus] Couldn't reach Dulus to renew your sign-in — carrying on offline.", "gray"),
13151
+ file=sys.stderr)
13152
+ return
13153
+
13154
+ if status == "invalid_key":
13155
+ message = ("DULUS_API_KEY is set but isn't a Dulus key (those start with dulus_sk_). Replace "
13156
+ "it with one minted by `/login dulus key`, or unset it to use your sign-in.")
13157
+ elif not can_prompt:
13158
+ message = ("Dulus needs a free Dulus account. Sign in once with `dulus -c \"login dulus\"` "
13159
+ "(or just run `dulus` in a terminal). For CI, scripts and servers, set "
13160
+ "DULUS_API_KEY to a dulus_sk_* key minted with `/login dulus key`.")
13161
+ else:
13162
+ message = ""
13163
+ if message:
13164
+ if protocol:
13165
+ _emit_error(message)
13166
+ else:
13167
+ print(f"[dulus] {message}", file=sys.stderr)
13168
+ sys.exit(2)
13169
+
13170
+ print(clr("\n Dulus runs on a free Dulus account — sign in once and you're in.\n", "cyan"))
13171
+ try:
13172
+ token = dulus_account.login(notify=lambda m: print(clr(m, "cyan")))
13173
+ except KeyboardInterrupt:
13174
+ token = None
13175
+ if not token:
13176
+ print(clr("[dulus] Sign-in didn't finish. Run `dulus` again whenever you're ready.", "yellow"),
13177
+ file=sys.stderr)
13178
+ sys.exit(1)
13179
+ ok("Signed in — welcome aboard. 🦅")
13180
+
13181
+
13078
13182
  def main():
13079
13183
  parser = argparse.ArgumentParser(
13080
13184
  prog="dulus",
@@ -13139,6 +13243,9 @@ def main():
13139
13243
  from config import load_config, save_config, has_api_key
13140
13244
  from providers import detect_provider, PROVIDERS
13141
13245
 
13246
+ # No account, no Dulus: sign in (or learn how) before anything else runs.
13247
+ _require_dulus_login(args)
13248
+
13142
13249
  # ── First-run welcome wizard ──────────────────────────────────────────
13143
13250
  # Runs BEFORE load_config() so we detect the absence of config.json.
13144
13251
  # The wizard mutates config in-place and persists it; subsequent
@@ -43,6 +43,7 @@ _AUTH0_SCOPES = "openid profile email"
43
43
  # Refresh slightly early so a request never leaves with a token that expires
44
44
  # while it is in flight.
45
45
  _EXPIRY_BUFFER_SECONDS = 60
46
+ API_KEY_PREFIX = "dulus_sk_" # what `/login dulus key` mints
46
47
 
47
48
 
48
49
  def _is_text_browser(name: str) -> bool:
@@ -157,12 +158,13 @@ def _persist_tokens(payload: dict) -> dict:
157
158
  return store
158
159
 
159
160
 
160
- def refresh(refresh_token: str) -> dict | None:
161
- """Exchange a refresh token for a fresh access token.
161
+ def _renew(refresh_token: str, timeout: float = 30) -> tuple[str, dict | None]:
162
+ """One refresh round-trip against ``/v1/oauth/token``.
162
163
 
163
- The server rotates refresh tokens, so the response carries a new one that
164
- replaces the old. A failure here means the token was revoked or replayed
165
- and the user has to sign in again.
164
+ Returns ``("ok", store)`` once the new tokens are saved, ``("refused", None)``
165
+ when Dulus said no (revoked, expired for good, account gone) and
166
+ ``("unreachable", None)`` when it could not answer — no network, a timeout,
167
+ or a 5xx/429 on our side.
166
168
  """
167
169
  import requests
168
170
 
@@ -174,13 +176,37 @@ def refresh(refresh_token: str) -> dict | None:
174
176
  "client_id": DULUS_OAUTH_CLIENT_ID,
175
177
  "refresh_token": refresh_token,
176
178
  },
177
- timeout=30,
179
+ timeout=timeout,
178
180
  )
179
- if response.status_code != 200:
180
- return None
181
- return _persist_tokens(response.json())
182
181
  except Exception:
183
- return None
182
+ return "unreachable", None
183
+ if response.status_code in (400, 401, 403):
184
+ return "refused", None
185
+ if response.status_code != 200:
186
+ return "unreachable", None
187
+ try:
188
+ payload = dict(response.json())
189
+ except Exception:
190
+ return "unreachable", None
191
+ if not payload.get("access_token"):
192
+ return "unreachable", None
193
+ if not payload.get("refresh_token"):
194
+ # RFC 6749 §6: no new refresh token issued means the old one stays valid.
195
+ payload["refresh_token"] = refresh_token
196
+ try:
197
+ return "ok", _persist_tokens(payload)
198
+ except Exception:
199
+ return "unreachable", None
200
+
201
+
202
+ def refresh(refresh_token: str) -> dict | None:
203
+ """Exchange a refresh token for a fresh access token.
204
+
205
+ The server rotates refresh tokens, so the response carries a new one that
206
+ replaces the old. A failure here means the token was revoked or replayed
207
+ and the user has to sign in again.
208
+ """
209
+ return _renew(refresh_token)[1]
184
210
 
185
211
 
186
212
  def login(notify: Callable[[str], Any] = print, force_browser: bool = False) -> str | None:
@@ -379,6 +405,37 @@ def access_token(notify: Callable[[str], Any] = print) -> str:
379
405
  return ""
380
406
 
381
407
 
408
+ def session_status(renew: bool = True) -> str:
409
+ """Is this machine signed in to a Dulus account? Never prompts.
410
+
411
+ ``"active"`` a live session, or a ``dulus_sk_*`` key in DULUS_API_KEY
412
+ ``"offline"`` a session exists but its renewal couldn't reach Dulus —
413
+ let the person keep working
414
+ ``"invalid_key"`` DULUS_API_KEY is set but isn't a Dulus key
415
+ ``"none"`` never signed in, signed out, or Dulus refused the renewal
416
+
417
+ With ``renew`` it talks to Dulus only when the access token has expired —
418
+ the same renewal :func:`access_token` runs on first use anyway. Without it
419
+ (background children, scripts) a stored refresh token is taken at its word
420
+ and renewed lazily, so parallel launches never race to rotate it.
421
+ """
422
+ explicit = (os.environ.get("DULUS_API_KEY") or "").strip()
423
+ if explicit:
424
+ looks_right = explicit.startswith(API_KEY_PREFIX) and len(explicit) >= len(API_KEY_PREFIX) + 16
425
+ return "active" if looks_right else "invalid_key"
426
+
427
+ store = load_store()
428
+ if store.get("access_token") and not _token_expired(store):
429
+ return "active"
430
+ refresh_token = store.get("refresh_token")
431
+ if not refresh_token:
432
+ return "none"
433
+ if not renew:
434
+ return "active"
435
+ state, _ = _renew(refresh_token, timeout=10)
436
+ return {"ok": "active", "refused": "none"}.get(state, "offline")
437
+
438
+
382
439
  def auth_headers(notify: Callable[[str], Any] = print) -> dict:
383
440
  """Authorization header for Dulus API calls, or empty when signed out."""
384
441
  token = access_token(notify)
@@ -3801,7 +3801,11 @@ def messages_to_anthropic(messages: list) -> list:
3801
3801
  "name": tc["name"],
3802
3802
  "input": tc["input"],
3803
3803
  })
3804
- result.append({"role": "assistant", "content": blocks})
3804
+ # An empty assistant turn (reasoning-only stall) has no blocks, and
3805
+ # Anthropic rejects empty content. Skip it — the API merges the
3806
+ # surrounding consecutive user turns.
3807
+ if blocks:
3808
+ result.append({"role": "assistant", "content": blocks})
3805
3809
  i += 1
3806
3810
 
3807
3811
  elif role == "tool":
@@ -3890,11 +3894,15 @@ def messages_to_openai(messages: list, ollama_native_images: bool = False, model
3890
3894
  result.append(msg_out)
3891
3895
 
3892
3896
  elif role == "assistant":
3893
- msg: dict = {"role": "assistant", "content": m.get("content") or None}
3897
+ tcs = m.get("tool_calls", [])
3898
+ # null content is only legal next to tool_calls. A reasoning-only
3899
+ # stall leaves an empty assistant turn in history; sending it as
3900
+ # null 400s every later request in the session.
3901
+ msg: dict = {"role": "assistant",
3902
+ "content": m.get("content") or (None if tcs else "")}
3894
3903
  if "thinking" in m and m["thinking"]:
3895
3904
  msg["reasoning_content"] = m["thinking"]
3896
3905
 
3897
- tcs = m.get("tool_calls", [])
3898
3906
  if tcs:
3899
3907
  msg["tool_calls"] = []
3900
3908
  for tc in tcs:
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
10
10
 
11
11
  [project]
12
12
  name = "dulus"
13
- version = "3.14.18"
13
+ version = "3.15.0"
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"
@@ -0,0 +1,38 @@
1
+ """A reasoning-only stall leaves an empty assistant turn in history.
2
+
3
+ agent.run keeps {"role": "assistant", "content": "", "tool_calls": []} after
4
+ the model spends its whole budget on reasoning. Both converters must turn it
5
+ into something the API accepts, or every later prompt in the session 400s.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import providers
10
+
11
+ STALL = [
12
+ {"role": "user", "content": "Prove the theorem."},
13
+ {"role": "assistant", "content": "", "thinking": "", "tool_calls": []},
14
+ {"role": "user", "content": "Try again, shorter."},
15
+ ]
16
+
17
+
18
+ def test_openai_empty_assistant_gets_string_content():
19
+ out = providers.messages_to_openai(STALL)
20
+ assert out[1] == {"role": "assistant", "content": ""}
21
+
22
+
23
+ def test_openai_tool_call_turn_keeps_null_content():
24
+ msgs = [
25
+ {"role": "user", "content": "ls"},
26
+ {"role": "assistant", "content": "",
27
+ "tool_calls": [{"id": "c1", "name": "Bash", "input": {"command": "ls"}}]},
28
+ {"role": "tool", "tool_call_id": "c1", "name": "Bash", "content": "a.txt"},
29
+ ]
30
+ out = providers.messages_to_openai(msgs)
31
+ assert out[1]["content"] is None
32
+ assert out[1]["tool_calls"][0]["id"] == "c1"
33
+
34
+
35
+ def test_anthropic_empty_assistant_is_dropped():
36
+ out = providers.messages_to_anthropic(STALL)
37
+ assert all(m["content"] for m in out)
38
+ assert [m["role"] for m in out] == ["user", "user"]