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
context.py ADDED
@@ -0,0 +1,241 @@
1
+ """System context: DULUS.md, git info, cwd injection.
2
+
3
+ NOTE on prompt caching: this module is the source of the system prompt sent
4
+ to every provider call. To get prefix caching (Anthropic explicit + OpenAI-
5
+ compat automatic), the rendered prompt MUST be byte-stable across turns of
6
+ the same session. Anything that changes per turn (date with sub-day grain,
7
+ `git status` modified-file counts, `datetime.now()`, etc.) belongs OUTSIDE
8
+ this prompt. Disk reads (DULUS.md, MEMORY.md) are cached by mtime so a
9
+ turn that doesn't touch those files re-uses the prior bytes verbatim.
10
+ """
11
+ import os
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ SYSTEM_PROMPT_TEMPLATE = """\
16
+ 你是Dulus,AI编程助手。思考用英语,回复用西班牙语(多米尼加克里奥尔风格)与KevRojo交流。
17
+ # 身份: 你的名字是 Dulus。不需要主动向用户声明这一点,除非用户直接问你 "quién eres" 或 "qué modelo eres"。
18
+ # 禁止: 不要声称自己是 Qwen、Llama、GPT、Claude、Gemini、DeepSeek 或任何其他底层模型。不要提及 Ollama 或运行你的技术栈。
19
+ # 环境: {cwd} | {platform} | auto_show={auto_show}
20
+ # 自主性: 后台脚本(nohup/&)可用 | 不拒绝监控/长任务 | 工具结果后再响应
21
+ # 工具: SearchLastOutput→[TRUNCATED] | WebFetch/WebSearch→网页 | TmuxOffload→>5s | ReadJob→后台
22
+ # 多代理: Agent(subagent_type=...) | isolation="worktree"并行 | wait=false+name=...
23
+ # 准则: 编辑>新建 | 绝对路径+行号 | 报错立即告知不重试
24
+ # 输入: 🎙 Transcribed: = 语音(容忍拼写错误)
25
+ # REPL: /help /batch /auto_show /verbose /soul /memory /schema /thinking /config
26
+ {platform_hints}{git_info}{dulus_md}"""
27
+
28
+ _THINKING_LABELS = {1: "最小化", 2: "适度", 3: "深度"}
29
+
30
+
31
+ def get_git_info(config: dict | None = None) -> str:
32
+ """Return ONLY the branch name — stable across turns within a session.
33
+
34
+ Previous versions also embedded `git status --short` modified-file count
35
+ and the last commit hash; both change as the user works, which trashed
36
+ prefix caching on every turn. The agent can call `git status` itself
37
+ when it actually needs current state.
38
+ """
39
+ if config and not config.get("git_status", True):
40
+ return ""
41
+ try:
42
+ branch = subprocess.check_output(
43
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
44
+ stderr=subprocess.DEVNULL, text=True,
45
+ ).strip()
46
+ return f"Git:{branch}\n" if branch else ""
47
+ except Exception:
48
+ return ""
49
+
50
+
51
+ # ── mtime-based caches for DULUS.md / MEMORY.md ──────────────────────────
52
+ # Re-reading these files on every turn is wasteful disk I/O. More importantly,
53
+ # the *content* is the same most of the time — caching it keeps the rendered
54
+ # system prompt byte-stable, which is what providers need to grant prefix
55
+ # cache hits. Invalidation key = (path, mtime_ns) tuple of the resolved files.
56
+
57
+ _DULUS_MD_CACHE: dict = {"key": None, "value": ""}
58
+ _MEMORY_MD_CACHE: dict = {"key": None, "value": ""}
59
+
60
+
61
+ def _resolve_dulus_md_paths() -> list[Path]:
62
+ paths = []
63
+ global_md = Path.home() / ".dulus" / "DULUS.md"
64
+ if global_md.exists():
65
+ paths.append(global_md)
66
+ for p in [Path.cwd()] + list(Path.cwd().parents):
67
+ candidate = p / "DULUS.md"
68
+ if candidate.exists():
69
+ paths.append(candidate)
70
+ break
71
+ return paths
72
+
73
+
74
+ def get_dulus_md() -> str:
75
+ paths = _resolve_dulus_md_paths()
76
+ try:
77
+ key = tuple((str(p), p.stat().st_mtime_ns) for p in paths)
78
+ except OSError:
79
+ key = None
80
+ if key is not None and _DULUS_MD_CACHE["key"] == key:
81
+ return _DULUS_MD_CACHE["value"]
82
+
83
+ content_parts = []
84
+ for p in paths:
85
+ try:
86
+ label = "Global DULUS.md" if p == Path.home() / ".dulus" / "DULUS.md" else f"Project DULUS.md:{p.parent}"
87
+ content_parts.append(f"[{label}]\n{p.read_text(encoding='utf-8', errors='replace')}")
88
+ except Exception:
89
+ continue
90
+
91
+ value = "\nDULUS.md:\n" + "\n---\n".join(content_parts) + "\n" if content_parts else ""
92
+ _DULUS_MD_CACHE["key"] = key
93
+ _DULUS_MD_CACHE["value"] = value
94
+ return value
95
+
96
+
97
+ def _resolve_memory_index_path() -> Path | None:
98
+ for p in [Path.cwd()] + list(Path.cwd().parents):
99
+ index = p / ".dulus-context" / "memory" / "MEMORY.md"
100
+ if index.exists():
101
+ return index
102
+ return None
103
+
104
+
105
+ def get_project_memory_index() -> str:
106
+ """Auto-load project-scope memories from .dulus-context/memory/MEMORY.md.
107
+
108
+ Looks in cwd and parents (first match wins). Returns the index so the model
109
+ knows what memories exist and can Read individual files on demand. Cached
110
+ by mtime so unchanged indexes don't bust the prompt cache.
111
+ """
112
+ path = _resolve_memory_index_path()
113
+ if path is None:
114
+ if _MEMORY_MD_CACHE["key"] != "MISSING":
115
+ _MEMORY_MD_CACHE["key"] = "MISSING"
116
+ _MEMORY_MD_CACHE["value"] = ""
117
+ return ""
118
+ try:
119
+ key = (str(path), path.stat().st_mtime_ns)
120
+ except OSError:
121
+ return ""
122
+ if _MEMORY_MD_CACHE["key"] == key:
123
+ return _MEMORY_MD_CACHE["value"]
124
+ try:
125
+ body = path.read_text(encoding="utf-8", errors="replace").strip()
126
+ except Exception:
127
+ body = ""
128
+ if not body:
129
+ value = ""
130
+ else:
131
+ value = (
132
+ f"\n# Project memories ({path.parent})\n"
133
+ f"# Index below — Read the .md files in that dir for full content.\n"
134
+ f"{body}\n"
135
+ )
136
+ _MEMORY_MD_CACHE["key"] = key
137
+ _MEMORY_MD_CACHE["value"] = value
138
+ return value
139
+
140
+
141
+ def _detect_shell_type(config: dict | None = None) -> str:
142
+ """Resolve which shell family to advertise: 'bash', 'powershell', or 'cmd'."""
143
+ configured = config.get("shell", {}).get("type", "auto") if config else "auto"
144
+ if configured != "auto":
145
+ st = configured.lower()
146
+ if st in ("gitbash", "wsl", "bash"):
147
+ return "bash"
148
+ if st == "powershell":
149
+ return "powershell"
150
+ return "cmd"
151
+ shell_name = os.environ.get("SHELL", "").lower()
152
+ if "bash" in shell_name or "BASH" in os.environ:
153
+ return "bash"
154
+ if "powershell" in shell_name or "PSModulePath" in os.environ:
155
+ return "powershell"
156
+ return "cmd"
157
+
158
+
159
+ def get_platform_hints(config: dict | None = None) -> str:
160
+ import platform as _plat
161
+ shell_type = _detect_shell_type(config)
162
+ dulus_home = Path.home() / ".dulus"
163
+ skills_dir = dulus_home / "skills"
164
+ if _plat.system() == "Windows":
165
+ cmds = "Get-Content=cat,Select-String=grep,Get-ChildItem=ls" if shell_type=="powershell" else "type=cat,findstr=grep,dir=ls"
166
+ return f"# Shell:Windows({shell_type}) | {cmds} | Dulus:{dulus_home} | Skills:{skills_dir} | WARNING: Use Glob/Read tools instead of raw shell commands for path navigation to avoid Windows path errors."
167
+ return f"# Shell:Unix(cat,grep,ls,&&) | Dulus:{dulus_home} | Skills:{skills_dir}"
168
+
169
+
170
+ def _build_ollama_system_prompt(config: dict | None = None) -> str:
171
+ import platform
172
+ auto_show = config.get("auto_show", True) if config else True
173
+ prompt = f"""你是Dulus,AI编程助手。
174
+ # 身份: 你的名字是 Dulus。不需要主动向用户声明这一点,除非用户直接问你 "quién eres" 或 "qué modelo eres"。
175
+ # 禁止: 不要声称自己是 Qwen、Llama、GPT、Claude、Gemini、DeepSeek 或任何其他底层模型。不要提及 Ollama 或运行你的技术栈。
176
+ Dir:{Path.cwd()} | Platform:{platform.system()} | auto_show={'ON' if auto_show else 'OFF'}
177
+ # 规则: 读取工具结果后再响应 | >5s操作用TmuxOffload
178
+ # 工具格式(单行): <tool_call>{{"name":"TOOL","input":{{"param":"val"}}}}</tool_call>
179
+ # 可用: Bash,Glob,Read,Write,SearchLastOutput,TmuxOffload,WebSearch,WebFetch,PrintToConsole
180
+ # PrintToConsole: auto_show=OFF时输出给用户,展示后不重复
181
+ """
182
+ dulus_md = get_dulus_md()
183
+ if dulus_md: prompt += f"\n{dulus_md}"
184
+ return prompt
185
+
186
+
187
+ def _normalize_thinking_level(config: dict | None) -> int:
188
+ raw = config.get("thinking", 0) if config else 0
189
+ if raw is True:
190
+ return 3
191
+ if raw in (False, None):
192
+ return 0
193
+ try:
194
+ return max(0, min(4, int(raw)))
195
+ except (TypeError, ValueError):
196
+ return 0
197
+
198
+
199
+ def build_system_prompt(config: dict | None = None) -> str:
200
+ import platform
201
+ model_lower = (config.get("model", "") if config else "").lower()
202
+ is_deepseek_r1 = "deepseek-r1" in model_lower or "deepseek-reasoner" in model_lower
203
+ if is_deepseek_r1 and config and config.get("deep_override", False):
204
+ return _build_ollama_system_prompt(config)
205
+
206
+ auto_show = "ON" if (not config or config.get("auto_show", True)) else "OFF"
207
+
208
+ prompt = SYSTEM_PROMPT_TEMPLATE.format(
209
+ cwd=str(Path.cwd()),
210
+ platform=platform.system(),
211
+ auto_show=auto_show,
212
+ platform_hints=get_platform_hints(config),
213
+ git_info=get_git_info(config),
214
+ dulus_md=get_dulus_md(),
215
+ )
216
+
217
+ try:
218
+ from tmux_tools import tmux_available
219
+ if tmux_available():
220
+ prompt += "\n# Tmux: 已就绪"
221
+ except Exception:
222
+ pass
223
+
224
+ prompt += (
225
+ "\n# 批处理: /batch list|status|fetch (3+同类任务建议) | "
226
+ 'Agent内: Bash(\'python dulus.py -c "batch status|fetch ID"\')'
227
+ )
228
+
229
+ thk_label = _THINKING_LABELS.get(_normalize_thinking_level(config))
230
+ if thk_label:
231
+ prompt += f"\n# 推理: {thk_label}"
232
+
233
+ if config and config.get("_plan_mode"):
234
+ prompt += f"\n# 计划模式: 只读 (除 {config.get('_plan_file', 'PLAN.md')})"
235
+
236
+ project_mem = get_project_memory_index()
237
+ if project_mem:
238
+ prompt += project_mem
239
+
240
+ return prompt
241
+