dulus 0.2.8__tar.gz → 0.2.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. {dulus-0.2.8/dulus.egg-info → dulus-0.2.10}/PKG-INFO +15 -2
  2. {dulus-0.2.8 → dulus-0.2.10}/README.md +13 -1
  3. {dulus-0.2.8 → dulus-0.2.10}/common.py +54 -25
  4. {dulus-0.2.8 → dulus-0.2.10/dulus.egg-info}/PKG-INFO +15 -2
  5. {dulus-0.2.8 → dulus-0.2.10}/dulus.egg-info/requires.txt +1 -0
  6. {dulus-0.2.8 → dulus-0.2.10}/dulus.py +42 -4
  7. {dulus-0.2.8 → dulus-0.2.10}/memory/offload.py +22 -6
  8. {dulus-0.2.8 → dulus-0.2.10}/pyproject.toml +6 -1
  9. {dulus-0.2.8 → dulus-0.2.10}/LICENSE +0 -0
  10. {dulus-0.2.8 → dulus-0.2.10}/MANIFEST.in +0 -0
  11. {dulus-0.2.8 → dulus-0.2.10}/agent.py +0 -0
  12. {dulus-0.2.8 → dulus-0.2.10}/backend/__init__.py +0 -0
  13. {dulus-0.2.8 → dulus-0.2.10}/backend/compressor.py +0 -0
  14. {dulus-0.2.8 → dulus-0.2.10}/backend/context.py +0 -0
  15. {dulus-0.2.8 → dulus-0.2.10}/backend/githook.py +0 -0
  16. {dulus-0.2.8 → dulus-0.2.10}/backend/marketplace.py +0 -0
  17. {dulus-0.2.8 → dulus-0.2.10}/backend/mempalace_bridge.py +0 -0
  18. {dulus-0.2.8 → dulus-0.2.10}/backend/personas.py +0 -0
  19. {dulus-0.2.8 → dulus-0.2.10}/backend/plugins.py +0 -0
  20. {dulus-0.2.8 → dulus-0.2.10}/backend/server.py +0 -0
  21. {dulus-0.2.8 → dulus-0.2.10}/backend/tasks.py +0 -0
  22. {dulus-0.2.8 → dulus-0.2.10}/batch_api.py +0 -0
  23. {dulus-0.2.8 → dulus-0.2.10}/checkpoint/__init__.py +0 -0
  24. {dulus-0.2.8 → dulus-0.2.10}/checkpoint/hooks.py +0 -0
  25. {dulus-0.2.8 → dulus-0.2.10}/checkpoint/store.py +0 -0
  26. {dulus-0.2.8 → dulus-0.2.10}/checkpoint/types.py +0 -0
  27. {dulus-0.2.8 → dulus-0.2.10}/claude_code_watcher.py +0 -0
  28. {dulus-0.2.8 → dulus-0.2.10}/clipboard_utils.py +0 -0
  29. {dulus-0.2.8 → dulus-0.2.10}/cloudsave.py +0 -0
  30. {dulus-0.2.8 → dulus-0.2.10}/compaction.py +0 -0
  31. {dulus-0.2.8 → dulus-0.2.10}/config.py +0 -0
  32. {dulus-0.2.8 → dulus-0.2.10}/context.py +0 -0
  33. {dulus-0.2.8 → dulus-0.2.10}/data/__init__.py +0 -0
  34. {dulus-0.2.8 → dulus-0.2.10}/data/active_persona.json +0 -0
  35. {dulus-0.2.8 → dulus-0.2.10}/data/context.json +0 -0
  36. {dulus-0.2.8 → dulus-0.2.10}/data/marketplace.json +0 -0
  37. {dulus-0.2.8 → dulus-0.2.10}/data/personas.json +0 -0
  38. {dulus-0.2.8 → dulus-0.2.10}/data/tasks.json +0 -0
  39. {dulus-0.2.8 → dulus-0.2.10}/docs/README.md +0 -0
  40. {dulus-0.2.8 → dulus-0.2.10}/docs/__init__.py +0 -0
  41. {dulus-0.2.8 → dulus-0.2.10}/docs/api.html +0 -0
  42. {dulus-0.2.8 → dulus-0.2.10}/docs/architecture.md +0 -0
  43. {dulus-0.2.8 → dulus-0.2.10}/docs/azure-speech-template.json +0 -0
  44. {dulus-0.2.8 → dulus-0.2.10}/docs/dashboard/index.html +0 -0
  45. {dulus-0.2.8 → dulus-0.2.10}/docs/divider.svg +0 -0
  46. {dulus-0.2.8 → dulus-0.2.10}/docs/generate.py +0 -0
  47. {dulus-0.2.8 → dulus-0.2.10}/docs/hero.svg +0 -0
  48. {dulus-0.2.8 → dulus-0.2.10}/docs/index.html +0 -0
  49. {dulus-0.2.8 → dulus-0.2.10}/docs/news.md +0 -0
  50. {dulus-0.2.8 → dulus-0.2.10}/docs/nvidia-models.svg +0 -0
  51. {dulus-0.2.8 → dulus-0.2.10}/docs/particle-playground.html +0 -0
  52. {dulus-0.2.8 → dulus-0.2.10}/docs/personas/index.html +0 -0
  53. {dulus-0.2.8 → dulus-0.2.10}/docs/preview.html +0 -0
  54. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-agents.svg +0 -0
  55. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-brainstorm.svg +0 -0
  56. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-bridges.svg +0 -0
  57. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-features.svg +0 -0
  58. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-freetier.svg +0 -0
  59. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-memory.svg +0 -0
  60. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-models.svg +0 -0
  61. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-perms.svg +0 -0
  62. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-plugins.svg +0 -0
  63. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-quickstart.svg +0 -0
  64. {dulus-0.2.8 → dulus-0.2.10}/docs/sec-ssj.svg +0 -0
  65. {dulus-0.2.8 → dulus-0.2.10}/docs/spinners.svg +0 -0
  66. {dulus-0.2.8 → dulus-0.2.10}/docs/split-pane.svg +0 -0
  67. {dulus-0.2.8 → dulus-0.2.10}/docs/terminal-boot.svg +0 -0
  68. {dulus-0.2.8 → dulus-0.2.10}/docs/uploads/particle-playground.html +0 -0
  69. {dulus-0.2.8 → dulus-0.2.10}/dulus.egg-info/SOURCES.txt +0 -0
  70. {dulus-0.2.8 → dulus-0.2.10}/dulus.egg-info/dependency_links.txt +0 -0
  71. {dulus-0.2.8 → dulus-0.2.10}/dulus.egg-info/entry_points.txt +0 -0
  72. {dulus-0.2.8 → dulus-0.2.10}/dulus.egg-info/top_level.txt +0 -0
  73. {dulus-0.2.8 → dulus-0.2.10}/dulus_gui.py +0 -0
  74. {dulus-0.2.8 → dulus-0.2.10}/dulus_mcp/__init__.py +0 -0
  75. {dulus-0.2.8 → dulus-0.2.10}/dulus_mcp/client.py +0 -0
  76. {dulus-0.2.8 → dulus-0.2.10}/dulus_mcp/config.py +0 -0
  77. {dulus-0.2.8 → dulus-0.2.10}/dulus_mcp/tools.py +0 -0
  78. {dulus-0.2.8 → dulus-0.2.10}/dulus_mcp/types.py +0 -0
  79. {dulus-0.2.8 → dulus-0.2.10}/gui/__init__.py +0 -0
  80. {dulus-0.2.8 → dulus-0.2.10}/gui/agent_bridge.py +0 -0
  81. {dulus-0.2.8 → dulus-0.2.10}/gui/chat_widget.py +0 -0
  82. {dulus-0.2.8 → dulus-0.2.10}/gui/main_window.py +0 -0
  83. {dulus-0.2.8 → dulus-0.2.10}/gui/personas.py +0 -0
  84. {dulus-0.2.8 → dulus-0.2.10}/gui/session_utils.py +0 -0
  85. {dulus-0.2.8 → dulus-0.2.10}/gui/settings_dialog.py +0 -0
  86. {dulus-0.2.8 → dulus-0.2.10}/gui/sidebar.py +0 -0
  87. {dulus-0.2.8 → dulus-0.2.10}/gui/tasks_view.py +0 -0
  88. {dulus-0.2.8 → dulus-0.2.10}/gui/themes.py +0 -0
  89. {dulus-0.2.8 → dulus-0.2.10}/gui/tool_panel.py +0 -0
  90. {dulus-0.2.8 → dulus-0.2.10}/input.py +0 -0
  91. {dulus-0.2.8 → dulus-0.2.10}/license_manager.py +0 -0
  92. {dulus-0.2.8 → dulus-0.2.10}/memory/__init__.py +0 -0
  93. {dulus-0.2.8 → dulus-0.2.10}/memory/audit.py +0 -0
  94. {dulus-0.2.8 → dulus-0.2.10}/memory/consolidator.py +0 -0
  95. {dulus-0.2.8 → dulus-0.2.10}/memory/context.py +0 -0
  96. {dulus-0.2.8 → dulus-0.2.10}/memory/palace.py +0 -0
  97. {dulus-0.2.8 → dulus-0.2.10}/memory/scan.py +0 -0
  98. {dulus-0.2.8 → dulus-0.2.10}/memory/sessions.py +0 -0
  99. {dulus-0.2.8 → dulus-0.2.10}/memory/store.py +0 -0
  100. {dulus-0.2.8 → dulus-0.2.10}/memory/tools.py +0 -0
  101. {dulus-0.2.8 → dulus-0.2.10}/memory/types.py +0 -0
  102. {dulus-0.2.8 → dulus-0.2.10}/memory/vector_search.py +0 -0
  103. {dulus-0.2.8 → dulus-0.2.10}/multi_agent/__init__.py +0 -0
  104. {dulus-0.2.8 → dulus-0.2.10}/multi_agent/subagent.py +0 -0
  105. {dulus-0.2.8 → dulus-0.2.10}/multi_agent/tools.py +0 -0
  106. {dulus-0.2.8 → dulus-0.2.10}/offload_helper.py +0 -0
  107. {dulus-0.2.8 → dulus-0.2.10}/plugin/__init__.py +0 -0
  108. {dulus-0.2.8 → dulus-0.2.10}/plugin/autoadapter.py +0 -0
  109. {dulus-0.2.8 → dulus-0.2.10}/plugin/loader.py +0 -0
  110. {dulus-0.2.8 → dulus-0.2.10}/plugin/recommend.py +0 -0
  111. {dulus-0.2.8 → dulus-0.2.10}/plugin/store.py +0 -0
  112. {dulus-0.2.8 → dulus-0.2.10}/plugin/types.py +0 -0
  113. {dulus-0.2.8 → dulus-0.2.10}/providers.py +0 -0
  114. {dulus-0.2.8 → dulus-0.2.10}/setup.cfg +0 -0
  115. {dulus-0.2.8 → dulus-0.2.10}/skill/__init__.py +0 -0
  116. {dulus-0.2.8 → dulus-0.2.10}/skill/builtin.py +0 -0
  117. {dulus-0.2.8 → dulus-0.2.10}/skill/clawhub.py +0 -0
  118. {dulus-0.2.8 → dulus-0.2.10}/skill/executor.py +0 -0
  119. {dulus-0.2.8 → dulus-0.2.10}/skill/loader.py +0 -0
  120. {dulus-0.2.8 → dulus-0.2.10}/skill/tools.py +0 -0
  121. {dulus-0.2.8 → dulus-0.2.10}/skills.py +0 -0
  122. {dulus-0.2.8 → dulus-0.2.10}/spinner.py +0 -0
  123. {dulus-0.2.8 → dulus-0.2.10}/string_utils.py +0 -0
  124. {dulus-0.2.8 → dulus-0.2.10}/subagent.py +0 -0
  125. {dulus-0.2.8 → dulus-0.2.10}/task/__init__.py +0 -0
  126. {dulus-0.2.8 → dulus-0.2.10}/task/store.py +0 -0
  127. {dulus-0.2.8 → dulus-0.2.10}/task/tools.py +0 -0
  128. {dulus-0.2.8 → dulus-0.2.10}/task/types.py +0 -0
  129. {dulus-0.2.8 → dulus-0.2.10}/tests/test_checkpoint.py +0 -0
  130. {dulus-0.2.8 → dulus-0.2.10}/tests/test_compaction.py +0 -0
  131. {dulus-0.2.8 → dulus-0.2.10}/tests/test_diff_view.py +0 -0
  132. {dulus-0.2.8 → dulus-0.2.10}/tests/test_injection_fix.py +0 -0
  133. {dulus-0.2.8 → dulus-0.2.10}/tests/test_license.py +0 -0
  134. {dulus-0.2.8 → dulus-0.2.10}/tests/test_mcp.py +0 -0
  135. {dulus-0.2.8 → dulus-0.2.10}/tests/test_memory.py +0 -0
  136. {dulus-0.2.8 → dulus-0.2.10}/tests/test_plugin.py +0 -0
  137. {dulus-0.2.8 → dulus-0.2.10}/tests/test_skills.py +0 -0
  138. {dulus-0.2.8 → dulus-0.2.10}/tests/test_subagent.py +0 -0
  139. {dulus-0.2.8 → dulus-0.2.10}/tests/test_task.py +0 -0
  140. {dulus-0.2.8 → dulus-0.2.10}/tests/test_telegram_buffer.py +0 -0
  141. {dulus-0.2.8 → dulus-0.2.10}/tests/test_tool_registry.py +0 -0
  142. {dulus-0.2.8 → dulus-0.2.10}/tests/test_voice.py +0 -0
  143. {dulus-0.2.8 → dulus-0.2.10}/tmux_offloader.py +0 -0
  144. {dulus-0.2.8 → dulus-0.2.10}/tmux_tools.py +0 -0
  145. {dulus-0.2.8 → dulus-0.2.10}/tool_registry.py +0 -0
  146. {dulus-0.2.8 → dulus-0.2.10}/tools.py +0 -0
  147. {dulus-0.2.8 → dulus-0.2.10}/ui/__init__.py +0 -0
  148. {dulus-0.2.8 → dulus-0.2.10}/ui/input.py +0 -0
  149. {dulus-0.2.8 → dulus-0.2.10}/ui/render.py +0 -0
  150. {dulus-0.2.8 → dulus-0.2.10}/voice/__init__.py +0 -0
  151. {dulus-0.2.8 → dulus-0.2.10}/voice/keyterms.py +0 -0
  152. {dulus-0.2.8 → dulus-0.2.10}/voice/recorder.py +0 -0
  153. {dulus-0.2.8 → dulus-0.2.10}/voice/stt.py +0 -0
  154. {dulus-0.2.8 → dulus-0.2.10}/voice/tts.py +0 -0
  155. {dulus-0.2.8 → dulus-0.2.10}/webchat.py +0 -0
  156. {dulus-0.2.8 → dulus-0.2.10}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.8
3
+ Version: 0.2.10
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
@@ -35,6 +35,7 @@ Requires-Dist: bubblewrap-cli>=1.0.0
35
35
  Requires-Dist: sounddevice
36
36
  Requires-Dist: customtkinter>=5.2.0
37
37
  Requires-Dist: Pillow>=10.0.0
38
+ Requires-Dist: typing-extensions>=4.10.0
38
39
  Dynamic: license-file
39
40
 
40
41
  # ▲ DULUS
@@ -84,7 +85,7 @@ SET /sticky_input ON since the first run for the best experience!
84
85
 
85
86
  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
87
 
87
- > **v0.2.8 — May 8, 2026** — MemPalace injection: removed confidence-bias ranker, added relevance threshold + elastic char budget, fixed comma-trip on trivial filter. Job offload no longer leaks parent config (API keys, tokens) to `~/.dulus/jobs/*.json`.
88
+ > **v0.2.10 — May 8, 2026** — Pin `typing-extensions>=4.10` to keep pip's resolver from blowing up (`dependency resolution exceeded maximum depth`) when reconciling anthropic / openai / pydantic / chromadb floors. New Termux/Android section in the README with a `--no-deps` workaround for the NumPy build.
88
89
  > Type `/news` to see what changed.
89
90
 
90
91
  ---
@@ -133,6 +134,18 @@ pip install -e . # editable install
133
134
  dulus
134
135
  ```
135
136
 
137
+ ### Termux / Android
138
+
139
+ The default install pulls `mempalace` and `sounddevice`, both of which need a NumPy that has no prebuilt wheel for `aarch64-android` — pip will try to build NumPy from source and fail. Install around it:
140
+
141
+ ```bash
142
+ pkg install python python-numpy python-pillow build-essential
143
+ pip install --no-deps dulus
144
+ pip install anthropic openai httpx requests rich prompt_toolkit Flask bubblewrap-cli mempalace
145
+ ```
146
+
147
+ Skip `sounddevice` (no usable PortAudio on Android — voice features won't work anyway). Dulus's runtime is graceful: voice / MemPalace just degrade if their deps aren't there, the CLI still boots and chats fine.
148
+
136
149
  ### Pick a model
137
150
 
138
151
  ```bash
@@ -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
- > **v0.2.8 — May 8, 2026** — MemPalace injection: removed confidence-bias ranker, added relevance threshold + elastic char budget, fixed comma-trip on trivial filter. Job offload no longer leaks parent config (API keys, tokens) to `~/.dulus/jobs/*.json`.
48
+ > **v0.2.10 — May 8, 2026** — Pin `typing-extensions>=4.10` to keep pip's resolver from blowing up (`dependency resolution exceeded maximum depth`) when reconciling anthropic / openai / pydantic / chromadb floors. New Termux/Android section in the README with a `--no-deps` workaround for the NumPy build.
49
49
  > Type `/news` to see what changed.
50
50
 
51
51
  ---
@@ -94,6 +94,18 @@ pip install -e . # editable install
94
94
  dulus
95
95
  ```
96
96
 
97
+ ### Termux / Android
98
+
99
+ The default install pulls `mempalace` and `sounddevice`, both of which need a NumPy that has no prebuilt wheel for `aarch64-android` — pip will try to build NumPy from source and fail. Install around it:
100
+
101
+ ```bash
102
+ pkg install python python-numpy python-pillow build-essential
103
+ pip install --no-deps dulus
104
+ pip install anthropic openai httpx requests rich prompt_toolkit Flask bubblewrap-cli mempalace
105
+ ```
106
+
107
+ Skip `sounddevice` (no usable PortAudio on Android — voice features won't work anyway). Dulus's runtime is graceful: voice / MemPalace just degrade if their deps aren't there, the CLI still boots and chats fine.
108
+
97
109
  ### Pick a model
98
110
 
99
111
  ```bash
@@ -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 (hex per semantic role). `cyan/green/blue` collapse to the
42
- # theme's accent color since Dulus uses them all for primary chrome.
43
- # Add new ones here and they show up in `/theme` automatically.
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": {"accent": "#FF8700", "warn": "#FFAF00", "code": "monokai"},
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": "#38bdf8", "warn": "#fbbf24", "code": "nord"},
56
- "monokai": {"accent": "#a6e22e", "warn": "#e6db74", "code": "monokai"},
57
- "mono": {"accent": "#E0E0E0", "warn": "#A0A0A0", "code": "bw"},
58
- "none": {"accent": "#FFFFFF", "warn": "#FFFFFF", "code": "default"},
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
- warn = _rgb(p["warn"])
81
- C["cyan"] = C["green"] = C["blue"] = accent
82
- C["yellow"] = C["magenta"] = warn
83
- C["red"] = "\033[38;5;196m" # errors stay red across all themes
84
- C["white"] = "\033[97m"
85
- C["gray"] = "\033[90m"
86
- CODE_THEME = p["code"]
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.8
3
+ Version: 0.2.10
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
@@ -35,6 +35,7 @@ Requires-Dist: bubblewrap-cli>=1.0.0
35
35
  Requires-Dist: sounddevice
36
36
  Requires-Dist: customtkinter>=5.2.0
37
37
  Requires-Dist: Pillow>=10.0.0
38
+ Requires-Dist: typing-extensions>=4.10.0
38
39
  Dynamic: license-file
39
40
 
40
41
  # ▲ DULUS
@@ -84,7 +85,7 @@ SET /sticky_input ON since the first run for the best experience!
84
85
 
85
86
  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
87
 
87
- > **v0.2.8 — May 8, 2026** — MemPalace injection: removed confidence-bias ranker, added relevance threshold + elastic char budget, fixed comma-trip on trivial filter. Job offload no longer leaks parent config (API keys, tokens) to `~/.dulus/jobs/*.json`.
88
+ > **v0.2.10 — May 8, 2026** — Pin `typing-extensions>=4.10` to keep pip's resolver from blowing up (`dependency resolution exceeded maximum depth`) when reconciling anthropic / openai / pydantic / chromadb floors. New Termux/Android section in the README with a `--no-deps` workaround for the NumPy build.
88
89
  > Type `/news` to see what changed.
89
90
 
90
91
  ---
@@ -133,6 +134,18 @@ pip install -e . # editable install
133
134
  dulus
134
135
  ```
135
136
 
137
+ ### Termux / Android
138
+
139
+ The default install pulls `mempalace` and `sounddevice`, both of which need a NumPy that has no prebuilt wheel for `aarch64-android` — pip will try to build NumPy from source and fail. Install around it:
140
+
141
+ ```bash
142
+ pkg install python python-numpy python-pillow build-essential
143
+ pip install --no-deps dulus
144
+ pip install anthropic openai httpx requests rich prompt_toolkit Flask bubblewrap-cli mempalace
145
+ ```
146
+
147
+ Skip `sounddevice` (no usable PortAudio on Android — voice features won't work anyway). Dulus's runtime is graceful: voice / MemPalace just degrade if their deps aren't there, the CLI still boots and chats fine.
148
+
136
149
  ### Pick a model
137
150
 
138
151
  ```bash
@@ -10,3 +10,4 @@ bubblewrap-cli>=1.0.0
10
10
  sounddevice
11
11
  customtkinter>=5.2.0
12
12
  Pillow>=10.0.0
13
+ typing-extensions>=4.10.0
@@ -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
- swatch = f"{_cm._rgb(p['accent'])}■{_cm.C['reset']}{_cm._rgb(p['warn'])}■{_cm.C['reset']}"
3017
- print(f" {marker} {swatch} {t}")
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):
@@ -7047,10 +7057,38 @@ def repl(config: dict, initial_prompt: str = None):
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 = find_relevant_memories(_q, max_results=3)
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)
7089
+ if not _raw_hits:
7090
+ from memory import find_relevant_memories
7091
+ _raw_hits = find_relevant_memories(_q, max_results=3)
7054
7092
  _MIN_SCORE = 0.15
7055
7093
  if _mp_dbg:
7056
7094
  for _h in _raw_hits:
@@ -20,7 +20,15 @@ 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 = params.get("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]
@@ -73,9 +81,6 @@ def _tmux_offload(params: dict, config: dict) -> str:
73
81
  if sys.platform == "win32":
74
82
  # Windows: Use absolute path to dulus.py since tmux starts in home dir, not DULUS dir
75
83
  dulus_path_str = str(dulus_script).replace("\\", "/")
76
- # Write a wrapper script that handles errors properly
77
- # Use & instead of ; so kill-session runs regardless, and capture output
78
- # Quote paths with spaces to prevent cmd.exe from splitting them
79
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}'
80
85
  else:
81
86
  # Unix/Linux: unset PSMUX vars and use tee
@@ -84,6 +89,13 @@ def _tmux_offload(params: dict, config: dict) -> str:
84
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}"
85
90
 
86
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)
87
99
  if "failed" in send_result.lower() or "error" in send_result.lower():
88
100
  # Clean up the session since we can't send keys
89
101
  _run(f"tmux kill-session -t {session_name}", timeout=2)
@@ -138,10 +150,14 @@ def register_offload_tool():
138
150
  },
139
151
  "tool_params": {
140
152
  "type": "object",
141
- "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."
142
158
  },
143
159
  },
144
- "required": ["tool_name", "tool_params"],
160
+ "required": ["tool_name"],
145
161
  },
146
162
  },
147
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.8"
7
+ version = "0.2.10"
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"
@@ -40,6 +40,11 @@ dependencies = [
40
40
  "sounddevice",
41
41
  "customtkinter>=5.2.0",
42
42
  "Pillow>=10.0.0",
43
+ # Pin typing-extensions floor: anthropic, openai, pydantic, chromadb (via
44
+ # mempalace) each pull it with different lower bounds — without an explicit
45
+ # floor here pip's resolver explodes with "dependency resolution exceeded
46
+ # maximum depth" trying to reconcile them.
47
+ "typing-extensions>=4.10.0",
43
48
  ]
44
49
 
45
50
  [project.scripts]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes