dulus 0.2.18__tar.gz → 0.2.19__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.18/dulus.egg-info → dulus-0.2.19}/PKG-INFO +2 -2
  2. {dulus-0.2.18 → dulus-0.2.19}/README.md +1 -1
  3. {dulus-0.2.18 → dulus-0.2.19}/docs/news.md +7 -0
  4. {dulus-0.2.18 → dulus-0.2.19/dulus.egg-info}/PKG-INFO +2 -2
  5. {dulus-0.2.18 → dulus-0.2.19}/dulus.py +195 -1
  6. {dulus-0.2.18 → dulus-0.2.19}/pyproject.toml +1 -1
  7. {dulus-0.2.18 → dulus-0.2.19}/LICENSE +0 -0
  8. {dulus-0.2.18 → dulus-0.2.19}/MANIFEST.in +0 -0
  9. {dulus-0.2.18 → dulus-0.2.19}/agent.py +0 -0
  10. {dulus-0.2.18 → dulus-0.2.19}/backend/__init__.py +0 -0
  11. {dulus-0.2.18 → dulus-0.2.19}/backend/compressor.py +0 -0
  12. {dulus-0.2.18 → dulus-0.2.19}/backend/context.py +0 -0
  13. {dulus-0.2.18 → dulus-0.2.19}/backend/githook.py +0 -0
  14. {dulus-0.2.18 → dulus-0.2.19}/backend/marketplace.py +0 -0
  15. {dulus-0.2.18 → dulus-0.2.19}/backend/mempalace_bridge.py +0 -0
  16. {dulus-0.2.18 → dulus-0.2.19}/backend/personas.py +0 -0
  17. {dulus-0.2.18 → dulus-0.2.19}/backend/plugins.py +0 -0
  18. {dulus-0.2.18 → dulus-0.2.19}/backend/server.py +0 -0
  19. {dulus-0.2.18 → dulus-0.2.19}/backend/tasks.py +0 -0
  20. {dulus-0.2.18 → dulus-0.2.19}/batch_api.py +0 -0
  21. {dulus-0.2.18 → dulus-0.2.19}/checkpoint/__init__.py +0 -0
  22. {dulus-0.2.18 → dulus-0.2.19}/checkpoint/hooks.py +0 -0
  23. {dulus-0.2.18 → dulus-0.2.19}/checkpoint/store.py +0 -0
  24. {dulus-0.2.18 → dulus-0.2.19}/checkpoint/types.py +0 -0
  25. {dulus-0.2.18 → dulus-0.2.19}/claude_code_watcher.py +0 -0
  26. {dulus-0.2.18 → dulus-0.2.19}/clipboard_utils.py +0 -0
  27. {dulus-0.2.18 → dulus-0.2.19}/cloudsave.py +0 -0
  28. {dulus-0.2.18 → dulus-0.2.19}/common.py +0 -0
  29. {dulus-0.2.18 → dulus-0.2.19}/compaction.py +0 -0
  30. {dulus-0.2.18 → dulus-0.2.19}/config.py +0 -0
  31. {dulus-0.2.18 → dulus-0.2.19}/context.py +0 -0
  32. {dulus-0.2.18 → dulus-0.2.19}/data/__init__.py +0 -0
  33. {dulus-0.2.18 → dulus-0.2.19}/data/active_persona.json +0 -0
  34. {dulus-0.2.18 → dulus-0.2.19}/data/context.json +0 -0
  35. {dulus-0.2.18 → dulus-0.2.19}/data/marketplace.json +0 -0
  36. {dulus-0.2.18 → dulus-0.2.19}/data/personas.json +0 -0
  37. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/__init__.py +0 -0
  38. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/__init__.py +0 -0
  39. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  40. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  41. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  42. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/plugin.json +0 -0
  43. {dulus-0.2.18 → dulus-0.2.19}/data/plugins/composio/plugin_tool.py +0 -0
  44. {dulus-0.2.18 → dulus-0.2.19}/data/tasks.json +0 -0
  45. {dulus-0.2.18 → dulus-0.2.19}/docs/README.md +0 -0
  46. {dulus-0.2.18 → dulus-0.2.19}/docs/__init__.py +0 -0
  47. {dulus-0.2.18 → dulus-0.2.19}/docs/api.html +0 -0
  48. {dulus-0.2.18 → dulus-0.2.19}/docs/architecture.md +0 -0
  49. {dulus-0.2.18 → dulus-0.2.19}/docs/azure-speech-template.json +0 -0
  50. {dulus-0.2.18 → dulus-0.2.19}/docs/dashboard/index.html +0 -0
  51. {dulus-0.2.18 → dulus-0.2.19}/docs/divider.svg +0 -0
  52. {dulus-0.2.18 → dulus-0.2.19}/docs/generate.py +0 -0
  53. {dulus-0.2.18 → dulus-0.2.19}/docs/hero.svg +0 -0
  54. {dulus-0.2.18 → dulus-0.2.19}/docs/index.html +0 -0
  55. {dulus-0.2.18 → dulus-0.2.19}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.18 → dulus-0.2.19}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.18 → dulus-0.2.19}/docs/personas/index.html +0 -0
  58. {dulus-0.2.18 → dulus-0.2.19}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.18 → dulus-0.2.19}/docs/preview.html +0 -0
  60. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.18 → dulus-0.2.19}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.18 → dulus-0.2.19}/docs/spinners.svg +0 -0
  72. {dulus-0.2.18 → dulus-0.2.19}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.18 → dulus-0.2.19}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.18 → dulus-0.2.19}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.18 → dulus-0.2.19}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.18 → dulus-0.2.19}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.18 → dulus-0.2.19}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.18 → dulus-0.2.19}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.18 → dulus-0.2.19}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.18 → dulus-0.2.19}/dulus_gui.py +0 -0
  81. {dulus-0.2.18 → dulus-0.2.19}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.18 → dulus-0.2.19}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.18 → dulus-0.2.19}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.18 → dulus-0.2.19}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.18 → dulus-0.2.19}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.18 → dulus-0.2.19}/gui/__init__.py +0 -0
  87. {dulus-0.2.18 → dulus-0.2.19}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.18 → dulus-0.2.19}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.18 → dulus-0.2.19}/gui/main_window.py +0 -0
  90. {dulus-0.2.18 → dulus-0.2.19}/gui/personas.py +0 -0
  91. {dulus-0.2.18 → dulus-0.2.19}/gui/session_utils.py +0 -0
  92. {dulus-0.2.18 → dulus-0.2.19}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.18 → dulus-0.2.19}/gui/sidebar.py +0 -0
  94. {dulus-0.2.18 → dulus-0.2.19}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.18 → dulus-0.2.19}/gui/themes.py +0 -0
  96. {dulus-0.2.18 → dulus-0.2.19}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.18 → dulus-0.2.19}/input.py +0 -0
  98. {dulus-0.2.18 → dulus-0.2.19}/license_manager.py +0 -0
  99. {dulus-0.2.18 → dulus-0.2.19}/memory/__init__.py +0 -0
  100. {dulus-0.2.18 → dulus-0.2.19}/memory/audit.py +0 -0
  101. {dulus-0.2.18 → dulus-0.2.19}/memory/consolidator.py +0 -0
  102. {dulus-0.2.18 → dulus-0.2.19}/memory/context.py +0 -0
  103. {dulus-0.2.18 → dulus-0.2.19}/memory/offload.py +0 -0
  104. {dulus-0.2.18 → dulus-0.2.19}/memory/palace.py +0 -0
  105. {dulus-0.2.18 → dulus-0.2.19}/memory/scan.py +0 -0
  106. {dulus-0.2.18 → dulus-0.2.19}/memory/sessions.py +0 -0
  107. {dulus-0.2.18 → dulus-0.2.19}/memory/store.py +0 -0
  108. {dulus-0.2.18 → dulus-0.2.19}/memory/tools.py +0 -0
  109. {dulus-0.2.18 → dulus-0.2.19}/memory/types.py +0 -0
  110. {dulus-0.2.18 → dulus-0.2.19}/memory/vector_search.py +0 -0
  111. {dulus-0.2.18 → dulus-0.2.19}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.18 → dulus-0.2.19}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.18 → dulus-0.2.19}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.18 → dulus-0.2.19}/offload_helper.py +0 -0
  115. {dulus-0.2.18 → dulus-0.2.19}/plugin/__init__.py +0 -0
  116. {dulus-0.2.18 → dulus-0.2.19}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.18 → dulus-0.2.19}/plugin/loader.py +0 -0
  118. {dulus-0.2.18 → dulus-0.2.19}/plugin/recommend.py +0 -0
  119. {dulus-0.2.18 → dulus-0.2.19}/plugin/store.py +0 -0
  120. {dulus-0.2.18 → dulus-0.2.19}/plugin/types.py +0 -0
  121. {dulus-0.2.18 → dulus-0.2.19}/providers.py +0 -0
  122. {dulus-0.2.18 → dulus-0.2.19}/setup.cfg +0 -0
  123. {dulus-0.2.18 → dulus-0.2.19}/skill/__init__.py +0 -0
  124. {dulus-0.2.18 → dulus-0.2.19}/skill/builtin.py +0 -0
  125. {dulus-0.2.18 → dulus-0.2.19}/skill/clawhub.py +0 -0
  126. {dulus-0.2.18 → dulus-0.2.19}/skill/executor.py +0 -0
  127. {dulus-0.2.18 → dulus-0.2.19}/skill/loader.py +0 -0
  128. {dulus-0.2.18 → dulus-0.2.19}/skill/tools.py +0 -0
  129. {dulus-0.2.18 → dulus-0.2.19}/skills.py +0 -0
  130. {dulus-0.2.18 → dulus-0.2.19}/spinner.py +0 -0
  131. {dulus-0.2.18 → dulus-0.2.19}/string_utils.py +0 -0
  132. {dulus-0.2.18 → dulus-0.2.19}/subagent.py +0 -0
  133. {dulus-0.2.18 → dulus-0.2.19}/task/__init__.py +0 -0
  134. {dulus-0.2.18 → dulus-0.2.19}/task/store.py +0 -0
  135. {dulus-0.2.18 → dulus-0.2.19}/task/tools.py +0 -0
  136. {dulus-0.2.18 → dulus-0.2.19}/task/types.py +0 -0
  137. {dulus-0.2.18 → dulus-0.2.19}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.18 → dulus-0.2.19}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.18 → dulus-0.2.19}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.18 → dulus-0.2.19}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.18 → dulus-0.2.19}/tests/test_license.py +0 -0
  142. {dulus-0.2.18 → dulus-0.2.19}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.18 → dulus-0.2.19}/tests/test_memory.py +0 -0
  144. {dulus-0.2.18 → dulus-0.2.19}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.18 → dulus-0.2.19}/tests/test_skills.py +0 -0
  146. {dulus-0.2.18 → dulus-0.2.19}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.18 → dulus-0.2.19}/tests/test_task.py +0 -0
  148. {dulus-0.2.18 → dulus-0.2.19}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.18 → dulus-0.2.19}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.18 → dulus-0.2.19}/tests/test_voice.py +0 -0
  151. {dulus-0.2.18 → dulus-0.2.19}/tmux_offloader.py +0 -0
  152. {dulus-0.2.18 → dulus-0.2.19}/tmux_tools.py +0 -0
  153. {dulus-0.2.18 → dulus-0.2.19}/tool_registry.py +0 -0
  154. {dulus-0.2.18 → dulus-0.2.19}/tools.py +0 -0
  155. {dulus-0.2.18 → dulus-0.2.19}/ui/__init__.py +0 -0
  156. {dulus-0.2.18 → dulus-0.2.19}/ui/input.py +0 -0
  157. {dulus-0.2.18 → dulus-0.2.19}/ui/render.py +0 -0
  158. {dulus-0.2.18 → dulus-0.2.19}/voice/__init__.py +0 -0
  159. {dulus-0.2.18 → dulus-0.2.19}/voice/keyterms.py +0 -0
  160. {dulus-0.2.18 → dulus-0.2.19}/voice/recorder.py +0 -0
  161. {dulus-0.2.18 → dulus-0.2.19}/voice/stt.py +0 -0
  162. {dulus-0.2.18 → dulus-0.2.19}/voice/tts.py +0 -0
  163. {dulus-0.2.18 → dulus-0.2.19}/webchat.py +0 -0
  164. {dulus-0.2.18 → dulus-0.2.19}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.18
3
+ Version: 0.2.19
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.17-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.19-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.17-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
25
+ <img src="https://img.shields.io/badge/version-v0.2.19-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,13 @@
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
5
 
6
+ - May 09, 2026 (**v0.2.19**): **Shared sessions via tiny TCP socket — daemon workaround supremo**
7
+ - **One Dulus, many shells.** When a Dulus REPL or `--daemon` is running, it now listens on `127.0.0.1:5151`. Any subsequent `dulus "do X"` from another shell forwards the prompt to that live session over the socket and prints back the reply — same history, same memory, same plugins, same tool state. No session manager, no IPC framework, no systemd unit. 80 lines of plain TCP.
8
+ - **Falls back gracefully.** If no listener is up, the CLI behaves exactly as before (spawns its own `--print` process). Daemon/gui/`--cmd`/`--run-tool` modes intentionally bypass the IPC dispatch — they need their own process.
9
+ - **Why this matters.** The competition wires up `multiprocessing.Manager`/grpc/zmq/dbus + a daemon CLI + config files + service installers to do the same thing. Dulus does it with `socket.bind` and a thread.
10
+
11
+ - May 09, 2026 (**v0.2.18**): **Add `beautifulsoup4` as default dep** — needed by web scraping / harvest flows and several plugins. Tiny dep, ships by default.
12
+
6
13
  - May 09, 2026 (**v0.2.17**): **Mega-release — Composio bundled, awesome skills live, lite mode fixed, English prompt**
7
14
  - **Composio plugin shipped in the wheel.** `pip install dulus` now bundles the Composio Tool Router plugin (no MCP needed) and copies it into `~/.dulus/plugins/composio/` on first launch. The composio Python SDK (~1MB) is now a default dep — Slack, Gmail, GitHub, Notion, Asana, ClickUp, Linear, etc. all available via `composio_create_session`.
8
15
  - **`/skill list` interactive picker.** Calling `/skill list` without args opens a menu: awesome (~235 skills via GitHub), composio (1000+ toolkits via API), local (Anthropic marketplace on disk), installed, or all. Catalogs are cached 24h in `~/.dulus/cache/`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.18
3
+ Version: 0.2.19
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.17-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.19-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"/>
@@ -218,7 +218,7 @@ try:
218
218
  from importlib.metadata import version as _pkg_version
219
219
  VERSION = _pkg_version("dulus")
220
220
  except Exception:
221
- VERSION = "0.2.18" # dev fallback — keep in sync with pyproject.toml
221
+ VERSION = "0.2.19" # dev fallback — keep in sync with pyproject.toml
222
222
 
223
223
  # ── ANSI helpers (used even with rich for non-markdown output) ─────────────
224
224
  from common import C, clr, info, ok, warn, err, stream_thinking, print_tool_start, print_tool_end, sanitize_text
@@ -3714,6 +3714,162 @@ def _print_background_notifications(state=None):
3714
3714
  return new_found
3715
3715
 
3716
3716
 
3717
+ # ── IPC server: shared session via TCP socket ─────────────────────────────
3718
+ # When a Dulus REPL or daemon is running, it listens on 127.0.0.1:5151. Any
3719
+ # `dulus "..."` invocation from another shell first probes this port — if the
3720
+ # server answers, the prompt is forwarded over the wire and the response is
3721
+ # streamed back, so multiple shells share the SAME live session (history,
3722
+ # memory, tool state, all of it). If the port is dead, the CLI falls back to
3723
+ # spawning its own --print process.
3724
+ #
3725
+ # This is the dominican workaround: 80 lines of socket code instead of a
3726
+ # session manager + IPC framework + daemon orchestrator. Same UX, 1/100th
3727
+ # the surface area.
3728
+
3729
+ DULUS_IPC_HOST = "127.0.0.1"
3730
+ DULUS_IPC_PORT = 5151
3731
+
3732
+
3733
+ def _ipc_server_loop(config, state):
3734
+ """Tiny TCP server: accepts one JSON request per connection, runs it on
3735
+ the live session, and writes the assistant reply back as JSON.
3736
+ Robust to port-already-in-use (we just exit silently — another instance
3737
+ is the listener and that's fine)."""
3738
+ import socket as _socket
3739
+ import json as _json
3740
+
3741
+ sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
3742
+ sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
3743
+ try:
3744
+ sock.bind((DULUS_IPC_HOST, DULUS_IPC_PORT))
3745
+ except OSError:
3746
+ return # another Dulus already listening — fine, we're the client one
3747
+ sock.listen(4)
3748
+ sock.settimeout(1.0)
3749
+ config["_ipc_listening"] = True
3750
+
3751
+ while not config.get("_ipc_stop"):
3752
+ try:
3753
+ conn, _addr = sock.accept()
3754
+ except _socket.timeout:
3755
+ continue
3756
+ except Exception:
3757
+ continue
3758
+ try:
3759
+ conn.settimeout(60.0)
3760
+ buf = b""
3761
+ while b"\n" not in buf and len(buf) < 64 * 1024:
3762
+ chunk = conn.recv(4096)
3763
+ if not chunk:
3764
+ break
3765
+ buf += chunk
3766
+ line = buf.split(b"\n", 1)[0].decode("utf-8", errors="ignore").strip()
3767
+ if not line:
3768
+ conn.close()
3769
+ continue
3770
+ try:
3771
+ req = _json.loads(line)
3772
+ except Exception:
3773
+ conn.sendall(b'{"error":"bad json"}\n')
3774
+ conn.close()
3775
+ continue
3776
+
3777
+ prompt = (req.get("prompt") or "").strip()
3778
+ if not prompt:
3779
+ conn.sendall(b'{"error":"empty prompt"}\n')
3780
+ conn.close()
3781
+ continue
3782
+
3783
+ cb = config.get("_run_query_callback")
3784
+ if not cb:
3785
+ conn.sendall(b'{"error":"no run_query callback registered"}\n')
3786
+ conn.close()
3787
+ continue
3788
+
3789
+ # Snapshot the message count so we can lift the new assistant
3790
+ # reply after the turn completes.
3791
+ before = len(state.messages) if state else 0
3792
+ try:
3793
+ cb(prompt)
3794
+ except Exception as e:
3795
+ conn.sendall(_json.dumps({"error": f"{type(e).__name__}: {e}"}).encode() + b"\n")
3796
+ conn.close()
3797
+ continue
3798
+
3799
+ response_text = ""
3800
+ if state and state.messages:
3801
+ for m in reversed(state.messages[before:] or state.messages):
3802
+ if m.get("role") == "assistant":
3803
+ content = m.get("content", "")
3804
+ if isinstance(content, list):
3805
+ parts = []
3806
+ for block in content:
3807
+ if isinstance(block, dict) and block.get("type") == "text":
3808
+ parts.append(block["text"])
3809
+ elif isinstance(block, str):
3810
+ parts.append(block)
3811
+ content = "\n".join(parts)
3812
+ if content:
3813
+ response_text = content
3814
+ break
3815
+ payload = _json.dumps({"response": response_text or "(no reply)"}).encode() + b"\n"
3816
+ try:
3817
+ conn.sendall(payload)
3818
+ except Exception:
3819
+ pass
3820
+ finally:
3821
+ try:
3822
+ conn.close()
3823
+ except Exception:
3824
+ pass
3825
+
3826
+ try:
3827
+ sock.close()
3828
+ except Exception:
3829
+ pass
3830
+
3831
+
3832
+ def _try_ipc_dispatch(prompt: str, timeout: float = 0.4) -> bool:
3833
+ """Client side: probe the IPC server, send a prompt, print the response,
3834
+ return True if it succeeded. Returns False if no server is listening,
3835
+ so callers can fall back to the in-process --print path."""
3836
+ import socket as _socket
3837
+ import json as _json
3838
+
3839
+ try:
3840
+ sock = _socket.create_connection(
3841
+ (DULUS_IPC_HOST, DULUS_IPC_PORT), timeout=timeout
3842
+ )
3843
+ except (_socket.timeout, ConnectionRefusedError, OSError):
3844
+ return False
3845
+ try:
3846
+ sock.settimeout(180.0)
3847
+ sock.sendall((_json.dumps({"prompt": prompt, "v": 1}) + "\n").encode())
3848
+ buf = b""
3849
+ while True:
3850
+ chunk = sock.recv(8192)
3851
+ if not chunk:
3852
+ break
3853
+ buf += chunk
3854
+ if b"\n" in buf:
3855
+ break
3856
+ line = buf.split(b"\n", 1)[0].decode("utf-8", errors="ignore").strip()
3857
+ try:
3858
+ data = _json.loads(line)
3859
+ except Exception:
3860
+ return False
3861
+ if "error" in data:
3862
+ print(f"[ipc] {data['error']}", flush=True)
3863
+ return True # we did get a reply, just an error one — don't fall back
3864
+ print(data.get("response", ""), flush=True)
3865
+ return True
3866
+ finally:
3867
+ try:
3868
+ sock.close()
3869
+ except Exception:
3870
+ pass
3871
+
3872
+
3717
3873
  def _job_sentinel_loop(config, state):
3718
3874
  """Background daemon that triggers run_query as soon as a job finishes.
3719
3875
 
@@ -5173,6 +5329,15 @@ def _run_daemon(config: dict) -> None:
5173
5329
  # Same callback used by the REPL so Telegram can trigger runs
5174
5330
  config["_run_query_callback"] = lambda msg: run_query(msg, is_background=True)
5175
5331
 
5332
+ # IPC server — same socket the REPL uses, so external `dulus "..."` calls
5333
+ # land in this daemon's session.
5334
+ if config.get("_ipc_thread") is None and not config.get("_ipc_disabled"):
5335
+ ti = threading.Thread(
5336
+ target=_ipc_server_loop, args=(config, state), daemon=True
5337
+ )
5338
+ config["_ipc_thread"] = ti
5339
+ ti.start()
5340
+
5176
5341
  print(clr("\n ▲ DULUS DAEMON", "accent", "bold"))
5177
5342
  print(clr(" " + "─" * 40, "dim"))
5178
5343
  info(f"Session: {session_id}")
@@ -7101,6 +7266,16 @@ def repl(config: dict, initial_prompt: str = None):
7101
7266
  tj = threading.Thread(target=_job_sentinel_loop, args=(config, state), daemon=True)
7102
7267
  config["_job_sentinel_thread"] = tj
7103
7268
  tj.start()
7269
+
7270
+ # IPC server — lets `dulus "..."` from another shell join this REPL's
7271
+ # session instead of spawning a fresh process. Tiny TCP socket on
7272
+ # 127.0.0.1:5151, no daemon manager required.
7273
+ if config.get("_ipc_thread") is None and not config.get("_ipc_disabled"):
7274
+ ti = threading.Thread(
7275
+ target=_ipc_server_loop, args=(config, state), daemon=True
7276
+ )
7277
+ config["_ipc_thread"] = ti
7278
+ ti.start()
7104
7279
 
7105
7280
  def run_query(user_input: str, is_background: bool = False):
7106
7281
  nonlocal verbose
@@ -8652,6 +8827,25 @@ def main():
8652
8827
 
8653
8828
  initial = " ".join(args.prompt) if args.prompt else None
8654
8829
 
8830
+ # ── IPC dispatch: if a Dulus REPL/daemon is already running on
8831
+ # 127.0.0.1:5151, forward this prompt to it (shared session) and exit.
8832
+ # Falls through silently when no listener is up.
8833
+ # Only kicks in for plain `dulus "..."` and `dulus -p "..."` — not for
8834
+ # daemon/gui/cmd/run-tool/job invocations, which need their own process.
8835
+ if (initial
8836
+ and not args.daemon
8837
+ and not args.gui
8838
+ and not args.exec_cmd
8839
+ and not args.run_tool
8840
+ and not args.job_id
8841
+ and not args.job_path
8842
+ ):
8843
+ try:
8844
+ if _try_ipc_dispatch(initial):
8845
+ sys.exit(0)
8846
+ except Exception:
8847
+ pass # any IPC error → fall through to in-process path
8848
+
8655
8849
  # ── Daemon mode ──
8656
8850
  if args.daemon:
8657
8851
  _run_daemon(config)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.18"
7
+ version = "0.2.19"
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