dulus 0.2.0__py3-none-any.whl

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 (101) hide show
  1. agent.py +363 -0
  2. backend/__init__.py +63 -0
  3. backend/compressor.py +261 -0
  4. backend/context.py +329 -0
  5. backend/githook.py +166 -0
  6. backend/marketplace.py +141 -0
  7. backend/mempalace_bridge.py +182 -0
  8. backend/personas.py +297 -0
  9. backend/plugins.py +222 -0
  10. backend/server.py +411 -0
  11. backend/tasks.py +213 -0
  12. batch_api.py +307 -0
  13. checkpoint/__init__.py +27 -0
  14. checkpoint/hooks.py +90 -0
  15. checkpoint/store.py +314 -0
  16. checkpoint/types.py +80 -0
  17. claude_code_watcher.py +214 -0
  18. clipboard_utils.py +246 -0
  19. cloudsave.py +159 -0
  20. common.py +177 -0
  21. compaction.py +378 -0
  22. config.py +180 -0
  23. context.py +241 -0
  24. dulus-0.2.0.dist-info/METADATA +600 -0
  25. dulus-0.2.0.dist-info/RECORD +101 -0
  26. dulus-0.2.0.dist-info/WHEEL +5 -0
  27. dulus-0.2.0.dist-info/entry_points.txt +2 -0
  28. dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
  29. dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
  30. dulus-0.2.0.dist-info/top_level.txt +36 -0
  31. dulus.py +8455 -0
  32. dulus_gui.py +331 -0
  33. dulus_mcp/__init__.py +43 -0
  34. dulus_mcp/client.py +546 -0
  35. dulus_mcp/config.py +133 -0
  36. dulus_mcp/tools.py +131 -0
  37. dulus_mcp/types.py +124 -0
  38. gui/__init__.py +18 -0
  39. gui/agent_bridge.py +283 -0
  40. gui/chat_widget.py +448 -0
  41. gui/main_window.py +485 -0
  42. gui/personas.py +230 -0
  43. gui/session_utils.py +189 -0
  44. gui/settings_dialog.py +146 -0
  45. gui/sidebar.py +515 -0
  46. gui/tasks_view.py +499 -0
  47. gui/themes.py +256 -0
  48. gui/tool_panel.py +94 -0
  49. input.py +1030 -0
  50. license_manager.py +187 -0
  51. memory/__init__.py +93 -0
  52. memory/audit.py +51 -0
  53. memory/consolidator.py +312 -0
  54. memory/context.py +270 -0
  55. memory/offload.py +148 -0
  56. memory/palace.py +127 -0
  57. memory/scan.py +146 -0
  58. memory/sessions.py +100 -0
  59. memory/store.py +395 -0
  60. memory/tools.py +408 -0
  61. memory/types.py +114 -0
  62. memory/vector_search.py +92 -0
  63. multi_agent/__init__.py +23 -0
  64. multi_agent/subagent.py +501 -0
  65. multi_agent/tools.py +393 -0
  66. offload_helper.py +183 -0
  67. plugin/__init__.py +22 -0
  68. plugin/autoadapter.py +1641 -0
  69. plugin/loader.py +156 -0
  70. plugin/recommend.py +211 -0
  71. plugin/store.py +387 -0
  72. plugin/types.py +147 -0
  73. providers.py +3750 -0
  74. skill/__init__.py +14 -0
  75. skill/builtin.py +100 -0
  76. skill/clawhub.py +270 -0
  77. skill/executor.py +66 -0
  78. skill/loader.py +199 -0
  79. skill/tools.py +110 -0
  80. skills.py +14 -0
  81. spinner.py +42 -0
  82. string_utils.py +42 -0
  83. subagent.py +11 -0
  84. task/__init__.py +12 -0
  85. task/store.py +199 -0
  86. task/tools.py +265 -0
  87. task/types.py +92 -0
  88. tmux_offloader.py +177 -0
  89. tmux_tools.py +410 -0
  90. tool_registry.py +214 -0
  91. tools.py +2694 -0
  92. ui/__init__.py +1 -0
  93. ui/input.py +464 -0
  94. ui/render.py +272 -0
  95. voice/__init__.py +56 -0
  96. voice/keyterms.py +179 -0
  97. voice/recorder.py +263 -0
  98. voice/stt.py +408 -0
  99. voice/tts.py +570 -0
  100. webchat.py +432 -0
  101. webchat_server.py +1761 -0
dulus_gui.py ADDED
@@ -0,0 +1,331 @@
1
+ """Dulus GUI Entry Point — professional desktop interface.
2
+
3
+ Usage:
4
+ python dulus_gui.py
5
+ python dulus.py --gui
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import datetime
10
+ import queue
11
+ import sys
12
+ import traceback
13
+ from pathlib import Path
14
+ from typing import Callable
15
+
16
+ sys.path.insert(0, str(Path(__file__).parent))
17
+
18
+ try:
19
+ import customtkinter as ctk
20
+ except ImportError:
21
+ print("Error: customtkinter is required. Install: pip install customtkinter")
22
+ sys.exit(1)
23
+
24
+ from config import load_config
25
+ from gui import DulusMainWindow, DulusBridge
26
+ from gui.themes import get_theme, set_theme
27
+ from gui.session_utils import scan_sessions
28
+
29
+ # Session directories
30
+ from config import SESSIONS_DIR, DAILY_DIR, MR_SESSION_DIR
31
+
32
+
33
+ # ── Helpers ───────────────────────────────────────────────────────────────────
34
+
35
+
36
+ def _center_on_parent(dialog: ctk.CTkToplevel, parent: ctk.CTk) -> None:
37
+ """Center a Toplevel over its parent window."""
38
+ dialog.update_idletasks()
39
+ pw, ph = parent.winfo_width(), parent.winfo_height()
40
+ px, py = parent.winfo_x(), parent.winfo_y()
41
+ dw, dh = dialog.winfo_width(), dialog.winfo_height()
42
+ x = px + (pw - dw) // 2
43
+ y = py + (ph - dh) // 2
44
+ dialog.geometry(f"+{x}+{y}")
45
+
46
+
47
+ class _PermissionDialog(ctk.CTkToplevel):
48
+ """Modal permission request dialog centered on the parent."""
49
+
50
+ def __init__(self, parent: ctk.CTk, description: str, on_resolve: Callable[[bool], None]):
51
+ super().__init__(parent)
52
+ self._on_resolve = on_resolve
53
+ self._create_ui(description)
54
+ self._setup_window(parent)
55
+
56
+ def _create_ui(self, description: str) -> None:
57
+ t = get_theme()
58
+ self.configure(fg_color=t["bg"])
59
+
60
+ ctk.CTkLabel(
61
+ self,
62
+ text="🔒 Permission Required",
63
+ font=("Segoe UI", 16, "bold"),
64
+ text_color=t["accent"],
65
+ ).pack(pady=(20, 10))
66
+
67
+ ctk.CTkLabel(
68
+ self,
69
+ text=description,
70
+ font=("Segoe UI", 12),
71
+ text_color=t["text"],
72
+ wraplength=450,
73
+ ).pack(pady=10, padx=20)
74
+
75
+ btn_frame = ctk.CTkFrame(self, fg_color="transparent")
76
+ btn_frame.pack(pady=15)
77
+
78
+ ctk.CTkButton(
79
+ btn_frame,
80
+ text="Deny",
81
+ font=("Segoe UI", 12, "bold"),
82
+ fg_color=t["border"],
83
+ hover_color=t["error"],
84
+ width=100,
85
+ command=self._deny,
86
+ ).pack(side="left", padx=10)
87
+
88
+ ctk.CTkButton(
89
+ btn_frame,
90
+ text="Allow",
91
+ font=("Segoe UI", 12, "bold"),
92
+ fg_color=t["accent"],
93
+ hover_color=t["accent_hover"],
94
+ width=100,
95
+ command=self._allow,
96
+ ).pack(side="left", padx=10)
97
+
98
+ def _setup_window(self, parent: ctk.CTk) -> None:
99
+ self.title("Permission Required")
100
+ self.geometry("500x220")
101
+ self.transient(parent)
102
+ self.grab_set()
103
+ self.resizable(False, False)
104
+ _center_on_parent(self, parent)
105
+
106
+ def _allow(self) -> None:
107
+ self.destroy()
108
+ self._on_resolve(True)
109
+
110
+ def _deny(self) -> None:
111
+ self.destroy()
112
+ self._on_resolve(False)
113
+
114
+
115
+ # ── Main launcher ─────────────────────────────────────────────────────────────
116
+
117
+
118
+ # _scan_sessions refactored to gui/session_utils.py
119
+
120
+
121
+ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) -> None:
122
+ """Launch the Dulus desktop GUI.
123
+
124
+ Args:
125
+ config: Dulus configuration dict (loaded from disk if None).
126
+ initial_prompt: Optional initial user message to send on startup.
127
+ """
128
+ cfg = config or load_config()
129
+
130
+ # Theme
131
+ ctk.set_appearance_mode(cfg.get("appearance", "dark"))
132
+ ctk.set_default_color_theme("dark-blue")
133
+ set_theme(cfg.get("theme", "midnight"))
134
+ t = get_theme()
135
+
136
+ # Create GUI window FIRST so user sees something immediately
137
+ app = DulusMainWindow()
138
+ app.set_model(cfg.get("model", "default"))
139
+
140
+ # Create bridge (but don't start yet)
141
+ bridge = DulusBridge(config=cfg)
142
+
143
+ # Wire bridge into sidebar so context bar / model list work
144
+ app.sidebar.bridge = bridge
145
+
146
+ # ── Wire callbacks ────────────────────────────────────────────────────────
147
+
148
+ def _on_send(text: str) -> None:
149
+ if text.strip():
150
+ # NOTE: message bubble is already added by main_window._on_send_click
151
+ app.show_thinking()
152
+ bridge.send_message(text)
153
+
154
+ def _on_new_chat() -> None:
155
+ # Save current session if active (it will return a new ID if it was new)
156
+ sid = bridge.save_current_session()
157
+ if sid:
158
+ # If a new session was created, refresh sidebar to show it
159
+ app.set_sessions(scan_sessions())
160
+
161
+ app.hide_thinking()
162
+ app.chat.clear_chat()
163
+ bridge.clear_session()
164
+ app.set_active_session(None)
165
+ app.sidebar.update_context_bar()
166
+ app.set_status("Listo", t["success"])
167
+
168
+ def _on_session_select(session_id: str) -> None:
169
+ # Save current session before switching to ensure no loss
170
+ sid = bridge.save_current_session()
171
+
172
+ # If we were in a new chat that just got saved, refresh sidebar to show it
173
+ if sid:
174
+ app.set_sessions(scan_sessions())
175
+
176
+ app.hide_thinking()
177
+
178
+ # 1. Use cached data from sidebar for instant switching
179
+ session_data = app.sidebar._session_cache.get(session_id)
180
+ if not session_data:
181
+ # Fallback to scanning if cache missed (rare)
182
+ for s in scan_sessions():
183
+ if s["id"] == session_id:
184
+ session_data = s
185
+ break
186
+
187
+ if not session_data:
188
+ return
189
+
190
+ # 2. Update UI instantly (fluid)
191
+ messages = session_data.get("messages", [])
192
+ app.chat.load_messages(messages)
193
+
194
+ # 3. Defer bridge loading until first message (user request)
195
+ bridge.pending_history = messages
196
+ bridge.session_id = session_id
197
+ # Important: clear actual AI state so it's fresh until sync
198
+ from agent import AgentState
199
+ bridge.state = AgentState()
200
+
201
+ app.set_active_session(session_id)
202
+ app.sidebar.update_context_bar()
203
+ app.set_status("Sesión lista (Contexto diferido)", t["success"])
204
+
205
+ def _on_settings() -> None:
206
+ from gui.settings_dialog import SettingsDialog
207
+ SettingsDialog(app, cfg)
208
+
209
+ def _on_model_change(model: str) -> None:
210
+ bridge.set_model(model)
211
+ app.set_model(model)
212
+
213
+ app.on_send = _on_send
214
+ app.on_new_chat = _on_new_chat
215
+ app.sidebar.on_settings = _on_settings
216
+ app.on_model_change = _on_model_change
217
+ app.on_session_select = _on_session_select
218
+
219
+ # Load existing sessions into sidebar
220
+ app.set_sessions(scan_sessions())
221
+ app.sidebar._refresh_model_list()
222
+ app.sidebar.update_context_bar()
223
+
224
+ # ── Permission dialog handling ────────────────────────────────────────────
225
+ _perm_dialog: _PermissionDialog | None = None
226
+
227
+ def _close_perm() -> None:
228
+ nonlocal _perm_dialog
229
+ if _perm_dialog is not None:
230
+ _perm_dialog.destroy()
231
+ _perm_dialog = None
232
+
233
+ def _resolve_perm(granted: bool) -> None:
234
+ _close_perm()
235
+ bridge.grant_permission(granted)
236
+
237
+ def _show_perm(description: str) -> None:
238
+ nonlocal _perm_dialog
239
+ _close_perm()
240
+ _perm_dialog = _PermissionDialog(app, description, _resolve_perm)
241
+
242
+ # ── Event polling loop ────────────────────────────────────────────────────
243
+ def _poll_events() -> None:
244
+ if not app.winfo_exists():
245
+ return # App destroyed, stop polling
246
+
247
+ try:
248
+ while True:
249
+ event = bridge.event_queue.get_nowait()
250
+ etype = event.get("type")
251
+
252
+ if etype == "text":
253
+ app.add_assistant_chunk(event.get("text", ""))
254
+
255
+ elif etype == "thinking":
256
+ app.show_thinking()
257
+
258
+ elif etype == "tool_start":
259
+ app.add_tool_call(event.get("name", "tool"), "running")
260
+
261
+ elif etype == "tool_end":
262
+ app.add_tool_call(event.get("name", ""), "done")
263
+
264
+ elif etype == "turn_done":
265
+ app.hide_thinking()
266
+ itok = event.get("input_tokens", 0)
267
+ otok = event.get("output_tokens", 0)
268
+ app.set_status(f"Listo (+{itok}/{otok} tok)", t["success"])
269
+
270
+ # Refresh sessions list to show the newly saved session (with its title)
271
+ app.set_sessions(scan_sessions())
272
+ if event.get("session_id"):
273
+ app.set_active_session(event.get("session_id"))
274
+
275
+ elif etype == "permission":
276
+ _show_perm(event.get("description", ""))
277
+
278
+ elif etype == "error":
279
+ app.hide_thinking()
280
+ app.chat.add_assistant_message(
281
+ f"**Error:** {event.get('message', 'Unknown error')}"
282
+ )
283
+ app.set_status("Error", t["error"])
284
+
285
+ except queue.Empty:
286
+ pass
287
+ except Exception as exc:
288
+ # Log to file so we know what crashed the UI
289
+ try:
290
+ with open("gui_error.log", "a", encoding="utf-8") as f:
291
+ f.write(f"\n[{datetime.datetime.now()}] POLL ERROR: {exc}\n")
292
+ traceback.print_exc(file=f)
293
+ except Exception:
294
+ pass
295
+ finally:
296
+ # ALWAYS reschedule — if we don't, the GUI stops responding
297
+ if app.winfo_exists():
298
+ app.after(50, _poll_events)
299
+
300
+ app.after(50, _poll_events)
301
+
302
+ # ── Start bridge AFTER UI is ready ────────────────────────────────────────
303
+ try:
304
+ bridge.start()
305
+ except Exception as exc:
306
+ app.chat.add_assistant_message(f"**Fatal:** Could not start Dulus bridge: {exc}")
307
+ app.set_status("Fatal error", t["error"])
308
+
309
+ # ── Initial prompt ────────────────────────────────────────────────────────
310
+ if initial_prompt:
311
+ app.chat.add_user_message(initial_prompt)
312
+ bridge.send_message(initial_prompt)
313
+ app.show_thinking()
314
+
315
+ # ── Cleanup ───────────────────────────────────────────────────────────────
316
+ def _on_close() -> None:
317
+ bridge.stop()
318
+ app.destroy()
319
+
320
+ app.protocol("WM_DELETE_WINDOW", _on_close)
321
+ app.run()
322
+
323
+
324
+ def main() -> None:
325
+ """CLI entry point."""
326
+ cfg = load_config()
327
+ launch_gui(config=cfg)
328
+
329
+
330
+ if __name__ == "__main__":
331
+ main()
dulus_mcp/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """mcp package — Model Context Protocol client for dulus.
2
+
3
+ Usage
4
+ -----
5
+ MCP servers are configured in one of two JSON files:
6
+
7
+ ~/.dulus/mcp.json (user-level, all projects)
8
+ .mcp.json (project-level, current dir, overrides user)
9
+
10
+ Format:
11
+ {
12
+ "mcpServers": {
13
+ "my-git-server": {
14
+ "type": "stdio",
15
+ "command": "uvx",
16
+ "args": ["mcp-server-git"]
17
+ },
18
+ "my-remote": {
19
+ "type": "sse",
20
+ "url": "http://localhost:8080/sse"
21
+ }
22
+ }
23
+ }
24
+
25
+ Supported transports:
26
+ stdio — spawn a local subprocess (most common)
27
+ sse — HTTP Server-Sent Events stream
28
+ http — plain HTTP POST (Streamable HTTP transport)
29
+
30
+ MCP tools are automatically discovered on startup and registered into the
31
+ tool_registry under the name mcp__<server>__<tool>.
32
+ Claude can invoke them just like built-in tools.
33
+ """
34
+ from .types import MCPServerConfig, MCPTool, MCPServerState, MCPTransport # noqa: F401
35
+ from .client import MCPClient, MCPManager, get_mcp_manager # noqa: F401
36
+ from .config import ( # noqa: F401
37
+ load_mcp_configs,
38
+ save_user_mcp_config,
39
+ add_server_to_user_config,
40
+ remove_server_from_user_config,
41
+ list_config_files,
42
+ )
43
+ from .tools import initialize_mcp, reload_mcp, refresh_server # noqa: F401