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
tool_registry.py ADDED
@@ -0,0 +1,214 @@
1
+ """Tool plugin registry for dulus.
2
+
3
+ Provides a central registry for tool definitions, lookup, schema export,
4
+ and dispatch with output truncation.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+
14
+ @dataclass
15
+ class ToolDef:
16
+ """Definition of a single tool plugin.
17
+
18
+ Attributes:
19
+ name: unique tool identifier
20
+ schema: JSON-schema dict sent to the API (name, description, input_schema)
21
+ func: callable(params: dict, config: dict) -> str
22
+ read_only: True if the tool never mutates state
23
+ concurrent_safe: True if safe to run in parallel with other tools
24
+ display_only: True if output is visual/display only and should NOT be read back
25
+ (saves tokens - use for ASCII art, charts, visual output)
26
+ """
27
+ name: str
28
+ schema: Dict[str, Any]
29
+ func: Callable[[Dict[str, Any], Dict[str, Any]], str]
30
+ read_only: bool = False
31
+ concurrent_safe: bool = False
32
+ display_only: bool = False # NEW: visual output, don't read back
33
+
34
+
35
+ # --------------- internal state ---------------
36
+
37
+ _registry: Dict[str, ToolDef] = {}
38
+ _last_seen_turn: int = -1
39
+
40
+
41
+ # --------------- public API ---------------
42
+
43
+ def register_tool(tool_def: ToolDef) -> None:
44
+ """Register a tool, overwriting any existing tool with the same name."""
45
+ _registry[tool_def.name] = tool_def
46
+
47
+
48
+ def get_tool(name: str) -> Optional[ToolDef]:
49
+ """Look up a tool by name. Returns None if not found."""
50
+ return _registry.get(name)
51
+
52
+
53
+ def get_all_tools() -> List[ToolDef]:
54
+ """Return all registered tools (insertion order)."""
55
+ return list(_registry.values())
56
+
57
+
58
+ def get_tool_schemas() -> List[Dict[str, Any]]:
59
+ """Return the schemas of all registered tools (for API tool parameter)."""
60
+ return [t.schema for t in _registry.values()]
61
+
62
+
63
+ def is_display_only(name: str) -> bool:
64
+ """Check if a tool is display-only (visual output, don't read back).
65
+
66
+ Returns True if the tool's output should not be fed back to the model,
67
+ typically for ASCII art, visual charts, or display-only content.
68
+ """
69
+ tool = get_tool(name)
70
+ return tool.display_only if tool else False
71
+
72
+
73
+ def execute_tool(
74
+ name: str,
75
+ params: Dict[str, Any],
76
+ config: Dict[str, Any],
77
+ max_output: int = 2500,
78
+ ) -> str:
79
+ """Dispatch a tool call by name.
80
+
81
+ Args:
82
+ name: tool name
83
+ params: tool input parameters dict
84
+ config: runtime configuration dict
85
+ max_output: maximum allowed output length in characters.
86
+ Default 2500 — applies uniformly to built-ins AND plugins.
87
+ Tools that need more MUST paginate explicitly (Read with offset/limit, etc).
88
+ Callers can override via config["max_tool_output"] (see tools.execute_tool).
89
+ This prevents context bloat from 0.5% → 7% on large outputs.
90
+
91
+ Returns:
92
+ Tool result string, possibly truncated with navigation hints.
93
+ """
94
+ tool = get_tool(name)
95
+ if tool is None:
96
+ return f"Error: tool '{name}' not found."
97
+
98
+ import io
99
+ from contextlib import redirect_stdout, redirect_stderr
100
+
101
+ f_stdout = io.StringIO()
102
+ f_stderr = io.StringIO()
103
+
104
+ try:
105
+ with redirect_stdout(f_stdout), redirect_stderr(f_stderr):
106
+ result = tool.func(params, config)
107
+ except Exception as e:
108
+ out = f_stdout.getvalue()
109
+ err = f_stderr.getvalue()
110
+ msg = f"Error executing {name}: {e}"
111
+ if out: msg += f"\nSTDOUT:\n{out}"
112
+ if err: msg += f"\nSTDERR:\n{err}"
113
+
114
+ # Add a heuristic hint if a plugin tool crashes
115
+ _mod = getattr(tool.func, "__module__", "")
116
+ if _mod.startswith("_plugin_"):
117
+ parts = _mod.split("_")
118
+ p_name = parts[2] if len(parts) > 2 else "unknown"
119
+ msg += f"\n\nšŸ’” Hint: This plugin tool failed. Do not guess the fix. Use Read/Bash to view ~/.dulus/plugins/{p_name}/plugin_tool.py and its documentation to understand the correct API usage."
120
+
121
+ return msg
122
+
123
+ out = f_stdout.getvalue()
124
+ err = f_stderr.getvalue()
125
+
126
+ if result is None:
127
+ result = ""
128
+
129
+ if not isinstance(result, str):
130
+ result = json.dumps(result, ensure_ascii=False, default=str)
131
+
132
+ # Merge captured output with return value
133
+ final_parts = []
134
+ if out.strip():
135
+ final_parts.append(out.strip())
136
+ if err.strip():
137
+ final_parts.append(f"--- STDERR ---\n{err.strip()}")
138
+
139
+ r_strip = result.strip()
140
+ if r_strip:
141
+ if final_parts:
142
+ # If the function printed something AND returned something, distinguish them
143
+ final_parts.append(f"--- RESULT ---\n{r_strip}")
144
+ else:
145
+ final_parts.append(r_strip)
146
+
147
+ result = "\n\n".join(final_parts) if final_parts else "(ok)"
148
+ total_lines = result.count("\n") + 1 if result else 0
149
+
150
+ # ── Audit trail: log all mutating tool operations ──
151
+ try:
152
+ if name in ("Write", "Edit", "Bash"):
153
+ from memory.audit import log_operation
154
+ log_operation(name, params, result[:200])
155
+ except Exception:
156
+ pass
157
+
158
+ # Save full un-truncated output for persistent access.
159
+ # Shield: tools that only READ the saved output must never overwrite it.
160
+ _read_only_tools = ("Read", "LineCount", "SearchLastOutput")
161
+ is_exploring_persistence = (
162
+ name == "Grep"
163
+ and "last_tool_output.txt" in params.get("path", "")
164
+ )
165
+
166
+ if name not in _read_only_tools and not is_exploring_persistence and not tool.display_only:
167
+ try:
168
+ global _last_seen_turn
169
+ curr_turn = config.get("_turn_count", -1)
170
+ out_file = Path.home() / ".dulus" / "last_tool_output.txt"
171
+ out_file.parent.mkdir(parents=True, exist_ok=True)
172
+
173
+ # If this is a new TURN (assistant turn) and it's NOT a diagnostic search,
174
+ # we overwrite to start fresh. Within the same turn, we append.
175
+ mode = "w" if curr_turn != _last_seen_turn else "a"
176
+ _last_seen_turn = curr_turn
177
+
178
+ with out_file.open(mode, encoding="utf-8", errors="replace") as f:
179
+ if mode == "a":
180
+ f.write(f"\n\n--- [TOOL CALL: {name}] ---\n")
181
+ f.write(result)
182
+ if mode == "a":
183
+ f.write("\n")
184
+ except Exception:
185
+ pass
186
+
187
+ # NO TRUNCATION for display-only tools (PrintToConsole, etc.)
188
+ # These tools output directly to console and don't consume context tokens
189
+ if not tool.display_only and len(result) > max_output:
190
+ total_lines = result.count("\n") + 1
191
+ first_chunk = max_output // 3 # Less upfront, force pagination
192
+ last_chunk = max_output // 6 # Even smaller tail
193
+
194
+ # Show small preview + force explicit pagination pattern
195
+ result = (
196
+ result[:first_chunk]+" >>>>>> THE RESULT WAS TRUNCATED TO AVOID TOKEN WASTE,Read last_tool_output.txt file if complete output needed <<<<<<< "+result[-last_chunk:]
197
+ )
198
+
199
+ return result
200
+
201
+
202
+ def clear_last_output() -> None:
203
+ """Reset the last_tool_output.txt file. Should be called at turn start."""
204
+ try:
205
+ out_file = Path.home() / ".dulus" / "last_tool_output.txt"
206
+ if out_file.exists():
207
+ out_file.unlink()
208
+ except Exception:
209
+ pass
210
+
211
+
212
+ def clear_registry() -> None:
213
+ """Remove all registered tools. Intended for testing."""
214
+ _registry.clear()