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
backend/server.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Zero-dependency HTTP server for Dulus Dashboard + API + SSE Live Updates."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import queue
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import parse_qs, urlparse
|
|
10
|
+
|
|
11
|
+
from backend.context import build_context, build_smart_context, get_compact_context
|
|
12
|
+
from backend.personas import create_persona, get_active_persona, get_all_personas, get_persona, load_personas, set_active_persona, update_persona
|
|
13
|
+
from backend.plugins import load_all_plugins, get_plugin_info, start_watcher, stop_watcher, watcher_status, reload_plugin, unload_plugin
|
|
14
|
+
from backend.tasks import create_task, load_tasks, update_task
|
|
15
|
+
|
|
16
|
+
DASHBOARD_DIR = Path(__file__).parent.parent / "docs" / "dashboard"
|
|
17
|
+
|
|
18
|
+
# ─────────── SSE Broadcast System ───────────
|
|
19
|
+
_sse_clients: list[queue.Queue] = []
|
|
20
|
+
_sse_lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _add_sse_client(q: queue.Queue):
|
|
24
|
+
with _sse_lock:
|
|
25
|
+
_sse_clients.append(q)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _remove_sse_client(q: queue.Queue):
|
|
29
|
+
with _sse_lock:
|
|
30
|
+
if q in _sse_clients:
|
|
31
|
+
_sse_clients.remove(q)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def broadcast_event(event_type: str, payload: dict):
|
|
35
|
+
"""Broadcast JSON event to all connected SSE clients."""
|
|
36
|
+
data = json.dumps({"type": event_type, "data": payload, "ts": time.time()})
|
|
37
|
+
msg = f"event: {event_type}\ndata: {data}\n\n"
|
|
38
|
+
with _sse_lock:
|
|
39
|
+
dead = []
|
|
40
|
+
for q in _sse_clients:
|
|
41
|
+
try:
|
|
42
|
+
q.put_nowait(msg)
|
|
43
|
+
except queue.Full:
|
|
44
|
+
dead.append(q)
|
|
45
|
+
for q in dead:
|
|
46
|
+
_sse_clients.remove(q)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sse_heartbeat():
|
|
50
|
+
"""Send periodic ping to keep connections alive."""
|
|
51
|
+
while True:
|
|
52
|
+
time.sleep(15)
|
|
53
|
+
broadcast_event("ping", {"status": "ok"})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
threading.Thread(target=_sse_heartbeat, daemon=True, name="sse-heartbeat").start()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DulusHandler(SimpleHTTPRequestHandler):
|
|
60
|
+
def log_message(self, fmt, *args):
|
|
61
|
+
# Suppress default logging
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _safe_handle(self, handler_fn):
|
|
65
|
+
"""Wrap request handlers so unhandled exceptions return 500 instead of killing the server thread."""
|
|
66
|
+
try:
|
|
67
|
+
handler_fn()
|
|
68
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
69
|
+
pass
|
|
70
|
+
except Exception as e:
|
|
71
|
+
try:
|
|
72
|
+
self._error(f"Internal server error: {e}", 500)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _json_response(self, data, status=200):
|
|
77
|
+
self.send_response(status)
|
|
78
|
+
self.send_header("Content-Type", "application/json")
|
|
79
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
80
|
+
self.end_headers()
|
|
81
|
+
self.wfile.write(json.dumps(data).encode("utf-8"))
|
|
82
|
+
|
|
83
|
+
def _text_response(self, text, status=200, content_type="text/plain; charset=utf-8"):
|
|
84
|
+
self.send_response(status)
|
|
85
|
+
self.send_header("Content-Type", content_type)
|
|
86
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
87
|
+
self.end_headers()
|
|
88
|
+
self.wfile.write(text.encode("utf-8"))
|
|
89
|
+
|
|
90
|
+
def _error(self, msg, status=400):
|
|
91
|
+
self._json_response({"error": msg}, status)
|
|
92
|
+
|
|
93
|
+
def _parse_query(self):
|
|
94
|
+
return parse_qs(urlparse(self.path).query)
|
|
95
|
+
|
|
96
|
+
def _sse_stream(self, client_q: queue.Queue):
|
|
97
|
+
"""Send SSE headers and stream from queue until client disconnects."""
|
|
98
|
+
self.send_response(200)
|
|
99
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
100
|
+
self.send_header("Cache-Control", "no-cache")
|
|
101
|
+
self.send_header("Connection", "keep-alive")
|
|
102
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
103
|
+
self.end_headers()
|
|
104
|
+
|
|
105
|
+
self.wfile.write(f"event: connected\ndata: {json.dumps({'message':'Dulus SSE active'})}\n\n".encode("utf-8"))
|
|
106
|
+
self.wfile.flush()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
while True:
|
|
110
|
+
try:
|
|
111
|
+
msg = client_q.get(timeout=30)
|
|
112
|
+
self.wfile.write(msg.encode("utf-8"))
|
|
113
|
+
self.wfile.flush()
|
|
114
|
+
except queue.Empty:
|
|
115
|
+
self.wfile.write(b":\n\n")
|
|
116
|
+
self.wfile.flush()
|
|
117
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
118
|
+
pass
|
|
119
|
+
finally:
|
|
120
|
+
_remove_sse_client(client_q)
|
|
121
|
+
|
|
122
|
+
def _do_GET(self):
|
|
123
|
+
parsed = urlparse(self.path)
|
|
124
|
+
path = parsed.path
|
|
125
|
+
query = parse_qs(parsed.query)
|
|
126
|
+
|
|
127
|
+
# ── SSE Live Events ──
|
|
128
|
+
if path == "/api/events":
|
|
129
|
+
q = queue.Queue(maxsize=100)
|
|
130
|
+
_add_sse_client(q)
|
|
131
|
+
self._sse_stream(q)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# ── Health ──
|
|
135
|
+
if path == "/api/health":
|
|
136
|
+
self._json_response({
|
|
137
|
+
"status": "ok",
|
|
138
|
+
"agent": "Dulus",
|
|
139
|
+
"mode": "proactive",
|
|
140
|
+
"version": "2026.04.26"
|
|
141
|
+
})
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# ── Tasks ──
|
|
145
|
+
if path == "/api/tasks":
|
|
146
|
+
self._json_response(load_tasks())
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# ── Context ──
|
|
150
|
+
if path == "/api/context":
|
|
151
|
+
self._json_response(build_context())
|
|
152
|
+
return
|
|
153
|
+
if path == "/api/context/compact":
|
|
154
|
+
self._text_response(get_compact_context())
|
|
155
|
+
return
|
|
156
|
+
if path == "/api/smart-context":
|
|
157
|
+
self._json_response(build_smart_context())
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# ── Agents ──
|
|
161
|
+
if path == "/api/agents":
|
|
162
|
+
ctx = build_context()
|
|
163
|
+
self._json_response(ctx.get("agents", []))
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# ── Personas ──
|
|
167
|
+
if path == "/api/personas":
|
|
168
|
+
try:
|
|
169
|
+
self._json_response({
|
|
170
|
+
"personas": get_all_personas(),
|
|
171
|
+
"active": get_active_persona(),
|
|
172
|
+
})
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self._error(f"Personas error: {e}", 500)
|
|
175
|
+
return
|
|
176
|
+
if path == "/api/personas/active":
|
|
177
|
+
self._json_response(get_active_persona())
|
|
178
|
+
return
|
|
179
|
+
if path.startswith("/api/personas/") and len(path.split("/")) == 4:
|
|
180
|
+
pid = path.split("/")[-1]
|
|
181
|
+
try:
|
|
182
|
+
p = get_persona(pid)
|
|
183
|
+
if p:
|
|
184
|
+
self._json_response(p)
|
|
185
|
+
else:
|
|
186
|
+
self._error("Persona not found", 404)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
self._error(f"Personas error: {e}", 500)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# ── MemPalace ──
|
|
192
|
+
if path == "/api/mempalace":
|
|
193
|
+
try:
|
|
194
|
+
from backend.mempalace_bridge import load_cache, get_mempalace_compact_text
|
|
195
|
+
data = load_cache()
|
|
196
|
+
data["compact_text"] = get_mempalace_compact_text()
|
|
197
|
+
self._json_response(data)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self._error(f"MemPalace error: {e}", 500)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# ── Themes ──
|
|
203
|
+
if path == "/api/themes":
|
|
204
|
+
try:
|
|
205
|
+
from gui.theme_pack import list_themes
|
|
206
|
+
self._json_response({"themes": list_themes()})
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self._error(f"Theme pack unavailable: {e}", 500)
|
|
209
|
+
return
|
|
210
|
+
if path.startswith("/api/themes/") and path.endswith("/css"):
|
|
211
|
+
theme_name = path.split("/")[-2]
|
|
212
|
+
try:
|
|
213
|
+
from gui.theme_pack import generate_css_variables
|
|
214
|
+
css = generate_css_variables(theme_name)
|
|
215
|
+
self._text_response(css, content_type="text/css; charset=utf-8")
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self._error(f"Theme error: {e}", 500)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# ── Plugins ──
|
|
221
|
+
if path == "/api/plugins":
|
|
222
|
+
try:
|
|
223
|
+
load_all_plugins()
|
|
224
|
+
self._json_response({"plugins": get_plugin_info()})
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self._error(f"Plugin error: {e}", 500)
|
|
227
|
+
return
|
|
228
|
+
if path == "/api/plugins/status":
|
|
229
|
+
try:
|
|
230
|
+
self._json_response(watcher_status())
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self._error(f"Plugin status error: {e}", 500)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# ── Marketplace ──
|
|
236
|
+
if path == "/api/marketplace":
|
|
237
|
+
try:
|
|
238
|
+
from backend.marketplace import load_registry, search_plugins
|
|
239
|
+
q = query.get("q", [""])[0]
|
|
240
|
+
tag = query.get("tag", [""])[0]
|
|
241
|
+
self._json_response({"plugins": search_plugins(q, tag)})
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self._error(f"Marketplace error: {e}", 500)
|
|
244
|
+
return
|
|
245
|
+
if path == "/api/marketplace/stats":
|
|
246
|
+
try:
|
|
247
|
+
from backend.marketplace import get_stats
|
|
248
|
+
self._json_response(get_stats())
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self._error(f"Marketplace error: {e}", 500)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# ── Static files from dashboard ──
|
|
254
|
+
if path == "/" or path == "/index.html":
|
|
255
|
+
target = DASHBOARD_DIR / "index.html"
|
|
256
|
+
else:
|
|
257
|
+
target = DASHBOARD_DIR / path.lstrip("/")
|
|
258
|
+
|
|
259
|
+
if target.exists() and target.is_file():
|
|
260
|
+
self.send_response(200)
|
|
261
|
+
ctype = "text/html"
|
|
262
|
+
if path.endswith(".css"):
|
|
263
|
+
ctype = "text/css"
|
|
264
|
+
elif path.endswith(".js"):
|
|
265
|
+
ctype = "application/javascript"
|
|
266
|
+
elif path.endswith(".json"):
|
|
267
|
+
ctype = "application/json"
|
|
268
|
+
self.send_header("Content-Type", ctype)
|
|
269
|
+
self.end_headers()
|
|
270
|
+
with open(target, "rb") as f:
|
|
271
|
+
self.wfile.write(f.read())
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
self.send_error(404)
|
|
275
|
+
|
|
276
|
+
def _do_POST(self):
|
|
277
|
+
parsed = urlparse(self.path)
|
|
278
|
+
path = parsed.path
|
|
279
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
280
|
+
body = self.rfile.read(content_len).decode("utf-8")
|
|
281
|
+
try:
|
|
282
|
+
data = json.loads(body) if body else {}
|
|
283
|
+
except Exception:
|
|
284
|
+
return self._error("Invalid JSON")
|
|
285
|
+
|
|
286
|
+
# ── Tasks ──
|
|
287
|
+
task = create_task(data)
|
|
288
|
+
broadcast_event("task_created", task)
|
|
289
|
+
return self._json_response(task, 201)
|
|
290
|
+
if path.startswith("/api/tasks/"):
|
|
291
|
+
tid = path.split("/")[-1]
|
|
292
|
+
task = update_task(tid, data)
|
|
293
|
+
if task:
|
|
294
|
+
broadcast_event("task_updated", task)
|
|
295
|
+
return self._json_response(task)
|
|
296
|
+
return self._error("Task not found", 404)
|
|
297
|
+
|
|
298
|
+
# ── Marketplace Install / Uninstall ──
|
|
299
|
+
if path == "/api/marketplace/install":
|
|
300
|
+
plugin_id = data.get("id")
|
|
301
|
+
if not plugin_id:
|
|
302
|
+
return self._error("Missing plugin id")
|
|
303
|
+
try:
|
|
304
|
+
from backend.marketplace import install_plugin
|
|
305
|
+
result = install_plugin(plugin_id)
|
|
306
|
+
if result:
|
|
307
|
+
broadcast_event("marketplace_install", result)
|
|
308
|
+
return self._json_response({"installed": True, "plugin": result})
|
|
309
|
+
return self._error("Plugin not found", 404)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
return self._error(str(e), 500)
|
|
312
|
+
if path == "/api/marketplace/uninstall":
|
|
313
|
+
plugin_id = data.get("id")
|
|
314
|
+
if not plugin_id:
|
|
315
|
+
return self._error("Missing plugin id")
|
|
316
|
+
try:
|
|
317
|
+
from backend.marketplace import uninstall_plugin
|
|
318
|
+
result = uninstall_plugin(plugin_id)
|
|
319
|
+
if result:
|
|
320
|
+
broadcast_event("marketplace_uninstall", result)
|
|
321
|
+
return self._json_response({"uninstalled": True, "plugin": result})
|
|
322
|
+
return self._error("Plugin not found", 404)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
return self._error(str(e), 500)
|
|
325
|
+
|
|
326
|
+
# ── Plugins ──
|
|
327
|
+
if path == "/api/plugins/reload":
|
|
328
|
+
name = data.get("name")
|
|
329
|
+
try:
|
|
330
|
+
if name:
|
|
331
|
+
from backend.plugins import PLUGINS_DIR
|
|
332
|
+
result = reload_plugin(PLUGINS_DIR / f"{name}.py")
|
|
333
|
+
clean_result = {"name": result.get("name", name), "version": result.get("version", "?"), "status": result.get("status", "?")}
|
|
334
|
+
broadcast_event("plugin_reloaded", clean_result)
|
|
335
|
+
return self._json_response(clean_result)
|
|
336
|
+
else:
|
|
337
|
+
load_all_plugins()
|
|
338
|
+
info = get_plugin_info()
|
|
339
|
+
broadcast_event("plugins_reloaded", {"count": len(info)})
|
|
340
|
+
return self._json_response({"plugins": info})
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return self._error(str(e), 500)
|
|
343
|
+
|
|
344
|
+
# ── Personas ──
|
|
345
|
+
if path == "/api/personas/activate":
|
|
346
|
+
pid = data.get("id")
|
|
347
|
+
if not pid:
|
|
348
|
+
return self._error("Missing persona id")
|
|
349
|
+
try:
|
|
350
|
+
result = set_active_persona(pid)
|
|
351
|
+
if result:
|
|
352
|
+
broadcast_event("persona_activated", result)
|
|
353
|
+
return self._json_response({"activated": True, "persona": result})
|
|
354
|
+
return self._error("Persona not found", 404)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
return self._error(str(e), 500)
|
|
357
|
+
if path == "/api/personas":
|
|
358
|
+
try:
|
|
359
|
+
result = create_persona(data)
|
|
360
|
+
broadcast_event("persona_created", result)
|
|
361
|
+
return self._json_response(result, 201)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
return self._error(str(e), 500)
|
|
364
|
+
|
|
365
|
+
self._error("Not found", 404)
|
|
366
|
+
|
|
367
|
+
def do_GET(self):
|
|
368
|
+
self._safe_handle(self._do_GET)
|
|
369
|
+
|
|
370
|
+
def do_POST(self):
|
|
371
|
+
self._safe_handle(self._do_POST)
|
|
372
|
+
|
|
373
|
+
def do_OPTIONS(self):
|
|
374
|
+
self.send_response(200)
|
|
375
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
376
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
377
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
378
|
+
self.end_headers()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def run_server(port: int = 8000):
|
|
382
|
+
# Start plugin hot-reload watcher with SSE broadcast
|
|
383
|
+
started = start_watcher(broadcast_event)
|
|
384
|
+
if started:
|
|
385
|
+
print("[DULUS] Plugin hot-reload watcher started")
|
|
386
|
+
server = HTTPServer(("", port), DulusHandler)
|
|
387
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
388
|
+
thread.start()
|
|
389
|
+
print(f"[DULUS] Server running at http://localhost:{port}")
|
|
390
|
+
print(f" Dashboard: http://localhost:{port}/")
|
|
391
|
+
print(f" API Tasks: http://localhost:{port}/api/tasks")
|
|
392
|
+
print(f" Context: http://localhost:{port}/api/context")
|
|
393
|
+
print(f" Smart Ctx: http://localhost:{port}/api/smart-context")
|
|
394
|
+
print(f" Agents: http://localhost:{port}/api/agents")
|
|
395
|
+
print(f" Personas: http://localhost:{port}/api/personas")
|
|
396
|
+
print(f" Themes: http://localhost:{port}/api/themes")
|
|
397
|
+
print(f" Plugins: http://localhost:{port}/api/plugins")
|
|
398
|
+
print(f" Marketplace: http://localhost:{port}/api/marketplace")
|
|
399
|
+
print(f" MemPalace: http://localhost:{port}/api/mempalace")
|
|
400
|
+
print(f" SSE Events: http://localhost:{port}/api/events")
|
|
401
|
+
print(" Press Ctrl+C to stop")
|
|
402
|
+
try:
|
|
403
|
+
thread.join()
|
|
404
|
+
except KeyboardInterrupt:
|
|
405
|
+
print("\n[DULUS] Shutting down...")
|
|
406
|
+
stop_watcher()
|
|
407
|
+
server.shutdown()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
if __name__ == "__main__":
|
|
411
|
+
run_server()
|
backend/tasks.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Task storage with JSON persistence."""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
|
8
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
9
|
+
TASKS_FILE = DATA_DIR / "tasks.json"
|
|
10
|
+
|
|
11
|
+
DEFAULT_TASKS = [
|
|
12
|
+
{
|
|
13
|
+
"id": "T-001",
|
|
14
|
+
"subject": "Setup Dulus Backend",
|
|
15
|
+
"status": "completed",
|
|
16
|
+
"owner": "Dulus",
|
|
17
|
+
"created_at": "2026-04-26",
|
|
18
|
+
"updated_at": "2026-04-26",
|
|
19
|
+
"metadata": {
|
|
20
|
+
"phase": "Infrastructure",
|
|
21
|
+
"priority": "high",
|
|
22
|
+
"blocked_by": [],
|
|
23
|
+
"tags": ["backend", "api", "server"],
|
|
24
|
+
"description": "Create Python backend to serve dashboard and manage tasks."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "T-002",
|
|
29
|
+
"subject": "Smart Context Manager (#23)",
|
|
30
|
+
"status": "completed",
|
|
31
|
+
"owner": "Dulus",
|
|
32
|
+
"created_at": "2026-04-26",
|
|
33
|
+
"updated_at": "2026-04-26",
|
|
34
|
+
"metadata": {
|
|
35
|
+
"phase": "Core",
|
|
36
|
+
"priority": "high",
|
|
37
|
+
"blocked_by": [],
|
|
38
|
+
"tags": ["context", "llm", "memory"],
|
|
39
|
+
"description": "Build intelligent context generator for multi-agent sessions."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "T-003",
|
|
44
|
+
"subject": "Plugin System",
|
|
45
|
+
"status": "completed",
|
|
46
|
+
"owner": "Dulus",
|
|
47
|
+
"created_at": "2026-04-26",
|
|
48
|
+
"updated_at": "2026-04-26",
|
|
49
|
+
"metadata": {
|
|
50
|
+
"phase": "Extensibility",
|
|
51
|
+
"priority": "medium",
|
|
52
|
+
"blocked_by": [],
|
|
53
|
+
"tags": ["plugins", "extensions"],
|
|
54
|
+
"description": "Hot-loadable plugin architecture for custom tools."
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "T-004",
|
|
59
|
+
"subject": "Command Center HTML Dashboard",
|
|
60
|
+
"status": "completed",
|
|
61
|
+
"owner": "kimi-code",
|
|
62
|
+
"created_at": "2026-04-26",
|
|
63
|
+
"updated_at": "2026-04-26",
|
|
64
|
+
"metadata": {
|
|
65
|
+
"phase": "UI",
|
|
66
|
+
"priority": "high",
|
|
67
|
+
"blocked_by": [],
|
|
68
|
+
"tags": ["ui", "dashboard", "html"],
|
|
69
|
+
"description": "Standalone premium HTML dashboard with 4 functional tabs."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "T-005",
|
|
74
|
+
"subject": "Theme Pack Premium",
|
|
75
|
+
"status": "completed",
|
|
76
|
+
"owner": "kimi-code",
|
|
77
|
+
"created_at": "2026-04-26",
|
|
78
|
+
"updated_at": "2026-04-26",
|
|
79
|
+
"metadata": {
|
|
80
|
+
"phase": "UI",
|
|
81
|
+
"priority": "medium",
|
|
82
|
+
"blocked_by": [],
|
|
83
|
+
"tags": ["ui", "themes", "customtkinter"],
|
|
84
|
+
"description": "4 premium themes mapped per agent for GUI integration."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "T-006",
|
|
89
|
+
"subject": "API Docs Generator",
|
|
90
|
+
"status": "completed",
|
|
91
|
+
"owner": "kimi-code3",
|
|
92
|
+
"created_at": "2026-04-26",
|
|
93
|
+
"updated_at": "2026-04-26",
|
|
94
|
+
"metadata": {
|
|
95
|
+
"phase": "Docs",
|
|
96
|
+
"priority": "medium",
|
|
97
|
+
"blocked_by": [],
|
|
98
|
+
"tags": ["docs", "api", "automation"],
|
|
99
|
+
"description": "Auto-scan 167 modules and generate docs/api.html with dependency graph."
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "T-007",
|
|
104
|
+
"subject": "MemPalace Integration",
|
|
105
|
+
"status": "completed",
|
|
106
|
+
"owner": "Dulus",
|
|
107
|
+
"created_at": "2026-04-26",
|
|
108
|
+
"updated_at": "2026-04-26",
|
|
109
|
+
"metadata": {
|
|
110
|
+
"phase": "Integration",
|
|
111
|
+
"priority": "high",
|
|
112
|
+
"blocked_by": [],
|
|
113
|
+
"tags": ["memory", "mempalace", "persistence"],
|
|
114
|
+
"description": "Wire Smart Context into MemPalace for infinite agent memory."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "T-008",
|
|
119
|
+
"subject": "Hybrid Compressor (qwen + rule-based)",
|
|
120
|
+
"status": "completed",
|
|
121
|
+
"owner": "kimi-code2",
|
|
122
|
+
"created_at": "2026-04-26",
|
|
123
|
+
"updated_at": "2026-04-26",
|
|
124
|
+
"metadata": {
|
|
125
|
+
"phase": "Core",
|
|
126
|
+
"priority": "high",
|
|
127
|
+
"blocked_by": [],
|
|
128
|
+
"tags": ["compression", "ollama", "qwen", "context"],
|
|
129
|
+
"description": "Context compressor with local LLM fallback and rule-based engine."
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "T-009",
|
|
134
|
+
"subject": "Test Coverage Expansion",
|
|
135
|
+
"status": "in_progress",
|
|
136
|
+
"owner": "kimi-code",
|
|
137
|
+
"created_at": "2026-04-26",
|
|
138
|
+
"updated_at": "2026-04-26",
|
|
139
|
+
"metadata": {
|
|
140
|
+
"phase": "Quality",
|
|
141
|
+
"priority": "medium",
|
|
142
|
+
"blocked_by": [],
|
|
143
|
+
"tags": ["pytest", "coverage", "testing"],
|
|
144
|
+
"description": "Backfill tests for context, tasks, githook, and compressor modules."
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "T-010",
|
|
149
|
+
"subject": "Multi-Agent Mesa Redonda",
|
|
150
|
+
"status": "in_progress",
|
|
151
|
+
"owner": "Dulus",
|
|
152
|
+
"created_at": "2026-04-26",
|
|
153
|
+
"updated_at": "2026-04-26",
|
|
154
|
+
"metadata": {
|
|
155
|
+
"phase": "Core",
|
|
156
|
+
"priority": "high",
|
|
157
|
+
"blocked_by": [],
|
|
158
|
+
"tags": ["multi-agent", "collaboration", "orchestration"],
|
|
159
|
+
"description": "Round-table mode for parallel agent collaboration with proactive work loops."
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def load_tasks() -> list[dict[str, Any]]:
|
|
166
|
+
if TASKS_FILE.exists():
|
|
167
|
+
try:
|
|
168
|
+
with open(TASKS_FILE, "r", encoding="utf-8") as f:
|
|
169
|
+
return json.load(f)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
save_tasks(DEFAULT_TASKS)
|
|
173
|
+
return DEFAULT_TASKS.copy()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def save_tasks(tasks: list[dict[str, Any]]) -> None:
|
|
177
|
+
with open(TASKS_FILE, "w", encoding="utf-8") as f:
|
|
178
|
+
json.dump(tasks, f, indent=2, ensure_ascii=False)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_task(tid: str) -> dict[str, Any] | None:
|
|
182
|
+
for t in load_tasks():
|
|
183
|
+
if t["id"] == tid:
|
|
184
|
+
return t
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def update_task(tid: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
189
|
+
tasks = load_tasks()
|
|
190
|
+
for i, t in enumerate(tasks):
|
|
191
|
+
if t["id"] == tid:
|
|
192
|
+
tasks[i].update(data)
|
|
193
|
+
tasks[i]["updated_at"] = time.strftime("%Y-%m-%d")
|
|
194
|
+
save_tasks(tasks)
|
|
195
|
+
return tasks[i]
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def create_task(data: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
tasks = load_tasks()
|
|
201
|
+
new_id = f"T-{len(tasks)+1:03d}"
|
|
202
|
+
task = {
|
|
203
|
+
"id": new_id,
|
|
204
|
+
"subject": data.get("subject", "New Task"),
|
|
205
|
+
"status": data.get("status", "pending"),
|
|
206
|
+
"owner": data.get("owner", "Unassigned"),
|
|
207
|
+
"created_at": time.strftime("%Y-%m-%d"),
|
|
208
|
+
"updated_at": time.strftime("%Y-%m-%d"),
|
|
209
|
+
"metadata": data.get("metadata", {})
|
|
210
|
+
}
|
|
211
|
+
tasks.append(task)
|
|
212
|
+
save_tasks(tasks)
|
|
213
|
+
return task
|