telar 0.1.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.
telar/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """telar — un telar para hilos de trabajo.
2
+
3
+ Cada hilo es una unidad de trabajo viva: un tab del multiplexor de terminal, una
4
+ carpeta del repositorio donde se trabaja, y lo que esa carpeta dice de sí misma.
5
+
6
+ telar NO decide qué es un proyecto. Eso lo declara el repositorio de trabajo en su
7
+ `telar-perfil.yaml` (ver `telar.perfil`); telar solo sabe tejer lo declarado.
8
+
9
+ Los módulos, y qué le toca a cada uno:
10
+
11
+ `telar.config` la configuración del usuario (`~/.config/telar/config.toml`).
12
+ `telar.perfil` el perfil que publica el repositorio de trabajo.
13
+ `telar.modelo` los datos puros que los demás módulos se pasan. Sin E/S.
14
+ `telar.estado` lo que telar recuerda entre corridas. Derivado y desechable.
15
+ `telar.lectura` leer un documento como lo declara el perfil, y dar una ficha.
16
+ `telar.mux` hablarle al multiplexor de terminal (tmux, zellij).
17
+ `telar.proveedores` fuentes externas opcionales, cada una declarada.
18
+ `telar.agente` reconocer y dirigirse al agente que corre en un hilo.
19
+ `telar.salida` que la codificación de la terminal no tumbe una orden.
20
+ `telar.cli` el despachador; cada orden vive en `telar.ordenes.<nombre>`.
21
+ """
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = ["__version__"]
@@ -0,0 +1,122 @@
1
+ """El agente que corre en un hilo: reconocerlo, ubicar su conversación, hablarle.
2
+
3
+ Un hilo suele tener un agente de línea de comandos trabajando dentro. telar no lo
4
+ lanza ni lo pilota: lo reconoce por el proceso que corre en el panel, guarda el
5
+ vínculo hilo → conversación, y sabe dejarle una frase escrita en su entrada sin
6
+ enviarla.
7
+
8
+ La lección que justifica este módulo: al resucitar una sesión, el multiplexor
9
+ vuelve a lanzar el comando que había, y el agente arranca **una conversación
10
+ nueva**. Las conversaciones no se pierden —están en disco— pero el vínculo con el
11
+ hilo sí, y sin ese vínculo veinte hilos en la misma carpeta son indistinguibles.
12
+ Por eso el vínculo se anota cuando el agente arranca, no cuando se necesita.
13
+
14
+ Un adaptador de agente cumple `Agente`; qué agentes hay, lo dice `REGISTRO`.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import Protocol, runtime_checkable
21
+
22
+ from telar.modelo import Atencion
23
+
24
+ __all__ = [
25
+ "Agente",
26
+ "Conversacion",
27
+ "ErrorDeAgente",
28
+ "REGISTRO",
29
+ "INCLUIDOS",
30
+ "registrar",
31
+ "obtener",
32
+ ]
33
+
34
+
35
+ class ErrorDeAgente(Exception):
36
+ """No se pudo reconocer o alcanzar al agente de un hilo."""
37
+
38
+
39
+ class Conversacion:
40
+ """El vínculo hilo → conversación, con lo mínimo para volver a abrirla.
41
+
42
+ `id` es del agente; `archivo` es dónde la guarda, si la guarda en disco.
43
+ """
44
+
45
+ __slots__ = ("id", "archivo", "hilo")
46
+
47
+ def __init__(self, id: str, hilo: str = "", archivo: Path | None = None) -> None:
48
+ self.id = id
49
+ self.hilo = hilo
50
+ self.archivo = archivo
51
+
52
+ def __repr__(self) -> str: # pragma: no cover
53
+ return f"Conversacion({self.id!r}, hilo={self.hilo!r})"
54
+
55
+
56
+ @runtime_checkable
57
+ class Agente(Protocol):
58
+ """Lo que telar le pide a un agente de línea de comandos."""
59
+
60
+ #: cómo se llama en la configuración y en el registro.
61
+ nombre: str
62
+
63
+ def corriendo(self, comando: str) -> bool:
64
+ """¿Ese comando de panel es este agente?
65
+
66
+ Se decide por el proceso que corre ahora, no por el título del panel: un
67
+ título miente en cuanto alguien lo renombra.
68
+ """
69
+ ...
70
+
71
+ def conversaciones(self, hilo: str) -> list[Conversacion]:
72
+ """Las conversaciones anotadas para un hilo, la principal primero."""
73
+ ...
74
+
75
+ def anotar(self, hilo: str, conversacion: Conversacion) -> None:
76
+ """Guarda el vínculo hilo → conversación. Se llama cuando el agente arranca."""
77
+ ...
78
+
79
+ def retomar(self, conversacion: Conversacion) -> list[str]:
80
+ """El comando que vuelve a abrir esa conversación, sin correrlo."""
81
+ ...
82
+
83
+ def nuevo(self, ruta: Path | None = None) -> list[str]:
84
+ """El comando que abre una conversación nueva, sin correrlo."""
85
+ ...
86
+
87
+ def atencion(self, hilo: str) -> Atencion:
88
+ """En qué está el agente de ese hilo, si se puede saber."""
89
+ ...
90
+
91
+
92
+ #: nombre → fábrica `(Config) -> Agente`.
93
+ REGISTRO: dict[str, object] = {}
94
+
95
+ #: Los adaptadores que vienen con telar: nombre → módulo que los registra al importarse.
96
+ #: Un agente de afuera no necesita estar aquí; le basta con llamar a `registrar`.
97
+ INCLUIDOS: dict[str, str] = {
98
+ "claude-code": "telar.agente.claude_code",
99
+ }
100
+
101
+
102
+ def registrar(nombre: str, fabrica) -> None:
103
+ if nombre in REGISTRO:
104
+ raise ValueError(f"ya hay un agente llamado {nombre!r}")
105
+ REGISTRO[nombre] = fabrica
106
+
107
+
108
+ def obtener(nombre: str, config) -> Agente:
109
+ """El agente que se llama así, construido. Los incluidos se importan al pedirlos.
110
+
111
+ Importar perezosamente es lo que permite que `telar.agente` no sepa nada de ningún
112
+ agente concreto: la dependencia va del adaptador al contrato, nunca al revés.
113
+ """
114
+ if nombre not in REGISTRO and nombre in INCLUIDOS:
115
+ import importlib
116
+
117
+ importlib.import_module(INCLUIDOS[nombre])
118
+ fabrica = REGISTRO.get(nombre)
119
+ if fabrica is None:
120
+ conocidos = ", ".join(sorted(set(REGISTRO) | set(INCLUIDOS))) or "ninguno"
121
+ raise ErrorDeAgente(f"no hay un agente llamado {nombre!r}; registrados: {conocidos}")
122
+ return fabrica(config) # type: ignore[operator]