dulus 0.2.13__tar.gz → 0.2.14__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.13/dulus.egg-info → dulus-0.2.14}/PKG-INFO +16 -5
- {dulus-0.2.13 → dulus-0.2.14}/README.md +15 -4
- {dulus-0.2.13 → dulus-0.2.14}/docs/news.md +4 -0
- {dulus-0.2.13 → dulus-0.2.14/dulus.egg-info}/PKG-INFO +16 -5
- {dulus-0.2.13 → dulus-0.2.14}/dulus.py +91 -31
- {dulus-0.2.13 → dulus-0.2.14}/pyproject.toml +1 -1
- {dulus-0.2.13 → dulus-0.2.14}/tools.py +3 -2
- {dulus-0.2.13 → dulus-0.2.14}/LICENSE +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/MANIFEST.in +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/agent.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/compressor.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/context.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/githook.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/marketplace.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/personas.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/plugins.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/server.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/backend/tasks.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/batch_api.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/checkpoint/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/checkpoint/hooks.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/checkpoint/store.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/checkpoint/types.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/claude_code_watcher.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/clipboard_utils.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/cloudsave.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/common.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/compaction.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/config.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/context.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/active_persona.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/context.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/marketplace.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/personas.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/data/tasks.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/README.md +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/api.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/architecture.md +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/dashboard/index.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/divider.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/generate.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/hero.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/index.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/particle-playground.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/personas/index.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/preview.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-agents.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-features.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-memory.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-models.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-perms.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/spinners.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/split-pane.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_gui.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_mcp/client.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_mcp/config.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/dulus_mcp/types.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/agent_bridge.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/chat_widget.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/main_window.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/personas.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/session_utils.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/settings_dialog.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/sidebar.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/tasks_view.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/themes.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/gui/tool_panel.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/input.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/license_manager.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/audit.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/consolidator.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/context.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/offload.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/palace.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/scan.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/sessions.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/store.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/types.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/memory/vector_search.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/multi_agent/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/multi_agent/subagent.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/multi_agent/tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/offload_helper.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/autoadapter.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/loader.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/recommend.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/store.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/plugin/types.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/providers.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/setup.cfg +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/builtin.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/clawhub.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/executor.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/loader.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skill/tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/skills.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/spinner.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/string_utils.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/subagent.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/task/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/task/store.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/task/tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/task/types.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_compaction.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_diff_view.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_license.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_mcp.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_memory.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_plugin.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_skills.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_subagent.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_task.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tests/test_voice.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tmux_offloader.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tmux_tools.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/tool_registry.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/ui/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/ui/input.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/ui/render.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/voice/__init__.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/voice/keyterms.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/voice/recorder.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/voice/stt.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/voice/tts.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/webchat.py +0 -0
- {dulus-0.2.13 → dulus-0.2.14}/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.14
|
|
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
|
|
@@ -67,7 +67,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
67
67
|
<a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
|
|
68
68
|
<img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
|
|
69
69
|
<img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
|
|
70
|
-
<img src="https://img.shields.io/badge/version-v0.2.
|
|
70
|
+
<img src="https://img.shields.io/badge/version-v0.2.14-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
|
|
71
71
|
<img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
|
|
72
72
|
<img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
|
|
73
73
|
<img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
|
|
@@ -87,6 +87,12 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
87
87
|
<p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
|
|
88
88
|
|
|
89
89
|
## What is this
|
|
90
|
+
Talent cant be copied.
|
|
91
|
+
|
|
92
|
+
Dulus Reduce your IA costs by 60% parsing webchats and claude-code directly. Write poetry while Anthropic only see text.
|
|
93
|
+
|
|
94
|
+
<img width="3189" height="1489" alt="image" src="https://github.com/user-attachments/assets/8fe96b65-6ae9-4ef7-9d85-0086abc64d23" />
|
|
95
|
+
|
|
90
96
|
|
|
91
97
|
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.
|
|
92
98
|
|
|
@@ -198,7 +204,7 @@ echo "explain this diff" | git diff | dulus -p --accept-all
|
|
|
198
204
|
| **Voice input** | Offline STT via Whisper. No API key. No cloud. |
|
|
199
205
|
| **Brainstorm** | Multi-persona AI debate. Auto-generated expert roles. |
|
|
200
206
|
| **SSJ Developer Mode** | Power menu: 10 workflow shortcuts behind one keystroke |
|
|
201
|
-
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. |
|
|
207
|
+
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. Multi-user authorized list. |
|
|
202
208
|
| **Checkpoints** | Auto-snapshot conversation + files. Rewind to any turn. |
|
|
203
209
|
| **Plan mode** | Read-only analysis phase before touching anything |
|
|
204
210
|
| **Context compression** | Auto-compact long sessions. Keep the signal, drop the slop. |
|
|
@@ -385,10 +391,15 @@ Then `/voice` in the REPL. Offline. Supports `/voice lang zh` and `/voice device
|
|
|
385
391
|
## Telegram bridge
|
|
386
392
|
|
|
387
393
|
```
|
|
388
|
-
/telegram <bot_token> <chat_id>
|
|
394
|
+
/telegram <bot_token> <chat_id> # single user
|
|
395
|
+
/telegram <bot_token> <id1>,<id2>,<id3> # multi-user — same Dulus, multiple authorized chats
|
|
389
396
|
```
|
|
390
397
|
|
|
391
|
-
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
398
|
+
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
399
|
+
Multi-user mode (v0.2.14+): each authorized chat gets its own replies — Dulus tracks who
|
|
400
|
+
sent each message and routes the response back. Trailing commas are ignored, so
|
|
401
|
+
`717151713,787615162,,` works fine. Useful when you want to poke a long-running agent
|
|
402
|
+
from the bus, or share one Dulus instance with your team.
|
|
392
403
|
|
|
393
404
|
---
|
|
394
405
|
|
|
@@ -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.14-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"/>
|
|
@@ -42,6 +42,12 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
42
42
|
<p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
|
|
43
43
|
|
|
44
44
|
## What is this
|
|
45
|
+
Talent cant be copied.
|
|
46
|
+
|
|
47
|
+
Dulus Reduce your IA costs by 60% parsing webchats and claude-code directly. Write poetry while Anthropic only see text.
|
|
48
|
+
|
|
49
|
+
<img width="3189" height="1489" alt="image" src="https://github.com/user-attachments/assets/8fe96b65-6ae9-4ef7-9d85-0086abc64d23" />
|
|
50
|
+
|
|
45
51
|
|
|
46
52
|
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
53
|
|
|
@@ -153,7 +159,7 @@ echo "explain this diff" | git diff | dulus -p --accept-all
|
|
|
153
159
|
| **Voice input** | Offline STT via Whisper. No API key. No cloud. |
|
|
154
160
|
| **Brainstorm** | Multi-persona AI debate. Auto-generated expert roles. |
|
|
155
161
|
| **SSJ Developer Mode** | Power menu: 10 workflow shortcuts behind one keystroke |
|
|
156
|
-
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. |
|
|
162
|
+
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. Multi-user authorized list. |
|
|
157
163
|
| **Checkpoints** | Auto-snapshot conversation + files. Rewind to any turn. |
|
|
158
164
|
| **Plan mode** | Read-only analysis phase before touching anything |
|
|
159
165
|
| **Context compression** | Auto-compact long sessions. Keep the signal, drop the slop. |
|
|
@@ -340,10 +346,15 @@ Then `/voice` in the REPL. Offline. Supports `/voice lang zh` and `/voice device
|
|
|
340
346
|
## Telegram bridge
|
|
341
347
|
|
|
342
348
|
```
|
|
343
|
-
/telegram <bot_token> <chat_id>
|
|
349
|
+
/telegram <bot_token> <chat_id> # single user
|
|
350
|
+
/telegram <bot_token> <id1>,<id2>,<id3> # multi-user — same Dulus, multiple authorized chats
|
|
344
351
|
```
|
|
345
352
|
|
|
346
|
-
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
353
|
+
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
354
|
+
Multi-user mode (v0.2.14+): each authorized chat gets its own replies — Dulus tracks who
|
|
355
|
+
sent each message and routes the response back. Trailing commas are ignored, so
|
|
356
|
+
`717151713,787615162,,` works fine. Useful when you want to poke a long-running agent
|
|
357
|
+
from the bus, or share one Dulus instance with your team.
|
|
347
358
|
|
|
348
359
|
---
|
|
349
360
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
## 🔥🔥🔥 News (Pacific Time)
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
- May 09, 2026 (**v0.2.14**): **Multi-user Telegram bridge**
|
|
7
|
+
- **Telegram bridge now supports multiple authorized chat_ids.** Configure with `/telegram <token> <id1>,<id2>,<id3>` or set `telegram_chat_ids` in `config.json` as a comma-separated string (trailing commas like `717151713,787615162,,` are ignored). Each authorized chat gets its own replies — Dulus tracks who sent each message via `_active_tg_chat_id` and routes the response back to the right user. Welcome message is broadcast to all configured users on bridge start. The legacy single-int `telegram_chat_id` still works for backwards-compat.
|
|
8
|
+
- **Why this matters.** One Dulus instance, multiple humans poking it from their phones — useful for teams sharing a long-running agent, or for paired-up users running the same MemPalace from different devices.
|
|
9
|
+
|
|
6
10
|
- Apr 09, 2026 (**v1.01.20**): **Automated Plugin Adapter System, Premium UI, and Hot-Reloading**
|
|
7
11
|
- **Automated Plugin Adapter (`plugin/autoadapter.py`)** — Dulus can now intelligently onboard any Python repository without a manual manifest. Using AST-based static analysis and AI-driven generation, it creates `plugin.json` and `plugin_tool.py` on the fly, handling complex dependencies and constructor arguments.
|
|
8
12
|
- **Intelligent Library Handling** — The AI generation pipeline now includes specialized instructions for terminal-based libraries (e.g., `asciimatics`), ensuring correct usage of patterns like `Screen.wrapper` to prevent runtime errors.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
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
|
|
@@ -67,7 +67,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
67
67
|
<a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
|
|
68
68
|
<img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
|
|
69
69
|
<img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
|
|
70
|
-
<img src="https://img.shields.io/badge/version-v0.2.
|
|
70
|
+
<img src="https://img.shields.io/badge/version-v0.2.14-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
|
|
71
71
|
<img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
|
|
72
72
|
<img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
|
|
73
73
|
<img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
|
|
@@ -87,6 +87,12 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
87
87
|
<p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
|
|
88
88
|
|
|
89
89
|
## What is this
|
|
90
|
+
Talent cant be copied.
|
|
91
|
+
|
|
92
|
+
Dulus Reduce your IA costs by 60% parsing webchats and claude-code directly. Write poetry while Anthropic only see text.
|
|
93
|
+
|
|
94
|
+
<img width="3189" height="1489" alt="image" src="https://github.com/user-attachments/assets/8fe96b65-6ae9-4ef7-9d85-0086abc64d23" />
|
|
95
|
+
|
|
90
96
|
|
|
91
97
|
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.
|
|
92
98
|
|
|
@@ -198,7 +204,7 @@ echo "explain this diff" | git diff | dulus -p --accept-all
|
|
|
198
204
|
| **Voice input** | Offline STT via Whisper. No API key. No cloud. |
|
|
199
205
|
| **Brainstorm** | Multi-persona AI debate. Auto-generated expert roles. |
|
|
200
206
|
| **SSJ Developer Mode** | Power menu: 10 workflow shortcuts behind one keystroke |
|
|
201
|
-
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. |
|
|
207
|
+
| **Telegram bridge** | Run Dulus from your phone. Slash commands. Vision. Voice. Multi-user authorized list. |
|
|
202
208
|
| **Checkpoints** | Auto-snapshot conversation + files. Rewind to any turn. |
|
|
203
209
|
| **Plan mode** | Read-only analysis phase before touching anything |
|
|
204
210
|
| **Context compression** | Auto-compact long sessions. Keep the signal, drop the slop. |
|
|
@@ -385,10 +391,15 @@ Then `/voice` in the REPL. Offline. Supports `/voice lang zh` and `/voice device
|
|
|
385
391
|
## Telegram bridge
|
|
386
392
|
|
|
387
393
|
```
|
|
388
|
-
/telegram <bot_token> <chat_id>
|
|
394
|
+
/telegram <bot_token> <chat_id> # single user
|
|
395
|
+
/telegram <bot_token> <id1>,<id2>,<id3> # multi-user — same Dulus, multiple authorized chats
|
|
389
396
|
```
|
|
390
397
|
|
|
391
|
-
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
398
|
+
Auto-starts next launch. Supports slash commands, vision, and voice from your phone.
|
|
399
|
+
Multi-user mode (v0.2.14+): each authorized chat gets its own replies — Dulus tracks who
|
|
400
|
+
sent each message and routes the response back. Trailing commas are ignored, so
|
|
401
|
+
`717151713,787615162,,` works fine. Useful when you want to poke a long-running agent
|
|
402
|
+
from the bus, or share one Dulus instance with your team.
|
|
392
403
|
|
|
393
404
|
---
|
|
394
405
|
|
|
@@ -631,8 +631,11 @@ def ask_permission_interactive(desc: str, config: dict) -> bool:
|
|
|
631
631
|
config["permission_mode"] = "accept-all"
|
|
632
632
|
if _is_in_tg_turn(config):
|
|
633
633
|
token = config.get("telegram_token")
|
|
634
|
-
|
|
635
|
-
|
|
634
|
+
# Reply to the user who actually triggered this prompt; fall back
|
|
635
|
+
# to the first configured chat_id if the active one is unknown.
|
|
636
|
+
cid = config.get("_active_tg_chat_id") or (_tg_get_chat_ids(config) or [None])[0]
|
|
637
|
+
if cid:
|
|
638
|
+
_tg_send(token, cid, "✅ Permission mode set to accept-all for this session.")
|
|
636
639
|
else:
|
|
637
640
|
ok(" Permission mode set to accept-all for this session.")
|
|
638
641
|
return True
|
|
@@ -4743,8 +4746,50 @@ def _tg_typing_loop(token: str, chat_id: int, stop_event: threading.Event, confi
|
|
|
4743
4746
|
_tg_api(token, "sendChatAction", {"chat_id": chat_id, "action": "typing"})
|
|
4744
4747
|
stop_event.wait(4)
|
|
4745
4748
|
|
|
4746
|
-
def
|
|
4747
|
-
"""
|
|
4749
|
+
def _parse_chat_ids(value) -> list[int]:
|
|
4750
|
+
"""Accept int, list, or comma-separated string ('123,456,,') → list[int].
|
|
4751
|
+
Empty parts (from trailing commas) are dropped.
|
|
4752
|
+
"""
|
|
4753
|
+
if not value:
|
|
4754
|
+
return []
|
|
4755
|
+
if isinstance(value, int):
|
|
4756
|
+
return [value]
|
|
4757
|
+
if isinstance(value, list):
|
|
4758
|
+
out = []
|
|
4759
|
+
for x in value:
|
|
4760
|
+
try:
|
|
4761
|
+
out.append(int(x))
|
|
4762
|
+
except (TypeError, ValueError):
|
|
4763
|
+
continue
|
|
4764
|
+
return out
|
|
4765
|
+
if isinstance(value, str):
|
|
4766
|
+
out = []
|
|
4767
|
+
for p in value.split(","):
|
|
4768
|
+
p = p.strip()
|
|
4769
|
+
if not p:
|
|
4770
|
+
continue
|
|
4771
|
+
try:
|
|
4772
|
+
out.append(int(p))
|
|
4773
|
+
except ValueError:
|
|
4774
|
+
continue
|
|
4775
|
+
return out
|
|
4776
|
+
return []
|
|
4777
|
+
|
|
4778
|
+
def _tg_get_chat_ids(config: dict) -> list[int]:
|
|
4779
|
+
"""Read configured chat ids from config. Supports legacy single int and
|
|
4780
|
+
new comma-separated string / list."""
|
|
4781
|
+
ids = _parse_chat_ids(config.get("telegram_chat_ids")) or _parse_chat_ids(config.get("telegram_chat_id"))
|
|
4782
|
+
return ids
|
|
4783
|
+
|
|
4784
|
+
def _tg_poll_loop(token: str, chat_ids, config: dict):
|
|
4785
|
+
"""Long-polling loop. chat_ids: int (legacy) or list[int].
|
|
4786
|
+
All listed users are authorized; replies go back to whoever sent the msg.
|
|
4787
|
+
"""
|
|
4788
|
+
if isinstance(chat_ids, int):
|
|
4789
|
+
chat_ids = [chat_ids]
|
|
4790
|
+
chat_ids = list(chat_ids or [])
|
|
4791
|
+
authorized = set(chat_ids)
|
|
4792
|
+
|
|
4748
4793
|
run_query_cb = config.get("_run_query_callback")
|
|
4749
4794
|
# Flush old messages so we don't process stale commands on startup
|
|
4750
4795
|
flush = _tg_api(token, "getUpdates", {"offset": -1, "timeout": 0})
|
|
@@ -4757,8 +4802,9 @@ def _tg_poll_loop(token: str, chat_id: int, config: dict):
|
|
|
4757
4802
|
_tg_register_commands(token)
|
|
4758
4803
|
except Exception:
|
|
4759
4804
|
pass
|
|
4760
|
-
# Notify
|
|
4761
|
-
|
|
4805
|
+
# Notify all configured users that the bot is online
|
|
4806
|
+
for cid in chat_ids:
|
|
4807
|
+
_tg_send(token, cid, "🟢 Dulus\nSend me a message and I'll process it.")
|
|
4762
4808
|
|
|
4763
4809
|
while not _telegram_stop.is_set():
|
|
4764
4810
|
try:
|
|
@@ -4779,13 +4825,21 @@ def _tg_poll_loop(token: str, chat_id: int, config: dict):
|
|
|
4779
4825
|
msg_chat_id = msg.get("chat", {}).get("id")
|
|
4780
4826
|
text = sanitize_text(msg.get("text", ""))
|
|
4781
4827
|
|
|
4782
|
-
if msg_chat_id
|
|
4828
|
+
if msg_chat_id not in authorized:
|
|
4783
4829
|
_tg_api(token, "sendMessage", {
|
|
4784
4830
|
"chat_id": msg_chat_id,
|
|
4785
4831
|
"text": "⛔ Unauthorized."
|
|
4786
4832
|
})
|
|
4787
4833
|
continue
|
|
4788
4834
|
|
|
4835
|
+
# Track who is currently active so other code (permission
|
|
4836
|
+
# prompts, etc.) can reply to the right user.
|
|
4837
|
+
config["_active_tg_chat_id"] = msg_chat_id
|
|
4838
|
+
# Bind chat_id to the originating user so all downstream
|
|
4839
|
+
# references in this iteration (and closures spawned below)
|
|
4840
|
+
# send replies back to whoever messaged.
|
|
4841
|
+
chat_id = msg_chat_id
|
|
4842
|
+
|
|
4789
4843
|
# ── Handle photo messages from Telegram ──
|
|
4790
4844
|
photo_list = msg.get("photo")
|
|
4791
4845
|
if photo_list:
|
|
@@ -5040,18 +5094,18 @@ def _run_daemon(config: dict) -> None:
|
|
|
5040
5094
|
|
|
5041
5095
|
# Start Telegram bridge if previously configured
|
|
5042
5096
|
token = config.get("telegram_token", "")
|
|
5043
|
-
|
|
5044
|
-
if token and
|
|
5097
|
+
chat_ids = _tg_get_chat_ids(config)
|
|
5098
|
+
if token and chat_ids:
|
|
5045
5099
|
global _telegram_stop, _telegram_thread
|
|
5046
5100
|
_telegram_stop = threading.Event()
|
|
5047
5101
|
_telegram_thread = threading.Thread(
|
|
5048
|
-
target=_tg_poll_loop, args=(token,
|
|
5102
|
+
target=_tg_poll_loop, args=(token, chat_ids, config), daemon=True
|
|
5049
5103
|
)
|
|
5050
5104
|
_telegram_thread.start()
|
|
5051
|
-
ok(f"Telegram bridge started →
|
|
5105
|
+
ok(f"Telegram bridge started → chats: {', '.join(str(c) for c in chat_ids)}")
|
|
5052
5106
|
else:
|
|
5053
5107
|
warn("No Telegram config found. Bridge not started.")
|
|
5054
|
-
info("Set it later with: /telegram <token> <chat_id>")
|
|
5108
|
+
info("Set it later with: /telegram <token> <chat_id>[,<chat_id>...]")
|
|
5055
5109
|
|
|
5056
5110
|
info("Press Ctrl+C to stop.\n")
|
|
5057
5111
|
|
|
@@ -5113,34 +5167,37 @@ def cmd_telegram(args: str, _state, config) -> bool:
|
|
|
5113
5167
|
if parts and parts[0].lower() == "status":
|
|
5114
5168
|
running = _telegram_thread and _telegram_thread.is_alive()
|
|
5115
5169
|
token = config.get("telegram_token", "")
|
|
5116
|
-
|
|
5170
|
+
chat_ids = _tg_get_chat_ids(config)
|
|
5171
|
+
ids_str = ",".join(str(c) for c in chat_ids) if chat_ids else "(none)"
|
|
5117
5172
|
if running:
|
|
5118
|
-
ok(f"Telegram bridge is running. Chat
|
|
5173
|
+
ok(f"Telegram bridge is running. Chat IDs: {ids_str}")
|
|
5119
5174
|
elif token:
|
|
5120
5175
|
info(f"Configured but not running. Use /telegram to start.")
|
|
5121
5176
|
else:
|
|
5122
|
-
info("Not configured. Use /telegram <bot_token> <chat_id>")
|
|
5177
|
+
info("Not configured. Use /telegram <bot_token> <chat_id>[,<chat_id>...]")
|
|
5123
5178
|
return True
|
|
5124
5179
|
|
|
5125
|
-
# /telegram <token> <chat_id> — configure and start
|
|
5180
|
+
# /telegram <token> <chat_id>[,<chat_id>...] — configure and start
|
|
5126
5181
|
if len(parts) >= 2:
|
|
5127
5182
|
token = parts[0]
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
err("Chat ID must be a number. Send a message to your bot, then check getUpdates.")
|
|
5183
|
+
chat_ids = _parse_chat_ids(parts[1])
|
|
5184
|
+
if not chat_ids:
|
|
5185
|
+
err("Chat ID must be a number (or comma-separated list, e.g. 12345,67890).")
|
|
5132
5186
|
return True
|
|
5133
5187
|
config["telegram_token"] = token
|
|
5134
|
-
|
|
5188
|
+
# Persist as comma-separated string in the new key; clear the legacy
|
|
5189
|
+
# single-id key so the file stays clean.
|
|
5190
|
+
config["telegram_chat_ids"] = ",".join(str(c) for c in chat_ids)
|
|
5191
|
+
config.pop("telegram_chat_id", None)
|
|
5135
5192
|
save_config(config)
|
|
5136
|
-
ok("Telegram config saved.")
|
|
5193
|
+
ok(f"Telegram config saved. Authorized chats: {', '.join(str(c) for c in chat_ids)}")
|
|
5137
5194
|
else:
|
|
5138
5195
|
# Try to use saved config
|
|
5139
5196
|
token = config.get("telegram_token", "")
|
|
5140
|
-
|
|
5197
|
+
chat_ids = _tg_get_chat_ids(config)
|
|
5141
5198
|
|
|
5142
|
-
if not token or not
|
|
5143
|
-
err("No config found. Usage: /telegram <bot_token> <chat_id>")
|
|
5199
|
+
if not token or not chat_ids:
|
|
5200
|
+
err("No config found. Usage: /telegram <bot_token> <chat_id>[,<chat_id>...]")
|
|
5144
5201
|
return True
|
|
5145
5202
|
|
|
5146
5203
|
# Already running?
|
|
@@ -5162,10 +5219,10 @@ def cmd_telegram(args: str, _state, config) -> bool:
|
|
|
5162
5219
|
|
|
5163
5220
|
_telegram_stop = threading.Event()
|
|
5164
5221
|
_telegram_thread = threading.Thread(
|
|
5165
|
-
target=_tg_poll_loop, args=(token,
|
|
5222
|
+
target=_tg_poll_loop, args=(token, chat_ids, config), daemon=True
|
|
5166
5223
|
)
|
|
5167
5224
|
_telegram_thread.start()
|
|
5168
|
-
ok(f"Telegram bridge active. Chat
|
|
5225
|
+
ok(f"Telegram bridge active. Chat IDs: {', '.join(str(c) for c in chat_ids)}")
|
|
5169
5226
|
info("Send messages to your bot — they'll be processed here.")
|
|
5170
5227
|
info("Stop with /telegram stop or send /stop in Telegram.")
|
|
5171
5228
|
return True
|
|
@@ -6907,7 +6964,7 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
6907
6964
|
active_flags.append("proactive")
|
|
6908
6965
|
if config.get("lite_mode"):
|
|
6909
6966
|
active_flags.append("lite")
|
|
6910
|
-
if config.get("telegram_token") and config
|
|
6967
|
+
if config.get("telegram_token") and _tg_get_chat_ids(config):
|
|
6911
6968
|
active_flags.append("telegram")
|
|
6912
6969
|
if active_flags:
|
|
6913
6970
|
flags_str = " · ".join(clr(f, "green") for f in active_flags)
|
|
@@ -7330,7 +7387,10 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7330
7387
|
if is_background:
|
|
7331
7388
|
is_tg_turn = config.get("_in_telegram_turn", False)
|
|
7332
7389
|
ttok = config.get("telegram_token")
|
|
7333
|
-
|
|
7390
|
+
# Background broadcasts go to whoever was last active in TG
|
|
7391
|
+
# (or the first configured chat as fallback).
|
|
7392
|
+
_tids = _tg_get_chat_ids(config)
|
|
7393
|
+
tchat = config.get("_active_tg_chat_id") or (_tids[0] if _tids else 0)
|
|
7334
7394
|
# Check that Telegram is still active (_telegram_stop not set)
|
|
7335
7395
|
if not is_tg_turn and ttok and tchat and _telegram_stop and not _telegram_stop.is_set():
|
|
7336
7396
|
if state.messages and state.messages[-1].get("role") == "assistant":
|
|
@@ -7431,14 +7491,14 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7431
7491
|
config["_handle_slash_callback"] = _handle_slash_from_telegram
|
|
7432
7492
|
|
|
7433
7493
|
# ── Auto-start Telegram bridge if configured ──────────────────────
|
|
7434
|
-
if config.get("telegram_token") and config
|
|
7494
|
+
if config.get("telegram_token") and _tg_get_chat_ids(config):
|
|
7435
7495
|
global _telegram_thread, _telegram_stop
|
|
7436
7496
|
if not (_telegram_thread and _telegram_thread.is_alive()):
|
|
7437
7497
|
config["_state"] = state
|
|
7438
7498
|
_telegram_stop = threading.Event()
|
|
7439
7499
|
_telegram_thread = threading.Thread(
|
|
7440
7500
|
target=_tg_poll_loop,
|
|
7441
|
-
args=(config["telegram_token"], config
|
|
7501
|
+
args=(config["telegram_token"], _tg_get_chat_ids(config), config),
|
|
7442
7502
|
daemon=True
|
|
7443
7503
|
)
|
|
7444
7504
|
_telegram_thread.start()
|
|
@@ -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.14"
|
|
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"
|
|
@@ -1746,7 +1746,8 @@ def ask_input_interactive(prompt: str, config: dict, menu_text: str = None) -> s
|
|
|
1746
1746
|
is_tg = _is_in_tg_turn(config)
|
|
1747
1747
|
if is_tg and "_tg_send_callback" in config:
|
|
1748
1748
|
token = config.get("telegram_token")
|
|
1749
|
-
|
|
1749
|
+
# Reply to the user who triggered the current TG turn (multi-user support).
|
|
1750
|
+
chat_id = config.get("_active_tg_chat_id") or config.get("telegram_chat_id")
|
|
1750
1751
|
import re, threading
|
|
1751
1752
|
clean_prompt = re.sub(r'\x1b\[[0-9;]*m', '', prompt).strip()
|
|
1752
1753
|
|
|
@@ -1981,7 +1982,7 @@ def _print_to_console(content: str = "", style: str = "normal", prefix: str = ""
|
|
|
1981
1982
|
# If in Telegram turn, also send to Telegram
|
|
1982
1983
|
if config and _is_in_tg_turn(config):
|
|
1983
1984
|
token = config.get("telegram_token")
|
|
1984
|
-
chat_id = config.get("telegram_chat_id")
|
|
1985
|
+
chat_id = config.get("_active_tg_chat_id") or config.get("telegram_chat_id")
|
|
1985
1986
|
if token and chat_id and "_tg_send_callback" in config:
|
|
1986
1987
|
import re
|
|
1987
1988
|
# Clean ANSI codes and send
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|