agents-city 0.1.0 → 0.2.1
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 +12 -3
- package/README.md +10 -2
- package/bin/hall.html +113 -62
- package/bin/navegador.mjs +91 -5
- package/bin/serve.py +87 -0
- package/bin/test-cities.py +45 -0
- package/bin/test-navegador.py +8 -1
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +21 -10
- package/city/web/dist-hall/hall.js +605 -32
- package/city/web/src/bienvenida.ts +524 -0
- package/city/web/src/hall.ts +290 -45
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/reset.py +50 -14
package/city/web/src/hall.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* ever serves this file.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { Bienvenida } from './bienvenida';
|
|
14
15
|
import {
|
|
15
16
|
ActivityEvent,
|
|
16
17
|
MAP_ACTIVITY_PROTOCOL,
|
|
@@ -63,6 +64,8 @@ interface Ciudad {
|
|
|
63
64
|
slug?: string;
|
|
64
65
|
id?: string;
|
|
65
66
|
actual: boolean;
|
|
67
|
+
/** How many agents live in it. An agent belongs to exactly one city. */
|
|
68
|
+
agentes?: number;
|
|
66
69
|
}
|
|
67
70
|
interface Road {
|
|
68
71
|
id: string;
|
|
@@ -187,6 +190,16 @@ interface DomainInfo {
|
|
|
187
190
|
name: string;
|
|
188
191
|
summary: string;
|
|
189
192
|
}
|
|
193
|
+
/** What a reset would do, as the server computes it — never guessed here. */
|
|
194
|
+
interface Efectos {
|
|
195
|
+
city: string;
|
|
196
|
+
backup: string;
|
|
197
|
+
agents: number;
|
|
198
|
+
roads: number;
|
|
199
|
+
deliberations: number;
|
|
200
|
+
keeps: string[];
|
|
201
|
+
loses: string[];
|
|
202
|
+
}
|
|
190
203
|
interface RepoLocal {
|
|
191
204
|
nombre: string;
|
|
192
205
|
ruta: string;
|
|
@@ -263,14 +276,33 @@ const SECCIONES: Array<[string, string]> = [
|
|
|
263
276
|
['resumen', 'Overview'],
|
|
264
277
|
['mapa', 'The map'],
|
|
265
278
|
['puesto', 'My seat'],
|
|
266
|
-
['barrios', 'Districts
|
|
279
|
+
['barrios', 'Districts'],
|
|
267
280
|
['red', 'Roads'],
|
|
268
281
|
['committee', 'Committee'],
|
|
269
|
-
['gente', '
|
|
282
|
+
['gente', 'Houses'],
|
|
283
|
+
['ciudades', 'Cities'],
|
|
270
284
|
];
|
|
271
285
|
|
|
286
|
+
/** True until the person navigates: the landing section is decided once, from
|
|
287
|
+
* the state, and never yanked out from under them afterwards. */
|
|
288
|
+
let primeraCarga = true;
|
|
289
|
+
|
|
272
290
|
async function refresca(): Promise<void> {
|
|
273
291
|
E = await api<Estado>('/api/estado');
|
|
292
|
+
if (primeraCarga) {
|
|
293
|
+
primeraCarga = false;
|
|
294
|
+
// A city with no seat or no agents has nothing to draw, and "Not drawn
|
|
295
|
+
// yet" is a dead end as a first impression. Start where the next step is.
|
|
296
|
+
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
297
|
+
// Never land on a dead end. Three cases, in order of what the person needs:
|
|
298
|
+
// nothing set up -> the guide, which asks the questions
|
|
299
|
+
// set up, no map yet -> the overview, which says what is left and how
|
|
300
|
+
// set up, map running -> the map, which is the point of the product
|
|
301
|
+
// Landing on the map view without a map was how "Not drawn yet" became the
|
|
302
|
+
// first thing an owner saw every single morning.
|
|
303
|
+
if (!mia || (E.agents ?? []).length === 0) SECCION = 'bienvenida';
|
|
304
|
+
else if (!E.mapa) SECCION = 'resumen';
|
|
305
|
+
}
|
|
274
306
|
pinta();
|
|
275
307
|
pintaAvisoDeVersion();
|
|
276
308
|
pintaDemoControles();
|
|
@@ -706,22 +738,36 @@ function icono(kind: string): string {
|
|
|
706
738
|
}
|
|
707
739
|
|
|
708
740
|
function rail(): void {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
741
|
+
// Everything above the divider belongs to ONE city — its seat, its agents,
|
|
742
|
+
// its map. Saying so here is the difference between a menu and a place: an
|
|
743
|
+
// agent's workspace and mounts live inside this city's folder, and a flat
|
|
744
|
+
// list of "Cities" and "Agents" as siblings hid that completely.
|
|
745
|
+
const cuantos = (E.agents ?? []).length;
|
|
746
|
+
q('#railCiudad').innerHTML = `<span class="railEtiqueta">you are in</span>
|
|
747
|
+
<b>${esc(E.city_name)}</b>
|
|
748
|
+
<span class="railDe">${esc(E.domain)} · ${cuantos} house${cuantos === 1 ? '' : 's'}</span>`;
|
|
749
|
+
q('#rail').innerHTML = SECCIONES.filter(([id]) => id !== 'ciudades')
|
|
750
|
+
.map(([id, et]) => {
|
|
751
|
+
const n =
|
|
752
|
+
id === 'gente'
|
|
753
|
+
? String(Object.keys(E.skills).length)
|
|
754
|
+
: id === 'committee'
|
|
755
|
+
? String(E.deliberations.length)
|
|
756
|
+
: id === 'red'
|
|
757
|
+
? String(E.roads.length)
|
|
758
|
+
: id === 'barrios'
|
|
759
|
+
? String(E.parcelas.length)
|
|
760
|
+
: '';
|
|
761
|
+
return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}">
|
|
721
762
|
<span>${et}</span>${n ? `<span class="n">${n}</span>` : ''}</li>`;
|
|
722
|
-
|
|
723
|
-
|
|
763
|
+
})
|
|
764
|
+
.join('');
|
|
765
|
+
q('#railOtras').innerHTML =
|
|
766
|
+
`<li class="${SECCION === 'ciudades' ? 'aqui' : ''}" data-s="ciudades">
|
|
767
|
+
<span>All cities</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
768
|
+
todos<HTMLElement>('#rail li,#railOtras li').forEach((li) => {
|
|
724
769
|
li.onclick = () => {
|
|
770
|
+
document.body.classList.remove('enGuia');
|
|
725
771
|
SECCION = li.dataset.s ?? 'mapa';
|
|
726
772
|
pinta();
|
|
727
773
|
};
|
|
@@ -768,17 +814,185 @@ function pinta(): void {
|
|
|
768
814
|
const VISTAS: Record<string, () => void> = {};
|
|
769
815
|
|
|
770
816
|
// ── overview ─────────────────────────────────────────────────────────────────
|
|
817
|
+
/** The guided first run. Built once and kept, so a half-finished answer
|
|
818
|
+
* survives a repaint. */
|
|
819
|
+
let guia: Bienvenida | null = null;
|
|
820
|
+
|
|
821
|
+
VISTAS.bienvenida = () => {
|
|
822
|
+
document.body.classList.add('enGuia');
|
|
823
|
+
guia ??= new Bienvenida({
|
|
824
|
+
api,
|
|
825
|
+
esc,
|
|
826
|
+
aviso: toast,
|
|
827
|
+
refresca,
|
|
828
|
+
vete: (seccion: string) => {
|
|
829
|
+
document.body.classList.remove('enGuia');
|
|
830
|
+
SECCION = seccion;
|
|
831
|
+
pinta();
|
|
832
|
+
},
|
|
833
|
+
ciudad: E.city_name,
|
|
834
|
+
yo: E.yo,
|
|
835
|
+
datos: corto(E.datos),
|
|
836
|
+
});
|
|
837
|
+
void guia.pinta(q<HTMLElement>('#lienzo'));
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
VISTAS.ciudades = () => {
|
|
841
|
+
const filas = E.ciudades
|
|
842
|
+
.map(
|
|
843
|
+
(c) => `
|
|
844
|
+
<div class="fila ${c.actual ? 'on' : ''}">
|
|
845
|
+
<span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">· open now</b>' : ''}</span>
|
|
846
|
+
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? '' : 's'} of its own</span>
|
|
847
|
+
<span class="de mono">${esc(corto(c.ruta))}</span>
|
|
848
|
+
<span style="margin-left:auto;display:flex;gap:6px">
|
|
849
|
+
${
|
|
850
|
+
c.actual
|
|
851
|
+
? ''
|
|
852
|
+
: `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
853
|
+
<button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
|
|
854
|
+
data-nombre="${esc(c.nombre)}">archive</button>`
|
|
855
|
+
}
|
|
856
|
+
</span>
|
|
857
|
+
</div>`,
|
|
858
|
+
)
|
|
859
|
+
.join('');
|
|
860
|
+
q('#lienzo').innerHTML = `<div><span class="sub">cities</span>
|
|
861
|
+
<h1 style="margin-top:6px">Your cities</h1>
|
|
862
|
+
<p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
|
|
863
|
+
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
864
|
+
shared — it stands inside the city that owns it, with its workspace and mounts under that
|
|
865
|
+
city's folder — so two cities can each have a <code class="mono">docs</code> house and
|
|
866
|
+
they are two different workers. They share nothing unless you build a road between
|
|
867
|
+
them.</p></div>
|
|
868
|
+
<div class="lista"><div class="filas">${filas}</div></div>
|
|
869
|
+
<div class="campos" style="max-width:420px">
|
|
870
|
+
<div class="campo"><label>Start another city</label>
|
|
871
|
+
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book…"></div>
|
|
872
|
+
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="peligro">
|
|
875
|
+
<h3>Start this city over</h3>
|
|
876
|
+
<p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
|
|
877
|
+
agents, no committee history, no map. Your repositories and document folders are
|
|
878
|
+
<b>never touched</b> — only the city that points at them.</p>
|
|
879
|
+
<button class="bt malo" id="reiniciaCiudad">Start over…</button>
|
|
880
|
+
</div>
|
|
881
|
+
<p class="pista">Archiving <b>moves</b> a city into
|
|
882
|
+
<code class="mono">.backups/</code> — nothing is deleted, and you can bring it back with
|
|
883
|
+
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
884
|
+
cannot be archived.</p>`;
|
|
885
|
+
enlaza();
|
|
886
|
+
const campo = q<HTMLInputElement>('#nuevaCiudad');
|
|
887
|
+
q<HTMLButtonElement>('#creaCiudad').onclick = async () => {
|
|
888
|
+
const nombre = campo.value.trim();
|
|
889
|
+
if (!nombre) {
|
|
890
|
+
toast('Give it a name', true);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const r = await api<Respuesta>('/api/ciudades', {
|
|
894
|
+
method: 'POST',
|
|
895
|
+
body: JSON.stringify({ name: nombre }),
|
|
896
|
+
});
|
|
897
|
+
if (!r.ok) {
|
|
898
|
+
toast(r.error ?? 'Could not create it', true);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
toast(`${nombre} created`);
|
|
902
|
+
await refresca();
|
|
903
|
+
};
|
|
904
|
+
const reiniciar = document.getElementById('reiniciaCiudad');
|
|
905
|
+
if (reiniciar)
|
|
906
|
+
reiniciar.onclick = async () => {
|
|
907
|
+
// Two round-trips on purpose: the first asks the server what this would
|
|
908
|
+
// actually do, and the person reads it before typing anything. "Are you
|
|
909
|
+
// sure?" is not information; a list of what disappears is.
|
|
910
|
+
const previa = await api<{ preview?: Efectos; error?: string }>('/api/ciudad-reinicia', {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
body: '{}',
|
|
913
|
+
});
|
|
914
|
+
if (!previa.preview) {
|
|
915
|
+
toast(previa.error ?? 'Could not read what a reset would do', true);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const p = previa.preview;
|
|
919
|
+
const texto =
|
|
920
|
+
`Start over ${p.city}?\n\nYou lose:\n` +
|
|
921
|
+
p.loses.map((l) => ` · ${l}`).join('\n') +
|
|
922
|
+
`\n\nRight now that is ${p.agents} agent(s), ${p.roads} road(s) and ` +
|
|
923
|
+
`${p.deliberations} committee act(s).\n\nYou keep:\n` +
|
|
924
|
+
p.keeps.map((l) => ` · ${l}`).join('\n') +
|
|
925
|
+
`\n\nType the city's name to confirm:`;
|
|
926
|
+
const escrito = window.prompt(texto, '');
|
|
927
|
+
if (escrito === null) return;
|
|
928
|
+
if (escrito.trim() !== p.city) {
|
|
929
|
+
toast('That is not the name — nothing was changed', true);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
|
|
933
|
+
'/api/ciudad-reinicia',
|
|
934
|
+
{ method: 'POST', body: JSON.stringify({ confirm: p.city }) },
|
|
935
|
+
);
|
|
936
|
+
if (!r.ok) {
|
|
937
|
+
toast(r.error ?? 'Could not reset it', true);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
toast(`${p.city} starts over — the old one is at ${r.backup ?? 'its backup'}`);
|
|
941
|
+
primeraCarga = true; // land wherever a fresh city should land: the guide
|
|
942
|
+
await refresca();
|
|
943
|
+
};
|
|
944
|
+
todos<HTMLButtonElement>('.archiva').forEach((boton) => {
|
|
945
|
+
boton.onclick = async () => {
|
|
946
|
+
const nombre = boton.dataset.nombre ?? '';
|
|
947
|
+
if (!window.confirm(`Archive ${nombre}? It moves into .backups — nothing is deleted.`))
|
|
948
|
+
return;
|
|
949
|
+
const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
|
|
950
|
+
'/api/ciudad-archiva',
|
|
951
|
+
{ method: 'POST', body: JSON.stringify({ city: boton.dataset.ciudad ?? '' }) },
|
|
952
|
+
);
|
|
953
|
+
if (!r.ok) {
|
|
954
|
+
toast(r.error ?? 'Could not archive it', true);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
toast(`${nombre} archived — it is in ${r.backup ?? '.backups'}`);
|
|
958
|
+
await refresca();
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
};
|
|
962
|
+
|
|
771
963
|
VISTAS.resumen = () => {
|
|
772
964
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
773
965
|
const sinUnidad = E.parcelas.filter((p) => p.unidad === 'none' || p.unidad === 'mine').length;
|
|
966
|
+
const agentes = E.agents ?? [];
|
|
967
|
+
// The order somebody actually does it in: take the chair, say who works
|
|
968
|
+
// here, say what for. The old list asked for "folders" — the vocabulary of
|
|
969
|
+
// the model before agents existed — and never mentioned the roster at all,
|
|
970
|
+
// so a new city could look finished with nobody in it.
|
|
774
971
|
const tareas = [
|
|
775
972
|
{
|
|
776
973
|
ok: !!mia,
|
|
777
974
|
txt: mia
|
|
778
|
-
? `Your
|
|
779
|
-
: 'Take your seat: role
|
|
975
|
+
? `Your seat is taken — you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>`
|
|
976
|
+
: 'Take your seat: the work domain and your role in it',
|
|
780
977
|
ir: 'puesto',
|
|
781
978
|
},
|
|
979
|
+
{
|
|
980
|
+
ok: agentes.length > 0,
|
|
981
|
+
txt:
|
|
982
|
+
agentes.length === 0
|
|
983
|
+
? 'Build the <b>houses</b> of this city — one per worker, each whole: its kind, its role, and everything it works on'
|
|
984
|
+
: `<b>${agentes.length}</b> house${agentes.length === 1 ? '' : 's'} in this city: ${esc(
|
|
985
|
+
agentes.map((a) => a.name).join(', '),
|
|
986
|
+
)}`,
|
|
987
|
+
ir: 'gente',
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
991
|
+
txt: agentes.some((a) => (a.mounts ?? []).length)
|
|
992
|
+
? 'Every house works on something real'
|
|
993
|
+
: 'Give a house something to work on — a repository, a worktree, a folder of documents',
|
|
994
|
+
ir: 'gente',
|
|
995
|
+
},
|
|
782
996
|
{
|
|
783
997
|
ok: !!(mia && mia.goals_defined),
|
|
784
998
|
txt:
|
|
@@ -788,23 +1002,33 @@ VISTAS.resumen = () => {
|
|
|
788
1002
|
ir: 'puesto',
|
|
789
1003
|
},
|
|
790
1004
|
{
|
|
791
|
-
ok: E.parcelas.length
|
|
1005
|
+
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
792
1006
|
txt:
|
|
793
1007
|
E.parcelas.length === 0
|
|
794
|
-
? '
|
|
1008
|
+
? 'Districts appear on the map once agents have houses'
|
|
795
1009
|
: sinUnidad
|
|
796
1010
|
? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district — assign them`
|
|
797
1011
|
: 'Every house has its district',
|
|
798
1012
|
ir: 'barrios',
|
|
799
1013
|
},
|
|
800
1014
|
];
|
|
1015
|
+
const empezando = !mia || agentes.length === 0;
|
|
801
1016
|
const sesionArriba = E.tmux.includes(E.sesion);
|
|
802
1017
|
q('#lienzo').innerHTML = `
|
|
803
|
-
<div><span class="sub"
|
|
804
|
-
<h1 style="margin-top:6px">${esc(E.city_name)}</h1>
|
|
1018
|
+
<div><span class="sub">${empezando ? 'welcome' : 'your city'}</span>
|
|
1019
|
+
<h1 style="margin-top:6px">${empezando ? 'Let’s build ' + esc(E.city_name) : esc(E.city_name)}</h1>
|
|
805
1020
|
<p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
|
|
806
|
-
|
|
807
|
-
|
|
1021
|
+
${
|
|
1022
|
+
empezando
|
|
1023
|
+
? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
1024
|
+
it. Two steps and it is alive: take your seat — the work domain and your role in
|
|
1025
|
+
it — then add the agents, each one asked for whole: what kind of work it does,
|
|
1026
|
+
its role, everything it works on, the engine that runs it and the skills it
|
|
1027
|
+
starts with. Everything below is a plain file in
|
|
1028
|
+
<code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>`
|
|
1029
|
+
: `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
|
|
1030
|
+
counted with <code class="mono">${esc(E.grow || 'nothing yet')}</code>.</p>`
|
|
1031
|
+
}</div>
|
|
808
1032
|
<div class="cifras">
|
|
809
1033
|
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
810
1034
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
@@ -910,12 +1134,23 @@ VISTAS.mapa = () => {
|
|
|
910
1134
|
};
|
|
911
1135
|
return;
|
|
912
1136
|
}
|
|
1137
|
+
const cuantos = (E.agents ?? []).length;
|
|
913
1138
|
q('#lienzo').innerHTML = `<div><span class="sub">the map</span>
|
|
914
|
-
<h1 style="margin-top:6px">
|
|
915
|
-
<p class="prosa" style="margin-top:8px"
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1139
|
+
<h1 style="margin-top:6px">Your city, drawn</h1>
|
|
1140
|
+
<p class="prosa" style="margin-top:8px">${
|
|
1141
|
+
cuantos
|
|
1142
|
+
? `<b>${cuantos}</b> agent${cuantos === 1 ? '' : 's'} live here, and the map shows
|
|
1143
|
+
them as houses that grow with the work they do — with the town hall in the middle
|
|
1144
|
+
when a committee is sitting, and a gate for every road out.`
|
|
1145
|
+
: `The map draws one house per agent. There are none yet, so it would be an empty
|
|
1146
|
+
plot — build one from <b>Houses</b> first.`
|
|
1147
|
+
}</p>
|
|
1148
|
+
<p class="prosa">The map is its own little server, so it is not running until you say so.
|
|
1149
|
+
The first time takes about a minute: it builds the front end and seeds a local database.
|
|
1150
|
+
After that it opens instantly, and it is yours — nothing is uploaded anywhere.</p></div>
|
|
1151
|
+
<div><button class="bt ppal" id="arrancaMapa">${
|
|
1152
|
+
cuantos ? 'Draw my city' : 'Draw it anyway'
|
|
1153
|
+
}</button></div>
|
|
919
1154
|
<p class="cargando" id="mapaEspera" style="display:none">baking the map — first time takes a minute</p>`;
|
|
920
1155
|
q<HTMLButtonElement>('#arrancaMapa').onclick = async () => {
|
|
921
1156
|
q<HTMLButtonElement>('#arrancaMapa').disabled = true;
|
|
@@ -1511,16 +1746,22 @@ VISTAS.gente = () => {
|
|
|
1511
1746
|
</div>`;
|
|
1512
1747
|
};
|
|
1513
1748
|
|
|
1514
|
-
q('#lienzo').innerHTML = `<div><span class="sub">
|
|
1515
|
-
<h1 style="margin-top:6px">
|
|
1516
|
-
<p class="prosa" style="margin-top:8px">A
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1749
|
+
q('#lienzo').innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
|
|
1750
|
+
<h1 style="margin-top:6px">Who lives in ${esc(E.city_name)}</h1>
|
|
1751
|
+
<p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
|
|
1752
|
+
and many houses are a city — it is the same thing the map draws, growing with what that
|
|
1753
|
+
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1754
|
+
here you see their houses.</p>
|
|
1755
|
+
<p class="prosa">They belong to <b>this</b> city and only
|
|
1756
|
+
to it: each one's workspace and its mounts live inside
|
|
1757
|
+
<code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
|
|
1758
|
+
people even if you give them the same names. Every agent is whole here: the kind of work
|
|
1759
|
+
it does, its role, everything it works on — any number of repositories and document
|
|
1760
|
+
folders at once — the engine and effort that run it, and the skills in its own home.</p>
|
|
1761
|
+
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
1762
|
+
skills from live read-only recognition. Changes persist to the card and apply the next
|
|
1763
|
+
time the session opens.</p>
|
|
1764
|
+
<button class="rpgAlta" type="button" id="altaAgente">+ Build a house</button></div>
|
|
1524
1765
|
<div class="fichasRPG">
|
|
1525
1766
|
<div class="fichaRPG rpgSeat">
|
|
1526
1767
|
<div class="rpgCab">${caraRPG(E.avatars?.['seat'])}
|
|
@@ -1662,7 +1903,7 @@ VISTAS.gente = () => {
|
|
|
1662
1903
|
toast(r.error || 'Could not add the agent', true);
|
|
1663
1904
|
return;
|
|
1664
1905
|
}
|
|
1665
|
-
toast(`${nombre.trim()}
|
|
1906
|
+
toast(`${nombre.trim()} has a house now — give it something to work on`);
|
|
1666
1907
|
void refresca();
|
|
1667
1908
|
} catch (e) {
|
|
1668
1909
|
toast(String(e), true);
|
|
@@ -1909,7 +2150,9 @@ window.addEventListener('message', (message) => {
|
|
|
1909
2150
|
function temaActual(): 'light' | 'dark' {
|
|
1910
2151
|
const elegido = document.documentElement.getAttribute('data-tema');
|
|
1911
2152
|
if (elegido) return elegido === 'claro' ? 'light' : 'dark';
|
|
1912
|
-
|
|
2153
|
+
// Day is THE default, full stop: a desktop set to dark does not decide this
|
|
2154
|
+
// for the owner. Only the switch does, and it is remembered.
|
|
2155
|
+
return 'light';
|
|
1913
2156
|
}
|
|
1914
2157
|
|
|
1915
2158
|
function tema(): void {
|
|
@@ -1921,14 +2164,16 @@ function tema(): void {
|
|
|
1921
2164
|
return ''; // a browser with site data blocked still gets a working page
|
|
1922
2165
|
}
|
|
1923
2166
|
})();
|
|
1924
|
-
const
|
|
2167
|
+
const oscuroDelSistema = false; // daylight is the default; only the switch changes it
|
|
1925
2168
|
|
|
1926
2169
|
const pon = (elegido: string): void => {
|
|
1927
2170
|
const raiz = document.documentElement;
|
|
1928
2171
|
if (elegido) raiz.setAttribute('data-tema', elegido);
|
|
1929
2172
|
else raiz.removeAttribute('data-tema');
|
|
1930
|
-
const claro = elegido ? elegido === 'claro' :
|
|
1931
|
-
|
|
2173
|
+
const claro = elegido ? elegido === 'claro' : !oscuroDelSistema;
|
|
2174
|
+
// The button says where a click TAKES you, not where you are: a control
|
|
2175
|
+
// labelled with the state you can already see is a riddle.
|
|
2176
|
+
if (boton) boton.textContent = claro ? '☾ Night' : '☀ Day';
|
|
1932
2177
|
const frame = document.querySelector<HTMLIFrameElement>('#cityMapFrame');
|
|
1933
2178
|
frame?.contentWindow?.postMessage(
|
|
1934
2179
|
{ type: 'agents-city-map-theme/1', theme: claro ? 'light' : 'dark' },
|
|
@@ -1941,7 +2186,7 @@ function tema(): void {
|
|
|
1941
2186
|
boton.onclick = () => {
|
|
1942
2187
|
const claroAhora = document.documentElement.getAttribute('data-tema')
|
|
1943
2188
|
? document.documentElement.getAttribute('data-tema') === 'claro'
|
|
1944
|
-
:
|
|
2189
|
+
: !oscuroDelSistema;
|
|
1945
2190
|
const elegido = claroAhora ? 'oscuro' : 'claro';
|
|
1946
2191
|
try {
|
|
1947
2192
|
localStorage.setItem('hall-tema', elegido);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "city",
|
|
3
3
|
"displayName": "Agents City",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
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",
|
package/plugin/channel/bus.js
CHANGED
|
@@ -20100,7 +20100,7 @@ try {
|
|
|
20100
20100
|
}
|
|
20101
20101
|
var chair = Boolean(context && context.actors[actor]?.role === "chair" && actor === "seat");
|
|
20102
20102
|
var server = new Server(
|
|
20103
|
-
{ name: "agents-city-bus", version: "0.1
|
|
20103
|
+
{ name: "agents-city-bus", version: "0.2.1" },
|
|
20104
20104
|
{
|
|
20105
20105
|
capabilities: {
|
|
20106
20106
|
...channelEnabled ? { experimental: { "claude/channel": {} } } : {},
|
package/plugin/channel/bus.ts
CHANGED
|
@@ -24,7 +24,7 @@ try {
|
|
|
24
24
|
|
|
25
25
|
const chair = Boolean(context && context.actors[actor]?.role === 'chair' && actor === 'seat');
|
|
26
26
|
const server = new Server(
|
|
27
|
-
{ name: 'agents-city-bus', version: '0.1
|
|
27
|
+
{ name: 'agents-city-bus', version: '0.2.1' },
|
|
28
28
|
{
|
|
29
29
|
capabilities: {
|
|
30
30
|
...(channelEnabled ? { experimental: { 'claude/channel': {} } } : {}),
|
|
@@ -67,7 +67,7 @@ export class CodexConnector implements RuntimeConnector {
|
|
|
67
67
|
async (method, params) => this.providerRequest(method, params),
|
|
68
68
|
);
|
|
69
69
|
await this.rpc.request('initialize', {
|
|
70
|
-
clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.1
|
|
70
|
+
clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.2.1' },
|
|
71
71
|
capabilities: { experimentalApi: true },
|
|
72
72
|
});
|
|
73
73
|
await this.rpc.notify('initialized');
|
|
@@ -4894,7 +4894,7 @@ var CodexConnector = class {
|
|
|
4894
4894
|
async (method, params) => this.providerRequest(method, params)
|
|
4895
4895
|
);
|
|
4896
4896
|
await this.rpc.request("initialize", {
|
|
4897
|
-
clientInfo: { name: "agents-city", title: "Agents City", version: "0.1
|
|
4897
|
+
clientInfo: { name: "agents-city", title: "Agents City", version: "0.2.1" },
|
|
4898
4898
|
capabilities: { experimentalApi: true }
|
|
4899
4899
|
});
|
|
4900
4900
|
await this.rpc.notify("initialized");
|
package/plugin/scripts/cities.py
CHANGED
|
@@ -248,6 +248,40 @@ def crea(usuario, nombre_ciudad, usar=True):
|
|
|
248
248
|
return _real(destino)
|
|
249
249
|
|
|
250
250
|
|
|
251
|
+
def archiva(datos, usuario=''):
|
|
252
|
+
"""Take one city out of use, keeping every byte of it.
|
|
253
|
+
|
|
254
|
+
Deliberately NOT a delete. A city is somebody's cards, deliberations and
|
|
255
|
+
map; a product that offers to erase that with one click will eventually
|
|
256
|
+
erase the wrong one. So the folder MOVES into `<user>/.backups/` with a
|
|
257
|
+
timestamp, the registry forgets it, and the selection falls back to another
|
|
258
|
+
city — recoverable with `mv`, which is a sentence a person can act on.
|
|
259
|
+
|
|
260
|
+
Returns the backup path. Refuses a city that is not this owner's managed
|
|
261
|
+
one, and refuses the last one standing: a city list with nothing in it is
|
|
262
|
+
not a state this product knows how to be in.
|
|
263
|
+
"""
|
|
264
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
265
|
+
real = _real(datos)
|
|
266
|
+
if not es_ciudad(real):
|
|
267
|
+
raise ValueError(f'{datos} is not a city')
|
|
268
|
+
if not gestionada(real, usuario):
|
|
269
|
+
raise ValueError('that city lives outside this owner\'s folder; move it by hand')
|
|
270
|
+
restantes = [c for c in lista(usuario) if _real(c['ruta']) != real]
|
|
271
|
+
if not restantes:
|
|
272
|
+
raise ValueError('this is your only city — create another one first')
|
|
273
|
+
base = os.path.join(carpeta_usuario(usuario), '.backups')
|
|
274
|
+
os.makedirs(base, mode=0o700, exist_ok=True)
|
|
275
|
+
destino, _ = _respaldo_libre(base, f'archivada-{nombre(real) or "city"}')
|
|
276
|
+
# The registry only ever held EXTERNAL cities — a managed one is found by
|
|
277
|
+
# walking the owner's folder, so moving it out is the whole removal.
|
|
278
|
+
seleccionada = _real(actual(usuario, crear=False) or '')
|
|
279
|
+
shutil.move(real, destino)
|
|
280
|
+
if seleccionada == real:
|
|
281
|
+
selecciona(usuario, restantes[0]['ruta'])
|
|
282
|
+
return destino
|
|
283
|
+
|
|
284
|
+
|
|
251
285
|
def _registro_para(usuario):
|
|
252
286
|
try:
|
|
253
287
|
lineas = [l.strip() for l in open(REGISTRO, encoding='utf-8') if l.strip()]
|
package/plugin/scripts/reset.py
CHANGED
|
@@ -151,28 +151,64 @@ def reinicia(datos, usuario="", dry_run=False):
|
|
|
151
151
|
return ctx["backup"]
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def _resuelve_todas(pedidas, usuario):
|
|
155
|
+
"""Every requested city as a path, or (None, complaint).
|
|
156
|
+
|
|
157
|
+
`all` means every managed city this owner has. Names resolve one by one and
|
|
158
|
+
a single unknown name aborts the WHOLE run: resetting three of four cities
|
|
159
|
+
and then stopping on a typo is the worst possible outcome of a destructive
|
|
160
|
+
command, so nothing starts until every name is known.
|
|
161
|
+
"""
|
|
162
|
+
if len(pedidas) == 1 and pedidas[0].lower() == "all":
|
|
163
|
+
todas = [c["ruta"] for c in cities.lista(usuario)]
|
|
164
|
+
if not todas:
|
|
165
|
+
return None, "there are no cities here to reset"
|
|
166
|
+
return todas, ""
|
|
167
|
+
fuera, desconocidas = [], []
|
|
168
|
+
for nombre in pedidas:
|
|
169
|
+
datos = cities.resuelve(nombre, usuario)
|
|
170
|
+
if datos:
|
|
171
|
+
if datos not in fuera:
|
|
172
|
+
fuera.append(datos)
|
|
173
|
+
else:
|
|
174
|
+
desconocidas.append(nombre)
|
|
175
|
+
if desconocidas:
|
|
176
|
+
conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
|
|
177
|
+
return None, (f"no city called {', '.join(repr(d) for d in desconocidas)}. "
|
|
178
|
+
f"Known here: {conocidas}. Nothing was reset.")
|
|
179
|
+
return fuera, ""
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
def main():
|
|
155
183
|
ap = argparse.ArgumentParser(
|
|
156
|
-
description="Reset
|
|
184
|
+
description="Reset cities to onboarding; repos stay untouched and data is backed up"
|
|
157
185
|
)
|
|
158
|
-
ap.add_argument("city",
|
|
186
|
+
ap.add_argument("city", nargs="+",
|
|
187
|
+
help="one or more city names or paths, space separated — or `all`")
|
|
159
188
|
ap.add_argument("--dry-run", action="store_true", help="show every effect, change nothing")
|
|
160
189
|
a = ap.parse_args()
|
|
161
190
|
usuario = cities.usuario_actual()
|
|
162
|
-
|
|
163
|
-
if not
|
|
164
|
-
|
|
165
|
-
print(f" No city called {a.city!r}. Known here: {conocidas}.", file=sys.stderr)
|
|
166
|
-
return 1
|
|
167
|
-
try:
|
|
168
|
-
backup = reinicia(datos, usuario, a.dry_run)
|
|
169
|
-
except ValueError as e:
|
|
170
|
-
print(f" {e}", file=sys.stderr)
|
|
191
|
+
objetivo, queja = _resuelve_todas(a.city, usuario)
|
|
192
|
+
if not objetivo:
|
|
193
|
+
print(f" {queja}", file=sys.stderr)
|
|
171
194
|
return 1
|
|
195
|
+
# More than one city at once is a bigger gesture than the command's name
|
|
196
|
+
# suggests, so it says out loud what it is about to do before doing it.
|
|
197
|
+
if len(objetivo) > 1 and not a.dry_run:
|
|
198
|
+
print(f" Resetting {len(objetivo)} cities: "
|
|
199
|
+
f"{', '.join(cities.slug_ciudad(d) for d in objetivo)}")
|
|
200
|
+
for datos in objetivo:
|
|
201
|
+
try:
|
|
202
|
+
backup = reinicia(datos, usuario, a.dry_run)
|
|
203
|
+
except ValueError as e:
|
|
204
|
+
print(f" {cities.slug_ciudad(datos)}: {e}", file=sys.stderr)
|
|
205
|
+
return 1
|
|
206
|
+
if not a.dry_run:
|
|
207
|
+
corto = backup.replace(os.path.expanduser("~"), "~")
|
|
208
|
+
print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
|
|
172
209
|
if not a.dry_run:
|
|
173
|
-
|
|
174
|
-
print(f"
|
|
175
|
-
print(f" Run `agents-city seat --city {cities.slug_ciudad(datos)}` for onboarding.")
|
|
210
|
+
primera = cities.slug_ciudad(objetivo[0])
|
|
211
|
+
print(f" Run `agents-city seat --city {primera}` for onboarding.")
|
|
176
212
|
return 0
|
|
177
213
|
|
|
178
214
|
|