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
ui/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # ui package – nothing special needed here
ui/input.py ADDED
@@ -0,0 +1,464 @@
1
+ """prompt_toolkit-based REPL input with typing-time slash-command autosuggest.
2
+
3
+ Optional dependency: when prompt_toolkit is not installed, HAS_PROMPT_TOOLKIT
4
+ is False and callers should fall through to readline-based input.
5
+
6
+ Dependency-injected: callers register command/meta providers via setup()
7
+ before calling read_line(). This module never imports Dulus core — keeping
8
+ the dependency one-way and eliminating any circular-import risk.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Optional
15
+
16
+ try:
17
+ from prompt_toolkit import PromptSession
18
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
19
+ from prompt_toolkit.completion import Completer, Completion
20
+ from prompt_toolkit.formatted_text import ANSI
21
+ from prompt_toolkit.history import FileHistory, InMemoryHistory
22
+ from prompt_toolkit.patch_stdout import patch_stdout
23
+ from prompt_toolkit.styles import Style
24
+ HAS_PROMPT_TOOLKIT = True
25
+ except ImportError:
26
+ HAS_PROMPT_TOOLKIT = False
27
+
28
+
29
+ # ── Injected providers ───────────────────────────────────────────────────────
30
+ # Callers (Dulus REPL) must call setup() before read_line().
31
+ _commands_provider: Optional[Callable[[], dict]] = None
32
+ _meta_provider: Optional[Callable[[], dict]] = None
33
+
34
+
35
+ def setup(
36
+ commands_provider: Callable[[], dict],
37
+ meta_provider: Callable[[], dict],
38
+ ) -> None:
39
+ """Register providers for the live command registry and metadata.
40
+
41
+ `commands_provider` returns the dispatcher's COMMANDS dict.
42
+ `meta_provider` returns the _CMD_META dict (descriptions + subcommands).
43
+ """
44
+ global _commands_provider, _meta_provider
45
+ _commands_provider = commands_provider
46
+ _meta_provider = meta_provider
47
+
48
+
49
+ # ── Completer ────────────────────────────────────────────────────────────────
50
+ if HAS_PROMPT_TOOLKIT:
51
+
52
+ class SlashCompleter(Completer):
53
+ """Two-level completer for slash commands.
54
+
55
+ Level 1: /partial (no space) → command names.
56
+ Level 2: /cmd partial → subcommands listed in the meta dict.
57
+
58
+ Providers default to the module-level ones registered via setup(),
59
+ but can be injected via the constructor for testing.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ commands_provider: Optional[Callable[[], dict]] = None,
65
+ meta_provider: Optional[Callable[[], dict]] = None,
66
+ ):
67
+ self._commands_override = commands_provider
68
+ self._meta_override = meta_provider
69
+ self._cache_key: Optional[tuple] = None
70
+ self._cache_names: list[str] = []
71
+
72
+ def _get_commands(self) -> dict:
73
+ provider = self._commands_override or _commands_provider
74
+ return (provider() if provider else {}) or {}
75
+
76
+ def _get_meta(self) -> dict:
77
+ provider = self._meta_override or _meta_provider
78
+ return (provider() if provider else {}) or {}
79
+
80
+ def _live_command_names(self) -> list[str]:
81
+ keys = sorted(set(self._get_commands().keys()) | set(self._get_meta().keys()))
82
+ sig = tuple(keys)
83
+ if self._cache_key == sig:
84
+ return self._cache_names
85
+ self._cache_key = sig
86
+ self._cache_names = keys
87
+ return keys
88
+
89
+ def get_completions(self, document, complete_event): # type: ignore[override]
90
+ text = document.text_before_cursor
91
+ if not text.startswith("/"):
92
+ return
93
+
94
+ meta = self._get_meta()
95
+
96
+ if " " not in text:
97
+ word = text[1:]
98
+ for name in self._live_command_names():
99
+ if not name.startswith(word):
100
+ continue
101
+ desc, subs = meta.get(name, ("", []))
102
+ hint = ""
103
+ if subs:
104
+ head = ", ".join(subs[:3])
105
+ more = "…" if len(subs) > 3 else ""
106
+ hint = f" [{head}{more}]"
107
+ yield Completion(
108
+ "/" + name,
109
+ start_position=-len(text),
110
+ display=ANSI(f"\x1b[36m/{name}\x1b[0m"),
111
+ display_meta=(desc + hint) if desc else hint.strip(),
112
+ )
113
+ return
114
+
115
+ head, _, tail = text.partition(" ")
116
+ cmd = head[1:]
117
+ meta_entry = meta.get(cmd)
118
+ if not meta_entry:
119
+ return
120
+ subs = meta_entry[1]
121
+ if not subs:
122
+ return
123
+ partial = tail.rsplit(" ", 1)[-1]
124
+ for sub in subs:
125
+ if sub.startswith(partial):
126
+ yield Completion(
127
+ sub,
128
+ start_position=-len(partial),
129
+ display_meta=f"{cmd} subcommand",
130
+ )
131
+
132
+ else: # pragma: no cover — unreachable when prompt_toolkit is installed
133
+ class SlashCompleter:
134
+ def __init__(self, *_args, **_kwargs):
135
+ raise RuntimeError("prompt_toolkit is not installed")
136
+
137
+
138
+ # ── Session cache ────────────────────────────────────────────────────────────
139
+ _SESSION = None
140
+ _SESSION_HISTORY_PATH: Optional[Path] = None
141
+
142
+
143
+ def reset_session() -> None:
144
+ """Drop the cached session so the next read_line() rebuilds from scratch."""
145
+ global _SESSION, _SESSION_HISTORY_PATH
146
+ _SESSION = None
147
+ _SESSION_HISTORY_PATH = None
148
+
149
+
150
+ def _build_session(history_path: Optional[Path]):
151
+ if not HAS_PROMPT_TOOLKIT:
152
+ raise RuntimeError("prompt_toolkit is not installed")
153
+ completer = SlashCompleter()
154
+ history = FileHistory(str(history_path)) if history_path else InMemoryHistory()
155
+ style = Style.from_dict({
156
+ "completion-menu.completion": "bg:#222222 #cccccc",
157
+ "completion-menu.completion.current": "bg:#005f87 #ffffff bold",
158
+ "completion-menu.meta.completion": "bg:#222222 #808080",
159
+ "completion-menu.meta.completion.current": "bg:#005f87 #eeeeee",
160
+ "auto-suggestion": "#606060 italic",
161
+ })
162
+ return PromptSession(
163
+ history=history,
164
+ completer=completer,
165
+ auto_suggest=AutoSuggestFromHistory(),
166
+ complete_while_typing=True,
167
+ enable_history_search=False,
168
+ mouse_support=False,
169
+ style=style,
170
+ )
171
+
172
+
173
+ def read_line(prompt_ansi: str, history_path: Optional[Path] = None) -> str:
174
+ """Read one line of input via prompt_toolkit; caches the session across calls.
175
+
176
+ The history file passed here MUST NOT be the readline history file — the
177
+ two line-editors use incompatible formats. See Dulus REPL for the
178
+ dedicated PT_HISTORY_FILE.
179
+ """
180
+ global _SESSION, _SESSION_HISTORY_PATH, _notification_callback
181
+
182
+ # Drain any pending background notifications before showing prompt
183
+ notifications = drain_notifications()
184
+ for note in notifications:
185
+ if _notification_callback:
186
+ _notification_callback(note)
187
+ else:
188
+ safe_print_notification(note)
189
+
190
+ if _SESSION is not None and _SESSION_HISTORY_PATH != history_path:
191
+ _SESSION = None
192
+ if _SESSION is None:
193
+ _SESSION = _build_session(history_path)
194
+ _SESSION_HISTORY_PATH = history_path
195
+ with patch_stdout(raw=True):
196
+ return _SESSION.prompt(ANSI(prompt_ansi))
197
+
198
+
199
+ # ── Split Layout Mode (Kimi/Claude style) ────────────────────────────────────
200
+ # Fixed bottom input bar with scrollable output area above
201
+
202
+ _split_app: Optional[Any] = None
203
+ _split_buffer: Optional[Any] = None
204
+ _output_buffer: list[str] = []
205
+ _original_stdout = None
206
+
207
+
208
+ class _OutputRedirector:
209
+ """Redirects stdout to the split layout output buffer."""
210
+ def __init__(self, original):
211
+ self._original = original
212
+ self._buffer = ""
213
+
214
+ def write(self, text: str) -> None:
215
+ if not text:
216
+ return
217
+ self._buffer += text
218
+ if "\n" in text:
219
+ lines = self._buffer.split("\n")
220
+ for line in lines[:-1]:
221
+ if line.strip():
222
+ append_output(line)
223
+ self._buffer = lines[-1]
224
+ # Also write to original for compatibility
225
+ self._original.write(text)
226
+
227
+ def flush(self) -> None:
228
+ if self._buffer:
229
+ if self._buffer.strip():
230
+ append_output(self._buffer)
231
+ self._buffer = ""
232
+ self._original.flush()
233
+
234
+ def isatty(self) -> bool:
235
+ return self._original.isatty()
236
+
237
+
238
+ def read_line_split(prompt: str = "> ", history_path: Optional[Path] = None) -> str:
239
+ """Read input with split layout - fixed bottom bar, scrollable output above.
240
+
241
+ Similar to Kimi Code and Claude Code interfaces.
242
+ """
243
+ global _split_app, _split_buffer, _output_buffer, _original_stdout, _notification_callback
244
+
245
+ # Drain any pending background notifications before showing prompt
246
+ notifications = drain_notifications()
247
+ for note in notifications:
248
+ if _notification_callback:
249
+ _notification_callback(note)
250
+ else:
251
+ safe_print_notification(note)
252
+
253
+ if not HAS_PROMPT_TOOLKIT:
254
+ raise RuntimeError("prompt_toolkit is not installed")
255
+
256
+ import sys
257
+ # Save and redirect stdout
258
+ _original_stdout = sys.stdout
259
+ sys.stdout = _OutputRedirector(_original_stdout)
260
+
261
+ from prompt_toolkit import Application
262
+ from prompt_toolkit.buffer import Buffer
263
+ from prompt_toolkit.layout import HSplit, Layout, Window, ConditionalContainer
264
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
265
+ from prompt_toolkit.layout.processors import BeforeInput
266
+ from prompt_toolkit.layout.menus import CompletionsMenu
267
+ from prompt_toolkit.key_binding import KeyBindings
268
+ from prompt_toolkit.filters import has_completions
269
+
270
+ # Output area (upper pane) - shows accumulated output with ANSI support
271
+ def get_output_text():
272
+ """Get formatted output text with ANSI codes parsed."""
273
+ text = "\n".join(_output_buffer[-1000:])
274
+ return ANSI(text) if text else ""
275
+
276
+ output_control = FormattedTextControl(
277
+ text=get_output_text,
278
+ focusable=False,
279
+ show_cursor=False,
280
+ )
281
+ output_window = Window(
282
+ content=output_control,
283
+ wrap_lines=True,
284
+ allow_scroll_beyond_bottom=True,
285
+ )
286
+
287
+ # Input buffer with completer
288
+ history = FileHistory(str(history_path)) if history_path else InMemoryHistory()
289
+ completer = SlashCompleter()
290
+
291
+ _split_buffer = Buffer(
292
+ history=history,
293
+ completer=completer,
294
+ auto_suggest=AutoSuggestFromHistory(),
295
+ complete_while_typing=True,
296
+ enable_history_search=False,
297
+ multiline=False,
298
+ )
299
+
300
+ # Input control with prompt
301
+ input_control = BufferControl(
302
+ buffer=_split_buffer,
303
+ input_processors=[BeforeInput(prompt, style="class:prompt")],
304
+ )
305
+ input_window = Window(
306
+ content=input_control,
307
+ height=1,
308
+ wrap_lines=False,
309
+ always_hide_cursor=False,
310
+ )
311
+
312
+ # Completions menu (floating)
313
+ completions_menu = ConditionalContainer(
314
+ content=CompletionsMenu(
315
+ max_height=8,
316
+ scroll_offset=1,
317
+ extra_filter=has_completions,
318
+ ),
319
+ filter=has_completions,
320
+ )
321
+
322
+ # Key bindings
323
+ kb = KeyBindings()
324
+
325
+ @kb.add("enter")
326
+ def submit(event):
327
+ """Submit input."""
328
+ event.app.exit(result=_split_buffer.text)
329
+
330
+ @kb.add("c-c")
331
+ @kb.add("c-d")
332
+ def cancel(event):
333
+ """Cancel/exit."""
334
+ event.app.exit(result=None)
335
+
336
+ @kb.add("c-l")
337
+ def clear(event):
338
+ """Clear output buffer."""
339
+ _output_buffer.clear()
340
+ output_control.text = ""
341
+
342
+ # Build layout: output on top, separator, input at bottom
343
+ root_container = HSplit([
344
+ output_window, # Flexible height for output
345
+ Window(height=1, char="─", style="class:separator"),
346
+ input_window, # Fixed height for input
347
+ completions_menu, # Floating completions
348
+ ])
349
+
350
+ style = Style.from_dict({
351
+ "completion-menu.completion": "bg:#222222 #cccccc",
352
+ "completion-menu.completion.current": "bg:#005f87 #ffffff bold",
353
+ "completion-menu.meta.completion": "bg:#222222 #808080",
354
+ "completion-menu.meta.completion.current": "bg:#005f87 #eeeeee",
355
+ "auto-suggestion": "#606060 italic",
356
+ "prompt": "#00aa00 bold",
357
+ "separator": "#444444",
358
+ })
359
+
360
+ layout = Layout(root_container, focused_element=input_window)
361
+
362
+ _split_app = Application(
363
+ layout=layout,
364
+ key_bindings=kb,
365
+ style=style,
366
+ mouse_support=False,
367
+ full_screen=False,
368
+ )
369
+
370
+ result = _split_app.run()
371
+
372
+ # Restore stdout
373
+ sys.stdout = _original_stdout
374
+ _original_stdout = None
375
+
376
+ # Reset buffer for next use
377
+ if _split_buffer:
378
+ _split_buffer.reset()
379
+
380
+ return result if result else ""
381
+
382
+
383
+ def append_output(text: str) -> None:
384
+ """Append text to the output buffer (for split layout mode).
385
+
386
+ Use this to display messages without interrupting the input bar.
387
+ """
388
+ global _output_buffer, _split_app
389
+ _output_buffer.append(text)
390
+ # Keep last 1000 lines
391
+ if len(_output_buffer) > 1000:
392
+ _output_buffer = _output_buffer[-1000:]
393
+ # Refresh display if app is running
394
+ if _split_app:
395
+ _split_app.invalidate()
396
+
397
+
398
+ def clear_split_output() -> None:
399
+ """Clear the split layout output buffer."""
400
+ global _output_buffer
401
+ _output_buffer.clear()
402
+
403
+
404
+ # ── Background Notification Queue ────────────────────────────────────────────
405
+ # Thread-safe queue for notifications that need to be displayed without
406
+ # corrupting the prompt_toolkit input rendering.
407
+
408
+ import queue
409
+ _notification_queue: queue.Queue = queue.Queue()
410
+ _notification_callback: Optional[Callable[[str], None]] = None
411
+
412
+
413
+ def set_notification_callback(callback: Callable[[str], None]) -> None:
414
+ """Register a callback to handle background notifications.
415
+
416
+ The callback will be called with the notification text when it's safe
417
+ to display (during the next input cycle or when input is not active).
418
+ """
419
+ global _notification_callback
420
+ _notification_callback = callback
421
+
422
+
423
+ def queue_notification(text: str) -> None:
424
+ """Queue a notification to be displayed safely.
425
+
426
+ This should be used by background threads (timers, jobs, etc.) to
427
+ display messages without corrupting the prompt_toolkit input bar.
428
+ """
429
+ _notification_queue.put(text)
430
+
431
+
432
+ def drain_notifications() -> list[str]:
433
+ """Drain all pending notifications from the queue.
434
+
435
+ Returns a list of notification texts. Should be called when it's
436
+ safe to display output (e.g., before showing a new prompt).
437
+ """
438
+ notifications = []
439
+ while not _notification_queue.empty():
440
+ try:
441
+ notifications.append(_notification_queue.get_nowait())
442
+ except queue.Empty:
443
+ break
444
+ return notifications
445
+
446
+
447
+ def safe_print_notification(text: str) -> None:
448
+ """Print a notification in a prompt_toolkit-safe way.
449
+
450
+ If split layout is active, uses append_output.
451
+ Otherwise prints directly (which may cause display issues in sticky mode).
452
+ """
453
+ global _split_app, _original_stdout
454
+
455
+ if _split_app:
456
+ # Split layout mode - use the safe append_output
457
+ append_output(text)
458
+ elif _original_stdout:
459
+ # We're in some form of redirected stdout
460
+ _original_stdout.write(text + "\n")
461
+ _original_stdout.flush()
462
+ else:
463
+ # Fallback to regular print (may have issues with sticky input)
464
+ print(text)