dulus 0.2.7__tar.gz → 0.2.9__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.7/dulus.egg-info → dulus-0.2.9}/PKG-INFO +4 -4
- {dulus-0.2.7 → dulus-0.2.9}/README.md +3 -3
- {dulus-0.2.7 → dulus-0.2.9}/common.py +54 -25
- {dulus-0.2.7 → dulus-0.2.9/dulus.egg-info}/PKG-INFO +4 -4
- {dulus-0.2.7 → dulus-0.2.9}/dulus.py +53 -8
- {dulus-0.2.7 → dulus-0.2.9}/memory/context.py +0 -2
- {dulus-0.2.7 → dulus-0.2.9}/memory/offload.py +26 -8
- {dulus-0.2.7 → dulus-0.2.9}/pyproject.toml +1 -1
- {dulus-0.2.7 → dulus-0.2.9}/LICENSE +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/MANIFEST.in +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/agent.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/compressor.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/context.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/githook.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/marketplace.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/personas.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/plugins.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/server.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/backend/tasks.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/batch_api.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/checkpoint/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/checkpoint/hooks.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/checkpoint/store.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/checkpoint/types.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/claude_code_watcher.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/clipboard_utils.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/cloudsave.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/compaction.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/config.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/context.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/active_persona.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/context.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/marketplace.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/personas.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/data/tasks.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/README.md +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/api.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/architecture.md +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/dashboard/index.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/divider.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/generate.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/hero.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/index.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/news.md +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/particle-playground.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/personas/index.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/preview.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-agents.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-features.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-memory.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-models.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-perms.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/spinners.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/split-pane.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_gui.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_mcp/client.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_mcp/config.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/dulus_mcp/types.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/agent_bridge.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/chat_widget.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/main_window.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/personas.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/session_utils.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/settings_dialog.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/sidebar.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/tasks_view.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/themes.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/gui/tool_panel.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/input.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/license_manager.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/audit.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/consolidator.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/palace.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/scan.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/sessions.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/store.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/types.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/memory/vector_search.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/multi_agent/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/multi_agent/subagent.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/multi_agent/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/offload_helper.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/autoadapter.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/loader.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/recommend.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/store.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/plugin/types.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/providers.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/setup.cfg +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/builtin.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/clawhub.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/executor.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/loader.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skill/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/skills.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/spinner.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/string_utils.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/subagent.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/task/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/task/store.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/task/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/task/types.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_compaction.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_diff_view.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_license.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_mcp.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_memory.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_plugin.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_skills.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_subagent.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_task.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tests/test_voice.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tmux_offloader.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tmux_tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tool_registry.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/tools.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/ui/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/ui/input.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/ui/render.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/voice/__init__.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/voice/keyterms.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/voice/recorder.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/voice/stt.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/voice/tts.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/webchat.py +0 -0
- {dulus-0.2.7 → dulus-0.2.9}/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.9
|
|
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
|
|
@@ -84,7 +84,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
84
84
|
|
|
85
85
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
86
86
|
|
|
87
|
-
> **
|
|
87
|
+
> **v0.2.9 — May 8, 2026** — Per-turn injection now queries the real MemPalace (`~/.mempalace/palace`) instead of the small local memory dir, so deep context (bond, soul, sessions) actually surfaces. `/theme` palette upgraded to 4 semantic roles (accent / ok / warn / err) with a live preview swatch in the listing. TmuxOffload accepts `tool_input` as alias of `tool_params`, sends an explicit second Enter on Windows so commands containing `&&`/`||`/`;` don't sit typed-but-unexecuted.
|
|
88
88
|
> Type `/news` to see what changed.
|
|
89
89
|
|
|
90
90
|
---
|
|
@@ -113,14 +113,14 @@ ROUND TABLE (DULUS UNIQUE FEATURE)
|
|
|
113
113
|
|
|
114
114
|
<img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
|
|
115
115
|
|
|
116
|
-
Dulus is the first one meeting
|
|
116
|
+
Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
### One-liner
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
pip install dulus
|
|
123
|
+
pip install dulus && dulus
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
That's it. Dulus prompts you for a key on first run.
|
|
@@ -45,7 +45,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
45
45
|
|
|
46
46
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
47
47
|
|
|
48
|
-
> **
|
|
48
|
+
> **v0.2.9 — May 8, 2026** — Per-turn injection now queries the real MemPalace (`~/.mempalace/palace`) instead of the small local memory dir, so deep context (bond, soul, sessions) actually surfaces. `/theme` palette upgraded to 4 semantic roles (accent / ok / warn / err) with a live preview swatch in the listing. TmuxOffload accepts `tool_input` as alias of `tool_params`, sends an explicit second Enter on Windows so commands containing `&&`/`||`/`;` don't sit typed-but-unexecuted.
|
|
49
49
|
> Type `/news` to see what changed.
|
|
50
50
|
|
|
51
51
|
---
|
|
@@ -74,14 +74,14 @@ ROUND TABLE (DULUS UNIQUE FEATURE)
|
|
|
74
74
|
|
|
75
75
|
<img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
|
|
76
76
|
|
|
77
|
-
Dulus is the first one meeting
|
|
77
|
+
Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
### One-liner
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
pip install dulus
|
|
84
|
+
pip install dulus && dulus
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
That's it. Dulus prompts you for a key on first run.
|
|
@@ -38,24 +38,30 @@ def _rgb(hex_str: str) -> str:
|
|
|
38
38
|
return f"\033[38;2;{r};{g};{b}m"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
# Curated palettes
|
|
42
|
-
#
|
|
43
|
-
#
|
|
41
|
+
# Curated palettes — each theme defines four semantic roles:
|
|
42
|
+
# accent : info / primary chrome (cyan, blue)
|
|
43
|
+
# ok : success / diff additions (green) — kept distinct from accent
|
|
44
|
+
# so info() and ok() stay visually separable
|
|
45
|
+
# warn : warnings (yellow, magenta)
|
|
46
|
+
# err : errors / diff removals (red)
|
|
47
|
+
# code : Rich Markdown code-block style (any Pygments style name)
|
|
48
|
+
# Use {"disable_color": True, "code": "default"} to ship a colorless theme.
|
|
49
|
+
# Add new entries here and they show up in `/theme` automatically.
|
|
44
50
|
THEMES: dict = {
|
|
45
|
-
"dulus":
|
|
46
|
-
"dracula": {"accent": "#BD93F9", "warn": "#FFB86C", "code": "dracula"},
|
|
47
|
-
"nord": {"accent": "#88C0D0", "warn": "#EBCB8B", "code": "nord"},
|
|
48
|
-
"gruvbox": {"accent": "#FABD2F", "warn": "#FE8019", "code": "gruvbox-dark"},
|
|
49
|
-
"solarized": {"accent": "#268BD2", "warn": "#B58900", "code": "solarized-dark"},
|
|
50
|
-
"tokyo-night": {"accent": "#7AA2F7", "warn": "#E0AF68", "code": "one-dark"},
|
|
51
|
-
"catppuccin": {"accent": "#F5C2E7", "warn": "#FAB387", "code": "one-dark"},
|
|
52
|
-
"matrix": {"accent": "#00FF41", "warn": "#CCFF00", "code": "monokai"},
|
|
53
|
-
"synthwave": {"accent": "#FF00FF", "warn": "#FFCC00", "code": "fruity"},
|
|
54
|
-
"midnight": {"accent": "#00BCD4", "warn": "#FFC107", "code": "dracula"},
|
|
55
|
-
"ocean": {"accent": "#
|
|
56
|
-
"monokai": {"accent": "#
|
|
57
|
-
"mono": {"accent": "#E0E0E0", "warn": "#A0A0A0", "code": "bw"},
|
|
58
|
-
"none": {"
|
|
51
|
+
"dulus": {"accent": "#FF8700", "ok": "#00FF87", "warn": "#FFAF00", "err": "#FF5F5F", "code": "monokai"},
|
|
52
|
+
"dracula": {"accent": "#BD93F9", "ok": "#50FA7B", "warn": "#FFB86C", "err": "#FF5555", "code": "dracula"},
|
|
53
|
+
"nord": {"accent": "#88C0D0", "ok": "#A3BE8C", "warn": "#EBCB8B", "err": "#BF616A", "code": "nord"},
|
|
54
|
+
"gruvbox": {"accent": "#FABD2F", "ok": "#B8BB26", "warn": "#FE8019", "err": "#FB4934", "code": "gruvbox-dark"},
|
|
55
|
+
"solarized": {"accent": "#268BD2", "ok": "#859900", "warn": "#B58900", "err": "#DC322F", "code": "solarized-dark"},
|
|
56
|
+
"tokyo-night": {"accent": "#7AA2F7", "ok": "#9ECE6A", "warn": "#E0AF68", "err": "#F7768E", "code": "one-dark"},
|
|
57
|
+
"catppuccin": {"accent": "#F5C2E7", "ok": "#A6E3A1", "warn": "#FAB387", "err": "#F38BA8", "code": "one-dark"},
|
|
58
|
+
"matrix": {"accent": "#00FF41", "ok": "#7FFF00", "warn": "#CCFF00", "err": "#FF0000", "code": "monokai"},
|
|
59
|
+
"synthwave": {"accent": "#FF00FF", "ok": "#39FF14", "warn": "#FFCC00", "err": "#FF3864", "code": "fruity"},
|
|
60
|
+
"midnight": {"accent": "#00BCD4", "ok": "#76FF03", "warn": "#FFC107", "err": "#FF1744", "code": "dracula"},
|
|
61
|
+
"ocean": {"accent": "#38BDF8", "ok": "#34D399", "warn": "#FBBF24", "err": "#F87171", "code": "nord"},
|
|
62
|
+
"monokai": {"accent": "#66D9EF", "ok": "#A6E22E", "warn": "#E6DB74", "err": "#F92672", "code": "monokai"},
|
|
63
|
+
"mono": {"accent": "#E0E0E0", "ok": "#C0C0C0", "warn": "#A0A0A0", "err": "#FFFFFF", "code": "bw"},
|
|
64
|
+
"none": {"disable_color": True, "code": "default"},
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
# Active code-block style for Rich Markdown rendering — read by dulus.py.
|
|
@@ -71,19 +77,42 @@ C = {
|
|
|
71
77
|
|
|
72
78
|
|
|
73
79
|
def apply_theme(name: str) -> bool:
|
|
74
|
-
"""Mutate the global ANSI color map in-place to a named theme.
|
|
80
|
+
"""Mutate the global ANSI color map in-place to a named theme.
|
|
81
|
+
|
|
82
|
+
Themes carry 4 semantic roles (accent / ok / warn / err) that map onto
|
|
83
|
+
Dulus's ANSI key set. `ok` is intentionally distinct from `accent` so
|
|
84
|
+
info() (cyan-keyed) and ok() (green-keyed) stay visually separable.
|
|
85
|
+
A theme with `disable_color: True` strips every escape for plain output.
|
|
86
|
+
"""
|
|
75
87
|
global CODE_THEME
|
|
76
88
|
p = THEMES.get(name)
|
|
77
89
|
if not p:
|
|
78
90
|
return False
|
|
91
|
+
|
|
92
|
+
# Plain-text mode: zero out every key so clr() returns naked strings.
|
|
93
|
+
if p.get("disable_color"):
|
|
94
|
+
for k in list(C.keys()):
|
|
95
|
+
C[k] = ""
|
|
96
|
+
CODE_THEME = p.get("code", "default")
|
|
97
|
+
return True
|
|
98
|
+
|
|
79
99
|
accent = _rgb(p["accent"])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
C["
|
|
85
|
-
C["
|
|
86
|
-
|
|
100
|
+
ok_col = _rgb(p.get("ok", p["accent"]))
|
|
101
|
+
warn_c = _rgb(p["warn"])
|
|
102
|
+
err_c = _rgb(p.get("err", "#FF5555"))
|
|
103
|
+
|
|
104
|
+
C["cyan"] = accent
|
|
105
|
+
C["blue"] = accent
|
|
106
|
+
C["green"] = ok_col
|
|
107
|
+
C["yellow"] = warn_c
|
|
108
|
+
C["magenta"] = warn_c
|
|
109
|
+
C["red"] = err_c
|
|
110
|
+
C["white"] = "\033[97m"
|
|
111
|
+
C["gray"] = "\033[90m"
|
|
112
|
+
C["bold"] = "\033[1m"
|
|
113
|
+
C["dim"] = "\033[2m"
|
|
114
|
+
C["reset"] = "\033[0m"
|
|
115
|
+
CODE_THEME = p["code"]
|
|
87
116
|
return True
|
|
88
117
|
|
|
89
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -84,7 +84,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
84
84
|
|
|
85
85
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
86
86
|
|
|
87
|
-
> **
|
|
87
|
+
> **v0.2.9 — May 8, 2026** — Per-turn injection now queries the real MemPalace (`~/.mempalace/palace`) instead of the small local memory dir, so deep context (bond, soul, sessions) actually surfaces. `/theme` palette upgraded to 4 semantic roles (accent / ok / warn / err) with a live preview swatch in the listing. TmuxOffload accepts `tool_input` as alias of `tool_params`, sends an explicit second Enter on Windows so commands containing `&&`/`||`/`;` don't sit typed-but-unexecuted.
|
|
88
88
|
> Type `/news` to see what changed.
|
|
89
89
|
|
|
90
90
|
---
|
|
@@ -113,14 +113,14 @@ ROUND TABLE (DULUS UNIQUE FEATURE)
|
|
|
113
113
|
|
|
114
114
|
<img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
|
|
115
115
|
|
|
116
|
-
Dulus is the first one meeting
|
|
116
|
+
Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
### One-liner
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
pip install dulus
|
|
123
|
+
pip install dulus && dulus
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
That's it. Dulus prompts you for a key on first run.
|
|
@@ -3011,10 +3011,20 @@ def cmd_theme(args: str, _state, config) -> bool:
|
|
|
3011
3011
|
if not name:
|
|
3012
3012
|
current = config.get("theme", "dulus")
|
|
3013
3013
|
print(clr(" ── Available themes ──", "cyan", "bold"))
|
|
3014
|
+
_RESET = "\033[0m"
|
|
3014
3015
|
for t, p in _cm.THEMES.items():
|
|
3015
3016
|
marker = "●" if t == current else " "
|
|
3016
|
-
|
|
3017
|
-
|
|
3017
|
+
if p.get("disable_color"):
|
|
3018
|
+
swatch = " (no color) "
|
|
3019
|
+
else:
|
|
3020
|
+
fb = p.get("accent", "#FFFFFF")
|
|
3021
|
+
swatch = (
|
|
3022
|
+
f"{_cm._rgb(p.get('accent', fb))} info {_RESET}"
|
|
3023
|
+
f"{_cm._rgb(p.get('ok', fb))} ok {_RESET}"
|
|
3024
|
+
f"{_cm._rgb(p.get('warn', fb))} warn {_RESET}"
|
|
3025
|
+
f"{_cm._rgb(p.get('err', '#FF5555'))} err {_RESET}"
|
|
3026
|
+
)
|
|
3027
|
+
print(f" {marker} {t:<14} {swatch} ({p['code']})")
|
|
3018
3028
|
print(clr(f" Use: /theme <name> (current: {current})", "dim"))
|
|
3019
3029
|
return True
|
|
3020
3030
|
if not _cm.apply_theme(name):
|
|
@@ -7041,25 +7051,60 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7041
7051
|
else:
|
|
7042
7052
|
_trivial = {"hola", "klk", "gracias", "ok", "si", "no", "dale",
|
|
7043
7053
|
"exit", "quit", "help", "thanks", "bien"}
|
|
7044
|
-
_first = user_input.strip().lower().split()[0]
|
|
7054
|
+
_first = user_input.strip().lower().split()[0].strip(".,!?;:")
|
|
7045
7055
|
if _first in _trivial:
|
|
7046
7056
|
_mp_log(f"skip: trivial first word '{_first}'", "dim")
|
|
7047
7057
|
else:
|
|
7048
7058
|
try:
|
|
7049
7059
|
import re as _re
|
|
7050
|
-
from memory import find_relevant_memories
|
|
7051
7060
|
_q = user_input.strip()[:200]
|
|
7052
7061
|
_mp_log(f"querying: {_q!r}")
|
|
7053
|
-
_raw_hits =
|
|
7062
|
+
_raw_hits = []
|
|
7063
|
+
# Primary: query the real MemPalace (~/.mempalace/palace) which holds
|
|
7064
|
+
# the rich corpus (hija_palace, soul, bond, sessions, knowledge, etc.).
|
|
7065
|
+
# Dulus's local find_relevant_memories only sees ~/.dulus/memory/*.md,
|
|
7066
|
+
# which is a tiny slice and was the reason the same 3 generic files
|
|
7067
|
+
# kept getting injected on every turn.
|
|
7068
|
+
try:
|
|
7069
|
+
from mempalace.searcher import search_memories as _mp_search
|
|
7070
|
+
from mempalace.config import MempalaceConfig as _MPCfg
|
|
7071
|
+
_palace = _MPCfg().palace_path
|
|
7072
|
+
_res = _mp_search(_q, _palace, n_results=3)
|
|
7073
|
+
for _hit in (_res or {}).get("results", []):
|
|
7074
|
+
_meta = _hit.get("metadata") or {}
|
|
7075
|
+
_src = _meta.get("source_file") or _meta.get("name") or "palace"
|
|
7076
|
+
_name = str(_src).rsplit("/", 1)[-1].rsplit("\\", 1)[-1].rsplit(".", 1)[0]
|
|
7077
|
+
_vec = max(0.0, 1.0 - float(_hit.get("distance", 1.0)))
|
|
7078
|
+
_bm = float(_hit.get("bm25_score", 0.0))
|
|
7079
|
+
_raw_hits.append({
|
|
7080
|
+
"name": _name,
|
|
7081
|
+
"description": _meta.get("wing") or _meta.get("room") or "",
|
|
7082
|
+
"content": _hit.get("text", ""),
|
|
7083
|
+
"keyword_score": max(_vec, _bm),
|
|
7084
|
+
})
|
|
7085
|
+
_mp_log(f"mempalace hits: {len(_raw_hits)}")
|
|
7086
|
+
except Exception as _mpe:
|
|
7087
|
+
_mp_log(f"mempalace unavailable, falling back to local: {type(_mpe).__name__}: {_mpe}", "dim")
|
|
7088
|
+
# Fallback: Dulus's local memory dir (the old path)
|
|
7054
7089
|
if not _raw_hits:
|
|
7055
|
-
|
|
7090
|
+
from memory import find_relevant_memories
|
|
7091
|
+
_raw_hits = find_relevant_memories(_q, max_results=3)
|
|
7092
|
+
_MIN_SCORE = 0.15
|
|
7093
|
+
if _mp_dbg:
|
|
7094
|
+
for _h in _raw_hits:
|
|
7095
|
+
_mp_log(f" hit: score={float(_h.get('keyword_score', 0.0)):.3f} {_h.get('name','?')}", "dim")
|
|
7096
|
+
_kept = [h for h in _raw_hits if float(h.get("keyword_score", 0.0)) >= _MIN_SCORE]
|
|
7097
|
+
if not _kept:
|
|
7098
|
+
_mp_log(f"skip: no hits above threshold {_MIN_SCORE} (raw={len(_raw_hits)})", "dim")
|
|
7056
7099
|
else:
|
|
7100
|
+
_BODY_BUDGET = 1800
|
|
7101
|
+
_per_hit = max(300, _BODY_BUDGET // len(_kept))
|
|
7057
7102
|
_parts = []
|
|
7058
|
-
for _i, _h in enumerate(
|
|
7103
|
+
for _i, _h in enumerate(_kept, 1):
|
|
7059
7104
|
_name = _h.get("name", f"hit_{_i}")
|
|
7060
7105
|
_desc = _h.get("description", "")
|
|
7061
7106
|
_body = _h.get("content", "").strip()
|
|
7062
|
-
_snip = _body[:
|
|
7107
|
+
_snip = _body[:_per_hit] + ("..." if len(_body) > _per_hit else "")
|
|
7063
7108
|
if _desc:
|
|
7064
7109
|
_parts.append(f"### {_name}\n_{_desc}_\n{_snip}")
|
|
7065
7110
|
else:
|
|
@@ -156,8 +156,6 @@ def find_relevant_memories(
|
|
|
156
156
|
if ks == 0.0 and vs == 0.0:
|
|
157
157
|
continue
|
|
158
158
|
score = 0.55 * vs + 0.45 * ks
|
|
159
|
-
# Tiny confidence nudge so high-confidence memories break ties
|
|
160
|
-
score += 0.01 * float(getattr(entry, "confidence", 1.0))
|
|
161
159
|
entry._search_score = score # type: ignore[attr-defined]
|
|
162
160
|
fused.append((score, entry))
|
|
163
161
|
|
|
@@ -20,14 +20,25 @@ def _tmux_offload(params: dict, config: dict) -> str:
|
|
|
20
20
|
# Note: We don't care if already inside tmux - just create the session
|
|
21
21
|
|
|
22
22
|
tool_name = params["tool_name"]
|
|
23
|
-
tool_params
|
|
23
|
+
# Accept either `tool_params` (canonical) or `tool_input` (Claude Code
|
|
24
|
+
# convention). Models trained on Anthropic tool-use schemas reach for
|
|
25
|
+
# `tool_input` by reflex; silently dropping it stranded jobs with empty
|
|
26
|
+
# params and no error.
|
|
27
|
+
tool_params = params.get("tool_params")
|
|
28
|
+
if tool_params is None:
|
|
29
|
+
tool_params = params.get("tool_input", {})
|
|
30
|
+
if not isinstance(tool_params, dict):
|
|
31
|
+
return f"Error: tool_params/tool_input must be an object, got {type(tool_params).__name__}"
|
|
24
32
|
|
|
25
33
|
# Create Job ID and directory
|
|
26
34
|
job_id = uuid.uuid4().hex[:8]
|
|
27
35
|
JOBS_DIR.mkdir(parents=True, exist_ok=True)
|
|
28
36
|
job_path = JOBS_DIR / f"{job_id}.json"
|
|
29
37
|
|
|
30
|
-
# Save initial job state
|
|
38
|
+
# Save initial job state.
|
|
39
|
+
# IMPORTANT: never persist the parent config here — the child process
|
|
40
|
+
# calls load_config() itself, and dumping the in-memory config leaks
|
|
41
|
+
# API keys, session tokens, telegram bots, etc. to ~/.dulus/jobs/*.json.
|
|
31
42
|
job_data = {
|
|
32
43
|
"id": job_id,
|
|
33
44
|
"tool_name": tool_name,
|
|
@@ -35,7 +46,6 @@ def _tmux_offload(params: dict, config: dict) -> str:
|
|
|
35
46
|
"status": "running",
|
|
36
47
|
"created_at": datetime.now().isoformat(),
|
|
37
48
|
"owner_pid": os.getpid(),
|
|
38
|
-
"config": {k: v for k, v in config.items() if not k.startswith("_")}
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
with open(job_path, "w", encoding="utf-8") as f:
|
|
@@ -71,9 +81,6 @@ def _tmux_offload(params: dict, config: dict) -> str:
|
|
|
71
81
|
if sys.platform == "win32":
|
|
72
82
|
# Windows: Use absolute path to dulus.py since tmux starts in home dir, not DULUS dir
|
|
73
83
|
dulus_path_str = str(dulus_script).replace("\\", "/")
|
|
74
|
-
# Write a wrapper script that handles errors properly
|
|
75
|
-
# Use & instead of ; so kill-session runs regardless, and capture output
|
|
76
|
-
# Quote paths with spaces to prevent cmd.exe from splitting them
|
|
77
84
|
cmd = f'python "{dulus_path_str}" --run-tool {tool_name} --job-id {job_id} --job-path "{job_path_str}" 2>&1 && echo SUCCESS || echo FAILED; tmux kill-session -t {session_name}'
|
|
78
85
|
else:
|
|
79
86
|
# Unix/Linux: unset PSMUX vars and use tee
|
|
@@ -82,6 +89,13 @@ def _tmux_offload(params: dict, config: dict) -> str:
|
|
|
82
89
|
cmd = f"unset PSMUX PSMUX_SESSION PSMUX_SOCKET 2>/dev/null; \"{python_exe}\" -u \"{dulus_script}\" --run-tool {tool_name} --job-id {job_id} --job-path \"{job_path}\" 2>&1 | tee \"{job_log}\" \"{last_log}\"; tmux kill-session -t {session_name}"
|
|
83
90
|
|
|
84
91
|
send_result = _tmux_send_keys({"keys": cmd, "target": f"{session_name}:0"}, config)
|
|
92
|
+
# Belt-and-suspenders: a second explicit Enter. On Windows tmux + cmd.exe the
|
|
93
|
+
# implicit `Enter` arg in the first send-keys sometimes gets swallowed by the
|
|
94
|
+
# cmd.exe outer parser when the keys string contains `&&` / `||` / `;`, so the
|
|
95
|
+
# command sits typed but unexecuted. The second send-keys is just an Enter — no
|
|
96
|
+
# special chars to fight with — and reliably submits the line.
|
|
97
|
+
if sys.platform == "win32":
|
|
98
|
+
_tmux_send_keys({"keys": "", "target": f"{session_name}:0", "press_enter": True}, config)
|
|
85
99
|
if "failed" in send_result.lower() or "error" in send_result.lower():
|
|
86
100
|
# Clean up the session since we can't send keys
|
|
87
101
|
_run(f"tmux kill-session -t {session_name}", timeout=2)
|
|
@@ -136,10 +150,14 @@ def register_offload_tool():
|
|
|
136
150
|
},
|
|
137
151
|
"tool_params": {
|
|
138
152
|
"type": "object",
|
|
139
|
-
"description": "Parameters for the target tool"
|
|
153
|
+
"description": "Parameters for the target tool. Alias `tool_input` is also accepted."
|
|
154
|
+
},
|
|
155
|
+
"tool_input": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"description": "Alias of tool_params for callers using Claude Code's tool-use convention."
|
|
140
158
|
},
|
|
141
159
|
},
|
|
142
|
-
"required": ["tool_name"
|
|
160
|
+
"required": ["tool_name"],
|
|
143
161
|
},
|
|
144
162
|
},
|
|
145
163
|
func=_tmux_offload,
|
|
@@ -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.9"
|
|
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
|