hanuscode 1.0.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 (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/tasks/manager.py ADDED
@@ -0,0 +1,330 @@
1
+ # hanus/tasks/manager.py
2
+ """
3
+ Gestor de tareas integrado en el motor del agente.
4
+ """
5
+ from __future__ import annotations
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+ from hanus.tasks.models import Task, TaskStatus
11
+
12
+
13
+ class TaskManager:
14
+ """
15
+ Gestiona el ciclo de vida de las tareas del agente.
16
+
17
+ Features:
18
+ - Crear/actualizar/listar/obtener tareas
19
+ - Tracking de dependencias (blocked_by/blocks)
20
+ - Tracking de progreso por tarea
21
+ - Persistencia en archivo JSON
22
+ """
23
+
24
+ TASKS_FILE = ".hanus_tasks.json"
25
+
26
+ def __init__(self, root_dir: Path):
27
+ self.root_dir = Path(root_dir)
28
+ self._tasks: Dict[str, Task] = {}
29
+ self._next_id: int = 1
30
+ self._load()
31
+
32
+ def _load(self):
33
+ """Carga tareas desde archivo JSON."""
34
+ tasks_file = self.root_dir / self.TASKS_FILE
35
+ if not tasks_file.exists():
36
+ return
37
+
38
+ try:
39
+ data = json.loads(tasks_file.read_text(encoding="utf-8"))
40
+ for task_data in data.get("tasks", []):
41
+ task = Task.from_dict(task_data)
42
+ self._tasks[task.id] = task
43
+ # Actualizar next_id
44
+ try:
45
+ task_id_int = int(task.id)
46
+ self._next_id = max(self._next_id, task_id_int + 1)
47
+ except ValueError:
48
+ pass
49
+ except Exception:
50
+ # Si hay error, empezar con tareas vacías
51
+ self._tasks = {}
52
+
53
+ def _save(self):
54
+ """Guarda tareas a archivo JSON."""
55
+ tasks_file = self.root_dir / self.TASKS_FILE
56
+ data = {
57
+ "tasks": [t.to_dict() for t in self._tasks.values()],
58
+ "updated_at": time.time(),
59
+ }
60
+ tasks_file.write_text(
61
+ json.dumps(data, indent=2, ensure_ascii=False),
62
+ encoding="utf-8"
63
+ )
64
+
65
+ def create(
66
+ self,
67
+ subject: str,
68
+ description: str = "",
69
+ activeForm: str = "",
70
+ metadata: Optional[Dict] = None
71
+ ) -> Task:
72
+ """
73
+ Crea una nueva tarea.
74
+
75
+ Args:
76
+ subject: Título breve de la tarea
77
+ description: Descripción completa
78
+ activeForm: Forma presente continuo (ej: "Implementando login")
79
+ metadata: Datos adicionales
80
+
81
+ Returns:
82
+ La tarea creada
83
+ """
84
+ task_id = str(self._next_id)
85
+ self._next_id += 1
86
+
87
+ task = Task(
88
+ id=task_id,
89
+ subject=subject,
90
+ description=description or subject,
91
+ activeForm=activeForm or f"{subject}...",
92
+ metadata=metadata or {},
93
+ )
94
+
95
+ self._tasks[task_id] = task
96
+ self._save()
97
+ return task
98
+
99
+ def update(self, task_id: str, **kwargs) -> Optional[Task]:
100
+ """
101
+ Actualiza campos de una tarea existente.
102
+
103
+ Args:
104
+ task_id: ID de la tarea
105
+ **kwargs: Campos a actualizar (status, subject, description, etc.)
106
+
107
+ Returns:
108
+ La tarea actualizada o None si no existe
109
+ """
110
+ task = self._tasks.get(task_id)
111
+ if not task:
112
+ return None
113
+
114
+ # Campos permitidos para actualizar
115
+ allowed = {
116
+ "status", "subject", "description", "owner",
117
+ "blocked_by", "blocks", "metadata", "activeForm"
118
+ }
119
+
120
+ for key, value in kwargs.items():
121
+ if key in allowed:
122
+ if key == "status" and isinstance(value, str):
123
+ value = TaskStatus(value)
124
+ setattr(task, key, value)
125
+
126
+ task.updated_at = time.time()
127
+
128
+ # Si se marca como completada, registrar timestamp
129
+ if task.status == TaskStatus.COMPLETED and not task.completed_at:
130
+ task.completed_at = time.time()
131
+
132
+ self._save()
133
+ return task
134
+
135
+ def get(self, task_id: str) -> Optional[Task]:
136
+ """Obtiene una tarea por ID."""
137
+ return self._tasks.get(task_id)
138
+
139
+ def list(
140
+ self,
141
+ status: Optional[TaskStatus] = None,
142
+ owner: Optional[str] = None
143
+ ) -> List[Task]:
144
+ """
145
+ Lista tareas con filtros opcionales.
146
+
147
+ Args:
148
+ status: Filtrar por estado
149
+ owner: Filtrar por owner
150
+
151
+ Returns:
152
+ Lista de tareas ordenadas por ID
153
+ """
154
+ tasks = list(self._tasks.values())
155
+
156
+ if status:
157
+ tasks = [t for t in tasks if t.status == status]
158
+ if owner:
159
+ tasks = [t for t in tasks if t.owner == owner]
160
+
161
+ # Ordenar por ID numérico
162
+ def sort_key(t: Task) -> int:
163
+ try:
164
+ return int(t.id)
165
+ except ValueError:
166
+ return 999999
167
+
168
+ return sorted(tasks, key=sort_key)
169
+
170
+ def claim(self, task_id: str, owner: str) -> Optional[Task]:
171
+ """
172
+ Reclama una tarea (la marca como in_progress y asigna owner).
173
+
174
+ Returns:
175
+ La tarea reclamada o None si no está disponible
176
+ """
177
+ task = self._tasks.get(task_id)
178
+ if not task:
179
+ return None
180
+
181
+ if task.status != TaskStatus.PENDING:
182
+ return None
183
+
184
+ # Verificar que no está bloqueada
185
+ if self._is_blocked(task):
186
+ return None
187
+
188
+ task.status = TaskStatus.IN_PROGRESS
189
+ task.owner = owner
190
+ task.updated_at = time.time()
191
+ self._save()
192
+ return task
193
+
194
+ def complete(self, task_id: str) -> Optional[Task]:
195
+ """Marca una tarea como completada."""
196
+ return self.update(task_id, status=TaskStatus.COMPLETED)
197
+
198
+ def fail(self, task_id: str, reason: str = "") -> Optional[Task]:
199
+ """Marca una tarea como fallida."""
200
+ task = self._tasks.get(task_id)
201
+ if not task:
202
+ return None
203
+
204
+ task.status = TaskStatus.FAILED
205
+ task.updated_at = time.time()
206
+ if reason:
207
+ task.metadata["fail_reason"] = reason
208
+
209
+ self._save()
210
+ return task
211
+
212
+ def get_next_available(self) -> Optional[Task]:
213
+ """
214
+ Obtiene la siguiente tarea disponible para trabajar.
215
+
216
+ Returns:
217
+ La siguiente tarea pending y no bloqueada, o None
218
+ """
219
+ # Primero, tareas que ya están in_progress
220
+ for task in self.list(status=TaskStatus.IN_PROGRESS):
221
+ return task
222
+
223
+ # Luego, tareas pending ordenadas por ID
224
+ for task in self.list(status=TaskStatus.PENDING):
225
+ if not self._is_blocked(task):
226
+ return task
227
+
228
+ return None
229
+
230
+ def _is_blocked(self, task: Task) -> bool:
231
+ """Verifica si una tarea está bloqueada por tareas incompletas."""
232
+ for blocker_id in task.blocked_by:
233
+ blocker = self._tasks.get(blocker_id)
234
+ if blocker and blocker.status != TaskStatus.COMPLETED:
235
+ return True
236
+ return False
237
+
238
+ def resolve_dependencies(self) -> List[str]:
239
+ """
240
+ Resuelve el orden de ejecución basado en dependencias.
241
+
242
+ Returns:
243
+ Lista de IDs de tareas en orden de ejecución
244
+ """
245
+ # Topological sort
246
+ result = []
247
+ visited = set()
248
+ temp_mark = set()
249
+
250
+ def visit(task_id: str):
251
+ if task_id in temp_mark:
252
+ # Ciclo detectado - ignorar
253
+ return
254
+ if task_id in visited:
255
+ return
256
+
257
+ temp_mark.add(task_id)
258
+ task = self._tasks.get(task_id)
259
+ if task:
260
+ for dep_id in task.blocked_by:
261
+ visit(dep_id)
262
+
263
+ temp_mark.remove(task_id)
264
+ visited.add(task_id)
265
+ result.append(task_id)
266
+
267
+ for task_id in self._tasks:
268
+ if task_id not in visited:
269
+ visit(task_id)
270
+
271
+ return result
272
+
273
+ def clear_completed(self) -> int:
274
+ """Elimina las tareas completadas. Retorna el número eliminadas."""
275
+ to_remove = [
276
+ tid for tid, t in self._tasks.items()
277
+ if t.status == TaskStatus.COMPLETED
278
+ ]
279
+ for tid in to_remove:
280
+ del self._tasks[tid]
281
+ if to_remove:
282
+ self._save()
283
+ return len(to_remove)
284
+
285
+ def reset(self) -> int:
286
+ """Elimina todas las tareas. Retorna el número eliminadas."""
287
+ count = len(self._tasks)
288
+ self._tasks.clear()
289
+ self._next_id = 1
290
+ if count:
291
+ self._save()
292
+ return count
293
+
294
+ def status_summary(self) -> Dict[str, int]:
295
+ """Retorna resumen de estados de tareas."""
296
+ summary = {s.value: 0 for s in TaskStatus}
297
+ for task in self._tasks.values():
298
+ summary[task.status.value] += 1
299
+ return summary
300
+
301
+ def format_status(self) -> str:
302
+ """Format current task status for display."""
303
+ summary = self.status_summary()
304
+ total = sum(summary.values())
305
+ if total == 0:
306
+ return "No tasks registered."
307
+
308
+ lines = [f"Tasks: {summary['pending']} pending, "
309
+ f"{summary['in_progress']} in progress, "
310
+ f"{summary['completed']} completed, "
311
+ f"{summary['failed']} failed"]
312
+
313
+ # Progress bar
314
+ done = summary['completed']
315
+ pct = done / total * 100 if total > 0 else 0
316
+ bar_len = 20
317
+ filled = int(bar_len * done / total) if total > 0 else 0
318
+ bar = "█" * filled + "░" * (bar_len - filled)
319
+ lines.append(f"[{bar}] {pct:.0f}%")
320
+
321
+ # Tasks in progress and pending
322
+ for task in self.list():
323
+ if task.status == TaskStatus.IN_PROGRESS:
324
+ blocked = " (blocked)" if self._is_blocked(task) else ""
325
+ lines.append(f" ● {task.id}: {task.subject}{blocked}")
326
+ elif task.status == TaskStatus.PENDING:
327
+ blocked = " ◎ blocked" if self._is_blocked(task) else ""
328
+ lines.append(f" ○ {task.id}: {task.subject}{blocked}")
329
+
330
+ return "\n".join(lines)
hanus/tasks/models.py ADDED
@@ -0,0 +1,106 @@
1
+ # hanus/tasks/models.py
2
+ """
3
+ Modelos de datos para el sistema de tareas.
4
+ """
5
+ from __future__ import annotations
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Dict, List, Any, Optional
9
+ from enum import Enum
10
+ import json
11
+
12
+
13
+ class TaskStatus(Enum):
14
+ """Estados posibles de una tarea."""
15
+ PENDING = "pending"
16
+ IN_PROGRESS = "in_progress"
17
+ COMPLETED = "completed"
18
+ FAILED = "failed"
19
+ BLOCKED = "blocked"
20
+
21
+
22
+ @dataclass
23
+ class Task:
24
+ """
25
+ Representa una tarea de seguimiento para el agente.
26
+
27
+ Attributes:
28
+ id: Identificador único de la tarea (string como "1", "2", etc.)
29
+ subject: Título breve de la tarea (imperativo, ej: "Implementar login")
30
+ description: Descripción completa de lo que se necesita hacer
31
+ status: Estado actual de la tarea
32
+ owner: ID del agente que tiene la tarea asignada (opcional)
33
+ created_at: Timestamp de creación
34
+ updated_at: Timestamp de última actualización
35
+ completed_at: Timestamp de completado (opcional)
36
+ blocked_by: Lista de IDs de tareas que deben completarse primero
37
+ blocks: Lista de IDs de tareas que dependen de esta
38
+ metadata: Datos adicionales arbitrarios
39
+ activeForm: Forma presente continuo para mostrar en UI (ej: "Implementando login")
40
+ """
41
+ id: str
42
+ subject: str
43
+ description: str
44
+ status: TaskStatus = TaskStatus.PENDING
45
+ owner: Optional[str] = None
46
+ created_at: float = field(default_factory=time.time)
47
+ updated_at: float = field(default_factory=time.time)
48
+ completed_at: Optional[float] = None
49
+ blocked_by: List[str] = field(default_factory=list)
50
+ blocks: List[str] = field(default_factory=list)
51
+ metadata: Dict[str, Any] = field(default_factory=dict)
52
+ activeForm: str = ""
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ """Serializa la tarea a diccionario para persistencia."""
56
+ return {
57
+ "id": self.id,
58
+ "subject": self.subject,
59
+ "description": self.description,
60
+ "status": self.status.value,
61
+ "owner": self.owner,
62
+ "created_at": self.created_at,
63
+ "updated_at": self.updated_at,
64
+ "completed_at": self.completed_at,
65
+ "blocked_by": self.blocked_by,
66
+ "blocks": self.blocks,
67
+ "metadata": self.metadata,
68
+ "activeForm": self.activeForm,
69
+ }
70
+
71
+ @classmethod
72
+ def from_dict(cls, d: Dict[str, Any]) -> "Task":
73
+ """Deserializa una tarea desde diccionario."""
74
+ return cls(
75
+ id=d["id"],
76
+ subject=d["subject"],
77
+ description=d["description"],
78
+ status=TaskStatus(d.get("status", "pending")),
79
+ owner=d.get("owner"),
80
+ created_at=d.get("created_at", time.time()),
81
+ updated_at=d.get("updated_at", time.time()),
82
+ completed_at=d.get("completed_at"),
83
+ blocked_by=d.get("blocked_by", []),
84
+ blocks=d.get("blocks", []),
85
+ metadata=d.get("metadata", {}),
86
+ activeForm=d.get("activeForm", ""),
87
+ )
88
+
89
+ def is_available(self) -> bool:
90
+ """Retorna True si la tarea puede ser empezada (pending y no bloqueada)."""
91
+ if self.status != TaskStatus.PENDING:
92
+ return False
93
+ # Las tareas bloqueadas no están disponibles
94
+ # (el manager debe verificar que las blocked_by estén completadas)
95
+ return True
96
+
97
+ def __str__(self) -> str:
98
+ status_icons = {
99
+ TaskStatus.PENDING: "○",
100
+ TaskStatus.IN_PROGRESS: "●",
101
+ TaskStatus.COMPLETED: "✓",
102
+ TaskStatus.FAILED: "✗",
103
+ TaskStatus.BLOCKED: "◎",
104
+ }
105
+ icon = status_icons.get(self.status, "?")
106
+ return f"[{icon}] {self.id}: {self.subject}"
@@ -0,0 +1,166 @@
1
+ # hanus/terminal_prompt.py — Prompt de terminal robusto
2
+ """
3
+ Maneja input del usuario con soporte para:
4
+ - Multiline (pegar código)
5
+ - Streaming toggle
6
+ - Caracteres de control correctos
7
+ - Background input (typing while agent works)
8
+ """
9
+ from __future__ import annotations
10
+ import sys
11
+ import os
12
+ import threading
13
+ import select
14
+
15
+
16
+ class TerminalPrompt:
17
+ """
18
+ Prompt de terminal que maneja correctamente el input del usuario.
19
+ """
20
+
21
+ def __init__(self):
22
+ self.streaming_enabled = False # Streaming DESACTIVADO por defecto
23
+ self._background_buffer: str = ""
24
+ self._background_thread: threading.Thread | None = None
25
+ self._background_stop = threading.Event()
26
+ self._background_active = False
27
+ self._buffer_lock = threading.Lock()
28
+
29
+ def read_line(self, prompt_text: str = "▶") -> str:
30
+ """
31
+ Lee una línea del usuario.
32
+ """
33
+ try:
34
+ # Usar input() estándar - más confiable
35
+ line = input(f"\033[96m\033[1m{prompt_text}\033[0m ")
36
+ return line.strip()
37
+
38
+ except KeyboardInterrupt:
39
+ print("^C")
40
+ return ""
41
+ except EOFError:
42
+ return ""
43
+
44
+ def read_multiline(self, prompt_text: str = "") -> str:
45
+ """
46
+ Lee múltiples líneas hasta recibir una línea vacía.
47
+ Útil para pegar código.
48
+ """
49
+ if prompt_text:
50
+ print(f"\n\033[96m▶ {prompt_text}\033[0m")
51
+
52
+ print("\033[90m (línea vacía para terminar)\033[0m")
53
+ print("\033[96m│\033[0m ", end="", flush=True)
54
+
55
+ lines = []
56
+ try:
57
+ while True:
58
+ line = input()
59
+ if not line: # Línea vacía termina
60
+ break
61
+ lines.append(line)
62
+ print("\033[96m│\033[0m ", end="", flush=True)
63
+
64
+ except KeyboardInterrupt:
65
+ print("^C")
66
+ except EOFError:
67
+ pass
68
+
69
+ return "\n".join(lines)
70
+
71
+ def toggle_streaming(self) -> bool:
72
+ """Activa/desactiva streaming. Retorna el nuevo estado."""
73
+ self.streaming_enabled = not self.streaming_enabled
74
+ return self.streaming_enabled
75
+
76
+ def is_streaming(self) -> bool:
77
+ """Retorna si el streaming está activado."""
78
+ return self.streaming_enabled
79
+
80
+ def start_background_input(self):
81
+ """
82
+ Start collecting input in the background while agent works.
83
+ User can type while the agent is processing.
84
+ Input is collected silently (blind typing) and shown after agent finishes.
85
+ """
86
+ if self._background_active:
87
+ return
88
+
89
+ self._background_active = True
90
+ self._background_stop.clear()
91
+ with self._buffer_lock:
92
+ self._background_buffer = ""
93
+
94
+ def collect_input():
95
+ """Background thread that collects stdin silently."""
96
+ try:
97
+ while not self._background_stop.is_set():
98
+ try:
99
+ if hasattr(sys.stdin, 'fileno') and os.isatty(sys.stdin.fileno()):
100
+ # Use select for non-blocking stdin check (Unix only)
101
+ ready, _, _ = select.select([sys.stdin], [], [], 0.1)
102
+ if ready:
103
+ line = sys.stdin.readline()
104
+ if line:
105
+ with self._buffer_lock:
106
+ self._background_buffer += line
107
+ except (ValueError, OSError):
108
+ # stdin might be closed or not a tty
109
+ break
110
+ except Exception:
111
+ pass
112
+ except Exception:
113
+ pass
114
+
115
+ # Only start background thread on Unix-like systems with a TTY
116
+ if hasattr(sys.stdin, 'fileno'):
117
+ try:
118
+ if os.isatty(sys.stdin.fileno()):
119
+ self._background_thread = threading.Thread(target=collect_input, daemon=True)
120
+ self._background_thread.start()
121
+ else:
122
+ self._background_active = False
123
+ except Exception:
124
+ self._background_active = False
125
+
126
+ def stop_background_input(self) -> str:
127
+ """
128
+ Stop background input collection and return collected input.
129
+ """
130
+ self._background_stop.set()
131
+ self._background_active = False
132
+
133
+ if self._background_thread:
134
+ self._background_thread.join(timeout=0.5)
135
+ self._background_thread = None
136
+
137
+ with self._buffer_lock:
138
+ collected = self._background_buffer
139
+ self._background_buffer = ""
140
+
141
+ # Show what was collected if any
142
+ if collected:
143
+ print(f"\n\033[90m 📝 Queued input ({len(collected)} chars): {collected[:50]}{'...' if len(collected) > 50 else ''}\033[0m")
144
+
145
+ return collected
146
+
147
+ def restore_terminal(self):
148
+ """No-op, mantenemos por compatibilidad."""
149
+ pass
150
+
151
+
152
+ # Singleton global
153
+ _prompt_instance: TerminalPrompt | None = None
154
+
155
+
156
+ def get_prompt() -> TerminalPrompt:
157
+ """Obtiene la instancia global del prompt."""
158
+ global _prompt_instance
159
+ if _prompt_instance is None:
160
+ _prompt_instance = TerminalPrompt()
161
+ return _prompt_instance
162
+
163
+
164
+ def restore_terminal():
165
+ """Restaura el terminal al salir."""
166
+ pass