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.
- agent.py +363 -0
- backend/__init__.py +63 -0
- backend/compressor.py +261 -0
- backend/context.py +329 -0
- backend/githook.py +166 -0
- backend/marketplace.py +141 -0
- backend/mempalace_bridge.py +182 -0
- backend/personas.py +297 -0
- backend/plugins.py +222 -0
- backend/server.py +411 -0
- backend/tasks.py +213 -0
- batch_api.py +307 -0
- checkpoint/__init__.py +27 -0
- checkpoint/hooks.py +90 -0
- checkpoint/store.py +314 -0
- checkpoint/types.py +80 -0
- claude_code_watcher.py +214 -0
- clipboard_utils.py +246 -0
- cloudsave.py +159 -0
- common.py +177 -0
- compaction.py +378 -0
- config.py +180 -0
- context.py +241 -0
- dulus-0.2.0.dist-info/METADATA +600 -0
- dulus-0.2.0.dist-info/RECORD +101 -0
- dulus-0.2.0.dist-info/WHEEL +5 -0
- dulus-0.2.0.dist-info/entry_points.txt +2 -0
- dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
- dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
- dulus-0.2.0.dist-info/top_level.txt +36 -0
- dulus.py +8455 -0
- dulus_gui.py +331 -0
- dulus_mcp/__init__.py +43 -0
- dulus_mcp/client.py +546 -0
- dulus_mcp/config.py +133 -0
- dulus_mcp/tools.py +131 -0
- dulus_mcp/types.py +124 -0
- gui/__init__.py +18 -0
- gui/agent_bridge.py +283 -0
- gui/chat_widget.py +448 -0
- gui/main_window.py +485 -0
- gui/personas.py +230 -0
- gui/session_utils.py +189 -0
- gui/settings_dialog.py +146 -0
- gui/sidebar.py +515 -0
- gui/tasks_view.py +499 -0
- gui/themes.py +256 -0
- gui/tool_panel.py +94 -0
- input.py +1030 -0
- license_manager.py +187 -0
- memory/__init__.py +93 -0
- memory/audit.py +51 -0
- memory/consolidator.py +312 -0
- memory/context.py +270 -0
- memory/offload.py +148 -0
- memory/palace.py +127 -0
- memory/scan.py +146 -0
- memory/sessions.py +100 -0
- memory/store.py +395 -0
- memory/tools.py +408 -0
- memory/types.py +114 -0
- memory/vector_search.py +92 -0
- multi_agent/__init__.py +23 -0
- multi_agent/subagent.py +501 -0
- multi_agent/tools.py +393 -0
- offload_helper.py +183 -0
- plugin/__init__.py +22 -0
- plugin/autoadapter.py +1641 -0
- plugin/loader.py +156 -0
- plugin/recommend.py +211 -0
- plugin/store.py +387 -0
- plugin/types.py +147 -0
- providers.py +3750 -0
- skill/__init__.py +14 -0
- skill/builtin.py +100 -0
- skill/clawhub.py +270 -0
- skill/executor.py +66 -0
- skill/loader.py +199 -0
- skill/tools.py +110 -0
- skills.py +14 -0
- spinner.py +42 -0
- string_utils.py +42 -0
- subagent.py +11 -0
- task/__init__.py +12 -0
- task/store.py +199 -0
- task/tools.py +265 -0
- task/types.py +92 -0
- tmux_offloader.py +177 -0
- tmux_tools.py +410 -0
- tool_registry.py +214 -0
- tools.py +2694 -0
- ui/__init__.py +1 -0
- ui/input.py +464 -0
- ui/render.py +272 -0
- voice/__init__.py +56 -0
- voice/keyterms.py +179 -0
- voice/recorder.py +263 -0
- voice/stt.py +408 -0
- voice/tts.py +570 -0
- webchat.py +432 -0
- 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
|
+
|