agents-city 0.3.0 → 0.4.0
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +54 -5
- package/README.md +53 -5
- package/bin/agents-city.js +4 -0
- package/bin/hall.html +92 -5
- package/bin/navegador.mjs +141 -12
- package/bin/serve.py +94 -38
- package/bin/test +6 -3
- package/bin/test-demo.py +67 -0
- package/bin/test-desinstala.py +170 -0
- package/bin/test-navegador.py +2 -0
- package/bin/test-serve.py +122 -2
- package/bin/uninstall +5 -0
- package/city/web/dist/city.js +65 -65
- package/city/web/dist/index.html +18 -5
- package/city/web/dist-hall/hall.js +941 -385
- package/city/web/index.html +17 -4
- package/city/web/src/bienvenida.ts +35 -329
- package/city/web/src/casa.ts +282 -0
- package/city/web/src/demo.ts +247 -0
- package/city/web/src/dialogo.ts +178 -0
- package/city/web/src/es.ts +96 -0
- package/city/web/src/explorador.ts +189 -0
- package/city/web/src/hall.ts +157 -54
- package/city/web/src/main.ts +28 -5
- package/demo/graba.py +132 -0
- package/demo/grabaciones/legal.jsonl +22 -0
- package/demo/grabaciones/medicina.jsonl +22 -0
- package/demo/grabaciones/software.jsonl +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/scripts/demos.py +107 -0
- package/plugin/scripts/desinstala.py +222 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{"actor": "seat", "details": ["Desired outcome: Que cada persona afectada recupere lo suyo, y saber qué contamos públicamente y cuándo.", "Definition of done: Cada cuenta afectada tiene un camino para recuperar su partida · Mensaje público con hora y explicación honesta", "Invited: nova, store-service, telemetry-collector"], "kind": "committee.opened", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "chair", "seq": 1, "summary": "¿Qué hacemos con las partidas que desaparecieron el sábado?", "title": "The chair opened a committee", "tone": "question"}
|
|
2
|
+
{"actor": "nova", "details": ["1/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 2, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "nova submitted an independent position", "tone": "work"}
|
|
3
|
+
{"actor": "store-service", "details": ["2/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 3, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "store-service submitted an independent position", "tone": "work"}
|
|
4
|
+
{"actor": "telemetry-collector", "details": ["3/3 positions received"], "kind": "committee.position.submitted", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 4, "summary": "The collection barrier is complete; all positions can now be revealed together.", "target": "seat", "title": "telemetry-collector submitted an independent position", "tone": "work"}
|
|
5
|
+
{"actor": "seat", "details": ["The chair can now compare evidence, conflicts and unknowns without anchoring bias."], "kind": "committee.positions.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 5, "summary": "3 specialists completed the blind first round.", "title": "All independent positions were revealed together", "tone": "evidence"}
|
|
6
|
+
{"actor": "nova", "details": ["Stance: support", "Evidence: 412 mensajes en soporte y subiendo", "Evidence: Una madre escribió: su hijo perdió seis meses de partida y no deja de llorar", "Expected impact: La gente aguanta un fallo; no aguanta el silencio", "Visible when: En cuanto publiquemos el primer mensaje", "Risk: Prometer una hora y no llegar"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 6, "summary": "Lo primero es la gente: congelar la actualización del viernes y decir hoy mismo, en público, que lo sabemos y estamos en ello.", "target": "committee", "title": "nova proposes", "tone": "evidence"}
|
|
7
|
+
{"actor": "store-service", "details": ["Stance: conditional", "Evidence: Las copias del jueves por la noche están completas y verificadas", "Evidence: Ningún acceso extraño en todo el fin de semana", "Expected impact: Recuperar lo perdido sin inventar nada", "Visible when: Al restaurar la primera tanda", "Risk: Restaurar borra lo que se jugó después del jueves"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 7, "summary": "Nadie ha entrado a robar nada: no es un ataque. El viernes cambiamos cómo se guardan las partidas sin dejar puerta de vuelta. Propongo recuperar las copias de la noche del jueves antes de tocar nada más.", "target": "committee", "title": "store-service proposes", "tone": "evidence"}
|
|
8
|
+
{"actor": "telemetry-collector", "details": ["Stance: conditional", "Evidence: 3.804 cuentas afectadas por ahora", "Evidence: Casi todas jugaron el sábado entre las 22h y la 1h", "Expected impact: Avisar a los afectados directamente, no a bulto", "Visible when: Con la lista de cuentas en la mano", "Unknown: Cuántos afectados aún no han abierto el juego y no lo saben"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 8, "summary": "Antes de decidir, contar bien a cuánta gente le pasa y a quién. No es todo el mundo: es una franja concreta, y eso dice dónde mirar.", "target": "committee", "title": "telemetry-collector proposes", "tone": "evidence"}
|
|
9
|
+
{"actor": "seat", "details": ["Agreement: No es un ataque", "Agreement: La causa es el cambio del viernes", "Agreement: Hay copias del jueves por la noche", "Conflict: Avisar primero o restaurar primero", "Unknown: Afectados que aún no lo saben"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 9, "summary": "Los tres coinciden: no es un ataque, viene del cambio del viernes y hay copias del jueves. La discusión es el orden: ¿avisamos primero o restauramos primero?", "title": "The chair published the synthesis", "tone": "evidence"}
|
|
10
|
+
{"actor": "nova", "details": ["Basis: new evidence", "Evidence: Un streamer con dos millones de seguidores acaba de subir un vídeo enseñando su partida vacía"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 10, "summary": "Esto acaba de dejar de ser solo un problema técnico.", "target": "seat", "title": "nova requested the floor", "tone": "floor"}
|
|
11
|
+
{"actor": "seat", "details": ["Request: Esto acaba de dejar de ser solo un problema técnico.", "Evidence: Un streamer con dos millones de seguidores acaba de subir un vídeo enseñando su partida vacía"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 11, "summary": "One scoped reply was granted for new evidence.", "target": "nova", "title": "The chair gave nova the floor", "tone": "floor"}
|
|
12
|
+
{"actor": "nova", "details": ["Evidence: El vídeo lleva 40.000 visitas en una hora", "Consequence: El mensaje público pasa a ser lo más urgente de la lista.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 12, "summary": "Si restauramos sin avisar y alguien pierde lo que jugó el domingo, el enfado será doble. Primero el aviso con hora, después la restauración.", "target": "committee", "title": "nova spoke on the granted floor", "tone": "floor"}
|
|
13
|
+
{"actor": "store-service", "details": ["Basis: risk", "Evidence: La copia es del jueves; lo jugado el viernes y el sábado no está en ella"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 13, "summary": "Restaurar tal cual tiene un precio que nadie ha dicho en voz alta.", "target": "seat", "title": "store-service requested the floor", "tone": "floor"}
|
|
14
|
+
{"actor": "seat", "details": ["Request: Restaurar tal cual tiene un precio que nadie ha dicho en voz alta.", "Evidence: La copia es del jueves; lo jugado el viernes y el sábado no está en ella"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 14, "summary": "One scoped reply was granted for risk.", "target": "store-service", "title": "The chair gave store-service the floor", "tone": "floor"}
|
|
15
|
+
{"actor": "store-service", "details": ["Evidence: Guardar aparte las 3.804 cuentas lleva unas dos horas", "Consequence: La restauración se retrasa dos horas y se vuelve segura.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 15, "summary": "Antes de restaurar hay que guardar aparte todo lo del fin de semana, aunque tardemos dos horas más. Nadie pierde nada dos veces.", "target": "committee", "title": "store-service spoke on the granted floor", "tone": "floor"}
|
|
16
|
+
{"actor": "seat", "details": ["Rationale: Primero la confianza, después la restauración segura: las tres posiciones caben en ese orden.", "Decisive contributors: nova, store-service, telemetry-collector", "Independent verifier: telemetry-collector"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 16, "summary": "A las 17h, mensaje público contando qué pasó y qué vamos a hacer. A las 18h, guardar aparte todo lo del fin de semana. A las 20h, restaurar las copias del jueves. Nadie pierde nada dos veces.", "target": "telemetry-collector", "title": "The chair recorded a decision", "tone": "decision"}
|
|
17
|
+
{"actor": "telemetry-collector", "details": ["Evidence: 71 cuentas de la franja empezaron a jugar el viernes: no existen en la copia del jueves", "Residual risk: Son precisamente los jugadores más nuevos"], "kind": "committee.verification.fail", "phase": "verification_failed", "protocol": "agents-city-activity/1", "role": "member", "seq": 17, "summary": "Cruce de la lista de afectados contra las copias del jueves · Para 71 personas el plan no recupera nada", "target": "seat", "title": "telemetry-collector reported verification FAIL", "tone": "verification"}
|
|
18
|
+
{"actor": "seat", "details": [], "kind": "committee.replanned", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 18, "summary": "El plan deja fuera a 71 personas que empezaron después de la copia. Un plan que recupera a casi todos no es un plan: se replantea.", "title": "The chair reopened the plan", "tone": "decision"}
|
|
19
|
+
{"actor": "seat", "details": ["Agreement: Mantener horario de aviso y restauración", "Unknown: Cuánto se puede reconstruir de cada cuenta nueva"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 19, "summary": "El plan general sigue en pie; las 71 cuentas nuevas necesitan un trato aparte: contacto directo y reconstrucción con lo que sí tenemos.", "title": "The chair published the synthesis", "tone": "evidence"}
|
|
20
|
+
{"actor": "seat", "details": ["Rationale: Nadie se queda fuera por haber llegado el último.", "Decisive contributors: telemetry-collector, store-service", "Independent verifier: telemetry-collector"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 20, "summary": "El mismo plan, más un equipo dedicado a las 71 cuentas nuevas: contacto personal, compensación y reconstrucción manual de su progreso con los registros del fin de semana.", "target": "telemetry-collector", "title": "The chair recorded a decision", "tone": "decision"}
|
|
21
|
+
{"actor": "telemetry-collector", "details": ["Evidence: Prueba con 50 cuentas piloto: todas recuperan su partida", "Evidence: Las 71 cuentas nuevas contactadas una a una; 68 ya respondieron", "Residual risk: Vigilar la franja del sábado durante una semana"], "kind": "committee.verification.pass", "phase": "verified", "protocol": "agents-city-activity/1", "role": "member", "seq": 21, "summary": "Restauración probada antes de la hora anunciada · Mensaje público revisado y con hora concreta", "target": "seat", "title": "telemetry-collector reported verification PASS", "tone": "verification"}
|
|
22
|
+
{"actor": "seat", "details": [], "kind": "committee.closed", "phase": "closed", "protocol": "agents-city-activity/1", "role": "chair", "seq": 22, "summary": "Partidas restauradas, mensaje publicado a su hora y las 71 cuentas nuevas con seguimiento personal.", "title": "The chair closed the committee", "tone": "decision"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "city",
|
|
3
3
|
"displayName": "Agents City",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "One autonomous city seat with a work domain, role, goal, repo support agents, recognised skills and explicit roads to other cities.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "jlcases",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The recorded demos: what exists, and what each one contains.
|
|
3
|
+
|
|
4
|
+
One reader for the recordings under `demo/grabaciones/`, shared by the Hall and
|
|
5
|
+
by the suite that checks they still match their stories. The recordings are made
|
|
6
|
+
by `demo/graba.py`, which plays each story over the real bus — nothing here
|
|
7
|
+
invents an event, and nothing here knows what a committee is. It reads files.
|
|
8
|
+
|
|
9
|
+
A missing `demo/` folder is not an error: an install can be trimmed, and a Hall
|
|
10
|
+
whose demo shelf is empty should say so rather than fail to load.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
RAIZ = os.path.dirname(os.path.dirname(AQUI))
|
|
18
|
+
DEMO = os.path.join(RAIZ, "demo")
|
|
19
|
+
|
|
20
|
+
#: What each story is, in the words somebody choosing one would use. The domain
|
|
21
|
+
#: is the one the wizard offers, so "see it in my own field" means something.
|
|
22
|
+
DOMINIOS = {
|
|
23
|
+
"software": ("Software", "Aurora Games"),
|
|
24
|
+
"medicina": ("Healthcare", "Clínica Alba"),
|
|
25
|
+
"legal": ("Legal", "Costa & Ley"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def carpeta():
|
|
30
|
+
return os.path.join(DEMO, "grabaciones")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _titulos():
|
|
34
|
+
"""The story titles, read from the stories module when it is installed."""
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
if DEMO not in sys.path:
|
|
38
|
+
sys.path.insert(0, DEMO)
|
|
39
|
+
try:
|
|
40
|
+
from stories import STORIES # noqa: PLC0415
|
|
41
|
+
|
|
42
|
+
return {k: v.get("title", k) for k, v in STORIES.items()}
|
|
43
|
+
except (ImportError, AttributeError):
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def catalogo():
|
|
48
|
+
"""Every demo with a recording on disk, in a stable order."""
|
|
49
|
+
titulos = _titulos()
|
|
50
|
+
fuera = []
|
|
51
|
+
for ident in sorted(DOMINIOS):
|
|
52
|
+
ruta = os.path.join(carpeta(), f"{ident}.jsonl")
|
|
53
|
+
if not os.path.isfile(ruta):
|
|
54
|
+
continue
|
|
55
|
+
dominio, ciudad = DOMINIOS[ident]
|
|
56
|
+
fuera.append(
|
|
57
|
+
{
|
|
58
|
+
"id": ident,
|
|
59
|
+
"dominio": dominio,
|
|
60
|
+
"ciudad": ciudad,
|
|
61
|
+
"titulo": titulos.get(ident, ident),
|
|
62
|
+
"turnos": _cuenta(ruta),
|
|
63
|
+
"reparto": _reparto(ruta),
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
return fuera
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _lineas(ruta):
|
|
70
|
+
try:
|
|
71
|
+
with open(ruta, encoding="utf-8") as f:
|
|
72
|
+
for linea in f:
|
|
73
|
+
linea = linea.strip()
|
|
74
|
+
if linea:
|
|
75
|
+
yield linea
|
|
76
|
+
except OSError:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cuenta(ruta):
|
|
81
|
+
return sum(1 for _ in _lineas(ruta))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _reparto(ruta):
|
|
85
|
+
"""Who speaks in this story, in the order they first do."""
|
|
86
|
+
visto = []
|
|
87
|
+
for linea in _lineas(ruta):
|
|
88
|
+
try:
|
|
89
|
+
actor = json.loads(linea).get("actor", "")
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
continue
|
|
92
|
+
if actor and actor not in visto:
|
|
93
|
+
visto.append(actor)
|
|
94
|
+
return visto
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def eventos(ident):
|
|
98
|
+
"""One recording, as the list of activity events it holds."""
|
|
99
|
+
if ident not in DOMINIOS:
|
|
100
|
+
return []
|
|
101
|
+
fuera = []
|
|
102
|
+
for linea in _lineas(os.path.join(carpeta(), f"{ident}.jsonl")):
|
|
103
|
+
try:
|
|
104
|
+
fuera.append(json.loads(linea))
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
continue
|
|
107
|
+
return fuera
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Remove this product from this machine, with everything it ever wrote.
|
|
3
|
+
|
|
4
|
+
agents-city uninstall say exactly what would go. Remove nothing.
|
|
5
|
+
agents-city uninstall --yes do it
|
|
6
|
+
agents-city uninstall --keep-cities --yes
|
|
7
|
+
unwire the machine, keep your cities
|
|
8
|
+
agents-city uninstall --npm --yes and remove the global package too
|
|
9
|
+
|
|
10
|
+
`reset` empties one city and keeps a backup, because a person resetting a city
|
|
11
|
+
means to keep using the product. This is the other question — "get it off my
|
|
12
|
+
machine" — and answering it with a list of eight folders to delete by hand is not
|
|
13
|
+
an answer. A product that is easy to install and hard to remove is a product that
|
|
14
|
+
does not respect the person running it.
|
|
15
|
+
|
|
16
|
+
What it never touches: your repositories, your worktrees and your document
|
|
17
|
+
folders. An agent's home holds *links* to those, never copies, and removing a
|
|
18
|
+
link removes a link. That is the one property of this file worth testing, and it
|
|
19
|
+
is tested.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
29
|
+
sys.path.insert(0, AQUI)
|
|
30
|
+
import atajos # noqa: E402 the desktop icons, one per city
|
|
31
|
+
import cities # noqa: E402
|
|
32
|
+
import city_env # noqa: E402 where the bus keeps its folder
|
|
33
|
+
|
|
34
|
+
def _sitios():
|
|
35
|
+
"""Everything on disk that belongs to the product and to nothing else, as
|
|
36
|
+
(what it is, absolute path, whether it holds your cities)."""
|
|
37
|
+
casa = os.path.expanduser("~")
|
|
38
|
+
cache = os.environ.get("XDG_CACHE_HOME") or os.path.join(casa, ".cache")
|
|
39
|
+
return [
|
|
40
|
+
("your cities, their state and their backups", cities.raiz(), True),
|
|
41
|
+
("the list of which cities exist", os.path.dirname(cities.REGISTRO), False),
|
|
42
|
+
("the cached index of your disk", os.path.join(cache, "agents-city"), False),
|
|
43
|
+
("the bus folder, its .env and its hooks", city_env.CANAL, False),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tamano(ruta):
|
|
48
|
+
"""How much is there, without following a single link out of it."""
|
|
49
|
+
if not os.path.exists(ruta):
|
|
50
|
+
return 0
|
|
51
|
+
if os.path.islink(ruta) or os.path.isfile(ruta):
|
|
52
|
+
return 0 if os.path.islink(ruta) else os.path.getsize(ruta)
|
|
53
|
+
total = 0
|
|
54
|
+
for base, dirs, ficheros in os.walk(ruta):
|
|
55
|
+
# A symlinked directory is somebody's repository. It is not ours to
|
|
56
|
+
# measure, and it is certainly not ours to delete.
|
|
57
|
+
dirs[:] = [d for d in dirs if not os.path.islink(os.path.join(base, d))]
|
|
58
|
+
for f in ficheros:
|
|
59
|
+
ruta_f = os.path.join(base, f)
|
|
60
|
+
if os.path.islink(ruta_f):
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
total += os.path.getsize(ruta_f)
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
return total
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _tamano_legible(n):
|
|
70
|
+
unidades = ["B", "KB", "MB", "GB", "TB"]
|
|
71
|
+
i = 0
|
|
72
|
+
valor = float(n)
|
|
73
|
+
while valor >= 1024 and i < len(unidades) - 1:
|
|
74
|
+
valor /= 1024.0
|
|
75
|
+
i += 1
|
|
76
|
+
return f"{valor:.0f} {unidades[i]}" if i == 0 else f"{valor:.1f} {unidades[i]}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def plan(usuario="", conservar_ciudades=False, con_npm=False):
|
|
80
|
+
"""What would be removed, in the order it would be removed."""
|
|
81
|
+
usuario = usuario or cities.usuario_actual()
|
|
82
|
+
pasos = [{"que": "stop every city session, hall and map this product started",
|
|
83
|
+
"clase": "procesos"}]
|
|
84
|
+
for datos in [c["ruta"] for c in cities.lista(usuario)]:
|
|
85
|
+
pasos.append({"que": f"the desktop shortcut for {cities.nombre(datos)}",
|
|
86
|
+
"clase": "atajo", "datos": datos})
|
|
87
|
+
pasos.append({"que": "the Claude plugin registration (city@agents-city)",
|
|
88
|
+
"clase": "plugin"})
|
|
89
|
+
for etiqueta, ruta, es_ciudades in _sitios():
|
|
90
|
+
if es_ciudades and conservar_ciudades:
|
|
91
|
+
continue
|
|
92
|
+
if not os.path.exists(ruta):
|
|
93
|
+
continue
|
|
94
|
+
pasos.append({"que": etiqueta, "clase": "carpeta", "ruta": ruta,
|
|
95
|
+
"bytes": _tamano(ruta)})
|
|
96
|
+
if sys.platform == "darwin":
|
|
97
|
+
pasos.append({"que": "the bus token in the macOS Keychain", "clase": "llavero"})
|
|
98
|
+
if con_npm:
|
|
99
|
+
pasos.append({"que": "the global npm package (agents-city)", "clase": "npm"})
|
|
100
|
+
return pasos
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _quita_carpeta(ruta):
|
|
104
|
+
"""Delete one of ours, following no link out of it.
|
|
105
|
+
|
|
106
|
+
`shutil.rmtree` unlinks a symlinked directory rather than descending into
|
|
107
|
+
it, which is exactly right here: an agent's home is full of links to
|
|
108
|
+
repositories that must survive this untouched.
|
|
109
|
+
"""
|
|
110
|
+
if os.path.islink(ruta):
|
|
111
|
+
os.unlink(ruta)
|
|
112
|
+
return True
|
|
113
|
+
if not os.path.exists(ruta):
|
|
114
|
+
return False
|
|
115
|
+
shutil.rmtree(ruta, ignore_errors=True)
|
|
116
|
+
return not os.path.exists(ruta)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _apaga():
|
|
120
|
+
guion = os.path.join(AQUI, "apaga.py")
|
|
121
|
+
return subprocess.run([sys.executable, guion], capture_output=True, text=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _quita_plugin():
|
|
125
|
+
"""Ask Claude to forget the plugin. Claude's own CLI owns that registry and
|
|
126
|
+
editing its files by hand would be this product reaching into another's."""
|
|
127
|
+
if not shutil.which("claude"):
|
|
128
|
+
return "claude is not on PATH — nothing registered to remove"
|
|
129
|
+
for verbo in ("uninstall", "remove"):
|
|
130
|
+
r = subprocess.run(
|
|
131
|
+
["claude", "plugin", verbo, "city@agents-city", "--scope", "user", "--yes"],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
)
|
|
135
|
+
if r.returncode == 0:
|
|
136
|
+
return "removed"
|
|
137
|
+
return "claude kept it — remove it with: claude plugin uninstall city@agents-city"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _quita_llavero():
|
|
141
|
+
r = subprocess.run(
|
|
142
|
+
["security", "delete-generic-password", "-s", "city@agents-city"],
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
)
|
|
146
|
+
return "removed" if r.returncode == 0 else "nothing stored"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _quita_npm():
|
|
150
|
+
if not shutil.which("npm"):
|
|
151
|
+
return "npm is not on PATH — remove the package yourself"
|
|
152
|
+
r = subprocess.run(["npm", "rm", "-g", "agents-city"], capture_output=True, text=True)
|
|
153
|
+
return "removed" if r.returncode == 0 else (r.stderr or "npm refused").strip().splitlines()[-1]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ejecuta(pasos, salida=print):
|
|
157
|
+
"""Carry out a plan. Each step reports for itself; a step that fails does not
|
|
158
|
+
stop the ones after it, because a half-removed install is the worst outcome
|
|
159
|
+
available."""
|
|
160
|
+
hechos = []
|
|
161
|
+
for paso in pasos:
|
|
162
|
+
clase = paso["clase"]
|
|
163
|
+
if clase == "procesos":
|
|
164
|
+
r = _apaga()
|
|
165
|
+
estado = "closed" if r.returncode == 0 else "some were already gone"
|
|
166
|
+
elif clase == "atajo":
|
|
167
|
+
estado = "removed" if atajos.quita(paso["datos"]) else "none there"
|
|
168
|
+
elif clase == "plugin":
|
|
169
|
+
estado = _quita_plugin()
|
|
170
|
+
elif clase == "carpeta":
|
|
171
|
+
estado = "removed" if _quita_carpeta(paso["ruta"]) else "could not remove"
|
|
172
|
+
elif clase == "llavero":
|
|
173
|
+
estado = _quita_llavero()
|
|
174
|
+
elif clase == "npm":
|
|
175
|
+
estado = _quita_npm()
|
|
176
|
+
else:
|
|
177
|
+
estado = "skipped"
|
|
178
|
+
salida(f" · {paso['que']} — {estado}")
|
|
179
|
+
hechos.append((paso["que"], estado))
|
|
180
|
+
return hechos
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _muestra(pasos, salida=print):
|
|
184
|
+
salida("\n Uninstalling agents-city would remove:\n")
|
|
185
|
+
for paso in pasos:
|
|
186
|
+
tamano = f" ({_tamano_legible(paso['bytes'])})" if paso.get("bytes") else ""
|
|
187
|
+
ruta = f"\n {paso['ruta']}" if paso.get("ruta") else ""
|
|
188
|
+
salida(f" · {paso['que']}{tamano}{ruta}")
|
|
189
|
+
salida(
|
|
190
|
+
"\n It would NOT touch your repositories, your worktrees or your document\n"
|
|
191
|
+
" folders. An agent's home holds links to those, and a link is all that goes.\n"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main(argv=None):
|
|
196
|
+
p = argparse.ArgumentParser(
|
|
197
|
+
prog="agents-city uninstall",
|
|
198
|
+
description="Remove agents-city and everything it wrote on this machine.",
|
|
199
|
+
)
|
|
200
|
+
p.add_argument("--yes", action="store_true", help="actually remove it")
|
|
201
|
+
p.add_argument("--keep-cities", action="store_true",
|
|
202
|
+
help="unwire the machine but leave ~/.agents-city alone")
|
|
203
|
+
p.add_argument("--npm", action="store_true",
|
|
204
|
+
help="also remove the globally installed npm package")
|
|
205
|
+
args = p.parse_args(argv)
|
|
206
|
+
|
|
207
|
+
pasos = plan(conservar_ciudades=args.keep_cities, con_npm=args.npm)
|
|
208
|
+
_muestra(pasos)
|
|
209
|
+
if not args.yes:
|
|
210
|
+
print(" Nothing was removed. Add --yes to go through with it.\n")
|
|
211
|
+
return 0
|
|
212
|
+
print(" Removing:\n")
|
|
213
|
+
ejecuta(pasos)
|
|
214
|
+
if not args.npm:
|
|
215
|
+
print("\n The package itself is still installed. To finish:\n"
|
|
216
|
+
" npm rm -g agents-city")
|
|
217
|
+
print("\n Gone. Thank you for trying it.\n")
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
sys.exit(main())
|