dulus 0.2.29__tar.gz → 0.2.31__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.29/dulus.egg-info → dulus-0.2.31}/PKG-INFO +2 -2
  2. {dulus-0.2.29 → dulus-0.2.31}/README.md +1 -1
  3. {dulus-0.2.29 → dulus-0.2.31}/docs/news.md +2 -0
  4. {dulus-0.2.29 → dulus-0.2.31/dulus.egg-info}/PKG-INFO +2 -2
  5. {dulus-0.2.29 → dulus-0.2.31}/dulus.py +213 -56
  6. {dulus-0.2.29 → dulus-0.2.31}/pyproject.toml +1 -1
  7. {dulus-0.2.29 → dulus-0.2.31}/LICENSE +0 -0
  8. {dulus-0.2.29 → dulus-0.2.31}/MANIFEST.in +0 -0
  9. {dulus-0.2.29 → dulus-0.2.31}/agent.py +0 -0
  10. {dulus-0.2.29 → dulus-0.2.31}/backend/__init__.py +0 -0
  11. {dulus-0.2.29 → dulus-0.2.31}/backend/compressor.py +0 -0
  12. {dulus-0.2.29 → dulus-0.2.31}/backend/context.py +0 -0
  13. {dulus-0.2.29 → dulus-0.2.31}/backend/githook.py +0 -0
  14. {dulus-0.2.29 → dulus-0.2.31}/backend/marketplace.py +0 -0
  15. {dulus-0.2.29 → dulus-0.2.31}/backend/mempalace_bridge.py +0 -0
  16. {dulus-0.2.29 → dulus-0.2.31}/backend/personas.py +0 -0
  17. {dulus-0.2.29 → dulus-0.2.31}/backend/plugins.py +0 -0
  18. {dulus-0.2.29 → dulus-0.2.31}/backend/server.py +0 -0
  19. {dulus-0.2.29 → dulus-0.2.31}/backend/tasks.py +0 -0
  20. {dulus-0.2.29 → dulus-0.2.31}/batch_api.py +0 -0
  21. {dulus-0.2.29 → dulus-0.2.31}/checkpoint/__init__.py +0 -0
  22. {dulus-0.2.29 → dulus-0.2.31}/checkpoint/hooks.py +0 -0
  23. {dulus-0.2.29 → dulus-0.2.31}/checkpoint/store.py +0 -0
  24. {dulus-0.2.29 → dulus-0.2.31}/checkpoint/types.py +0 -0
  25. {dulus-0.2.29 → dulus-0.2.31}/claude_code_watcher.py +0 -0
  26. {dulus-0.2.29 → dulus-0.2.31}/clipboard_utils.py +0 -0
  27. {dulus-0.2.29 → dulus-0.2.31}/cloudsave.py +0 -0
  28. {dulus-0.2.29 → dulus-0.2.31}/common.py +0 -0
  29. {dulus-0.2.29 → dulus-0.2.31}/compaction.py +0 -0
  30. {dulus-0.2.29 → dulus-0.2.31}/config.py +0 -0
  31. {dulus-0.2.29 → dulus-0.2.31}/context.py +0 -0
  32. {dulus-0.2.29 → dulus-0.2.31}/data/__init__.py +0 -0
  33. {dulus-0.2.29 → dulus-0.2.31}/data/active_persona.json +0 -0
  34. {dulus-0.2.29 → dulus-0.2.31}/data/context.json +0 -0
  35. {dulus-0.2.29 → dulus-0.2.31}/data/marketplace.json +0 -0
  36. {dulus-0.2.29 → dulus-0.2.31}/data/personas.json +0 -0
  37. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/__init__.py +0 -0
  38. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/__init__.py +0 -0
  39. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  40. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  41. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  42. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/plugin.json +0 -0
  43. {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/plugin_tool.py +0 -0
  44. {dulus-0.2.29 → dulus-0.2.31}/data/tasks.json +0 -0
  45. {dulus-0.2.29 → dulus-0.2.31}/docs/README.md +0 -0
  46. {dulus-0.2.29 → dulus-0.2.31}/docs/__init__.py +0 -0
  47. {dulus-0.2.29 → dulus-0.2.31}/docs/api.html +0 -0
  48. {dulus-0.2.29 → dulus-0.2.31}/docs/architecture.md +0 -0
  49. {dulus-0.2.29 → dulus-0.2.31}/docs/azure-speech-template.json +0 -0
  50. {dulus-0.2.29 → dulus-0.2.31}/docs/dashboard/index.html +0 -0
  51. {dulus-0.2.29 → dulus-0.2.31}/docs/divider.svg +0 -0
  52. {dulus-0.2.29 → dulus-0.2.31}/docs/generate.py +0 -0
  53. {dulus-0.2.29 → dulus-0.2.31}/docs/hero.svg +0 -0
  54. {dulus-0.2.29 → dulus-0.2.31}/docs/index.html +0 -0
  55. {dulus-0.2.29 → dulus-0.2.31}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.29 → dulus-0.2.31}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.29 → dulus-0.2.31}/docs/personas/index.html +0 -0
  58. {dulus-0.2.29 → dulus-0.2.31}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.29 → dulus-0.2.31}/docs/preview.html +0 -0
  60. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.29 → dulus-0.2.31}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.29 → dulus-0.2.31}/docs/spinners.svg +0 -0
  72. {dulus-0.2.29 → dulus-0.2.31}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.29 → dulus-0.2.31}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.29 → dulus-0.2.31}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.29 → dulus-0.2.31}/dulus_gui.py +0 -0
  81. {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.29 → dulus-0.2.31}/gui/__init__.py +0 -0
  87. {dulus-0.2.29 → dulus-0.2.31}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.29 → dulus-0.2.31}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.29 → dulus-0.2.31}/gui/main_window.py +0 -0
  90. {dulus-0.2.29 → dulus-0.2.31}/gui/personas.py +0 -0
  91. {dulus-0.2.29 → dulus-0.2.31}/gui/session_utils.py +0 -0
  92. {dulus-0.2.29 → dulus-0.2.31}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.29 → dulus-0.2.31}/gui/sidebar.py +0 -0
  94. {dulus-0.2.29 → dulus-0.2.31}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.29 → dulus-0.2.31}/gui/themes.py +0 -0
  96. {dulus-0.2.29 → dulus-0.2.31}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.29 → dulus-0.2.31}/input.py +0 -0
  98. {dulus-0.2.29 → dulus-0.2.31}/license_manager.py +0 -0
  99. {dulus-0.2.29 → dulus-0.2.31}/memory/__init__.py +0 -0
  100. {dulus-0.2.29 → dulus-0.2.31}/memory/audit.py +0 -0
  101. {dulus-0.2.29 → dulus-0.2.31}/memory/consolidator.py +0 -0
  102. {dulus-0.2.29 → dulus-0.2.31}/memory/context.py +0 -0
  103. {dulus-0.2.29 → dulus-0.2.31}/memory/offload.py +0 -0
  104. {dulus-0.2.29 → dulus-0.2.31}/memory/palace.py +0 -0
  105. {dulus-0.2.29 → dulus-0.2.31}/memory/scan.py +0 -0
  106. {dulus-0.2.29 → dulus-0.2.31}/memory/sessions.py +0 -0
  107. {dulus-0.2.29 → dulus-0.2.31}/memory/store.py +0 -0
  108. {dulus-0.2.29 → dulus-0.2.31}/memory/tools.py +0 -0
  109. {dulus-0.2.29 → dulus-0.2.31}/memory/types.py +0 -0
  110. {dulus-0.2.29 → dulus-0.2.31}/memory/vector_search.py +0 -0
  111. {dulus-0.2.29 → dulus-0.2.31}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.29 → dulus-0.2.31}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.29 → dulus-0.2.31}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.29 → dulus-0.2.31}/offload_helper.py +0 -0
  115. {dulus-0.2.29 → dulus-0.2.31}/plugin/__init__.py +0 -0
  116. {dulus-0.2.29 → dulus-0.2.31}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.29 → dulus-0.2.31}/plugin/loader.py +0 -0
  118. {dulus-0.2.29 → dulus-0.2.31}/plugin/recommend.py +0 -0
  119. {dulus-0.2.29 → dulus-0.2.31}/plugin/store.py +0 -0
  120. {dulus-0.2.29 → dulus-0.2.31}/plugin/types.py +0 -0
  121. {dulus-0.2.29 → dulus-0.2.31}/providers.py +0 -0
  122. {dulus-0.2.29 → dulus-0.2.31}/setup.cfg +0 -0
  123. {dulus-0.2.29 → dulus-0.2.31}/skill/__init__.py +0 -0
  124. {dulus-0.2.29 → dulus-0.2.31}/skill/builtin.py +0 -0
  125. {dulus-0.2.29 → dulus-0.2.31}/skill/clawhub.py +0 -0
  126. {dulus-0.2.29 → dulus-0.2.31}/skill/executor.py +0 -0
  127. {dulus-0.2.29 → dulus-0.2.31}/skill/loader.py +0 -0
  128. {dulus-0.2.29 → dulus-0.2.31}/skill/tools.py +0 -0
  129. {dulus-0.2.29 → dulus-0.2.31}/skills.py +0 -0
  130. {dulus-0.2.29 → dulus-0.2.31}/spinner.py +0 -0
  131. {dulus-0.2.29 → dulus-0.2.31}/string_utils.py +0 -0
  132. {dulus-0.2.29 → dulus-0.2.31}/subagent.py +0 -0
  133. {dulus-0.2.29 → dulus-0.2.31}/task/__init__.py +0 -0
  134. {dulus-0.2.29 → dulus-0.2.31}/task/store.py +0 -0
  135. {dulus-0.2.29 → dulus-0.2.31}/task/tools.py +0 -0
  136. {dulus-0.2.29 → dulus-0.2.31}/task/types.py +0 -0
  137. {dulus-0.2.29 → dulus-0.2.31}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.29 → dulus-0.2.31}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.29 → dulus-0.2.31}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.29 → dulus-0.2.31}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.29 → dulus-0.2.31}/tests/test_license.py +0 -0
  142. {dulus-0.2.29 → dulus-0.2.31}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.29 → dulus-0.2.31}/tests/test_memory.py +0 -0
  144. {dulus-0.2.29 → dulus-0.2.31}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.29 → dulus-0.2.31}/tests/test_skills.py +0 -0
  146. {dulus-0.2.29 → dulus-0.2.31}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.29 → dulus-0.2.31}/tests/test_task.py +0 -0
  148. {dulus-0.2.29 → dulus-0.2.31}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.29 → dulus-0.2.31}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.29 → dulus-0.2.31}/tests/test_voice.py +0 -0
  151. {dulus-0.2.29 → dulus-0.2.31}/tmux_offloader.py +0 -0
  152. {dulus-0.2.29 → dulus-0.2.31}/tmux_tools.py +0 -0
  153. {dulus-0.2.29 → dulus-0.2.31}/tool_registry.py +0 -0
  154. {dulus-0.2.29 → dulus-0.2.31}/tools.py +0 -0
  155. {dulus-0.2.29 → dulus-0.2.31}/ui/__init__.py +0 -0
  156. {dulus-0.2.29 → dulus-0.2.31}/ui/input.py +0 -0
  157. {dulus-0.2.29 → dulus-0.2.31}/ui/render.py +0 -0
  158. {dulus-0.2.29 → dulus-0.2.31}/voice/__init__.py +0 -0
  159. {dulus-0.2.29 → dulus-0.2.31}/voice/keyterms.py +0 -0
  160. {dulus-0.2.29 → dulus-0.2.31}/voice/recorder.py +0 -0
  161. {dulus-0.2.29 → dulus-0.2.31}/voice/stt.py +0 -0
  162. {dulus-0.2.29 → dulus-0.2.31}/voice/tts.py +0 -0
  163. {dulus-0.2.29 → dulus-0.2.31}/webchat.py +0 -0
  164. {dulus-0.2.29 → dulus-0.2.31}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.29
3
+ Version: 0.2.31
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.29-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.29-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,8 @@
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
+
6
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**
7
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.
8
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.29
3
+ Version: 0.2.31
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.29-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.29" # 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
@@ -1543,16 +1563,32 @@ def cmd_bg(args: str, _state, config) -> bool:
1543
1563
  def _is_alive(pid: int) -> bool:
1544
1564
  if pid <= 0:
1545
1565
  return False
1546
- try:
1547
- if _sys.platform == "win32":
1548
- # On Windows, os.kill(pid, 0) raises if the process doesn't exist.
1549
- _os.kill(pid, 0)
1566
+ if _sys.platform == "win32":
1567
+ # os.kill(pid, 0) on Windows is unreliable for GUI-subsystem
1568
+ # processes (pythonw.exe): it raises OSError(errno=22) even
1569
+ # when the process is alive. Use the native OpenProcess API.
1570
+ try:
1571
+ import ctypes
1572
+ kernel32 = ctypes.windll.kernel32
1573
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
1574
+ h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
1575
+ if h:
1576
+ kernel32.CloseHandle(h)
1577
+ return True
1578
+ # OpenProcess returned 0 — check last error
1579
+ err = kernel32.GetLastError()
1580
+ # ERROR_INVALID_PARAMETER (87) = PID does not exist
1581
+ return err != 87
1582
+ except Exception:
1583
+ # Fallback: if the native API fails, assume alive so we
1584
+ # still attempt taskkill downstream.
1550
1585
  return True
1551
- else:
1586
+ else:
1587
+ try:
1552
1588
  _os.kill(pid, 0)
1553
1589
  return True
1554
- except (ProcessLookupError, OSError, PermissionError):
1555
- return False
1590
+ except (ProcessLookupError, OSError):
1591
+ return False
1556
1592
 
1557
1593
  def _read_pid() -> int:
1558
1594
  try:
@@ -1614,19 +1650,34 @@ def cmd_bg(args: str, _state, config) -> bool:
1614
1650
  except FileNotFoundError:
1615
1651
  pass
1616
1652
  return True
1653
+ sigterm_ok = False
1617
1654
  try:
1655
+ _os.kill(pid, _signal.SIGTERM)
1656
+ sigterm_ok = True
1657
+ ok(f"Sent SIGTERM to PID {pid}.")
1658
+ except PermissionError:
1659
+ # On Windows os.kill() to a GUI-subsystem process (pythonw.exe)
1660
+ # often raises PermissionError. Escalate to taskkill immediately.
1618
1661
  if _sys.platform == "win32":
1619
- _os.kill(pid, _signal.SIGTERM)
1662
+ try:
1663
+ _sp.run(["taskkill", "/F", "/PID", str(pid)],
1664
+ capture_output=True, timeout=5)
1665
+ ok(f"Forced stop via taskkill /F on PID {pid}.")
1666
+ except Exception as tk_e:
1667
+ err(f"Failed to kill {pid}: {tk_e}")
1668
+ return True
1620
1669
  else:
1621
- _os.kill(pid, _signal.SIGTERM)
1622
- ok(f"Sent SIGTERM to PID {pid}.")
1670
+ err(f"Failed to kill {pid}: Permission denied")
1671
+ return True
1623
1672
  except Exception as e:
1624
1673
  err(f"Failed to kill {pid}: {e}")
1625
1674
  return True
1626
- for _ in range(20):
1627
- if not _is_alive(pid):
1628
- break
1629
- _time.sleep(0.25)
1675
+
1676
+ if sigterm_ok:
1677
+ for _ in range(20):
1678
+ if not _is_alive(pid):
1679
+ break
1680
+ _time.sleep(0.25)
1630
1681
  try:
1631
1682
  BG_PID.unlink()
1632
1683
  except FileNotFoundError:
@@ -1650,19 +1701,65 @@ def cmd_bg(args: str, _state, config) -> bool:
1650
1701
  return True
1651
1702
 
1652
1703
  # ── /bg kill ──────────────────────────────────────────────────────────
1653
- # Force-stop the background DAEMON only — never the user's own REPL.
1654
- # We use the BG_PID file as the source of truth: only /bg start writes
1655
- # it, so it uniquely identifies the detached daemon. If no BG_PID file
1656
- # exists, we refuse to kill anything (port may be held by a REPL the
1657
- # user wants to keep). For SIGKILL escalation we use taskkill on Windows.
1704
+ # Force-stop whatever is holding the IPC port.
1705
+ # Priority 1: BG_PID file (fastest, most reliable).
1706
+ # Priority 2: discover the PID from the OS by scanning port 5151.
1707
+ # We NEVER kill our own REPL process (own_pid check).
1708
+ # For SIGKILL escalation we use taskkill on Windows.
1658
1709
  if sub == "kill":
1659
1710
  f_pid = _read_pid()
1660
1711
  own_pid = _os.getpid()
1661
1712
 
1713
+ def _discover_pid_from_port(port: int) -> int:
1714
+ """Ask the OS which process owns the given TCP port."""
1715
+ try:
1716
+ if _sys.platform == "win32":
1717
+ # netstat -ano → find the line with :5151 in LISTENING state
1718
+ result = _sp.run(
1719
+ ["netstat", "-ano"],
1720
+ capture_output=True, text=True, timeout=5
1721
+ )
1722
+ for line in result.stdout.splitlines():
1723
+ if f":{port}" in line and ("LISTENING" in line or "ESTABLISHED" in line):
1724
+ parts = line.strip().split()
1725
+ if parts:
1726
+ try:
1727
+ return int(parts[-1])
1728
+ except ValueError:
1729
+ continue
1730
+ else:
1731
+ # lsof -ti :port (outputs PID only)
1732
+ result = _sp.run(
1733
+ ["lsof", "-ti", f":{port}"],
1734
+ capture_output=True, text=True, timeout=5
1735
+ )
1736
+ if result.stdout.strip():
1737
+ return int(result.stdout.strip().splitlines()[0])
1738
+ # Fallback to fuser
1739
+ result = _sp.run(
1740
+ ["fuser", f"{port}/tcp"],
1741
+ capture_output=True, text=True, timeout=5
1742
+ )
1743
+ if result.stdout.strip():
1744
+ parts = result.stdout.strip().split(":")
1745
+ if len(parts) > 1:
1746
+ return int(parts[1].strip().split()[0])
1747
+ except Exception:
1748
+ pass
1749
+ return 0
1750
+
1751
+ # No PID file? Discover from the OS if the port is in use.
1752
+ if not f_pid and _ipc_alive():
1753
+ discovered = _discover_pid_from_port(DULUS_IPC_PORT)
1754
+ if discovered and discovered != own_pid:
1755
+ f_pid = discovered
1756
+ info(f"No PID file — discovered process {f_pid} holding port {DULUS_IPC_PORT}.")
1757
+ elif discovered == own_pid:
1758
+ warn("Port is held by this REPL — close it with /exit instead.")
1759
+ return True
1760
+
1662
1761
  if not f_pid:
1663
- info("No background daemon to kill (BG_PID file missing).")
1664
- info(f" If port {DULUS_IPC_PORT} is in use, it's likely your own REPL.")
1665
- info(" Close your REPL (/exit) to free the port — /bg kill won't touch it.")
1762
+ info("No background daemon to kill (port free, PID file missing).")
1666
1763
  return True
1667
1764
 
1668
1765
  if f_pid == own_pid:
@@ -1682,29 +1779,48 @@ def cmd_bg(args: str, _state, config) -> bool:
1682
1779
  return True
1683
1780
 
1684
1781
  # Send SIGTERM, give it 1s, escalate to SIGKILL/taskkill if it lingers.
1782
+ # On Windows, os.kill() to a GUI-subsystem process (pythonw.exe) often
1783
+ # raises PermissionError. We catch that immediately and escalate to
1784
+ # taskkill /F instead of giving up.
1785
+ sigterm_ok = False
1685
1786
  try:
1686
1787
  _os.kill(f_pid, _signal.SIGTERM)
1788
+ sigterm_ok = True
1687
1789
  ok(f"Sent SIGTERM to daemon PID {f_pid}.")
1688
- except (ProcessLookupError, PermissionError, OSError) as e:
1790
+ except PermissionError:
1791
+ if _sys.platform == "win32":
1792
+ warn(f"Permission denied signalling PID {f_pid} — escalating to taskkill /F.")
1793
+ try:
1794
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1795
+ capture_output=True, timeout=5)
1796
+ ok(f"Forced kill via taskkill /F on PID {f_pid}.")
1797
+ except Exception as tk_e:
1798
+ err(f"taskkill failed: {tk_e}")
1799
+ return True
1800
+ else:
1801
+ err(f"Could not signal PID {f_pid}: Permission denied")
1802
+ return True
1803
+ except (ProcessLookupError, OSError) as e:
1689
1804
  err(f"Could not signal PID {f_pid}: {e}")
1690
1805
  return True
1691
1806
 
1692
- for _ in range(8):
1693
- if not _is_alive(f_pid):
1694
- break
1695
- _time.sleep(0.25)
1807
+ if sigterm_ok:
1808
+ for _ in range(8):
1809
+ if not _is_alive(f_pid):
1810
+ break
1811
+ _time.sleep(0.25)
1696
1812
 
1697
- if _is_alive(f_pid):
1698
- warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1699
- try:
1700
- if _sys.platform == "win32":
1701
- _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1702
- capture_output=True, timeout=5)
1703
- else:
1704
- _os.kill(f_pid, _signal.SIGKILL)
1705
- except Exception as e:
1706
- err(f"SIGKILL failed: {e}")
1707
- return True
1813
+ if _is_alive(f_pid):
1814
+ warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1815
+ try:
1816
+ if _sys.platform == "win32":
1817
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1818
+ capture_output=True, timeout=5)
1819
+ else:
1820
+ _os.kill(f_pid, _signal.SIGKILL)
1821
+ except Exception as e:
1822
+ err(f"SIGKILL failed: {e}")
1823
+ return True
1708
1824
 
1709
1825
  try:
1710
1826
  BG_PID.unlink()
@@ -1766,37 +1882,61 @@ def cmd_bg(args: str, _state, config) -> bool:
1766
1882
  from config import save_config
1767
1883
  save_config(config)
1768
1884
 
1769
- # Build the spawn command. Prefer the installed `dulus` shim; fall
1770
- # back to `python dulus.py` when running from source.
1771
- dulus_bin = None
1772
- for cand in ["dulus", "dulus.exe"]:
1773
- from shutil import which
1774
- p = which(cand)
1775
- if p:
1776
- dulus_bin = p
1777
- break
1778
- if dulus_bin:
1779
- cmd = [dulus_bin, "--daemon"]
1780
- else:
1885
+ # Snapshot current REPL session so the daemon can resume it.
1886
+ # This ensures Telegram/Web share the SAME session_id and context.
1887
+ current_sid = config.get("_session_id", "")
1888
+ if current_sid and _state and getattr(_state, "messages", None):
1889
+ save_latest("", _state, config)
1890
+
1891
+ # Build the spawn command. On Windows we MUST use pythonw.exe (windowless
1892
+ # variant) instead of the console-subsystem python.exe / dulus shim,
1893
+ # otherwise Windows creates a visible console window for the daemon
1894
+ # and closing it kills the process. The shim itself runs python.exe,
1895
+ # so we go around it by invoking pythonw -m dulus directly.
1896
+ if _sys.platform == "win32":
1897
+ pythonw = _sys.executable.replace("python.exe", "pythonw.exe")
1898
+ if not _os.path.exists(pythonw):
1899
+ # Fall back to python.exe if pythonw isn't shipped (rare;
1900
+ # mostly happens on stripped embeddable distributions).
1901
+ pythonw = _sys.executable
1781
1902
  dulus_script = _os.path.abspath(__file__)
1782
- cmd = [_sys.executable, dulus_script, "--daemon"]
1903
+ cmd = [pythonw, dulus_script, "--daemon"]
1904
+ else:
1905
+ from shutil import which
1906
+ dulus_bin = None
1907
+ for cand in ["dulus", "dulus.exe"]:
1908
+ p = which(cand)
1909
+ if p:
1910
+ dulus_bin = p
1911
+ break
1912
+ if dulus_bin:
1913
+ cmd = [dulus_bin, "--daemon"]
1914
+ else:
1915
+ dulus_script = _os.path.abspath(__file__)
1916
+ cmd = [_sys.executable, dulus_script, "--daemon"]
1783
1917
 
1784
1918
  # Pass the auto-webchat hint via env so the daemon picks it up.
1785
1919
  env = _os.environ.copy()
1786
1920
  env["DULUS_BG_AUTO_WEBCHAT"] = "1"
1787
1921
  env["DULUS_BG_WEBCHAT_PORT"] = str(web_port)
1922
+ env["DULUS_BG_SESSION_ID"] = current_sid
1788
1923
 
1789
1924
  # Detach properly per platform.
1790
1925
  log_fp = open(BG_LOG, "ab")
1791
1926
  try:
1792
1927
  if _sys.platform == "win32":
1793
- # CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
1794
- DETACHED = 0x00000008
1928
+ # CREATE_NO_WINDOW (0x08000000) suppresses the console window
1929
+ # entirely — cannot be combined with DETACHED_PROCESS, but
1930
+ # because we're invoking pythonw.exe (a GUI-subsystem binary)
1931
+ # there is no console to inherit from in the first place.
1932
+ # CREATE_NEW_PROCESS_GROUP keeps Ctrl+C in the parent shell
1933
+ # from killing the daemon when the parent later exits.
1934
+ CREATE_NO_WINDOW = 0x08000000
1795
1935
  NEW_GROUP = 0x00000200
1796
1936
  proc = _sp.Popen(
1797
1937
  cmd,
1798
1938
  stdout=log_fp, stderr=log_fp, stdin=_sp.DEVNULL,
1799
- creationflags=DETACHED | NEW_GROUP,
1939
+ creationflags=CREATE_NO_WINDOW | NEW_GROUP,
1800
1940
  close_fds=True,
1801
1941
  env=env,
1802
1942
  )
@@ -5703,10 +5843,27 @@ def _run_daemon(config: dict) -> None:
5703
5843
  from checkpoint import set_session
5704
5844
  from common import ok, info, warn, err, clr
5705
5845
 
5706
- session_id = config.get("_session_id") or uuid.uuid4().hex[:8]
5846
+ import os as _os_env
5847
+ bg_session_id = _os_env.environ.get("DULUS_BG_SESSION_ID", "")
5848
+ session_id = bg_session_id or config.get("_session_id") or uuid.uuid4().hex[:8]
5707
5849
  set_session(session_id)
5708
5850
 
5709
5851
  state = AgentState()
5852
+ # If spawned from /bg start with a session ID, resume that session's state.
5853
+ if bg_session_id:
5854
+ from config import MR_SESSION_DIR
5855
+ latest_path = MR_SESSION_DIR / "session_latest.json"
5856
+ if latest_path.exists():
5857
+ try:
5858
+ import json as _json
5859
+ data = _json.loads(latest_path.read_text(encoding="utf-8", errors="replace"))
5860
+ state.messages = data.get("messages", [])
5861
+ state.turn_count = data.get("turn_count", 0)
5862
+ state.total_input_tokens = data.get("total_input_tokens", 0)
5863
+ state.total_output_tokens = data.get("total_output_tokens", 0)
5864
+ info(f"Resumed session {session_id} ({len(state.messages)} messages)")
5865
+ except Exception as _load_e:
5866
+ warn(f"Could not resume session: {_load_e}")
5710
5867
  config["_state"] = state
5711
5868
  config["_session_id"] = session_id
5712
5869
  config["_last_interaction_time"] = time.time()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.29"
7
+ version = "0.2.31"
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