dulus 0.2.24__tar.gz → 0.2.26__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 (164) hide show
  1. {dulus-0.2.24/dulus.egg-info → dulus-0.2.26}/PKG-INFO +39 -4
  2. {dulus-0.2.24 → dulus-0.2.26}/README.md +38 -3
  3. {dulus-0.2.24 → dulus-0.2.26}/common.py +5 -1
  4. {dulus-0.2.24 → dulus-0.2.26}/docs/news.md +7 -0
  5. {dulus-0.2.24 → dulus-0.2.26/dulus.egg-info}/PKG-INFO +39 -4
  6. {dulus-0.2.24 → dulus-0.2.26}/dulus.py +22 -6
  7. {dulus-0.2.24 → dulus-0.2.26}/pyproject.toml +1 -1
  8. {dulus-0.2.24 → dulus-0.2.26}/skill/clawhub.py +145 -34
  9. {dulus-0.2.24 → dulus-0.2.26}/LICENSE +0 -0
  10. {dulus-0.2.24 → dulus-0.2.26}/MANIFEST.in +0 -0
  11. {dulus-0.2.24 → dulus-0.2.26}/agent.py +0 -0
  12. {dulus-0.2.24 → dulus-0.2.26}/backend/__init__.py +0 -0
  13. {dulus-0.2.24 → dulus-0.2.26}/backend/compressor.py +0 -0
  14. {dulus-0.2.24 → dulus-0.2.26}/backend/context.py +0 -0
  15. {dulus-0.2.24 → dulus-0.2.26}/backend/githook.py +0 -0
  16. {dulus-0.2.24 → dulus-0.2.26}/backend/marketplace.py +0 -0
  17. {dulus-0.2.24 → dulus-0.2.26}/backend/mempalace_bridge.py +0 -0
  18. {dulus-0.2.24 → dulus-0.2.26}/backend/personas.py +0 -0
  19. {dulus-0.2.24 → dulus-0.2.26}/backend/plugins.py +0 -0
  20. {dulus-0.2.24 → dulus-0.2.26}/backend/server.py +0 -0
  21. {dulus-0.2.24 → dulus-0.2.26}/backend/tasks.py +0 -0
  22. {dulus-0.2.24 → dulus-0.2.26}/batch_api.py +0 -0
  23. {dulus-0.2.24 → dulus-0.2.26}/checkpoint/__init__.py +0 -0
  24. {dulus-0.2.24 → dulus-0.2.26}/checkpoint/hooks.py +0 -0
  25. {dulus-0.2.24 → dulus-0.2.26}/checkpoint/store.py +0 -0
  26. {dulus-0.2.24 → dulus-0.2.26}/checkpoint/types.py +0 -0
  27. {dulus-0.2.24 → dulus-0.2.26}/claude_code_watcher.py +0 -0
  28. {dulus-0.2.24 → dulus-0.2.26}/clipboard_utils.py +0 -0
  29. {dulus-0.2.24 → dulus-0.2.26}/cloudsave.py +0 -0
  30. {dulus-0.2.24 → dulus-0.2.26}/compaction.py +0 -0
  31. {dulus-0.2.24 → dulus-0.2.26}/config.py +0 -0
  32. {dulus-0.2.24 → dulus-0.2.26}/context.py +0 -0
  33. {dulus-0.2.24 → dulus-0.2.26}/data/__init__.py +0 -0
  34. {dulus-0.2.24 → dulus-0.2.26}/data/active_persona.json +0 -0
  35. {dulus-0.2.24 → dulus-0.2.26}/data/context.json +0 -0
  36. {dulus-0.2.24 → dulus-0.2.26}/data/marketplace.json +0 -0
  37. {dulus-0.2.24 → dulus-0.2.26}/data/personas.json +0 -0
  38. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/__init__.py +0 -0
  39. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/__init__.py +0 -0
  40. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  41. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  42. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  43. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/plugin.json +0 -0
  44. {dulus-0.2.24 → dulus-0.2.26}/data/plugins/composio/plugin_tool.py +0 -0
  45. {dulus-0.2.24 → dulus-0.2.26}/data/tasks.json +0 -0
  46. {dulus-0.2.24 → dulus-0.2.26}/docs/README.md +0 -0
  47. {dulus-0.2.24 → dulus-0.2.26}/docs/__init__.py +0 -0
  48. {dulus-0.2.24 → dulus-0.2.26}/docs/api.html +0 -0
  49. {dulus-0.2.24 → dulus-0.2.26}/docs/architecture.md +0 -0
  50. {dulus-0.2.24 → dulus-0.2.26}/docs/azure-speech-template.json +0 -0
  51. {dulus-0.2.24 → dulus-0.2.26}/docs/dashboard/index.html +0 -0
  52. {dulus-0.2.24 → dulus-0.2.26}/docs/divider.svg +0 -0
  53. {dulus-0.2.24 → dulus-0.2.26}/docs/generate.py +0 -0
  54. {dulus-0.2.24 → dulus-0.2.26}/docs/hero.svg +0 -0
  55. {dulus-0.2.24 → dulus-0.2.26}/docs/index.html +0 -0
  56. {dulus-0.2.24 → dulus-0.2.26}/docs/nvidia-models.svg +0 -0
  57. {dulus-0.2.24 → dulus-0.2.26}/docs/particle-playground.html +0 -0
  58. {dulus-0.2.24 → dulus-0.2.26}/docs/personas/index.html +0 -0
  59. {dulus-0.2.24 → dulus-0.2.26}/docs/poetry-banner.png +0 -0
  60. {dulus-0.2.24 → dulus-0.2.26}/docs/preview.html +0 -0
  61. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-agents.svg +0 -0
  62. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-brainstorm.svg +0 -0
  63. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-bridges.svg +0 -0
  64. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-features.svg +0 -0
  65. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-freetier.svg +0 -0
  66. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-memory.svg +0 -0
  67. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-models.svg +0 -0
  68. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-perms.svg +0 -0
  69. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-plugins.svg +0 -0
  70. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-quickstart.svg +0 -0
  71. {dulus-0.2.24 → dulus-0.2.26}/docs/sec-ssj.svg +0 -0
  72. {dulus-0.2.24 → dulus-0.2.26}/docs/spinners.svg +0 -0
  73. {dulus-0.2.24 → dulus-0.2.26}/docs/split-pane.svg +0 -0
  74. {dulus-0.2.24 → dulus-0.2.26}/docs/terminal-boot.svg +0 -0
  75. {dulus-0.2.24 → dulus-0.2.26}/docs/uploads/particle-playground.html +0 -0
  76. {dulus-0.2.24 → dulus-0.2.26}/dulus.egg-info/SOURCES.txt +0 -0
  77. {dulus-0.2.24 → dulus-0.2.26}/dulus.egg-info/dependency_links.txt +0 -0
  78. {dulus-0.2.24 → dulus-0.2.26}/dulus.egg-info/entry_points.txt +0 -0
  79. {dulus-0.2.24 → dulus-0.2.26}/dulus.egg-info/requires.txt +0 -0
  80. {dulus-0.2.24 → dulus-0.2.26}/dulus.egg-info/top_level.txt +0 -0
  81. {dulus-0.2.24 → dulus-0.2.26}/dulus_gui.py +0 -0
  82. {dulus-0.2.24 → dulus-0.2.26}/dulus_mcp/__init__.py +0 -0
  83. {dulus-0.2.24 → dulus-0.2.26}/dulus_mcp/client.py +0 -0
  84. {dulus-0.2.24 → dulus-0.2.26}/dulus_mcp/config.py +0 -0
  85. {dulus-0.2.24 → dulus-0.2.26}/dulus_mcp/tools.py +0 -0
  86. {dulus-0.2.24 → dulus-0.2.26}/dulus_mcp/types.py +0 -0
  87. {dulus-0.2.24 → dulus-0.2.26}/gui/__init__.py +0 -0
  88. {dulus-0.2.24 → dulus-0.2.26}/gui/agent_bridge.py +0 -0
  89. {dulus-0.2.24 → dulus-0.2.26}/gui/chat_widget.py +0 -0
  90. {dulus-0.2.24 → dulus-0.2.26}/gui/main_window.py +0 -0
  91. {dulus-0.2.24 → dulus-0.2.26}/gui/personas.py +0 -0
  92. {dulus-0.2.24 → dulus-0.2.26}/gui/session_utils.py +0 -0
  93. {dulus-0.2.24 → dulus-0.2.26}/gui/settings_dialog.py +0 -0
  94. {dulus-0.2.24 → dulus-0.2.26}/gui/sidebar.py +0 -0
  95. {dulus-0.2.24 → dulus-0.2.26}/gui/tasks_view.py +0 -0
  96. {dulus-0.2.24 → dulus-0.2.26}/gui/themes.py +0 -0
  97. {dulus-0.2.24 → dulus-0.2.26}/gui/tool_panel.py +0 -0
  98. {dulus-0.2.24 → dulus-0.2.26}/input.py +0 -0
  99. {dulus-0.2.24 → dulus-0.2.26}/license_manager.py +0 -0
  100. {dulus-0.2.24 → dulus-0.2.26}/memory/__init__.py +0 -0
  101. {dulus-0.2.24 → dulus-0.2.26}/memory/audit.py +0 -0
  102. {dulus-0.2.24 → dulus-0.2.26}/memory/consolidator.py +0 -0
  103. {dulus-0.2.24 → dulus-0.2.26}/memory/context.py +0 -0
  104. {dulus-0.2.24 → dulus-0.2.26}/memory/offload.py +0 -0
  105. {dulus-0.2.24 → dulus-0.2.26}/memory/palace.py +0 -0
  106. {dulus-0.2.24 → dulus-0.2.26}/memory/scan.py +0 -0
  107. {dulus-0.2.24 → dulus-0.2.26}/memory/sessions.py +0 -0
  108. {dulus-0.2.24 → dulus-0.2.26}/memory/store.py +0 -0
  109. {dulus-0.2.24 → dulus-0.2.26}/memory/tools.py +0 -0
  110. {dulus-0.2.24 → dulus-0.2.26}/memory/types.py +0 -0
  111. {dulus-0.2.24 → dulus-0.2.26}/memory/vector_search.py +0 -0
  112. {dulus-0.2.24 → dulus-0.2.26}/multi_agent/__init__.py +0 -0
  113. {dulus-0.2.24 → dulus-0.2.26}/multi_agent/subagent.py +0 -0
  114. {dulus-0.2.24 → dulus-0.2.26}/multi_agent/tools.py +0 -0
  115. {dulus-0.2.24 → dulus-0.2.26}/offload_helper.py +0 -0
  116. {dulus-0.2.24 → dulus-0.2.26}/plugin/__init__.py +0 -0
  117. {dulus-0.2.24 → dulus-0.2.26}/plugin/autoadapter.py +0 -0
  118. {dulus-0.2.24 → dulus-0.2.26}/plugin/loader.py +0 -0
  119. {dulus-0.2.24 → dulus-0.2.26}/plugin/recommend.py +0 -0
  120. {dulus-0.2.24 → dulus-0.2.26}/plugin/store.py +0 -0
  121. {dulus-0.2.24 → dulus-0.2.26}/plugin/types.py +0 -0
  122. {dulus-0.2.24 → dulus-0.2.26}/providers.py +0 -0
  123. {dulus-0.2.24 → dulus-0.2.26}/setup.cfg +0 -0
  124. {dulus-0.2.24 → dulus-0.2.26}/skill/__init__.py +0 -0
  125. {dulus-0.2.24 → dulus-0.2.26}/skill/builtin.py +0 -0
  126. {dulus-0.2.24 → dulus-0.2.26}/skill/executor.py +0 -0
  127. {dulus-0.2.24 → dulus-0.2.26}/skill/loader.py +0 -0
  128. {dulus-0.2.24 → dulus-0.2.26}/skill/tools.py +0 -0
  129. {dulus-0.2.24 → dulus-0.2.26}/skills.py +0 -0
  130. {dulus-0.2.24 → dulus-0.2.26}/spinner.py +0 -0
  131. {dulus-0.2.24 → dulus-0.2.26}/string_utils.py +0 -0
  132. {dulus-0.2.24 → dulus-0.2.26}/subagent.py +0 -0
  133. {dulus-0.2.24 → dulus-0.2.26}/task/__init__.py +0 -0
  134. {dulus-0.2.24 → dulus-0.2.26}/task/store.py +0 -0
  135. {dulus-0.2.24 → dulus-0.2.26}/task/tools.py +0 -0
  136. {dulus-0.2.24 → dulus-0.2.26}/task/types.py +0 -0
  137. {dulus-0.2.24 → dulus-0.2.26}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.24 → dulus-0.2.26}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.24 → dulus-0.2.26}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.24 → dulus-0.2.26}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.24 → dulus-0.2.26}/tests/test_license.py +0 -0
  142. {dulus-0.2.24 → dulus-0.2.26}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.24 → dulus-0.2.26}/tests/test_memory.py +0 -0
  144. {dulus-0.2.24 → dulus-0.2.26}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.24 → dulus-0.2.26}/tests/test_skills.py +0 -0
  146. {dulus-0.2.24 → dulus-0.2.26}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.24 → dulus-0.2.26}/tests/test_task.py +0 -0
  148. {dulus-0.2.24 → dulus-0.2.26}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.24 → dulus-0.2.26}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.24 → dulus-0.2.26}/tests/test_voice.py +0 -0
  151. {dulus-0.2.24 → dulus-0.2.26}/tmux_offloader.py +0 -0
  152. {dulus-0.2.24 → dulus-0.2.26}/tmux_tools.py +0 -0
  153. {dulus-0.2.24 → dulus-0.2.26}/tool_registry.py +0 -0
  154. {dulus-0.2.24 → dulus-0.2.26}/tools.py +0 -0
  155. {dulus-0.2.24 → dulus-0.2.26}/ui/__init__.py +0 -0
  156. {dulus-0.2.24 → dulus-0.2.26}/ui/input.py +0 -0
  157. {dulus-0.2.24 → dulus-0.2.26}/ui/render.py +0 -0
  158. {dulus-0.2.24 → dulus-0.2.26}/voice/__init__.py +0 -0
  159. {dulus-0.2.24 → dulus-0.2.26}/voice/keyterms.py +0 -0
  160. {dulus-0.2.24 → dulus-0.2.26}/voice/recorder.py +0 -0
  161. {dulus-0.2.24 → dulus-0.2.26}/voice/stt.py +0 -0
  162. {dulus-0.2.24 → dulus-0.2.26}/voice/tts.py +0 -0
  163. {dulus-0.2.24 → dulus-0.2.26}/webchat.py +0 -0
  164. {dulus-0.2.24 → dulus-0.2.26}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.24
3
+ Version: 0.2.26
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.24-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.26-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -103,6 +103,26 @@ Use claude-code as an API without the new 'extra-usage' wall <3
103
103
  <sub>⚡ <b>Saves you Claude tokens?</b> Throw a sat — BTC: <code>1JzatQDn9fMLnKTd3KYgztsLHC95bJEzSN</code></sub>
104
104
  </p>
105
105
 
106
+
107
+ Another reminder of a Dulus magic spell:
108
+ Wanna get stock prices, history , etc?
109
+
110
+ /plugin install yfinance@https://github.com/ranaroussi/yfinance
111
+
112
+ them:
113
+ /plugin reload
114
+
115
+ dulus get the prices of NVDA, TSLA, SP500:
116
+
117
+ <img width="2094" height="1365" alt="image" src="https://github.com/user-attachments/assets/1551d651-9d69-4607-bac0-4adbde645783" />
118
+
119
+ Be creative!!!
120
+
121
+ Dulus adapt any python repository <3
122
+
123
+ <p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
124
+
125
+
106
126
  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.
107
127
 
108
128
  > **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
@@ -139,8 +159,6 @@ Dulus is a **lightweight Python reimplementation of Claude Code** that isn't loc
139
159
 
140
160
  ROUND TABLE (DULUS UNIQUE FEATURE)
141
161
 
142
- <img alt="image" src="https://github.com/user-attachments/assets/648ffe5e-28e2-49e0-bc27-362a585edd4f" />
143
-
144
162
  <img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
145
163
 
146
164
  Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
@@ -341,6 +359,23 @@ Dulus's **Auto-Adapter** reads a random Python repo and figures out its tools on
341
359
 
342
360
  Adapt-and-install runs in under a second. New tools register **live**, no restart.
343
361
 
362
+ Example adapting Sherlock repo:
363
+
364
+ <img width="1765" height="166" alt="image" src="https://github.com/user-attachments/assets/c67dc15e-a2e3-4575-be34-8c9b54045510" />
365
+
366
+ -----
367
+
368
+ <img width="1327" height="751" alt="image" src="https://github.com/user-attachments/assets/676a0ef5-3699-4960-98a4-14a55fbef081" />
369
+
370
+ -----
371
+
372
+ <img width="885" height="301" alt="image" src="https://github.com/user-attachments/assets/52c02444-2606-41dc-bc33-ebe26ac41e5e" />
373
+
374
+ ----
375
+
376
+ <img width="1006" height="271" alt="image" src="https://github.com/user-attachments/assets/d823428e-6344-4414-bf42-14ed3128f763" />
377
+
378
+
344
379
  ## MCP
345
380
 
346
381
  Drop a `.mcp.json` in your project root (or `~/.dulus/mcp.json` for user-wide):
@@ -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.24-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
25
+ <img src="https://img.shields.io/badge/version-v0.2.26-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"/>
@@ -56,6 +56,26 @@ Use claude-code as an API without the new 'extra-usage' wall <3
56
56
  <sub>⚡ <b>Saves you Claude tokens?</b> Throw a sat — BTC: <code>1JzatQDn9fMLnKTd3KYgztsLHC95bJEzSN</code></sub>
57
57
  </p>
58
58
 
59
+
60
+ Another reminder of a Dulus magic spell:
61
+ Wanna get stock prices, history , etc?
62
+
63
+ /plugin install yfinance@https://github.com/ranaroussi/yfinance
64
+
65
+ them:
66
+ /plugin reload
67
+
68
+ dulus get the prices of NVDA, TSLA, SP500:
69
+
70
+ <img width="2094" height="1365" alt="image" src="https://github.com/user-attachments/assets/1551d651-9d69-4607-bac0-4adbde645783" />
71
+
72
+ Be creative!!!
73
+
74
+ Dulus adapt any python repository <3
75
+
76
+ <p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
77
+
78
+
59
79
  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.
60
80
 
61
81
  > **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
@@ -92,8 +112,6 @@ Dulus is a **lightweight Python reimplementation of Claude Code** that isn't loc
92
112
 
93
113
  ROUND TABLE (DULUS UNIQUE FEATURE)
94
114
 
95
- <img alt="image" src="https://github.com/user-attachments/assets/648ffe5e-28e2-49e0-bc27-362a585edd4f" />
96
-
97
115
  <img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
98
116
 
99
117
  Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
@@ -294,6 +312,23 @@ Dulus's **Auto-Adapter** reads a random Python repo and figures out its tools on
294
312
 
295
313
  Adapt-and-install runs in under a second. New tools register **live**, no restart.
296
314
 
315
+ Example adapting Sherlock repo:
316
+
317
+ <img width="1765" height="166" alt="image" src="https://github.com/user-attachments/assets/c67dc15e-a2e3-4575-be34-8c9b54045510" />
318
+
319
+ -----
320
+
321
+ <img width="1327" height="751" alt="image" src="https://github.com/user-attachments/assets/676a0ef5-3699-4960-98a4-14a55fbef081" />
322
+
323
+ -----
324
+
325
+ <img width="885" height="301" alt="image" src="https://github.com/user-attachments/assets/52c02444-2606-41dc-bc33-ebe26ac41e5e" />
326
+
327
+ ----
328
+
329
+ <img width="1006" height="271" alt="image" src="https://github.com/user-attachments/assets/d823428e-6344-4414-bf42-14ed3128f763" />
330
+
331
+
297
332
  ## MCP
298
333
 
299
334
  Drop a `.mcp.json` in your project root (or `~/.dulus/mcp.json` for user-wide):
@@ -120,7 +120,11 @@ def apply_theme(name: str) -> bool:
120
120
  apply_theme("dulus")
121
121
 
122
122
  def clr(text: str, *keys: str) -> str:
123
- return "".join(C[k] for k in keys) + str(text) + C["reset"]
123
+ # Defensive: a missing color key (theme-specific names like "accent" or
124
+ # "orange" in palettes that don't define them) used to raise KeyError and
125
+ # could crash callers. Skip unknown keys instead so a stale theme name
126
+ # never takes down the daemon or REPL.
127
+ return "".join(C.get(k, "") for k in keys) + str(text) + C.get("reset", "")
124
128
 
125
129
  def info(msg: str): print(clr(msg, "cyan"))
126
130
  def ok(msg: str): print(clr(msg, "green"))
@@ -3,6 +3,13 @@
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
5
 
6
+ - May 09, 2026 (**v0.2.26**): **`/bg start` daemon crash fix + defensive `clr()` + composio fallback**
7
+ - **Daemon was silently crashing on launch.** `_run_daemon` printed its banner with `clr("...", "accent", "bold")`, but the default theme palette only ships {blue, cyan, gray, green, magenta, red, white, yellow} — `"accent"` raised `KeyError` and killed the process before the prompt loop started. WebChat + IPC threads, being daemon=True, died with it. Switched the banner to `"yellow"` and wrapped in try/except so a stale theme color name never takes the daemon down.
8
+ - **`clr()` is now defensive.** Missing color keys are silently dropped instead of raising. One typo in a theme name no longer crashes the REPL.
9
+ - **`/skill list composio` no longer errors out.** The public `/api/v3/toolkits` endpoint requires elevated auth (returns 401/403 even with a valid API key for free tiers). Added a curated 32-toolkit fallback (Gmail, Slack, GitHub, Notion, Linear, Asana, ClickUp, Jira, Discord, Stripe, etc.) so the menu always shows useful targets. Authenticated path is still attempted first when an API key is configured.
10
+
11
+ - May 09, 2026 (**v0.2.25**): **`/skill list awesome` no longer hangs** — was fetching 235 SKILL.md files sequentially (50-120 seconds, looked frozen). Now uses one GitHub tree API call (instant, <1s, names only) by default; pass `--full` to also pull per-skill descriptions in parallel via a 12-worker thread pool (~5s instead of 120s). Cache stores the with_descriptions flag so future calls reuse the right data.
12
+
6
13
  - May 09, 2026 (**v0.2.24**): **Auto-adapter prompt — 5 fixes from a sherlock postmortem**
7
14
  - **Reconciled `limit` default** — the prompt had two contradictory rules ("default: 50, max: 200" vs "default: 10, NOT 50"). Models burned tokens reasoning about which to follow. Unified on `default: 10, hard max: 200` everywhere.
8
15
  - **"READ the source first" rule** at the top of the wrapper guidelines. Adapters were inferring upstream function signatures from class names and shipping plugins that compile/import/export cleanly but crash at runtime due to type-shape mismatches. Now the prompt explicitly tells the model to read the consumer code (`param.get(...)` / `for x in param`) before guessing shapes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.24
3
+ Version: 0.2.26
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.24-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.26-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -103,6 +103,26 @@ Use claude-code as an API without the new 'extra-usage' wall <3
103
103
  <sub>⚡ <b>Saves you Claude tokens?</b> Throw a sat — BTC: <code>1JzatQDn9fMLnKTd3KYgztsLHC95bJEzSN</code></sub>
104
104
  </p>
105
105
 
106
+
107
+ Another reminder of a Dulus magic spell:
108
+ Wanna get stock prices, history , etc?
109
+
110
+ /plugin install yfinance@https://github.com/ranaroussi/yfinance
111
+
112
+ them:
113
+ /plugin reload
114
+
115
+ dulus get the prices of NVDA, TSLA, SP500:
116
+
117
+ <img width="2094" height="1365" alt="image" src="https://github.com/user-attachments/assets/1551d651-9d69-4607-bac0-4adbde645783" />
118
+
119
+ Be creative!!!
120
+
121
+ Dulus adapt any python repository <3
122
+
123
+ <p align="center"><img src="https://raw.githubusercontent.com/KevRojo/Dulus/main/docs/divider.svg" alt="" width="100%"></p>
124
+
125
+
106
126
  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.
107
127
 
108
128
  > **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
@@ -139,8 +159,6 @@ Dulus is a **lightweight Python reimplementation of Claude Code** that isn't loc
139
159
 
140
160
  ROUND TABLE (DULUS UNIQUE FEATURE)
141
161
 
142
- <img alt="image" src="https://github.com/user-attachments/assets/648ffe5e-28e2-49e0-bc27-362a585edd4f" />
143
-
144
162
  <img alt="image" src="https://github.com/user-attachments/assets/9e8f17ed-6ca2-4ae0-b8c3-146ae5fef491" />
145
163
 
146
164
  Dulus is the first one meeting multiple models at the same time working for the same objective and sharing their ideas.
@@ -341,6 +359,23 @@ Dulus's **Auto-Adapter** reads a random Python repo and figures out its tools on
341
359
 
342
360
  Adapt-and-install runs in under a second. New tools register **live**, no restart.
343
361
 
362
+ Example adapting Sherlock repo:
363
+
364
+ <img width="1765" height="166" alt="image" src="https://github.com/user-attachments/assets/c67dc15e-a2e3-4575-be34-8c9b54045510" />
365
+
366
+ -----
367
+
368
+ <img width="1327" height="751" alt="image" src="https://github.com/user-attachments/assets/676a0ef5-3699-4960-98a4-14a55fbef081" />
369
+
370
+ -----
371
+
372
+ <img width="885" height="301" alt="image" src="https://github.com/user-attachments/assets/52c02444-2606-41dc-bc33-ebe26ac41e5e" />
373
+
374
+ ----
375
+
376
+ <img width="1006" height="271" alt="image" src="https://github.com/user-attachments/assets/d823428e-6344-4414-bf42-14ed3128f763" />
377
+
378
+
344
379
  ## MCP
345
380
 
346
381
  Drop a `.mcp.json` in your project root (or `~/.dulus/mcp.json` for user-wide):
@@ -218,7 +218,7 @@ try:
218
218
  from importlib.metadata import version as _pkg_version
219
219
  VERSION = _pkg_version("dulus")
220
220
  except Exception:
221
- VERSION = "0.2.24" # dev fallback — keep in sync with pyproject.toml
221
+ VERSION = "0.2.26" # dev fallback — keep in sync with pyproject.toml
222
222
 
223
223
  # ── ANSI helpers (used even with rich for non-markdown output) ─────────────
224
224
  from common import C, clr, info, ok, warn, err, stream_thinking, print_tool_start, print_tool_end, sanitize_text
@@ -4251,17 +4251,27 @@ def cmd_skill(args: str, state, config) -> bool:
4251
4251
 
4252
4252
  if rest.startswith("awesome"):
4253
4253
  query = rest[7:].strip()
4254
- info("Fetching awesome skills from GitHub (cached 24h)...")
4255
- skills = list_awesome_remote(query)
4254
+ # `--full` flag pulls per-skill descriptions in parallel (slower but
4255
+ # informative). Default lists names only — instant.
4256
+ full = False
4257
+ if "--full" in query.split():
4258
+ full = True
4259
+ query = " ".join(t for t in query.split() if t != "--full").strip()
4260
+ if full:
4261
+ info("Fetching awesome skills + descriptions from GitHub (parallel, ~5s)...")
4262
+ else:
4263
+ info("Fetching awesome skill list from GitHub (instant)...")
4264
+ skills = list_awesome_remote(query, with_descriptions=full)
4256
4265
  if not skills:
4257
4266
  err("Could not fetch awesome skills (network or rate-limit).")
4258
4267
  return True
4259
4268
  lines = [
4260
- f" {clr(s['id'], 'cyan'):55s} {s['description'][:80]}"
4269
+ f" {clr(s['id'], 'cyan'):55s} {s.get('description', '')[:80]}"
4261
4270
  for s in skills
4262
4271
  ]
4263
4272
  header = f"Awesome skills ({len(skills)})" + (f" matching '{query}'" if query else "")
4264
- _pager(f"{header}n=next q=quit", lines)
4273
+ hint = "" if full else " add `--full` for descriptions"
4274
+ _pager(f"{header}{hint} — n=next q=quit", lines)
4265
4275
  return True
4266
4276
 
4267
4277
  if rest.startswith("composio"):
@@ -5602,7 +5612,13 @@ def _run_daemon(config: dict) -> None:
5602
5612
  config["_ipc_thread"] = ti
5603
5613
  ti.start()
5604
5614
 
5605
- print(clr("\n ▲ DULUS DAEMON", "accent", "bold"))
5615
+ # 'accent' / 'orange' are only present in some custom themes; default
5616
+ # palette is {blue, cyan, gray, green, magenta, red, white, yellow}.
5617
+ # KeyError here would crash the daemon before the user ever sees a prompt.
5618
+ try:
5619
+ print(clr("\n ▲ DULUS DAEMON", "yellow", "bold"))
5620
+ except KeyError:
5621
+ print("\n ▲ DULUS DAEMON")
5606
5622
  print(clr(" " + "─" * 40, "dim"))
5607
5623
  info(f"Session: {session_id}")
5608
5624
  info("Daemon active — waiting for triggers…")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.24"
7
+ version = "0.2.26"
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"
@@ -172,9 +172,14 @@ _AWESOME_EXCLUDE_REMOTE = {
172
172
  }
173
173
 
174
174
 
175
- def _fetch_awesome_remote() -> list[dict]:
176
- """Hit the GitHub tree API + raw URLs to build an awesome skills catalog.
177
- Returns [] on any network/parse error so callers can fall back gracefully.
175
+ def _fetch_awesome_remote(with_descriptions: bool = False) -> list[dict]:
176
+ """Hit the GitHub tree API to list awesome skills.
177
+
178
+ Default (with_descriptions=False): ONE API call, instant, no descriptions.
179
+ Returns 235 entries with name + url ready in <1s.
180
+
181
+ with_descriptions=True: also pulls each SKILL.md's frontmatter via
182
+ raw.githubusercontent.com — done with a thread pool so it stays under ~5s.
178
183
  """
179
184
  import time
180
185
  tree_url = (
@@ -199,6 +204,7 @@ def _fetch_awesome_remote() -> list[dict]:
199
204
  continue
200
205
  skill_paths.append(path)
201
206
 
207
+ # Build the skill list from paths alone — instant, no per-file fetch.
202
208
  skills = []
203
209
  for path in skill_paths:
204
210
  rel_dir = "/".join(path.split("/")[:-1])
@@ -207,26 +213,43 @@ def _fetch_awesome_remote() -> list[dict]:
207
213
  f"https://raw.githubusercontent.com/{_AWESOME_REPO}/"
208
214
  f"{_AWESOME_BRANCH}/{path}"
209
215
  )
210
- try:
211
- with urllib.request.urlopen(raw_url, timeout=10) as r:
212
- raw = r.read().decode("utf-8", errors="ignore")
213
- except Exception:
214
- continue
215
- meta = _parse_frontmatter(raw)
216
216
  skills.append({
217
217
  "id": f"awesome/{rel_dir}",
218
218
  "plugin": "awesome",
219
219
  "skill": skill_name,
220
- "description": meta.get("description", ""),
220
+ "description": "", # filled in below if with_descriptions
221
221
  "path": raw_url,
222
222
  "source": "awesome-remote",
223
223
  "_remote_dir": rel_dir,
224
224
  })
225
225
 
226
+ if with_descriptions and skills:
227
+ # Pull frontmatter in parallel via raw.githubusercontent.com (no
228
+ # rate limit). 12 workers keeps GitHub happy and 235 fetches done
229
+ # in 3-5 seconds instead of the original 50-120 seconds.
230
+ from concurrent.futures import ThreadPoolExecutor, as_completed
231
+
232
+ def _fetch_one(s):
233
+ try:
234
+ with urllib.request.urlopen(s["path"], timeout=8) as r:
235
+ raw = r.read().decode("utf-8", errors="ignore")
236
+ meta = _parse_frontmatter(raw)
237
+ s["description"] = meta.get("description", "")
238
+ except Exception:
239
+ pass
240
+ return s
241
+
242
+ with ThreadPoolExecutor(max_workers=12) as pool:
243
+ list(pool.map(_fetch_one, skills))
244
+
226
245
  _AWESOME_CACHE.parent.mkdir(parents=True, exist_ok=True)
227
246
  try:
228
247
  _AWESOME_CACHE.write_text(
229
- json.dumps({"fetched_at": time.time(), "skills": skills}, indent=2),
248
+ json.dumps({
249
+ "fetched_at": time.time(),
250
+ "with_descriptions": with_descriptions,
251
+ "skills": skills,
252
+ }, indent=2),
230
253
  encoding="utf-8",
231
254
  )
232
255
  except Exception:
@@ -234,21 +257,26 @@ def _fetch_awesome_remote() -> list[dict]:
234
257
  return skills
235
258
 
236
259
 
237
- def list_awesome_remote(query: Optional[str] = None, force_refresh: bool = False) -> list[dict]:
238
- """Return the awesome-skills catalog (cached). Falls through to live fetch
239
- when cache is missing or older than 24h.
260
+ def list_awesome_remote(query: Optional[str] = None, force_refresh: bool = False, with_descriptions: bool = False) -> list[dict]:
261
+ """Return the awesome-skills catalog (cached).
262
+
263
+ Default: one GitHub tree call (~1s, no descriptions), cached 24h.
264
+ with_descriptions=True: also fetches each SKILL.md frontmatter in parallel.
240
265
  """
241
266
  import time
242
267
  skills: list[dict] = []
268
+ cache_has_descriptions = False
243
269
  if not force_refresh and _AWESOME_CACHE.exists():
244
270
  try:
245
271
  data = json.loads(_AWESOME_CACHE.read_text(encoding="utf-8"))
246
272
  if time.time() - float(data.get("fetched_at", 0)) < _AWESOME_TTL_SEC:
247
273
  skills = data.get("skills", [])
274
+ cache_has_descriptions = bool(data.get("with_descriptions"))
248
275
  except Exception:
249
276
  skills = []
250
- if not skills:
251
- skills = _fetch_awesome_remote()
277
+ # Refetch if no cache, or if user wants descriptions but cache doesn't have them.
278
+ if not skills or (with_descriptions and not cache_has_descriptions):
279
+ skills = _fetch_awesome_remote(with_descriptions=with_descriptions)
252
280
 
253
281
  if query:
254
282
  q = query.lower()
@@ -268,8 +296,71 @@ _COMPOSIO_TOOLKITS_URL = "https://backend.composio.dev/api/v3/toolkits?cursor=&l
268
296
  _COMPOSIO_CACHE = Path.home() / ".dulus" / "cache" / "composio-toolkits.json"
269
297
 
270
298
 
299
+ # Curated fallback list — used when no Composio API key is available so the
300
+ # /skill list composio command still shows something useful instead of an
301
+ # empty result. ~30 of the most-requested toolkits.
302
+ _COMPOSIO_FALLBACK = [
303
+ ("gmail", "Gmail email — read, send, label, search messages."),
304
+ ("googlecalendar", "Google Calendar — events, attendees, schedules."),
305
+ ("googledrive", "Google Drive — files, folders, sharing."),
306
+ ("googlesheets", "Google Sheets — read/write spreadsheets."),
307
+ ("googledocs", "Google Docs — create and edit documents."),
308
+ ("slack", "Slack — messages, channels, files, search."),
309
+ ("github", "GitHub — repos, issues, PRs, releases, branches."),
310
+ ("gitlab", "GitLab — projects, issues, merge requests."),
311
+ ("notion", "Notion — pages, databases, blocks."),
312
+ ("linear", "Linear — issues, projects, cycles, teams."),
313
+ ("asana", "Asana — tasks, projects, sections."),
314
+ ("trello", "Trello — boards, cards, lists."),
315
+ ("clickup", "ClickUp — tasks, lists, spaces."),
316
+ ("jira", "Jira — issues, sprints, projects."),
317
+ ("confluence", "Confluence — pages, spaces, content."),
318
+ ("discord", "Discord — guilds, channels, messages."),
319
+ ("telegram", "Telegram bot API — messages, files."),
320
+ ("twitter", "Twitter/X — tweets, search, profiles."),
321
+ ("reddit", "Reddit — posts, comments, subreddits."),
322
+ ("hackernews", "Hacker News — stories, comments, search."),
323
+ ("youtube", "YouTube — videos, channels, comments, captions."),
324
+ ("spotify", "Spotify — playlists, search, playback."),
325
+ ("hubspot", "HubSpot — contacts, deals, companies."),
326
+ ("salesforce", "Salesforce — leads, accounts, opportunities."),
327
+ ("shopify", "Shopify — products, orders, customers."),
328
+ ("stripe", "Stripe — payments, customers, subscriptions."),
329
+ ("airtable", "Airtable — bases, tables, records."),
330
+ ("firebase", "Firebase — Firestore, Realtime DB, Auth."),
331
+ ("supabase", "Supabase — Postgres, auth, storage."),
332
+ ("perplexity", "Perplexity — AI-powered web search."),
333
+ ("firecrawl", "Firecrawl — scrape & crawl websites to markdown."),
334
+ ("exa", "Exa — semantic web search."),
335
+ ]
336
+
337
+
338
+ def _load_composio_api_key() -> str:
339
+ """Load API key from env, ~/.dulus/config.json, or ~/.falcon/config.json."""
340
+ import os as _os
341
+ key = _os.environ.get("COMPOSIO_API_KEY", "").strip()
342
+ if key:
343
+ return key
344
+ for cfg_path in (Path.home() / ".dulus" / "config.json",
345
+ Path.home() / ".falcon" / "config.json"):
346
+ if cfg_path.exists():
347
+ try:
348
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
349
+ k = cfg.get("composio_api_key", "")
350
+ if k:
351
+ return k
352
+ except Exception:
353
+ continue
354
+ return ""
355
+
356
+
271
357
  def list_composio_toolkits(query: Optional[str] = None, force_refresh: bool = False) -> list[dict]:
272
- """Return Composio toolkits as skill-like dicts. Cached 24h."""
358
+ """Return Composio toolkits as skill-like dicts. Cached 24h.
359
+
360
+ Authenticated path (API key set): hit the live `/api/v3/toolkits` endpoint.
361
+ Unauthenticated path: return the curated _COMPOSIO_FALLBACK list so the
362
+ /skill list composio UI still shows something useful.
363
+ """
273
364
  import time
274
365
  items: list[dict] = []
275
366
  if not force_refresh and _COMPOSIO_CACHE.exists():
@@ -280,23 +371,43 @@ def list_composio_toolkits(query: Optional[str] = None, force_refresh: bool = Fa
280
371
  except Exception:
281
372
  items = []
282
373
  if not items:
283
- try:
284
- with urllib.request.urlopen(_COMPOSIO_TOOLKITS_URL, timeout=15) as resp:
285
- payload = json.loads(resp.read())
286
- except Exception:
287
- return []
288
- for tk in payload.get("items", payload.get("data", [])):
289
- slug = tk.get("slug") or tk.get("name", "")
290
- if not slug:
291
- continue
292
- items.append({
293
- "id": f"composio/{slug}",
294
- "plugin": "composio",
295
- "skill": slug,
296
- "description": tk.get("description") or tk.get("meta", {}).get("description", ""),
297
- "path": f"https://composio.dev/apps/{slug}",
298
- "source": "composio",
299
- })
374
+ api_key = _load_composio_api_key()
375
+ if api_key:
376
+ req = urllib.request.Request(
377
+ _COMPOSIO_TOOLKITS_URL,
378
+ headers={"x-api-key": api_key, "Accept": "application/json"},
379
+ )
380
+ try:
381
+ with urllib.request.urlopen(req, timeout=15) as resp:
382
+ payload = json.loads(resp.read())
383
+ for tk in payload.get("items", payload.get("data", [])):
384
+ slug = tk.get("slug") or tk.get("name", "")
385
+ if not slug:
386
+ continue
387
+ items.append({
388
+ "id": f"composio/{slug}",
389
+ "plugin": "composio",
390
+ "skill": slug,
391
+ "description": tk.get("description") or tk.get("meta", {}).get("description", ""),
392
+ "path": f"https://composio.dev/apps/{slug}",
393
+ "source": "composio",
394
+ })
395
+ except Exception:
396
+ pass # fall through to fallback list below
397
+
398
+ # Fallback: no key, or auth call failed — show the curated list so the
399
+ # user still has something to browse / use as session toolkits.
400
+ if not items:
401
+ for slug, desc in _COMPOSIO_FALLBACK:
402
+ items.append({
403
+ "id": f"composio/{slug}",
404
+ "plugin": "composio",
405
+ "skill": slug,
406
+ "description": desc + ("" if api_key else " [curated fallback — set COMPOSIO_API_KEY for the full live catalog]"),
407
+ "path": f"https://composio.dev/apps/{slug}",
408
+ "source": "composio-fallback" if not api_key else "composio",
409
+ })
410
+
300
411
  _COMPOSIO_CACHE.parent.mkdir(parents=True, exist_ok=True)
301
412
  try:
302
413
  _COMPOSIO_CACHE.write_text(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes