dulus 0.2.28__tar.gz → 0.2.30__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 (164) hide show
  1. {dulus-0.2.28/dulus.egg-info → dulus-0.2.30}/PKG-INFO +2 -2
  2. {dulus-0.2.28 → dulus-0.2.30}/README.md +1 -1
  3. {dulus-0.2.28 → dulus-0.2.30}/docs/news.md +11 -0
  4. {dulus-0.2.28 → dulus-0.2.30/dulus.egg-info}/PKG-INFO +2 -2
  5. {dulus-0.2.28 → dulus-0.2.30}/dulus.py +212 -55
  6. {dulus-0.2.28 → dulus-0.2.30}/pyproject.toml +1 -1
  7. {dulus-0.2.28 → dulus-0.2.30}/LICENSE +0 -0
  8. {dulus-0.2.28 → dulus-0.2.30}/MANIFEST.in +0 -0
  9. {dulus-0.2.28 → dulus-0.2.30}/agent.py +0 -0
  10. {dulus-0.2.28 → dulus-0.2.30}/backend/__init__.py +0 -0
  11. {dulus-0.2.28 → dulus-0.2.30}/backend/compressor.py +0 -0
  12. {dulus-0.2.28 → dulus-0.2.30}/backend/context.py +0 -0
  13. {dulus-0.2.28 → dulus-0.2.30}/backend/githook.py +0 -0
  14. {dulus-0.2.28 → dulus-0.2.30}/backend/marketplace.py +0 -0
  15. {dulus-0.2.28 → dulus-0.2.30}/backend/mempalace_bridge.py +0 -0
  16. {dulus-0.2.28 → dulus-0.2.30}/backend/personas.py +0 -0
  17. {dulus-0.2.28 → dulus-0.2.30}/backend/plugins.py +0 -0
  18. {dulus-0.2.28 → dulus-0.2.30}/backend/server.py +0 -0
  19. {dulus-0.2.28 → dulus-0.2.30}/backend/tasks.py +0 -0
  20. {dulus-0.2.28 → dulus-0.2.30}/batch_api.py +0 -0
  21. {dulus-0.2.28 → dulus-0.2.30}/checkpoint/__init__.py +0 -0
  22. {dulus-0.2.28 → dulus-0.2.30}/checkpoint/hooks.py +0 -0
  23. {dulus-0.2.28 → dulus-0.2.30}/checkpoint/store.py +0 -0
  24. {dulus-0.2.28 → dulus-0.2.30}/checkpoint/types.py +0 -0
  25. {dulus-0.2.28 → dulus-0.2.30}/claude_code_watcher.py +0 -0
  26. {dulus-0.2.28 → dulus-0.2.30}/clipboard_utils.py +0 -0
  27. {dulus-0.2.28 → dulus-0.2.30}/cloudsave.py +0 -0
  28. {dulus-0.2.28 → dulus-0.2.30}/common.py +0 -0
  29. {dulus-0.2.28 → dulus-0.2.30}/compaction.py +0 -0
  30. {dulus-0.2.28 → dulus-0.2.30}/config.py +0 -0
  31. {dulus-0.2.28 → dulus-0.2.30}/context.py +0 -0
  32. {dulus-0.2.28 → dulus-0.2.30}/data/__init__.py +0 -0
  33. {dulus-0.2.28 → dulus-0.2.30}/data/active_persona.json +0 -0
  34. {dulus-0.2.28 → dulus-0.2.30}/data/context.json +0 -0
  35. {dulus-0.2.28 → dulus-0.2.30}/data/marketplace.json +0 -0
  36. {dulus-0.2.28 → dulus-0.2.30}/data/personas.json +0 -0
  37. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/__init__.py +0 -0
  38. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/__init__.py +0 -0
  39. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  40. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  41. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  42. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/plugin.json +0 -0
  43. {dulus-0.2.28 → dulus-0.2.30}/data/plugins/composio/plugin_tool.py +0 -0
  44. {dulus-0.2.28 → dulus-0.2.30}/data/tasks.json +0 -0
  45. {dulus-0.2.28 → dulus-0.2.30}/docs/README.md +0 -0
  46. {dulus-0.2.28 → dulus-0.2.30}/docs/__init__.py +0 -0
  47. {dulus-0.2.28 → dulus-0.2.30}/docs/api.html +0 -0
  48. {dulus-0.2.28 → dulus-0.2.30}/docs/architecture.md +0 -0
  49. {dulus-0.2.28 → dulus-0.2.30}/docs/azure-speech-template.json +0 -0
  50. {dulus-0.2.28 → dulus-0.2.30}/docs/dashboard/index.html +0 -0
  51. {dulus-0.2.28 → dulus-0.2.30}/docs/divider.svg +0 -0
  52. {dulus-0.2.28 → dulus-0.2.30}/docs/generate.py +0 -0
  53. {dulus-0.2.28 → dulus-0.2.30}/docs/hero.svg +0 -0
  54. {dulus-0.2.28 → dulus-0.2.30}/docs/index.html +0 -0
  55. {dulus-0.2.28 → dulus-0.2.30}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.28 → dulus-0.2.30}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.28 → dulus-0.2.30}/docs/personas/index.html +0 -0
  58. {dulus-0.2.28 → dulus-0.2.30}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.28 → dulus-0.2.30}/docs/preview.html +0 -0
  60. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.28 → dulus-0.2.30}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.28 → dulus-0.2.30}/docs/spinners.svg +0 -0
  72. {dulus-0.2.28 → dulus-0.2.30}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.28 → dulus-0.2.30}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.28 → dulus-0.2.30}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.28 → dulus-0.2.30}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.28 → dulus-0.2.30}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.28 → dulus-0.2.30}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.28 → dulus-0.2.30}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.28 → dulus-0.2.30}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.28 → dulus-0.2.30}/dulus_gui.py +0 -0
  81. {dulus-0.2.28 → dulus-0.2.30}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.28 → dulus-0.2.30}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.28 → dulus-0.2.30}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.28 → dulus-0.2.30}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.28 → dulus-0.2.30}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.28 → dulus-0.2.30}/gui/__init__.py +0 -0
  87. {dulus-0.2.28 → dulus-0.2.30}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.28 → dulus-0.2.30}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.28 → dulus-0.2.30}/gui/main_window.py +0 -0
  90. {dulus-0.2.28 → dulus-0.2.30}/gui/personas.py +0 -0
  91. {dulus-0.2.28 → dulus-0.2.30}/gui/session_utils.py +0 -0
  92. {dulus-0.2.28 → dulus-0.2.30}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.28 → dulus-0.2.30}/gui/sidebar.py +0 -0
  94. {dulus-0.2.28 → dulus-0.2.30}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.28 → dulus-0.2.30}/gui/themes.py +0 -0
  96. {dulus-0.2.28 → dulus-0.2.30}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.28 → dulus-0.2.30}/input.py +0 -0
  98. {dulus-0.2.28 → dulus-0.2.30}/license_manager.py +0 -0
  99. {dulus-0.2.28 → dulus-0.2.30}/memory/__init__.py +0 -0
  100. {dulus-0.2.28 → dulus-0.2.30}/memory/audit.py +0 -0
  101. {dulus-0.2.28 → dulus-0.2.30}/memory/consolidator.py +0 -0
  102. {dulus-0.2.28 → dulus-0.2.30}/memory/context.py +0 -0
  103. {dulus-0.2.28 → dulus-0.2.30}/memory/offload.py +0 -0
  104. {dulus-0.2.28 → dulus-0.2.30}/memory/palace.py +0 -0
  105. {dulus-0.2.28 → dulus-0.2.30}/memory/scan.py +0 -0
  106. {dulus-0.2.28 → dulus-0.2.30}/memory/sessions.py +0 -0
  107. {dulus-0.2.28 → dulus-0.2.30}/memory/store.py +0 -0
  108. {dulus-0.2.28 → dulus-0.2.30}/memory/tools.py +0 -0
  109. {dulus-0.2.28 → dulus-0.2.30}/memory/types.py +0 -0
  110. {dulus-0.2.28 → dulus-0.2.30}/memory/vector_search.py +0 -0
  111. {dulus-0.2.28 → dulus-0.2.30}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.28 → dulus-0.2.30}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.28 → dulus-0.2.30}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.28 → dulus-0.2.30}/offload_helper.py +0 -0
  115. {dulus-0.2.28 → dulus-0.2.30}/plugin/__init__.py +0 -0
  116. {dulus-0.2.28 → dulus-0.2.30}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.28 → dulus-0.2.30}/plugin/loader.py +0 -0
  118. {dulus-0.2.28 → dulus-0.2.30}/plugin/recommend.py +0 -0
  119. {dulus-0.2.28 → dulus-0.2.30}/plugin/store.py +0 -0
  120. {dulus-0.2.28 → dulus-0.2.30}/plugin/types.py +0 -0
  121. {dulus-0.2.28 → dulus-0.2.30}/providers.py +0 -0
  122. {dulus-0.2.28 → dulus-0.2.30}/setup.cfg +0 -0
  123. {dulus-0.2.28 → dulus-0.2.30}/skill/__init__.py +0 -0
  124. {dulus-0.2.28 → dulus-0.2.30}/skill/builtin.py +0 -0
  125. {dulus-0.2.28 → dulus-0.2.30}/skill/clawhub.py +0 -0
  126. {dulus-0.2.28 → dulus-0.2.30}/skill/executor.py +0 -0
  127. {dulus-0.2.28 → dulus-0.2.30}/skill/loader.py +0 -0
  128. {dulus-0.2.28 → dulus-0.2.30}/skill/tools.py +0 -0
  129. {dulus-0.2.28 → dulus-0.2.30}/skills.py +0 -0
  130. {dulus-0.2.28 → dulus-0.2.30}/spinner.py +0 -0
  131. {dulus-0.2.28 → dulus-0.2.30}/string_utils.py +0 -0
  132. {dulus-0.2.28 → dulus-0.2.30}/subagent.py +0 -0
  133. {dulus-0.2.28 → dulus-0.2.30}/task/__init__.py +0 -0
  134. {dulus-0.2.28 → dulus-0.2.30}/task/store.py +0 -0
  135. {dulus-0.2.28 → dulus-0.2.30}/task/tools.py +0 -0
  136. {dulus-0.2.28 → dulus-0.2.30}/task/types.py +0 -0
  137. {dulus-0.2.28 → dulus-0.2.30}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.28 → dulus-0.2.30}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.28 → dulus-0.2.30}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.28 → dulus-0.2.30}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.28 → dulus-0.2.30}/tests/test_license.py +0 -0
  142. {dulus-0.2.28 → dulus-0.2.30}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.28 → dulus-0.2.30}/tests/test_memory.py +0 -0
  144. {dulus-0.2.28 → dulus-0.2.30}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.28 → dulus-0.2.30}/tests/test_skills.py +0 -0
  146. {dulus-0.2.28 → dulus-0.2.30}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.28 → dulus-0.2.30}/tests/test_task.py +0 -0
  148. {dulus-0.2.28 → dulus-0.2.30}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.28 → dulus-0.2.30}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.28 → dulus-0.2.30}/tests/test_voice.py +0 -0
  151. {dulus-0.2.28 → dulus-0.2.30}/tmux_offloader.py +0 -0
  152. {dulus-0.2.28 → dulus-0.2.30}/tmux_tools.py +0 -0
  153. {dulus-0.2.28 → dulus-0.2.30}/tool_registry.py +0 -0
  154. {dulus-0.2.28 → dulus-0.2.30}/tools.py +0 -0
  155. {dulus-0.2.28 → dulus-0.2.30}/ui/__init__.py +0 -0
  156. {dulus-0.2.28 → dulus-0.2.30}/ui/input.py +0 -0
  157. {dulus-0.2.28 → dulus-0.2.30}/ui/render.py +0 -0
  158. {dulus-0.2.28 → dulus-0.2.30}/voice/__init__.py +0 -0
  159. {dulus-0.2.28 → dulus-0.2.30}/voice/keyterms.py +0 -0
  160. {dulus-0.2.28 → dulus-0.2.30}/voice/recorder.py +0 -0
  161. {dulus-0.2.28 → dulus-0.2.30}/voice/stt.py +0 -0
  162. {dulus-0.2.28 → dulus-0.2.30}/voice/tts.py +0 -0
  163. {dulus-0.2.28 → dulus-0.2.30}/webchat.py +0 -0
  164. {dulus-0.2.28 → dulus-0.2.30}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.28
3
+ Version: 0.2.30
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.28-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.30-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -22,7 +22,7 @@ SET /sticky_input ON since the first run for the best experience!
22
22
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
23
23
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
24
24
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
25
- <img src="https://img.shields.io/badge/version-v0.2.28-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
25
+ <img src="https://img.shields.io/badge/version-v0.2.30-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
26
26
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
27
27
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
28
28
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -3,6 +3,17 @@
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
5
 
6
+ - May 09, 2026 (**v0.2.30**): **`/bg start` daemon is now truly windowless on Windows** — `python.exe` is a console-subsystem binary, so even with `DETACHED_PROCESS` Windows still spun up a visible console window for the daemon. Closing that window killed the daemon. Switched to `pythonw.exe` (the GUI-subsystem variant) + `CREATE_NO_WINDOW` so the daemon spawns with NO console window at all. Verified: `Get-Process` reports `MainWindowHandle = 0` after spawn — there's literally nothing to close. Telegram + WebChat + IPC keep running in background until `/bg stop` or `/bg kill`.
7
+
8
+ - May 09, 2026 (**v0.2.29**): **`/bg start` actually works from inside a REPL + daemon-mode webchat default-on + tested end-to-end this time**
9
+ - **The whole point of `/bg start` was broken from day one.** A REPL itself binds `127.0.0.1:5151` to serve `dulus "..."` shell calls, so the moment you typed `/bg start` from inside that REPL, the duplicate-detection check saw "port in use" and refused — by the very REPL invoking the command. `/bg kill` then killed the only thing on the port: your own REPL. Pure logic flaw on me.
10
+ - **Now `/bg start` releases the REPL's own IPC first.** When invoked from inside a REPL, the command stops the REPL's IPC thread, force-closes the socket with `SO_LINGER {1, 0}` (skips TIME_WAIT), waits ~600ms for the OS to free the port, and only then spawns the daemon. The REPL keeps running — it just becomes a normal client whose `dulus "..."` dispatches now go to the daemon.
11
+ - **Daemon mode auto-starts WebChat by default.** Previously WebChat only fired when `/bg start` injected an env var; running `dulus --daemon` directly gave you IPC + Telegram but no browser endpoint. Now `--daemon` always starts WebChat on `127.0.0.1:5000` (loopback), opt-out with `webchat_disabled: true` in config or `DULUS_DAEMON_NO_WEB=1`.
12
+ - **Daemon callback was calling `agent.run()` with the wrong signature.** Earlier I passed `agent_run(state, msg, config, is_background=True)` but the real signature is `(user_message, state, config, system_prompt, ...)`. Every Telegram/IPC turn raised `TypeError` silently, daemon looked alive but never answered. Fixed and verified by `inspect.signature`.
13
+ - **`/bg status` now distinguishes REPL from daemon.** Source of truth is the `BG_PID` file (only `/bg start` writes it). If port is in use but no PID file, status says "likely your own Dulus REPL — for a true headless daemon, run `dulus --daemon` from a fresh shell".
14
+ - **`/bg kill` only targets the real daemon now.** Reads `BG_PID`, kills only that PID (refuses if PID matches the calling process). Will not nuke the REPL you're typing in. Falls through to `taskkill /F` on Windows if SIGTERM doesn't work.
15
+ - **End-to-end smoke tested.** A throwaway harness boots a fake REPL on 5151, sends a JSON request and gets `{"response": "REPL echoes: hello"}`, releases the port, spawns a fake daemon on the same port, sends another request and gets `{"response": "DAEMON echoes: klk papi"}`. Handoff works without TIME_WAIT bind failures.
16
+
6
17
  - May 09, 2026 (**v0.2.28**): **IPC server thread no longer crashes on idle / dropped connections** — `conn.recv()` was hitting its 60s read timeout and raising `TimeoutError` out of the worker, killing the IPC thread silently. After that, the daemon was still running but `dulus "..."` from any shell would hang forever. Caught socket.timeout / ConnectionReset / BrokenPipe / OSError around the request-handling block so a single bad client can't take down the server.
7
18
 
8
19
  - May 09, 2026 (**v0.2.27**): **`/bg` no longer leaves stale state** — when something else (a REPL, an old daemon) is already on `127.0.0.1:5151`, `/bg start` now refuses to spawn a duplicate that would just fail to bind, explains what's happening, and clears the stale PID file. `/bg status` self-heals: when it sees a stale PID it auto-removes it instead of complaining on every call. The previous "Some Dulus is listening on IPC, but our PID file is stale" warning is gone for good.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.28
3
+ Version: 0.2.30
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.28-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.30-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -122,6 +122,26 @@ if sys.platform == "win32":
122
122
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
123
123
  if hasattr(sys.stderr, "reconfigure"):
124
124
  sys.stderr.reconfigure(encoding="utf-8", errors="replace")
125
+
126
+ # ── Suppress noisy third-party startup warnings ──────────────────────────
127
+ # These don't affect functionality but pollute every Dulus boot (REPL,
128
+ # daemon, --print, every shell call). Filtered globally so logs stay clean.
129
+ import warnings as _warnings
130
+ # requests >= 2.32 nags about urllib3/chardet version pins on Python 3.13+.
131
+ _warnings.filterwarnings("ignore", message=r".*urllib3.*")
132
+ _warnings.filterwarnings("ignore", message=r".*chardet.*charset_normalizer.*")
133
+ _warnings.filterwarnings("ignore", message=r".*RequestsDependencyWarning.*")
134
+ # Dulus's own dev-license warning — only relevant if you're building
135
+ # license keys for production, not noise we want on every boot.
136
+ _warnings.filterwarnings("ignore", message=r".*DULUS_LICENSE_SECRET.*")
137
+ # Catch-all: any RequestsDependencyWarning by category, regardless of msg.
138
+ try:
139
+ from requests.exceptions import RequestsDependencyWarning as _RDW # type: ignore
140
+ _warnings.filterwarnings("ignore", category=_RDW)
141
+ except Exception:
142
+ pass
143
+ # pkg_resources / setuptools-based deprecations from optional plugins.
144
+ _warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"pkg_resources.*")
125
145
  from pathlib import Path
126
146
 
127
147
  # ── Global Import Hook ───────────────────────────────────────────────────────
@@ -218,7 +238,7 @@ try:
218
238
  from importlib.metadata import version as _pkg_version
219
239
  VERSION = _pkg_version("dulus")
220
240
  except Exception:
221
- VERSION = "0.2.28" # dev fallback — keep in sync with pyproject.toml
241
+ VERSION = "0.2.30" # dev fallback — keep in sync with pyproject.toml
222
242
 
223
243
  # ── ANSI helpers (used even with rich for non-markdown output) ─────────────
224
244
  from common import C, clr, info, ok, warn, err, stream_thinking, print_tool_start, print_tool_end, sanitize_text
@@ -1514,7 +1534,8 @@ def cmd_bg(args: str, _state, config) -> bool:
1514
1534
  and Telegram simultaneously.
1515
1535
 
1516
1536
  /bg start [--web-port PORT] — spawn detached daemon + webchat
1517
- /bg stop — kill the background daemon
1537
+ /bg stop — kill the background daemon (uses PID file)
1538
+ /bg kill — nuke whatever's on port 5151 (no PID file needed)
1518
1539
  /bg status — is it alive? on which ports?
1519
1540
  /bg attach — print how to attach (tmux on unix, URL on win)
1520
1541
 
@@ -1568,31 +1589,35 @@ def cmd_bg(args: str, _state, config) -> bool:
1568
1589
  return False
1569
1590
 
1570
1591
  # ── /bg status ────────────────────────────────────────────────────────
1592
+ # Source of truth for "is a real detached daemon running": the BG_PID
1593
+ # file. A REPL also binds 5151 but doesn't write the PID file, so we
1594
+ # can distinguish "the user's own REPL" from "a true headless daemon".
1571
1595
  if sub == "status":
1572
1596
  pid = _read_pid()
1573
1597
  alive = _is_alive(pid)
1574
1598
  ipc = _ipc_alive()
1575
1599
  if alive and ipc:
1576
- ok(f"Dulus background: RUNNING")
1600
+ ok(f"Dulus background daemon: RUNNING")
1577
1601
  info(f" PID: {pid}")
1578
1602
  info(f" IPC: 127.0.0.1:{DULUS_IPC_PORT} (responding)")
1579
1603
  info(f" Web: http://127.0.0.1:{config.get('_webchat_port', 5000)}/")
1580
1604
  info(f" Log: {BG_LOG}")
1581
1605
  elif alive and not ipc:
1582
- warn(f"PID {pid} alive but IPC port not responding (still booting?)")
1606
+ warn(f"Daemon PID {pid} alive but IPC not responding (still booting?)")
1583
1607
  elif ipc:
1584
- warn("Another Dulus (REPL or older daemon) is on the IPC port.")
1585
- info(f" IPC: 127.0.0.1:{DULUS_IPC_PORT} (responding, but not our daemon)")
1586
- info(" You can still reach it via `dulus \"...\"` from any shell.")
1587
- info(" /bg start won't spawn a duplicatekill the other one first if needed.")
1588
- # Clear the stale PID file so this warning self-heals next time.
1608
+ # No PID file (or stale) but port is in use this is almost
1609
+ # certainly the user's own REPL serving IPC, not a daemon.
1610
+ info("No background daemon running.")
1611
+ info(f" But port {DULUS_IPC_PORT} is in uselikely your own Dulus REPL.")
1612
+ info(" `dulus \"...\"` from any shell still works (it'll hit your REPL).")
1613
+ info(" For a TRUE headless daemon (Telegram surviving close), start it from")
1614
+ info(" a fresh shell with no REPL open: `dulus --daemon` or `/bg start`.")
1589
1615
  try:
1590
1616
  BG_PID.unlink()
1591
1617
  except FileNotFoundError:
1592
1618
  pass
1593
1619
  else:
1594
- info("Dulus background: NOT RUNNING")
1595
- # Clean up any stale PID file just in case.
1620
+ info("Dulus background daemon: NOT RUNNING")
1596
1621
  try:
1597
1622
  BG_PID.unlink()
1598
1623
  except FileNotFoundError:
@@ -1644,23 +1669,105 @@ def cmd_bg(args: str, _state, config) -> bool:
1644
1669
  info(f" • Tail log: tail -f {BG_LOG}")
1645
1670
  return True
1646
1671
 
1672
+ # ── /bg kill ──────────────────────────────────────────────────────────
1673
+ # Force-stop the background DAEMON only — never the user's own REPL.
1674
+ # We use the BG_PID file as the source of truth: only /bg start writes
1675
+ # it, so it uniquely identifies the detached daemon. If no BG_PID file
1676
+ # exists, we refuse to kill anything (port may be held by a REPL the
1677
+ # user wants to keep). For SIGKILL escalation we use taskkill on Windows.
1678
+ if sub == "kill":
1679
+ f_pid = _read_pid()
1680
+ own_pid = _os.getpid()
1681
+
1682
+ if not f_pid:
1683
+ info("No background daemon to kill (BG_PID file missing).")
1684
+ info(f" If port {DULUS_IPC_PORT} is in use, it's likely your own REPL.")
1685
+ info(" Close your REPL (/exit) to free the port — /bg kill won't touch it.")
1686
+ return True
1687
+
1688
+ if f_pid == own_pid:
1689
+ warn("Refusing to kill the current process (that's me, the REPL you're in).")
1690
+ try:
1691
+ BG_PID.unlink()
1692
+ except FileNotFoundError:
1693
+ pass
1694
+ return True
1695
+
1696
+ if not _is_alive(f_pid):
1697
+ info(f"Daemon PID {f_pid} is already dead. Cleaning up PID file.")
1698
+ try:
1699
+ BG_PID.unlink()
1700
+ except FileNotFoundError:
1701
+ pass
1702
+ return True
1703
+
1704
+ # Send SIGTERM, give it 1s, escalate to SIGKILL/taskkill if it lingers.
1705
+ try:
1706
+ _os.kill(f_pid, _signal.SIGTERM)
1707
+ ok(f"Sent SIGTERM to daemon PID {f_pid}.")
1708
+ except (ProcessLookupError, PermissionError, OSError) as e:
1709
+ err(f"Could not signal PID {f_pid}: {e}")
1710
+ return True
1711
+
1712
+ for _ in range(8):
1713
+ if not _is_alive(f_pid):
1714
+ break
1715
+ _time.sleep(0.25)
1716
+
1717
+ if _is_alive(f_pid):
1718
+ warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1719
+ try:
1720
+ if _sys.platform == "win32":
1721
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1722
+ capture_output=True, timeout=5)
1723
+ else:
1724
+ _os.kill(f_pid, _signal.SIGKILL)
1725
+ except Exception as e:
1726
+ err(f"SIGKILL failed: {e}")
1727
+ return True
1728
+
1729
+ try:
1730
+ BG_PID.unlink()
1731
+ except FileNotFoundError:
1732
+ pass
1733
+ ok(f"Daemon (PID {f_pid}) stopped.")
1734
+ return True
1735
+
1647
1736
  # ── /bg start ─────────────────────────────────────────────────────────
1648
1737
  if sub == "start":
1649
1738
  # Already running?
1650
1739
  existing_pid = _read_pid()
1651
- if _is_alive(existing_pid) and _ipc_alive():
1740
+ if _is_alive(existing_pid) and _ipc_alive() and existing_pid != _os.getpid():
1652
1741
  info(f"Already running (PID {existing_pid}). Use `/bg status` for details.")
1653
1742
  return True
1654
1743
 
1655
- # Some other Dulus (a REPL on this machine, an old daemon, etc.) is
1656
- # holding the IPC port. Spawning a new daemon would just fail to bind
1657
- # and leave a stale PID. Better: tell the user what's up.
1744
+ # If THIS REPL owns the IPC port, release it first so the spawned
1745
+ # daemon can bind. Without this, /bg start from inside a REPL would
1746
+ # always fail because the very REPL invoking it is holding 5151.
1747
+ # We stop our own IPC server thread, give the OS a moment to free
1748
+ # the socket, and then proceed to spawn the daemon. The REPL keeps
1749
+ # running fine — it just becomes a normal client (its `dulus "..."`
1750
+ # dispatches still work, they just go to the daemon now).
1751
+ if _ipc_alive() and config.get("_ipc_thread") is not None:
1752
+ info("Releasing this REPL's IPC port so the daemon can take over...")
1753
+ config["_ipc_stop"] = True
1754
+ ipc_thread = config.get("_ipc_thread")
1755
+ try:
1756
+ ipc_thread.join(timeout=2.5)
1757
+ except Exception:
1758
+ pass
1759
+ # Clear the marker so a future /bg stop or restart doesn't reuse it.
1760
+ config["_ipc_thread"] = None
1761
+ config.pop("_ipc_listening", None)
1762
+ # Brief sleep to let the OS reclaim the port (TIME_WAIT etc.).
1763
+ _time.sleep(0.6)
1764
+
1765
+ # If something *else* still holds the port (an external Dulus, a
1766
+ # stale daemon from a crash, etc.), refuse cleanly so we don't leave
1767
+ # a stale PID file.
1658
1768
  if _ipc_alive():
1659
- warn(f"Port {DULUS_IPC_PORT} is already in use by another Dulus process.")
1660
- info("If that's a REPL you're running, this Dulus is already reachable")
1661
- info(f" via `dulus \"...\"` from any shell — no /bg start needed.")
1662
- info("If it's a stale daemon, find and kill it manually, then retry.")
1663
- # Clean up the stale PID file so /bg status stops complaining.
1769
+ warn(f"Port {DULUS_IPC_PORT} is in use by another process I don't own.")
1770
+ info("Run `/bg kill` first if it's a stale daemon, or close the other Dulus.")
1664
1771
  try:
1665
1772
  BG_PID.unlink()
1666
1773
  except FileNotFoundError:
@@ -1679,20 +1786,32 @@ def cmd_bg(args: str, _state, config) -> bool:
1679
1786
  from config import save_config
1680
1787
  save_config(config)
1681
1788
 
1682
- # Build the spawn command. Prefer the installed `dulus` shim; fall
1683
- # back to `python dulus.py` when running from source.
1684
- dulus_bin = None
1685
- for cand in ["dulus", "dulus.exe"]:
1686
- from shutil import which
1687
- p = which(cand)
1688
- if p:
1689
- dulus_bin = p
1690
- break
1691
- if dulus_bin:
1692
- cmd = [dulus_bin, "--daemon"]
1693
- else:
1789
+ # Build the spawn command. On Windows we MUST use pythonw.exe (windowless
1790
+ # variant) instead of the console-subsystem python.exe / dulus shim,
1791
+ # otherwise Windows creates a visible console window for the daemon
1792
+ # and closing it kills the process. The shim itself runs python.exe,
1793
+ # so we go around it by invoking pythonw -m dulus directly.
1794
+ if _sys.platform == "win32":
1795
+ pythonw = _sys.executable.replace("python.exe", "pythonw.exe")
1796
+ if not _os.path.exists(pythonw):
1797
+ # Fall back to python.exe if pythonw isn't shipped (rare;
1798
+ # mostly happens on stripped embeddable distributions).
1799
+ pythonw = _sys.executable
1694
1800
  dulus_script = _os.path.abspath(__file__)
1695
- cmd = [_sys.executable, dulus_script, "--daemon"]
1801
+ cmd = [pythonw, dulus_script, "--daemon"]
1802
+ else:
1803
+ from shutil import which
1804
+ dulus_bin = None
1805
+ for cand in ["dulus", "dulus.exe"]:
1806
+ p = which(cand)
1807
+ if p:
1808
+ dulus_bin = p
1809
+ break
1810
+ if dulus_bin:
1811
+ cmd = [dulus_bin, "--daemon"]
1812
+ else:
1813
+ dulus_script = _os.path.abspath(__file__)
1814
+ cmd = [_sys.executable, dulus_script, "--daemon"]
1696
1815
 
1697
1816
  # Pass the auto-webchat hint via env so the daemon picks it up.
1698
1817
  env = _os.environ.copy()
@@ -1703,13 +1822,18 @@ def cmd_bg(args: str, _state, config) -> bool:
1703
1822
  log_fp = open(BG_LOG, "ab")
1704
1823
  try:
1705
1824
  if _sys.platform == "win32":
1706
- # CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
1707
- DETACHED = 0x00000008
1825
+ # CREATE_NO_WINDOW (0x08000000) suppresses the console window
1826
+ # entirely — cannot be combined with DETACHED_PROCESS, but
1827
+ # because we're invoking pythonw.exe (a GUI-subsystem binary)
1828
+ # there is no console to inherit from in the first place.
1829
+ # CREATE_NEW_PROCESS_GROUP keeps Ctrl+C in the parent shell
1830
+ # from killing the daemon when the parent later exits.
1831
+ CREATE_NO_WINDOW = 0x08000000
1708
1832
  NEW_GROUP = 0x00000200
1709
1833
  proc = _sp.Popen(
1710
1834
  cmd,
1711
1835
  stdout=log_fp, stderr=log_fp, stdin=_sp.DEVNULL,
1712
- creationflags=DETACHED | NEW_GROUP,
1836
+ creationflags=CREATE_NO_WINDOW | NEW_GROUP,
1713
1837
  close_fds=True,
1714
1838
  env=env,
1715
1839
  )
@@ -4095,10 +4219,26 @@ def _ipc_server_loop(config, state):
4095
4219
  except Exception:
4096
4220
  pass
4097
4221
 
4222
+ # Release the port immediately on shutdown so a daemon spawned right
4223
+ # after `/bg start` can bind without waiting for TIME_WAIT to expire.
4224
+ # SO_LINGER {onoff:1, linger:0} forces an RST close that bypasses
4225
+ # the TIME_WAIT state (cost: any in-flight bytes are dropped, which is
4226
+ # fine — we're not sending anything when we shut down).
4227
+ try:
4228
+ import struct as _struct
4229
+ sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_LINGER,
4230
+ _struct.pack("ii", 1, 0))
4231
+ except Exception:
4232
+ pass
4233
+ try:
4234
+ sock.shutdown(_socket.SHUT_RDWR)
4235
+ except Exception:
4236
+ pass
4098
4237
  try:
4099
4238
  sock.close()
4100
4239
  except Exception:
4101
4240
  pass
4241
+ config["_ipc_listening"] = False
4102
4242
 
4103
4243
 
4104
4244
  def _try_ipc_dispatch(prompt: str, timeout: float = 0.4) -> bool:
@@ -5608,29 +5748,46 @@ def _run_daemon(config: dict) -> None:
5608
5748
  config["_session_id"] = session_id
5609
5749
  config["_last_interaction_time"] = time.time()
5610
5750
 
5611
- # Same callback used by the REPL so Telegram can trigger runs
5612
- # NB: the run_query referenced here lives on the global namespace once the
5613
- # daemon is running with --daemon (we provide a thin wrapper below).
5614
- def _daemon_run_query(msg, is_background=True):
5751
+ # Same callback used by the REPL so Telegram / IPC can trigger runs.
5752
+ # The `agent.run()` signature is (user_message, state, config, system_prompt, ...)
5753
+ # earlier I called it with the wrong arg order + a non-existent
5754
+ # `is_background` kwarg, which made every Telegram/IPC turn raise
5755
+ # silently and never actually answer the user. Fixed now.
5756
+ def _daemon_run_query(msg):
5615
5757
  try:
5616
5758
  from agent import run as agent_run
5617
- for _ in agent_run(state, msg, config, is_background=is_background):
5618
- pass
5759
+ from context import build_system_prompt
5760
+ sys_prompt = build_system_prompt(config)
5761
+ # Append the user message to state so build_system_prompt-aware
5762
+ # turns and history work correctly.
5763
+ for ev in agent_run(msg, state, config, sys_prompt):
5764
+ # Drain the generator — we don't need to render in daemon mode,
5765
+ # the Telegram bridge / IPC server reads the final assistant
5766
+ # message off `state.messages` after this returns.
5767
+ _ = ev
5619
5768
  except Exception as _e:
5620
5769
  err(f"daemon run_query error: {type(_e).__name__}: {_e}")
5621
- config["_run_query_callback"] = lambda msg: _daemon_run_query(msg, is_background=True)
5622
-
5623
- # Auto-start the webchat server alongside the daemon when /bg start asked
5624
- # for it same session, same state. One Dulus, three entry points (CLI
5625
- # via IPC, browser via webchat, Telegram via bridge).
5770
+ config["_run_query_callback"] = _daemon_run_query
5771
+
5772
+ # Auto-start the webchat server alongside the daemon always, by default.
5773
+ # The whole point of daemon mode is "headless Dulus serving every entry
5774
+ # point at once" (CLI via IPC, browser via WebChat, Telegram via bridge).
5775
+ # Skip only if config["webchat_disabled"] is true OR env var
5776
+ # DULUS_DAEMON_NO_WEB=1 is set (escape hatch for users who explicitly
5777
+ # don't want a browser endpoint exposed even on loopback).
5626
5778
  import os as _os_d
5627
- if _os_d.environ.get("DULUS_BG_AUTO_WEBCHAT") == "1":
5628
- config["_bg_start_webchat"] = True
5629
- try:
5630
- config["_webchat_port"] = int(_os_d.environ.get("DULUS_BG_WEBCHAT_PORT", 5000))
5631
- except ValueError:
5632
- pass
5633
- if config.get("_bg_start_webchat"):
5779
+ _no_web = (
5780
+ config.get("webchat_disabled")
5781
+ or _os_d.environ.get("DULUS_DAEMON_NO_WEB") == "1"
5782
+ )
5783
+ if not _no_web:
5784
+ # If /bg start passed an explicit port through env, honor it.
5785
+ env_port = _os_d.environ.get("DULUS_BG_WEBCHAT_PORT")
5786
+ if env_port:
5787
+ try:
5788
+ config["_webchat_port"] = int(env_port)
5789
+ except ValueError:
5790
+ pass
5634
5791
  try:
5635
5792
  import webchat_server as _wc
5636
5793
  _wc_port = int(config.get("_webchat_port", 5000))
@@ -7135,7 +7292,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
7135
7292
  "todo", "in-progress", "done", "blocked"]),
7136
7293
  "proactive": ("Manage proactive background watcher", ["off"]),
7137
7294
  "daemon": ("Toggle daemon — allow external triggers (Telegram) to spawn Dulus", ["on", "off"]),
7138
- "bg": ("Background Dulus — one detached daemon for CLI + Web + Telegram", ["start", "stop", "status", "attach"]),
7295
+ "bg": ("Background Dulus — one detached daemon for CLI + Web + Telegram", ["start", "stop", "kill", "status", "attach"]),
7139
7296
  "lite": ("Toggle lite mode (reduce system prompt)", ["on", "off"]),
7140
7297
  "rtk": ("Toggle RTK token-optimized shell rewriting", ["on", "off"]),
7141
7298
  "cloudsave": ("Cloud-sync sessions to GitHub Gist", ["setup", "auto", "list", "load", "push"]),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.28"
7
+ version = "0.2.30"
8
8
  description = "Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
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
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