dulus 0.2.18__tar.gz → 0.2.20__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.
- {dulus-0.2.18/dulus.egg-info → dulus-0.2.20}/PKG-INFO +2 -2
- {dulus-0.2.18 → dulus-0.2.20}/README.md +1 -1
- {dulus-0.2.18 → dulus-0.2.20}/docs/news.md +9 -0
- {dulus-0.2.18 → dulus-0.2.20/dulus.egg-info}/PKG-INFO +2 -2
- {dulus-0.2.18 → dulus-0.2.20}/dulus.py +203 -1
- {dulus-0.2.18 → dulus-0.2.20}/pyproject.toml +1 -1
- {dulus-0.2.18 → dulus-0.2.20}/LICENSE +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/MANIFEST.in +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/agent.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/compressor.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/context.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/githook.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/marketplace.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/personas.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/plugins.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/server.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/backend/tasks.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/batch_api.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/checkpoint/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/checkpoint/hooks.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/checkpoint/store.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/checkpoint/types.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/claude_code_watcher.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/clipboard_utils.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/cloudsave.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/common.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/compaction.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/config.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/context.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/active_persona.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/context.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/marketplace.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/personas.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/data/tasks.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/README.md +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/api.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/architecture.md +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/dashboard/index.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/divider.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/generate.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/hero.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/index.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/particle-playground.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/personas/index.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/poetry-banner.png +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/preview.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-agents.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-features.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-memory.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-models.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-perms.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/spinners.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/split-pane.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_gui.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_mcp/client.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_mcp/config.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/dulus_mcp/types.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/agent_bridge.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/chat_widget.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/main_window.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/personas.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/session_utils.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/settings_dialog.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/sidebar.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/tasks_view.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/themes.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/gui/tool_panel.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/input.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/license_manager.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/audit.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/consolidator.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/context.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/offload.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/palace.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/scan.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/sessions.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/store.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/types.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/memory/vector_search.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/multi_agent/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/multi_agent/subagent.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/multi_agent/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/offload_helper.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/autoadapter.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/loader.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/recommend.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/store.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/plugin/types.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/providers.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/setup.cfg +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/builtin.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/clawhub.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/executor.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/loader.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skill/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/skills.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/spinner.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/string_utils.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/subagent.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/task/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/task/store.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/task/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/task/types.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_compaction.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_diff_view.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_license.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_mcp.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_memory.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_plugin.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_skills.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_subagent.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_task.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tests/test_voice.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tmux_offloader.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tmux_tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tool_registry.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/tools.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/ui/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/ui/input.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/ui/render.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/voice/__init__.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/voice/keyterms.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/voice/recorder.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/voice/stt.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/voice/tts.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/webchat.py +0 -0
- {dulus-0.2.18 → dulus-0.2.20}/webchat_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.20
|
|
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.
|
|
72
|
+
<img src="https://img.shields.io/badge/version-v0.2.20-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.
|
|
25
|
+
<img src="https://img.shields.io/badge/version-v0.2.20-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,15 @@
|
|
|
3
3
|
## 🔥🔥🔥 News (Pacific Time)
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
- May 09, 2026 (**v0.2.20**): **IPC port-collision fix on Windows** — `SO_REUSEADDR` on Windows lets two sockets share the same port, which would let a second Dulus instance silently "steal" the IPC listener. Switched to `SO_EXCLUSIVEADDRUSE` so the second instance correctly backs off and acts as a client. Verified end-to-end with the new test harness.
|
|
7
|
+
|
|
8
|
+
- May 09, 2026 (**v0.2.19**): **Shared sessions via tiny TCP socket — daemon workaround supremo**
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
6
15
|
- May 09, 2026 (**v0.2.17**): **Mega-release — Composio bundled, awesome skills live, lite mode fixed, English prompt**
|
|
7
16
|
- **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
17
|
- **`/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.
|
|
3
|
+
Version: 0.2.20
|
|
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.
|
|
72
|
+
<img src="https://img.shields.io/badge/version-v0.2.20-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.
|
|
221
|
+
VERSION = "0.2.20" # 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,170 @@ 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
|
+
# On Windows, SO_REUSEADDR lets two sockets share a port — wrong here; we
|
|
3743
|
+
# want a hard "port is taken, back off." SO_EXCLUSIVEADDRUSE gives us that.
|
|
3744
|
+
# On Linux, SO_REUSEADDR only matters for TIME_WAIT recovery, so skipping
|
|
3745
|
+
# it is fine — restart cooldown is a few seconds at worst.
|
|
3746
|
+
if hasattr(_socket, "SO_EXCLUSIVEADDRUSE"):
|
|
3747
|
+
try:
|
|
3748
|
+
sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_EXCLUSIVEADDRUSE, 1)
|
|
3749
|
+
except OSError:
|
|
3750
|
+
pass
|
|
3751
|
+
try:
|
|
3752
|
+
sock.bind((DULUS_IPC_HOST, DULUS_IPC_PORT))
|
|
3753
|
+
except OSError:
|
|
3754
|
+
return # another Dulus already listening — fine, we're the client one
|
|
3755
|
+
sock.listen(4)
|
|
3756
|
+
sock.settimeout(1.0)
|
|
3757
|
+
config["_ipc_listening"] = True
|
|
3758
|
+
|
|
3759
|
+
while not config.get("_ipc_stop"):
|
|
3760
|
+
try:
|
|
3761
|
+
conn, _addr = sock.accept()
|
|
3762
|
+
except _socket.timeout:
|
|
3763
|
+
continue
|
|
3764
|
+
except Exception:
|
|
3765
|
+
continue
|
|
3766
|
+
try:
|
|
3767
|
+
conn.settimeout(60.0)
|
|
3768
|
+
buf = b""
|
|
3769
|
+
while b"\n" not in buf and len(buf) < 64 * 1024:
|
|
3770
|
+
chunk = conn.recv(4096)
|
|
3771
|
+
if not chunk:
|
|
3772
|
+
break
|
|
3773
|
+
buf += chunk
|
|
3774
|
+
line = buf.split(b"\n", 1)[0].decode("utf-8", errors="ignore").strip()
|
|
3775
|
+
if not line:
|
|
3776
|
+
conn.close()
|
|
3777
|
+
continue
|
|
3778
|
+
try:
|
|
3779
|
+
req = _json.loads(line)
|
|
3780
|
+
except Exception:
|
|
3781
|
+
conn.sendall(b'{"error":"bad json"}\n')
|
|
3782
|
+
conn.close()
|
|
3783
|
+
continue
|
|
3784
|
+
|
|
3785
|
+
prompt = (req.get("prompt") or "").strip()
|
|
3786
|
+
if not prompt:
|
|
3787
|
+
conn.sendall(b'{"error":"empty prompt"}\n')
|
|
3788
|
+
conn.close()
|
|
3789
|
+
continue
|
|
3790
|
+
|
|
3791
|
+
cb = config.get("_run_query_callback")
|
|
3792
|
+
if not cb:
|
|
3793
|
+
conn.sendall(b'{"error":"no run_query callback registered"}\n')
|
|
3794
|
+
conn.close()
|
|
3795
|
+
continue
|
|
3796
|
+
|
|
3797
|
+
# Snapshot the message count so we can lift the new assistant
|
|
3798
|
+
# reply after the turn completes.
|
|
3799
|
+
before = len(state.messages) if state else 0
|
|
3800
|
+
try:
|
|
3801
|
+
cb(prompt)
|
|
3802
|
+
except Exception as e:
|
|
3803
|
+
conn.sendall(_json.dumps({"error": f"{type(e).__name__}: {e}"}).encode() + b"\n")
|
|
3804
|
+
conn.close()
|
|
3805
|
+
continue
|
|
3806
|
+
|
|
3807
|
+
response_text = ""
|
|
3808
|
+
if state and state.messages:
|
|
3809
|
+
for m in reversed(state.messages[before:] or state.messages):
|
|
3810
|
+
if m.get("role") == "assistant":
|
|
3811
|
+
content = m.get("content", "")
|
|
3812
|
+
if isinstance(content, list):
|
|
3813
|
+
parts = []
|
|
3814
|
+
for block in content:
|
|
3815
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
3816
|
+
parts.append(block["text"])
|
|
3817
|
+
elif isinstance(block, str):
|
|
3818
|
+
parts.append(block)
|
|
3819
|
+
content = "\n".join(parts)
|
|
3820
|
+
if content:
|
|
3821
|
+
response_text = content
|
|
3822
|
+
break
|
|
3823
|
+
payload = _json.dumps({"response": response_text or "(no reply)"}).encode() + b"\n"
|
|
3824
|
+
try:
|
|
3825
|
+
conn.sendall(payload)
|
|
3826
|
+
except Exception:
|
|
3827
|
+
pass
|
|
3828
|
+
finally:
|
|
3829
|
+
try:
|
|
3830
|
+
conn.close()
|
|
3831
|
+
except Exception:
|
|
3832
|
+
pass
|
|
3833
|
+
|
|
3834
|
+
try:
|
|
3835
|
+
sock.close()
|
|
3836
|
+
except Exception:
|
|
3837
|
+
pass
|
|
3838
|
+
|
|
3839
|
+
|
|
3840
|
+
def _try_ipc_dispatch(prompt: str, timeout: float = 0.4) -> bool:
|
|
3841
|
+
"""Client side: probe the IPC server, send a prompt, print the response,
|
|
3842
|
+
return True if it succeeded. Returns False if no server is listening,
|
|
3843
|
+
so callers can fall back to the in-process --print path."""
|
|
3844
|
+
import socket as _socket
|
|
3845
|
+
import json as _json
|
|
3846
|
+
|
|
3847
|
+
try:
|
|
3848
|
+
sock = _socket.create_connection(
|
|
3849
|
+
(DULUS_IPC_HOST, DULUS_IPC_PORT), timeout=timeout
|
|
3850
|
+
)
|
|
3851
|
+
except (_socket.timeout, ConnectionRefusedError, OSError):
|
|
3852
|
+
return False
|
|
3853
|
+
try:
|
|
3854
|
+
sock.settimeout(180.0)
|
|
3855
|
+
sock.sendall((_json.dumps({"prompt": prompt, "v": 1}) + "\n").encode())
|
|
3856
|
+
buf = b""
|
|
3857
|
+
while True:
|
|
3858
|
+
chunk = sock.recv(8192)
|
|
3859
|
+
if not chunk:
|
|
3860
|
+
break
|
|
3861
|
+
buf += chunk
|
|
3862
|
+
if b"\n" in buf:
|
|
3863
|
+
break
|
|
3864
|
+
line = buf.split(b"\n", 1)[0].decode("utf-8", errors="ignore").strip()
|
|
3865
|
+
try:
|
|
3866
|
+
data = _json.loads(line)
|
|
3867
|
+
except Exception:
|
|
3868
|
+
return False
|
|
3869
|
+
if "error" in data:
|
|
3870
|
+
print(f"[ipc] {data['error']}", flush=True)
|
|
3871
|
+
return True # we did get a reply, just an error one — don't fall back
|
|
3872
|
+
print(data.get("response", ""), flush=True)
|
|
3873
|
+
return True
|
|
3874
|
+
finally:
|
|
3875
|
+
try:
|
|
3876
|
+
sock.close()
|
|
3877
|
+
except Exception:
|
|
3878
|
+
pass
|
|
3879
|
+
|
|
3880
|
+
|
|
3717
3881
|
def _job_sentinel_loop(config, state):
|
|
3718
3882
|
"""Background daemon that triggers run_query as soon as a job finishes.
|
|
3719
3883
|
|
|
@@ -5173,6 +5337,15 @@ def _run_daemon(config: dict) -> None:
|
|
|
5173
5337
|
# Same callback used by the REPL so Telegram can trigger runs
|
|
5174
5338
|
config["_run_query_callback"] = lambda msg: run_query(msg, is_background=True)
|
|
5175
5339
|
|
|
5340
|
+
# IPC server — same socket the REPL uses, so external `dulus "..."` calls
|
|
5341
|
+
# land in this daemon's session.
|
|
5342
|
+
if config.get("_ipc_thread") is None and not config.get("_ipc_disabled"):
|
|
5343
|
+
ti = threading.Thread(
|
|
5344
|
+
target=_ipc_server_loop, args=(config, state), daemon=True
|
|
5345
|
+
)
|
|
5346
|
+
config["_ipc_thread"] = ti
|
|
5347
|
+
ti.start()
|
|
5348
|
+
|
|
5176
5349
|
print(clr("\n ▲ DULUS DAEMON", "accent", "bold"))
|
|
5177
5350
|
print(clr(" " + "─" * 40, "dim"))
|
|
5178
5351
|
info(f"Session: {session_id}")
|
|
@@ -7101,6 +7274,16 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7101
7274
|
tj = threading.Thread(target=_job_sentinel_loop, args=(config, state), daemon=True)
|
|
7102
7275
|
config["_job_sentinel_thread"] = tj
|
|
7103
7276
|
tj.start()
|
|
7277
|
+
|
|
7278
|
+
# IPC server — lets `dulus "..."` from another shell join this REPL's
|
|
7279
|
+
# session instead of spawning a fresh process. Tiny TCP socket on
|
|
7280
|
+
# 127.0.0.1:5151, no daemon manager required.
|
|
7281
|
+
if config.get("_ipc_thread") is None and not config.get("_ipc_disabled"):
|
|
7282
|
+
ti = threading.Thread(
|
|
7283
|
+
target=_ipc_server_loop, args=(config, state), daemon=True
|
|
7284
|
+
)
|
|
7285
|
+
config["_ipc_thread"] = ti
|
|
7286
|
+
ti.start()
|
|
7104
7287
|
|
|
7105
7288
|
def run_query(user_input: str, is_background: bool = False):
|
|
7106
7289
|
nonlocal verbose
|
|
@@ -8652,6 +8835,25 @@ def main():
|
|
|
8652
8835
|
|
|
8653
8836
|
initial = " ".join(args.prompt) if args.prompt else None
|
|
8654
8837
|
|
|
8838
|
+
# ── IPC dispatch: if a Dulus REPL/daemon is already running on
|
|
8839
|
+
# 127.0.0.1:5151, forward this prompt to it (shared session) and exit.
|
|
8840
|
+
# Falls through silently when no listener is up.
|
|
8841
|
+
# Only kicks in for plain `dulus "..."` and `dulus -p "..."` — not for
|
|
8842
|
+
# daemon/gui/cmd/run-tool/job invocations, which need their own process.
|
|
8843
|
+
if (initial
|
|
8844
|
+
and not args.daemon
|
|
8845
|
+
and not args.gui
|
|
8846
|
+
and not args.exec_cmd
|
|
8847
|
+
and not args.run_tool
|
|
8848
|
+
and not args.job_id
|
|
8849
|
+
and not args.job_path
|
|
8850
|
+
):
|
|
8851
|
+
try:
|
|
8852
|
+
if _try_ipc_dispatch(initial):
|
|
8853
|
+
sys.exit(0)
|
|
8854
|
+
except Exception:
|
|
8855
|
+
pass # any IPC error → fall through to in-process path
|
|
8856
|
+
|
|
8655
8857
|
# ── Daemon mode ──
|
|
8656
8858
|
if args.daemon:
|
|
8657
8859
|
_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.
|
|
7
|
+
version = "0.2.20"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|