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
multi_agent/tools.py ADDED
@@ -0,0 +1,393 @@
1
+ """Multi-agent tool registrations.
2
+
3
+ Registers the following tools into the central tool_registry:
4
+ Agent — spawn a sub-agent (always background)
5
+ SendMessage — send a message to a named background agent
6
+ CheckAgentResult — check status/result of a background agent
7
+ ListAgentTasks — list all active/finished agent tasks
8
+ ListAgentTypes — list available agent type definitions
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from tool_registry import ToolDef, register_tool
13
+ from .subagent import SubAgentManager, get_agent_definition, load_agent_definitions
14
+
15
+
16
+ # ── Singleton manager ──────────────────────────────────────────────────────
17
+
18
+ _agent_manager: SubAgentManager | None = None
19
+
20
+
21
+ def get_agent_manager() -> SubAgentManager:
22
+ """Return (and lazily create) the process-wide SubAgentManager."""
23
+ global _agent_manager
24
+ if _agent_manager is None:
25
+ _agent_manager = SubAgentManager()
26
+ return _agent_manager
27
+
28
+
29
+ # ── Tool implementations ───────────────────────────────────────────────────
30
+
31
+ def _agent_tool(params: dict, config: dict) -> str:
32
+ """Spawn a sub-agent.
33
+
34
+ Reads from config:
35
+ _system_prompt — injected by agent.py run(), used as base system prompt
36
+ _depth — current nesting depth (prevents infinite recursion)
37
+ """
38
+ mgr = get_agent_manager()
39
+
40
+ prompt = params["prompt"]
41
+ # Sub-agents ALWAYS run in background. A sub must never block the main
42
+ # agent's stream — `wait` is no longer accepted from the model.
43
+ wait = False
44
+ isolation = params.get("isolation", "")
45
+ name = params.get("name", "")
46
+ model_override = params.get("model", "")
47
+ subagent_type = params.get("subagent_type", "")
48
+
49
+ system_prompt = config.get("_system_prompt", "You are a helpful assistant.")
50
+ depth = config.get("_depth", 0)
51
+
52
+ # Strip private keys before passing to sub-agent, but preserve the hooks
53
+ # that AskMainAgentQuestion needs to reach back into the main agent.
54
+ eff_config = {k: v for k, v in config.items() if not k.startswith("_")}
55
+ for k in ("_run_query_callback", "_state"):
56
+ if k in config:
57
+ eff_config[k] = config[k]
58
+ if model_override:
59
+ eff_config["model"] = model_override
60
+
61
+ # Resolve agent definition
62
+ agent_def = None
63
+ if subagent_type:
64
+ agent_def = get_agent_definition(subagent_type)
65
+ if agent_def is None:
66
+ return (
67
+ f"Error: unknown subagent_type '{subagent_type}'. "
68
+ "Use ListAgentTypes to see available types."
69
+ )
70
+
71
+ task = mgr.spawn(
72
+ prompt, eff_config, system_prompt,
73
+ depth=depth,
74
+ agent_def=agent_def,
75
+ isolation=isolation,
76
+ name=name,
77
+ )
78
+
79
+ if task.status == "failed":
80
+ return f"Error spawning agent: {task.result}"
81
+
82
+ if wait:
83
+ mgr.wait(task.id, timeout=300)
84
+ result = task.result or f"(no output — status: {task.status})"
85
+ header = f"[Agent: {task.name}"
86
+ if subagent_type:
87
+ header += f" ({subagent_type})"
88
+ if task.worktree_branch:
89
+ header += f", branch: {task.worktree_branch}"
90
+ header += "]"
91
+ return f"{header}\n\n{result}"
92
+ else:
93
+ info_parts = [f"Task ID: {task.id}", f"Name: {task.name}", f"Status: {task.status}"]
94
+ if subagent_type:
95
+ info_parts.append(f"Type: {subagent_type}")
96
+ if task.worktree_branch:
97
+ info_parts.append(f"Worktree branch: {task.worktree_branch}")
98
+ info_parts.append("Use CheckAgentResult or SendMessage to interact with this agent.")
99
+ return "\n".join(info_parts)
100
+
101
+
102
+ def _send_message(params: dict, config: dict) -> str:
103
+ mgr = get_agent_manager()
104
+ target = params["to"]
105
+ message = params["message"]
106
+ ok = mgr.send_message(target, message)
107
+ if ok:
108
+ return f"Message queued for agent '{target}'. It will be processed after current work completes."
109
+ task_id = mgr._by_name.get(target, target)
110
+ task = mgr.tasks.get(task_id)
111
+ if task is None:
112
+ return f"Error: no agent found with id or name '{target}'"
113
+ return f"Error: agent '{target}' is not running (status: {task.status}). Cannot send message."
114
+
115
+
116
+ def _check_agent_result(params: dict, config: dict) -> str:
117
+ mgr = get_agent_manager()
118
+ task_id = params["task_id"]
119
+ task = mgr.tasks.get(task_id)
120
+ if task is None:
121
+ return f"Error: no task with id '{task_id}'"
122
+ lines = [f"Status: {task.status}", f"Name: {task.name}"]
123
+ if task.worktree_branch:
124
+ lines.append(f"Worktree branch: {task.worktree_branch}")
125
+ if task.result:
126
+ lines.append(f"\nResult:\n{task.result}")
127
+ return "\n".join(lines)
128
+
129
+
130
+ def _list_agent_tasks(params: dict, config: dict) -> str:
131
+ mgr = get_agent_manager()
132
+ tasks = mgr.list_tasks()
133
+ if not tasks:
134
+ return "No sub-agent tasks."
135
+ lines = ["ID | Name | Status | Worktree branch | Prompt"]
136
+ lines.append("-------------|----------|-----------|-----------------|------")
137
+ for t in tasks:
138
+ prompt_short = t.prompt[:50] + ("..." if len(t.prompt) > 50 else "")
139
+ wt = t.worktree_branch[:15] if t.worktree_branch else "-"
140
+ lines.append(f"{t.id} | {t.name[:8]:8s} | {t.status:9s} | {wt:15s} | {prompt_short}")
141
+ return "\n".join(lines)
142
+
143
+
144
+ def _ask_main_agent_question(params: dict, config: dict) -> str:
145
+ """Pause a sub-agent and ask the main agent a question.
146
+
147
+ The sub-agent blocks on a threading.Event in its current turn (preserving
148
+ full context). The main agent receives a system message naming the
149
+ sub-agent and the question; it replies using SendMessage(to=<name>, ...).
150
+ """
151
+ import threading as _threading
152
+
153
+ question = params.get("question", "").strip()
154
+ if not question:
155
+ return "Error: 'question' parameter is required."
156
+
157
+ task_id = config.get("_subagent_task_id")
158
+ if not task_id:
159
+ return (
160
+ "Error: AskMainAgentQuestion can only be called from within a "
161
+ "sub-agent. If you ARE the main agent, use AskUserQuestion instead."
162
+ )
163
+
164
+ mgr = get_agent_manager()
165
+ task = mgr.tasks.get(task_id)
166
+ if task is None:
167
+ return f"Error: sub-agent task '{task_id}' not found."
168
+
169
+ run_query_cb = config.get("_run_query_callback")
170
+ main_state = config.get("_state")
171
+ if run_query_cb is None or main_state is None:
172
+ return (
173
+ "Error: main-agent hooks unavailable. AskMainAgentQuestion requires "
174
+ "the sub-agent to have been spawned from an active REPL session."
175
+ )
176
+
177
+ # Register the pending question on the task.
178
+ event = _threading.Event()
179
+ holder: list[str] = []
180
+ task._pending_question = {
181
+ "question": question,
182
+ "event": event,
183
+ "result": holder,
184
+ }
185
+
186
+ # Inject a system message into the main agent's state and trigger a turn.
187
+ addr = task.name or task.id
188
+ sys_msg = (
189
+ f"(System Event): Sub-agent '{addr}' is paused and asking you a question:\n\n"
190
+ f" {question}\n\n"
191
+ f"Reply by calling SendMessage(to='{addr}', message='<your answer>'). "
192
+ f"The sub-agent is holding its full context and will resume the same "
193
+ f"turn once you respond."
194
+ )
195
+ try:
196
+ main_state.messages.append({"role": "system", "content": sys_msg})
197
+ except Exception:
198
+ pass
199
+ try:
200
+ run_query_cb(sys_msg)
201
+ except Exception as e:
202
+ task._pending_question = None
203
+ return f"Error: failed to notify main agent: {e}"
204
+
205
+ # Block the sub-agent until the main replies (or we hit the timeout).
206
+ got = event.wait(timeout=600)
207
+ task._pending_question = None
208
+ if not got:
209
+ return "(no answer — timeout after 600s waiting for main agent)"
210
+ return holder[0] if holder else "(no answer)"
211
+
212
+
213
+ def _list_agent_types(params: dict, config: dict) -> str:
214
+ defs = load_agent_definitions()
215
+ if not defs:
216
+ return "No agent types available."
217
+ lines = ["Available agent types:", ""]
218
+ for aname, d in sorted(defs.items()):
219
+ model_info = f" model: {d.model}" if d.model else ""
220
+ tools_info = f" tools: {', '.join(d.tools)}" if d.tools else ""
221
+ lines.append(f" {aname:20s} [{d.source:8s}] {d.description}")
222
+ if model_info:
223
+ lines.append(f" {model_info}")
224
+ if tools_info:
225
+ lines.append(f" {tools_info}")
226
+ lines.append("")
227
+ lines.append(
228
+ "Create custom agents: place .md files in ~/.dulus/agents/ or .dulus/agents/"
229
+ )
230
+ return "\n".join(lines)
231
+
232
+
233
+ # ── Tool registrations ─────────────────────────────────────────────────────
234
+
235
+ register_tool(ToolDef(
236
+ name="Agent",
237
+ schema={
238
+ "name": "Agent",
239
+ "description": (
240
+ "Spawn a sub-agent to handle a task autonomously. The sub-agent runs in a "
241
+ "separate thread with its own conversation history. Supports specialized agent "
242
+ "types (coder, reviewer, researcher, tester, or custom from .dulus/agents/), "
243
+ "isolated git worktrees for parallel work, and background execution.\n\n"
244
+ "When using isolation='worktree', the agent gets its own git branch and "
245
+ "working copy — ideal for parallel coding tasks that shouldn't interfere."
246
+ ),
247
+ "input_schema": {
248
+ "type": "object",
249
+ "properties": {
250
+ "prompt": {
251
+ "type": "string",
252
+ "description": "Task description for the sub-agent",
253
+ },
254
+ "subagent_type": {
255
+ "type": "string",
256
+ "description": (
257
+ "Specialized agent type: 'general-purpose', 'coder', 'reviewer', "
258
+ "'researcher', 'tester', or any custom type. "
259
+ "Use ListAgentTypes to see all available types."
260
+ ),
261
+ },
262
+ "name": {
263
+ "type": "string",
264
+ "description": (
265
+ "Human-readable name for this agent instance. "
266
+ "Makes it addressable via SendMessage while running in background."
267
+ ),
268
+ },
269
+ "model": {
270
+ "type": "string",
271
+ "description": "Model override for this specific agent (optional)",
272
+ },
273
+ "isolation": {
274
+ "type": "string",
275
+ "enum": ["worktree"],
276
+ "description": (
277
+ "'worktree' creates a temporary git worktree so the agent works "
278
+ "on an isolated copy of the repo. Changes stay on a separate branch "
279
+ "and can be reviewed/merged after completion."
280
+ ),
281
+ },
282
+ },
283
+ "required": ["prompt"],
284
+ },
285
+ },
286
+ func=_agent_tool,
287
+ read_only=False,
288
+ concurrent_safe=False,
289
+ ))
290
+
291
+ register_tool(ToolDef(
292
+ name="SendMessage",
293
+ schema={
294
+ "name": "SendMessage",
295
+ "description": (
296
+ "Send a follow-up message to a running background agent. "
297
+ "The message is queued and processed after the agent finishes its current work. "
298
+ "Reference agents by the name set via Agent(name=...) or by task ID."
299
+ ),
300
+ "input_schema": {
301
+ "type": "object",
302
+ "properties": {
303
+ "to": {"type": "string", "description": "Agent name or task ID"},
304
+ "message": {"type": "string", "description": "Message to send to the agent"},
305
+ },
306
+ "required": ["to", "message"],
307
+ },
308
+ },
309
+ func=_send_message,
310
+ read_only=False,
311
+ concurrent_safe=True,
312
+ ))
313
+
314
+ register_tool(ToolDef(
315
+ name="CheckAgentResult",
316
+ schema={
317
+ "name": "CheckAgentResult",
318
+ "description": "Check the status and result of a spawned sub-agent task.",
319
+ "input_schema": {
320
+ "type": "object",
321
+ "properties": {
322
+ "task_id": {"type": "string", "description": "Task ID returned by Agent tool"},
323
+ },
324
+ "required": ["task_id"],
325
+ },
326
+ },
327
+ func=_check_agent_result,
328
+ read_only=True,
329
+ concurrent_safe=True,
330
+ ))
331
+
332
+ register_tool(ToolDef(
333
+ name="ListAgentTasks",
334
+ schema={
335
+ "name": "ListAgentTasks",
336
+ "description": "List all sub-agent tasks and their statuses.",
337
+ "input_schema": {
338
+ "type": "object",
339
+ "properties": {},
340
+ },
341
+ },
342
+ func=_list_agent_tasks,
343
+ read_only=True,
344
+ concurrent_safe=True,
345
+ ))
346
+
347
+ register_tool(ToolDef(
348
+ name="AskMainAgentQuestion",
349
+ schema={
350
+ "name": "AskMainAgentQuestion",
351
+ "description": (
352
+ "Pause this sub-agent and ask the MAIN agent a question (not the user). "
353
+ "The sub-agent blocks in the current turn — full context is preserved — "
354
+ "and resumes once the main agent answers via SendMessage. "
355
+ "Use this when you need a decision, clarification, or permission from "
356
+ "the main agent before continuing. Examples: 'Should I also refactor X?', "
357
+ "'¿Deseas que haga algo más?', 'Plan looks like A or B — which one?'. "
358
+ "Only works when invoked from a sub-agent running in the background; "
359
+ "the main agent itself should use AskUserQuestion for the end user."
360
+ ),
361
+ "input_schema": {
362
+ "type": "object",
363
+ "properties": {
364
+ "question": {
365
+ "type": "string",
366
+ "description": "The question to ask the main agent.",
367
+ },
368
+ },
369
+ "required": ["question"],
370
+ },
371
+ },
372
+ func=_ask_main_agent_question,
373
+ read_only=False,
374
+ concurrent_safe=True,
375
+ ))
376
+
377
+ register_tool(ToolDef(
378
+ name="ListAgentTypes",
379
+ schema={
380
+ "name": "ListAgentTypes",
381
+ "description": (
382
+ "List all available agent types (built-in and custom). "
383
+ "Use the type names as subagent_type when calling Agent."
384
+ ),
385
+ "input_schema": {
386
+ "type": "object",
387
+ "properties": {},
388
+ },
389
+ },
390
+ func=_list_agent_types,
391
+ read_only=True,
392
+ concurrent_safe=True,
393
+ ))
offload_helper.py ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Offload Helper - Reemplazo para TmuxOffload
4
+ Funciona con las herramientas tmux que sí funcionan
5
+ """
6
+
7
+ import subprocess
8
+ import time
9
+ import uuid
10
+ from typing import Optional, Dict, Any
11
+
12
+
13
+ class TmuxJob:
14
+ """Representa un job ejecutado en tmux"""
15
+
16
+ def __init__(self, command: str):
17
+ self.command = command
18
+ self.session = f"dulus_{uuid.uuid4().hex[:8]}"
19
+ self._created = False
20
+ self._start_time = None
21
+
22
+ def start(self) -> str:
23
+ """Inicia el job en tmux detached. Retorna session ID."""
24
+ # Usar bash -c para soportar pipes y redirects
25
+ full_cmd = f"exec bash -c {repr(self.command)}"
26
+
27
+ result = subprocess.run(
28
+ ["tmux", "new-session", "-d", "-s", self.session, full_cmd],
29
+ capture_output=True,
30
+ text=True
31
+ )
32
+
33
+ if result.returncode != 0:
34
+ raise RuntimeError(f"tmux error: {result.stderr}")
35
+
36
+ self._created = True
37
+ self._start_time = time.time()
38
+ return self.session
39
+
40
+ def is_running(self) -> bool:
41
+ """Verifica si el job sigue corriendo"""
42
+ if not self._created:
43
+ return False
44
+ result = subprocess.run(
45
+ ["tmux", "has-session", "-t", self.session],
46
+ capture_output=True
47
+ )
48
+ return result.returncode == 0
49
+
50
+ def capture(self, lines: int = 1000) -> str:
51
+ """Captura el output del job"""
52
+ if not self._created:
53
+ raise RuntimeError("Job no iniciado")
54
+
55
+ result = subprocess.run(
56
+ ["tmux", "capture-pane", "-t", self.session, "-p", "-S", f"-{lines}"],
57
+ capture_output=True,
58
+ text=True
59
+ )
60
+ return result.stdout if result.returncode == 0 else ""
61
+
62
+ def kill(self):
63
+ """Mata el job y la sesión tmux"""
64
+ if self._created:
65
+ subprocess.run(
66
+ ["tmux", "kill-session", "-t", self.session],
67
+ capture_output=True
68
+ )
69
+
70
+ def wait(self, timeout: Optional[float] = None, poll_interval: float = 0.5) -> bool:
71
+ """
72
+ Espera a que termine el job.
73
+ Retorna True si terminó, False si timeout.
74
+ """
75
+ if not self._created:
76
+ raise RuntimeError("Job no iniciado")
77
+
78
+ start = time.time()
79
+ while self.is_running():
80
+ if timeout and (time.time() - start) > timeout:
81
+ return False
82
+ time.sleep(poll_interval)
83
+ return True
84
+
85
+
86
+ # === API SIMPLE ===
87
+
88
+ def offload(command: str) -> str:
89
+ """
90
+ Ejecuta un comando en tmux detached (fire-and-forget).
91
+ Retorna el session ID para capturar después.
92
+
93
+ Uso:
94
+ session = offload("sleep 10 && echo listo")
95
+ # ... más tarde ...
96
+ tmux capture-pane -t <session>:0.0 -p
97
+ """
98
+ job = TmuxJob(command)
99
+ return job.start()
100
+
101
+
102
+ def offload_and_wait(command: str, timeout: Optional[float] = None) -> Dict[str, Any]:
103
+ """
104
+ Ejecuta comando y espera a que termine.
105
+
106
+ Uso:
107
+ result = offload_and_wait("sleep 5 && date", timeout=10)
108
+ print(result['output']) # stdout del comando
109
+ """
110
+ job = TmuxJob(command)
111
+ job.start()
112
+ finished = job.wait(timeout=timeout)
113
+ output = job.capture()
114
+ job.kill()
115
+
116
+ return {
117
+ 'session': job.session,
118
+ 'output': output,
119
+ 'finished': finished,
120
+ 'timeout_reached': not finished
121
+ }
122
+
123
+
124
+ def list_offloaded():
125
+ """Lista todas las sesiones dulus activas"""
126
+ result = subprocess.run(
127
+ ["tmux", "list-sessions"],
128
+ capture_output=True,
129
+ text=True
130
+ )
131
+
132
+ if result.returncode != 0:
133
+ return []
134
+
135
+ sessions = []
136
+ for line in result.stdout.strip().split('\n'):
137
+ if line.startswith('dulus_'):
138
+ sessions.append(line.split(':')[0])
139
+ return sessions
140
+
141
+
142
+ # === EJEMPLOS ===
143
+
144
+ if __name__ == "__main__":
145
+ import sys
146
+
147
+ if len(sys.argv) > 1 and sys.argv[1] == "demo":
148
+ print("🦅 Demo de Offload Helper")
149
+ print("=" * 40)
150
+
151
+ # Demo 1: Fire and forget
152
+ print("\n1️⃣ Fire-and-forget:")
153
+ session = offload("echo 'Hola desde tmux!' && sleep 2 && date")
154
+ print(f" Session: {session}")
155
+ print(f" Para ver: tmux capture-pane -t {session}:0.0 -p")
156
+
157
+ time.sleep(3)
158
+ output = subprocess.run(
159
+ ["tmux", "capture-pane", "-t", f"{session}:0.0", "-p"],
160
+ capture_output=True, text=True
161
+ ).stdout
162
+ print(f" Output: {output.strip()[:50]}...")
163
+
164
+ # Limpiar
165
+ subprocess.run(["tmux", "kill-session", "-t", session], capture_output=True)
166
+
167
+ # Demo 2: Wait mode
168
+ print("\n2️⃣ Wait mode:")
169
+ result = offload_and_wait("echo 'Esperando...' && sleep 2 && echo 'Listo!' && date")
170
+ print(f" Output: {result['output'].strip()}")
171
+
172
+ # Demo 3: Listar
173
+ print("\n3️⃣ Sesiones activas:")
174
+ sessions = list_offloaded()
175
+ print(f" {len(sessions)} sesiones dulus activas")
176
+
177
+ print("\n✅ Todo funcionando!")
178
+ else:
179
+ print("Uso: python offload_helper.py demo")
180
+ print("\nFunciones:")
181
+ print(" offload(cmd) -> session_id")
182
+ print(" offload_and_wait(cmd, timeout) -> {output, session}")
183
+ print(" list_offloaded() -> [sessions]")
plugin/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Plugin system for dulus."""
2
+ from .types import PluginManifest, PluginEntry, PluginScope, parse_plugin_identifier
3
+ from .store import (
4
+ install_plugin, uninstall_plugin,
5
+ enable_plugin, disable_plugin, disable_all_plugins,
6
+ update_plugin, list_plugins, get_plugin,
7
+ )
8
+ from .loader import (
9
+ load_all_plugins, load_plugin_tools, load_plugin_skills,
10
+ load_plugin_mcp_configs, register_plugin_tools, reload_plugins,
11
+ )
12
+ from .recommend import recommend_plugins, recommend_from_files, format_recommendations
13
+
14
+ __all__ = [
15
+ "PluginManifest", "PluginEntry", "PluginScope", "parse_plugin_identifier",
16
+ "install_plugin", "uninstall_plugin",
17
+ "enable_plugin", "disable_plugin", "disable_all_plugins",
18
+ "update_plugin", "list_plugins", "get_plugin",
19
+ "load_all_plugins", "load_plugin_tools", "load_plugin_skills",
20
+ "load_plugin_mcp_configs", "register_plugin_tools", "reload_plugins",
21
+ "recommend_plugins", "recommend_from_files", "format_recommendations",
22
+ ]