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
webchat.py ADDED
@@ -0,0 +1,432 @@
1
+ """Dulus WebChat — standalone or in-process mirror of the terminal agent.
2
+
3
+ When launched via /webchat from backend.py, the in-process server in
4
+ webchat_server.py is used instead. This file remains usable as a
5
+ standalone fallback.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import queue
12
+ import threading
13
+ import time
14
+ import uuid
15
+ import webbrowser
16
+ from typing import Generator
17
+
18
+ from flask import Flask, request, jsonify, Response, stream_with_context
19
+
20
+ from agent import (
21
+ run as agent_run,
22
+ AgentState,
23
+ TextChunk,
24
+ ThinkingChunk,
25
+ ToolStart,
26
+ ToolEnd,
27
+ TurnDone,
28
+ PermissionRequest,
29
+ )
30
+ from context import build_system_prompt
31
+ from common import sanitize_text
32
+ from config import load_config
33
+
34
+ # Ensure tools are registered
35
+ import tools as _tools_init
36
+ import memory.tools as _mem_tools_init
37
+ import multi_agent.tools as _ma_tools_init
38
+ import skill.tools as _sk_tools_init
39
+ import dulus_mcp.tools as _mcp_tools_init
40
+ import task.tools as _task_tools_init
41
+
42
+ try:
43
+ import tmux_tools as _tmux_tools_init
44
+ except Exception:
45
+ pass
46
+
47
+ try:
48
+ from plugin.loader import register_plugin_tools
49
+ register_plugin_tools()
50
+ except Exception:
51
+ pass
52
+
53
+ # ── shared state for standalone mode ───────────────────────────────────────
54
+ HISTORY_LOCK = threading.Lock()
55
+ CONFIG = load_config()
56
+ STATE = AgentState()
57
+ _PENDING_PERMISSIONS: dict[str, tuple[PermissionRequest, threading.Event]] = {}
58
+
59
+
60
+ def _run_agent_standalone(user_message: str) -> Generator:
61
+ """Run agent loop with local state/config, yielding all events."""
62
+ cfg = CONFIG
63
+ state = STATE
64
+ user_input = sanitize_text(user_message)
65
+
66
+ # Skill inject
67
+ _skill_body = cfg.pop("_skill_inject", "")
68
+ if _skill_body:
69
+ user_input = (
70
+ "[SKILL CONTEXT — follow these instructions for this turn]\n\n"
71
+ + _skill_body
72
+ + "\n\n---\n\n[USER MESSAGE]\n"
73
+ + user_input
74
+ )
75
+
76
+ # MemPalace
77
+ if cfg.get("mem_palace", True) and user_input and len(user_input.strip()) >= 12:
78
+ _trivial = {
79
+ "hola", "klk", "gracias", "ok", "si", "no", "dale",
80
+ "exit", "quit", "help", "thanks", "bien",
81
+ }
82
+ _first = user_input.strip().lower().split()[0]
83
+ if _first not in _trivial:
84
+ try:
85
+ from memory import find_relevant_memories
86
+
87
+ _q = user_input.strip()[:200]
88
+ _raw_hits = find_relevant_memories(_q, max_results=3)
89
+ if _raw_hits:
90
+ _parts = []
91
+ for _i, _h in enumerate(_raw_hits, 1):
92
+ _name = _h.get("name", f"hit_{_i}")
93
+ _desc = _h.get("description", "")
94
+ _body = _h.get("content", "").strip()
95
+ _snip = _body[:300] + ("..." if len(_body) > 300 else "")
96
+ if _desc:
97
+ _parts.append(f"### {_name}\n_{_desc}_\n{_snip}")
98
+ else:
99
+ _parts.append(f"### {_name}\n{_snip}")
100
+ _hits_str = "\n\n".join(_parts)
101
+ if len(_hits_str) > 2000:
102
+ _hits_str = _hits_str[:2000] + "\n[...truncated]"
103
+ _inject = (
104
+ "[MemPalace — relevant memories pre-loaded for this turn. "
105
+ "Do NOT re-query unless the user explicitly asks for more.]\n\n"
106
+ + _hits_str
107
+ )
108
+ user_input = (
109
+ _inject + "\n\n---\n\n[USER MESSAGE]\n" + user_input
110
+ )
111
+ except Exception:
112
+ pass
113
+
114
+ system_prompt = build_system_prompt(cfg)
115
+ cfg["_last_interaction_time"] = time.time()
116
+
117
+ yield from agent_run(user_input, state, cfg, system_prompt)
118
+
119
+
120
+ def create_app() -> Flask:
121
+ app = Flask(__name__)
122
+
123
+ PAGE = """<!doctype html>
124
+ <html lang="es"><head><meta charset="utf-8"><title>Dulus WebChat</title>
125
+ <style>
126
+ *{box-sizing:border-box;margin:0;padding:0}
127
+ body{background:#0a0a0c;color:#e6e6e6;font:14px/1.5 Consolas,monospace;height:100vh;display:flex;flex-direction:column}
128
+ header{padding:10px 18px;background:#111;border-bottom:1px solid #222;display:flex;justify-content:space-between;align-items:center}
129
+ header h1{font-size:14px;color:#00ffa3;font-weight:600}
130
+ header .model{font-size:11px;color:#666}
131
+ header button{background:#222;color:#888;border:1px solid #333;padding:4px 10px;border-radius:3px;cursor:pointer;font:inherit}
132
+ header button:hover{color:#fff}
133
+ #log{flex:1;overflow-y:auto;padding:18px;display:flex;flex-direction:column;gap:14px}
134
+ .msg{max-width:780px;padding:10px 14px;border-radius:6px;white-space:pre-wrap;word-wrap:break-word}
135
+ .user{align-self:flex-end;background:#1a3a2a;border:1px solid #2a5a40}
136
+ .assistant{align-self:flex-start;background:#15151a;border:1px solid #2a2a30}
137
+ .meta{font-size:10px;color:#555;margin-top:4px}
138
+ .err{color:#ff6b6b}
139
+ form{display:flex;gap:8px;padding:12px 18px;background:#111;border-top:1px solid #222}
140
+ textarea{flex:1;background:#000;color:#e6e6e6;border:1px solid #333;padding:10px;border-radius:4px;font:inherit;resize:none;height:60px;outline:none}
141
+ textarea:focus{border-color:#00ffa3}
142
+ button.send{background:#00ffa3;color:#000;border:none;padding:0 22px;border-radius:4px;font-weight:600;cursor:pointer}
143
+ button.send:disabled{opacity:.4;cursor:wait}
144
+ .think{font-size:10px;color:#888;margin-top:6px;padding:6px;border-left:2px solid #444;background:#0d0d10;white-space:pre-wrap}
145
+ .tool{font-size:11px;color:#aaa;margin-top:6px;padding:8px;border-left:2px solid #00ffa3;background:#0d1a14;white-space:pre-wrap}
146
+ .tool-result{font-size:11px;color:#ccc;margin-top:4px;padding:6px;border-left:2px solid #444;background:#111;white-space:pre-wrap;max-height:200px;overflow-y:auto}
147
+ .perm{font-size:12px;color:#ffcc00;margin-top:6px;padding:8px;border:1px solid #443300;background:#1a1500;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
148
+ .perm button{background:#333;color:#fff;border:1px solid #555;padding:4px 12px;border-radius:3px;cursor:pointer}
149
+ .perm button.approve{background:#00ffa3;color:#000;border-color:#00ffa3}
150
+ </style></head><body>
151
+ <header><h1>DULUS WEBCHAT</h1><span class="model" id="modelTag">…</span><button onclick="clearChat()">clear</button></header>
152
+ <div id="log"></div>
153
+ <form id="f" onsubmit="return send(event)">
154
+ <textarea id="inp" placeholder="Mensaje a Dulus... (Enter envía, Shift+Enter nueva línea)" autofocus></textarea>
155
+ <button class="send" id="sendBtn">SEND</button>
156
+ </form>
157
+ <script>
158
+ const log=document.getElementById('log'),inp=document.getElementById('inp'),btn=document.getElementById('sendBtn'),modelTag=document.getElementById('modelTag');
159
+
160
+ function add(role,text,extra){
161
+ const d=document.createElement('div');d.className='msg '+role;
162
+ if(typeof text==='string') d.textContent=text; else d.appendChild(text);
163
+ if(extra){const m=document.createElement('div');m.className='meta';m.textContent=extra;d.appendChild(m);}
164
+ log.appendChild(d);log.scrollTop=log.scrollHeight;return d;
165
+ }
166
+
167
+ let currentAssistant=null, currentText='';
168
+
169
+ function ensureAssistant(){
170
+ if(!currentAssistant){currentAssistant=add('assistant','');}
171
+ return currentAssistant;
172
+ }
173
+
174
+ function appendText(text){
175
+ ensureAssistant();
176
+ currentText+=text;
177
+ currentAssistant.textContent=currentText;
178
+ log.scrollTop=log.scrollHeight;
179
+ }
180
+
181
+ function appendThinking(text){
182
+ ensureAssistant();
183
+ let th=currentAssistant.querySelector('.think');
184
+ if(!th){th=document.createElement('div');th.className='think';th.textContent='[thinking]\n';currentAssistant.appendChild(th);}
185
+ th.textContent+=text;
186
+ log.scrollTop=log.scrollHeight;
187
+ }
188
+
189
+ function startTool(name,inputs){
190
+ ensureAssistant();
191
+ const t=document.createElement('div');t.className='tool';
192
+ t.textContent='🔧 '+name+'\n'+JSON.stringify(inputs,null,2);
193
+ currentAssistant.appendChild(t);
194
+ log.scrollTop=log.scrollHeight;
195
+ }
196
+
197
+ function endTool(name,result,permitted){
198
+ ensureAssistant();
199
+ const r=document.createElement('div');r.className='tool-result';
200
+ r.textContent=(permitted?'✅':'❌')+' '+result;
201
+ currentAssistant.appendChild(r);
202
+ log.scrollTop=log.scrollHeight;
203
+ }
204
+
205
+ function showPermission(id,desc){
206
+ ensureAssistant();
207
+ const p=document.createElement('div');p.className='perm';
208
+ p.innerHTML='<span>⛔ '+desc+'</span>';
209
+ const yes=document.createElement('button');yes.textContent='Approve';yes.className='approve';
210
+ yes.onclick=()=>{sendPermission(id,true);p.remove();};
211
+ const no=document.createElement('button');no.textContent='Deny';
212
+ no.onclick=()=>{sendPermission(id,false);p.remove();};
213
+ p.appendChild(yes);p.appendChild(no);
214
+ currentAssistant.appendChild(p);
215
+ log.scrollTop=log.scrollHeight;
216
+ }
217
+
218
+ async function sendPermission(id,granted){
219
+ await fetch('/permission',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,granted})});
220
+ }
221
+
222
+ async function send(e){
223
+ if(e)e.preventDefault();
224
+ const t=inp.value.trim();if(!t)return false;
225
+ add('user',t);inp.value='';btn.disabled=true;
226
+ currentAssistant=null;currentText='';
227
+ try{
228
+ const resp=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:t})});
229
+ const reader=resp.body.getReader();
230
+ const decoder=new TextDecoder();
231
+ let buf='';
232
+ while(true){
233
+ const {done,value}=await reader.read();
234
+ if(done) break;
235
+ buf+=decoder.decode(value,{stream:true});
236
+ const lines=buf.split('\n');
237
+ buf=lines.pop();
238
+ for(const line of lines){
239
+ if(!line.startsWith('data: ')) continue;
240
+ let d;
241
+ try{d=JSON.parse(line.slice(6));}catch(_){continue;}
242
+ if(d.type==='text') appendText(d.text);
243
+ else if(d.type==='thinking') appendThinking(d.text);
244
+ else if(d.type==='tool_start') startTool(d.name,d.inputs);
245
+ else if(d.type==='tool_end') endTool(d.name,d.result,d.permitted);
246
+ else if(d.type==='permission') showPermission(d.id,d.description);
247
+ else if(d.type==='turn_done'){
248
+ const meta=document.createElement('div');meta.className='meta';
249
+ meta.textContent='in:'+d.in+' out:'+d.out;
250
+ ensureAssistant().appendChild(meta);
251
+ }
252
+ else if(d.type==='error') appendText('\n[error] '+d.message);
253
+ else if(d.type==='done'){}
254
+ }
255
+ }
256
+ }catch(err){
257
+ add('assistant','[network] '+err,'').classList.add('err');
258
+ }finally{
259
+ btn.disabled=false;inp.focus();
260
+ }
261
+ return false;
262
+ }
263
+
264
+ async function clearChat(){
265
+ await fetch('/clear',{method:'POST'});
266
+ log.innerHTML='';currentAssistant=null;currentText='';
267
+ }
268
+
269
+ async function syncChat(){
270
+ if(btn.disabled) return;
271
+ const r=await fetch('/state');const j=await r.json();
272
+ modelTag.textContent=j.model;
273
+ const currentMsgs = log.querySelectorAll('.msg').length;
274
+ if(j.history.length !== currentMsgs){
275
+ const wasNearBottom = log.scrollTop + log.clientHeight >= log.scrollHeight - 50;
276
+ log.innerHTML='';
277
+ currentAssistant=null;
278
+ currentText='';
279
+ for(const m of j.history){
280
+ if(m.role==='user') add('user',m.content||'');
281
+ else if(m.role==='assistant'){
282
+ const d=add('assistant',m.content||'');
283
+ if(m.thinking){const th=document.createElement('div');th.className='think';th.textContent='[thinking]\n'+m.thinking;d.appendChild(th);}
284
+ }
285
+ else if(m.role==='tool'){
286
+ const d=add('assistant','');
287
+ const t=document.createElement('div');t.className='tool-result';t.textContent='🔧 tool result:\n'+(m.content||'');d.appendChild(t);
288
+ }
289
+ }
290
+ if(wasNearBottom) log.scrollTop=log.scrollHeight;
291
+ }
292
+ }
293
+
294
+ async function loadHist(){ return syncChat(); }
295
+
296
+ inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
297
+ loadHist();
298
+ setInterval(syncChat, 5000);
299
+ </script>
300
+ </body></html>"""
301
+
302
+ @app.route("/")
303
+ def home() -> Response:
304
+ return Response(PAGE, mimetype="text/html")
305
+
306
+ @app.route("/state")
307
+ def state_endpoint() -> Response:
308
+ with HISTORY_LOCK:
309
+ hist = [dict(m) for m in STATE.messages]
310
+ model = CONFIG.get("model", "?")
311
+ return jsonify(model=model, history=hist)
312
+
313
+ @app.route("/clear", methods=["POST"])
314
+ def clear() -> Response:
315
+ with HISTORY_LOCK:
316
+ STATE.messages.clear()
317
+ return jsonify(ok=True)
318
+
319
+ @app.route("/permission", methods=["POST"])
320
+ def permission() -> Response:
321
+ body = request.get_json(silent=True) or {}
322
+ pid = body.get("id")
323
+ granted = body.get("granted", False)
324
+ with HISTORY_LOCK:
325
+ item = _PENDING_PERMISSIONS.get(pid)
326
+ if item is None:
327
+ return jsonify(error="not found"), 404
328
+ req, evt = item
329
+ req.granted = bool(granted)
330
+ evt.set()
331
+ return jsonify(ok=True)
332
+
333
+ @app.route("/chat", methods=["POST"])
334
+ def chat() -> Response:
335
+ body = request.get_json(silent=True) or {}
336
+ msg = (body.get("message") or "").strip()
337
+ if not msg:
338
+ return jsonify(error="empty message"), 400
339
+
340
+ def generate():
341
+ q: queue.Queue = queue.Queue(maxsize=512)
342
+ exc_holder = [None]
343
+
344
+ def producer():
345
+ try:
346
+ for ev in _run_agent_standalone(msg):
347
+ q.put(ev)
348
+ except Exception as e:
349
+ exc_holder[0] = e
350
+ finally:
351
+ q.put(None)
352
+
353
+ t = threading.Thread(target=producer, daemon=True)
354
+ t.start()
355
+
356
+ yield 'data: {"type":"start"}\n\n'
357
+
358
+ while True:
359
+ item = q.get()
360
+ if item is None:
361
+ break
362
+
363
+ payload = None
364
+ if isinstance(item, TextChunk):
365
+ payload = {"type": "text", "text": item.text}
366
+ elif isinstance(item, ThinkingChunk):
367
+ payload = {"type": "thinking", "text": item.text}
368
+ elif isinstance(item, ToolStart):
369
+ payload = {"type": "tool_start", "name": item.name, "inputs": item.inputs}
370
+ elif isinstance(item, ToolEnd):
371
+ payload = {
372
+ "type": "tool_end",
373
+ "name": item.name,
374
+ "result": item.result,
375
+ "permitted": item.permitted,
376
+ }
377
+ elif isinstance(item, TurnDone):
378
+ payload = {
379
+ "type": "turn_done",
380
+ "in": item.input_tokens,
381
+ "out": item.output_tokens,
382
+ }
383
+ elif isinstance(item, PermissionRequest):
384
+ pid = str(uuid.uuid4())
385
+ evt = threading.Event()
386
+ _PENDING_PERMISSIONS[pid] = (item, evt)
387
+ payload = {
388
+ "type": "permission",
389
+ "id": pid,
390
+ "description": item.description,
391
+ }
392
+ yield f"data: {json.dumps(payload)}\n\n"
393
+ evt.wait(timeout=300)
394
+ _PENDING_PERMISSIONS.pop(pid, None)
395
+ continue
396
+ else:
397
+ continue
398
+
399
+ yield f"data: {json.dumps(payload)}\n\n"
400
+
401
+ if exc_holder[0]:
402
+ err = exc_holder[0]
403
+ yield f'data: {json.dumps({"type":"error","message":f"{type(err).__name__}: {err}"})}\n\n'
404
+
405
+ yield 'data: {"type":"done"}\n\n'
406
+
407
+ return Response(
408
+ stream_with_context(generate()),
409
+ mimetype="text/event-stream",
410
+ )
411
+
412
+ return app
413
+
414
+
415
+ def main():
416
+ ap = argparse.ArgumentParser()
417
+ ap.add_argument("--port", type=int, default=5000)
418
+ ap.add_argument("--host", default="0.0.0.0")
419
+ ap.add_argument("--model", default="", help="Override model from config.json")
420
+ ap.add_argument("--open", action="store_true", help="open browser on start")
421
+ args = ap.parse_args()
422
+ if args.model:
423
+ CONFIG["model"] = args.model
424
+ app = create_app()
425
+ if args.open:
426
+ threading.Timer(1.0, lambda: webbrowser.open(f"http://{args.host}:{args.port}/")).start()
427
+ print(f"[webchat] model={CONFIG.get('model')} -> http://{args.host}:{args.port}/")
428
+ app.run(host=args.host, port=args.port, debug=False, use_reloader=False, threaded=True)
429
+
430
+
431
+ if __name__ == "__main__":
432
+ main()