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