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.
- {dulus-0.2.29/dulus.egg-info → dulus-0.2.31}/PKG-INFO +2 -2
- {dulus-0.2.29 → dulus-0.2.31}/README.md +1 -1
- {dulus-0.2.29 → dulus-0.2.31}/docs/news.md +2 -0
- {dulus-0.2.29 → dulus-0.2.31/dulus.egg-info}/PKG-INFO +2 -2
- {dulus-0.2.29 → dulus-0.2.31}/dulus.py +213 -56
- {dulus-0.2.29 → dulus-0.2.31}/pyproject.toml +1 -1
- {dulus-0.2.29 → dulus-0.2.31}/LICENSE +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/MANIFEST.in +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/agent.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/compressor.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/context.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/githook.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/marketplace.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/personas.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/plugins.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/server.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/backend/tasks.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/batch_api.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/checkpoint/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/checkpoint/hooks.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/checkpoint/store.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/checkpoint/types.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/claude_code_watcher.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/clipboard_utils.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/cloudsave.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/common.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/compaction.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/config.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/context.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/active_persona.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/context.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/marketplace.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/personas.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/data/tasks.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/README.md +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/api.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/architecture.md +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/dashboard/index.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/divider.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/generate.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/hero.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/index.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/particle-playground.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/personas/index.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/poetry-banner.png +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/preview.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-agents.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-features.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-memory.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-models.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-perms.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/spinners.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/split-pane.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_gui.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/client.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/config.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/dulus_mcp/types.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/agent_bridge.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/chat_widget.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/main_window.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/personas.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/session_utils.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/settings_dialog.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/sidebar.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/tasks_view.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/themes.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/gui/tool_panel.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/input.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/license_manager.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/audit.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/consolidator.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/context.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/offload.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/palace.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/scan.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/sessions.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/store.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/types.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/memory/vector_search.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/multi_agent/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/multi_agent/subagent.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/multi_agent/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/offload_helper.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/autoadapter.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/loader.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/recommend.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/store.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/plugin/types.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/providers.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/setup.cfg +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/builtin.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/clawhub.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/executor.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/loader.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skill/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/skills.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/spinner.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/string_utils.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/subagent.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/task/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/task/store.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/task/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/task/types.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_compaction.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_diff_view.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_license.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_mcp.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_memory.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_plugin.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_skills.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_subagent.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_task.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tests/test_voice.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tmux_offloader.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tmux_tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tool_registry.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/tools.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/ui/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/ui/input.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/ui/render.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/voice/__init__.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/voice/keyterms.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/voice/recorder.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/voice/stt.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/voice/tts.py +0 -0
- {dulus-0.2.29 → dulus-0.2.31}/webchat.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1586
|
+
else:
|
|
1587
|
+
try:
|
|
1552
1588
|
_os.kill(pid, 0)
|
|
1553
1589
|
return True
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
|
1654
|
-
#
|
|
1655
|
-
#
|
|
1656
|
-
#
|
|
1657
|
-
#
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
-
#
|
|
1770
|
-
#
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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 = [
|
|
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
|
-
#
|
|
1794
|
-
|
|
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=
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|