agents-city 0.1.0 → 0.2.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/bin/hall.html +103 -54
- package/bin/navegador.mjs +59 -1
- package/bin/serve.py +21 -0
- package/bin/test-navegador.py +8 -1
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +15 -9
- package/city/web/dist-hall/hall.js +514 -13
- package/city/web/src/bienvenida.ts +520 -0
- package/city/web/src/hall.ts +159 -13
- 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/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,
|
|
@@ -267,10 +268,24 @@ const SECCIONES: Array<[string, string]> = [
|
|
|
267
268
|
['red', 'Roads'],
|
|
268
269
|
['committee', 'Committee'],
|
|
269
270
|
['gente', 'Agents & skills'],
|
|
271
|
+
['ciudades', 'Cities'],
|
|
270
272
|
];
|
|
271
273
|
|
|
274
|
+
/** True until the person navigates: the landing section is decided once, from
|
|
275
|
+
* the state, and never yanked out from under them afterwards. */
|
|
276
|
+
let primeraCarga = true;
|
|
277
|
+
|
|
272
278
|
async function refresca(): Promise<void> {
|
|
273
279
|
E = await api<Estado>('/api/estado');
|
|
280
|
+
if (primeraCarga) {
|
|
281
|
+
primeraCarga = false;
|
|
282
|
+
// A city with no seat or no agents has nothing to draw, and "Not drawn
|
|
283
|
+
// yet" is a dead end as a first impression. Start where the next step is.
|
|
284
|
+
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
285
|
+
// A city with no seat and no agents has nothing to show and everything to
|
|
286
|
+
// ask, so it opens the guide instead of a map it cannot draw.
|
|
287
|
+
if (!mia || (E.agents ?? []).length === 0) SECCION = 'bienvenida';
|
|
288
|
+
}
|
|
274
289
|
pinta();
|
|
275
290
|
pintaAvisoDeVersion();
|
|
276
291
|
pintaDemoControles();
|
|
@@ -722,6 +737,7 @@ function rail(): void {
|
|
|
722
737
|
}).join('');
|
|
723
738
|
todos<HTMLElement>('#rail li').forEach((li) => {
|
|
724
739
|
li.onclick = () => {
|
|
740
|
+
document.body.classList.remove('enGuia');
|
|
725
741
|
SECCION = li.dataset.s ?? 'mapa';
|
|
726
742
|
pinta();
|
|
727
743
|
};
|
|
@@ -768,17 +784,134 @@ function pinta(): void {
|
|
|
768
784
|
const VISTAS: Record<string, () => void> = {};
|
|
769
785
|
|
|
770
786
|
// ── overview ─────────────────────────────────────────────────────────────────
|
|
787
|
+
/** The guided first run. Built once and kept, so a half-finished answer
|
|
788
|
+
* survives a repaint. */
|
|
789
|
+
let guia: Bienvenida | null = null;
|
|
790
|
+
|
|
791
|
+
VISTAS.bienvenida = () => {
|
|
792
|
+
document.body.classList.add('enGuia');
|
|
793
|
+
guia ??= new Bienvenida({
|
|
794
|
+
api,
|
|
795
|
+
esc,
|
|
796
|
+
aviso: toast,
|
|
797
|
+
refresca,
|
|
798
|
+
vete: (seccion: string) => {
|
|
799
|
+
document.body.classList.remove('enGuia');
|
|
800
|
+
SECCION = seccion;
|
|
801
|
+
pinta();
|
|
802
|
+
},
|
|
803
|
+
ciudad: E.city_name,
|
|
804
|
+
yo: E.yo,
|
|
805
|
+
datos: corto(E.datos),
|
|
806
|
+
});
|
|
807
|
+
void guia.pinta(q<HTMLElement>('#lienzo'));
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
VISTAS.ciudades = () => {
|
|
811
|
+
const filas = E.ciudades
|
|
812
|
+
.map(
|
|
813
|
+
(c) => `
|
|
814
|
+
<div class="fila ${c.actual ? 'on' : ''}">
|
|
815
|
+
<span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">· open now</b>' : ''}</span>
|
|
816
|
+
<span class="de mono">${esc(corto(c.ruta))}</span>
|
|
817
|
+
<span style="margin-left:auto;display:flex;gap:6px">
|
|
818
|
+
${
|
|
819
|
+
c.actual
|
|
820
|
+
? ''
|
|
821
|
+
: `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
822
|
+
<button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
|
|
823
|
+
data-nombre="${esc(c.nombre)}">archive</button>`
|
|
824
|
+
}
|
|
825
|
+
</span>
|
|
826
|
+
</div>`,
|
|
827
|
+
)
|
|
828
|
+
.join('');
|
|
829
|
+
q('#lienzo').innerHTML = `<div><span class="sub">cities</span>
|
|
830
|
+
<h1 style="margin-top:6px">Your cities</h1>
|
|
831
|
+
<p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
|
|
832
|
+
its own identity, domain, chair, agents and roads. They share nothing unless you build a
|
|
833
|
+
road between them. The one you have open is marked.</p></div>
|
|
834
|
+
<div class="lista"><div class="filas">${filas}</div></div>
|
|
835
|
+
<div class="campos" style="max-width:420px">
|
|
836
|
+
<div class="campo"><label>Start another city</label>
|
|
837
|
+
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book…"></div>
|
|
838
|
+
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
839
|
+
</div>
|
|
840
|
+
<p class="pista">Archiving <b>moves</b> a city into
|
|
841
|
+
<code class="mono">.backups/</code> — nothing is deleted, and you can bring it back with
|
|
842
|
+
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
843
|
+
cannot be archived.</p>`;
|
|
844
|
+
enlaza();
|
|
845
|
+
const campo = q<HTMLInputElement>('#nuevaCiudad');
|
|
846
|
+
q<HTMLButtonElement>('#creaCiudad').onclick = async () => {
|
|
847
|
+
const nombre = campo.value.trim();
|
|
848
|
+
if (!nombre) {
|
|
849
|
+
toast('Give it a name', true);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const r = await api<Respuesta>('/api/ciudades', {
|
|
853
|
+
method: 'POST',
|
|
854
|
+
body: JSON.stringify({ name: nombre }),
|
|
855
|
+
});
|
|
856
|
+
if (!r.ok) {
|
|
857
|
+
toast(r.error ?? 'Could not create it', true);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
toast(`${nombre} created`);
|
|
861
|
+
await refresca();
|
|
862
|
+
};
|
|
863
|
+
todos<HTMLButtonElement>('.archiva').forEach((boton) => {
|
|
864
|
+
boton.onclick = async () => {
|
|
865
|
+
const nombre = boton.dataset.nombre ?? '';
|
|
866
|
+
if (!window.confirm(`Archive ${nombre}? It moves into .backups — nothing is deleted.`))
|
|
867
|
+
return;
|
|
868
|
+
const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
|
|
869
|
+
'/api/ciudad-archiva',
|
|
870
|
+
{ method: 'POST', body: JSON.stringify({ city: boton.dataset.ciudad ?? '' }) },
|
|
871
|
+
);
|
|
872
|
+
if (!r.ok) {
|
|
873
|
+
toast(r.error ?? 'Could not archive it', true);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
toast(`${nombre} archived — it is in ${r.backup ?? '.backups'}`);
|
|
877
|
+
await refresca();
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
};
|
|
881
|
+
|
|
771
882
|
VISTAS.resumen = () => {
|
|
772
883
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
773
884
|
const sinUnidad = E.parcelas.filter((p) => p.unidad === 'none' || p.unidad === 'mine').length;
|
|
885
|
+
const agentes = E.agents ?? [];
|
|
886
|
+
// The order somebody actually does it in: take the chair, say who works
|
|
887
|
+
// here, say what for. The old list asked for "folders" — the vocabulary of
|
|
888
|
+
// the model before agents existed — and never mentioned the roster at all,
|
|
889
|
+
// so a new city could look finished with nobody in it.
|
|
774
890
|
const tareas = [
|
|
775
891
|
{
|
|
776
892
|
ok: !!mia,
|
|
777
893
|
txt: mia
|
|
778
|
-
? `Your
|
|
779
|
-
: 'Take your seat: role
|
|
894
|
+
? `Your seat is taken — you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>`
|
|
895
|
+
: 'Take your seat: the work domain and your role in it',
|
|
780
896
|
ir: 'puesto',
|
|
781
897
|
},
|
|
898
|
+
{
|
|
899
|
+
ok: agentes.length > 0,
|
|
900
|
+
txt:
|
|
901
|
+
agentes.length === 0
|
|
902
|
+
? 'Add the <b>agents</b> that work in this city — each one whole: its kind, its role, and everything it works on'
|
|
903
|
+
: `<b>${agentes.length}</b> agent${agentes.length === 1 ? '' : 's'} in this city: ${esc(
|
|
904
|
+
agentes.map((a) => a.name).join(', '),
|
|
905
|
+
)}`,
|
|
906
|
+
ir: 'gente',
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
910
|
+
txt: agentes.some((a) => (a.mounts ?? []).length)
|
|
911
|
+
? 'Your agents work on real repositories and folders'
|
|
912
|
+
: 'Give an agent something to work on — a repository, a worktree, a folder of documents',
|
|
913
|
+
ir: 'gente',
|
|
914
|
+
},
|
|
782
915
|
{
|
|
783
916
|
ok: !!(mia && mia.goals_defined),
|
|
784
917
|
txt:
|
|
@@ -788,23 +921,33 @@ VISTAS.resumen = () => {
|
|
|
788
921
|
ir: 'puesto',
|
|
789
922
|
},
|
|
790
923
|
{
|
|
791
|
-
ok: E.parcelas.length
|
|
924
|
+
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
792
925
|
txt:
|
|
793
926
|
E.parcelas.length === 0
|
|
794
|
-
? '
|
|
927
|
+
? 'Districts appear on the map once agents have houses'
|
|
795
928
|
: sinUnidad
|
|
796
929
|
? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district — assign them`
|
|
797
930
|
: 'Every house has its district',
|
|
798
931
|
ir: 'barrios',
|
|
799
932
|
},
|
|
800
933
|
];
|
|
934
|
+
const empezando = !mia || agentes.length === 0;
|
|
801
935
|
const sesionArriba = E.tmux.includes(E.sesion);
|
|
802
936
|
q('#lienzo').innerHTML = `
|
|
803
|
-
<div><span class="sub"
|
|
804
|
-
<h1 style="margin-top:6px">${esc(E.city_name)}</h1>
|
|
937
|
+
<div><span class="sub">${empezando ? 'welcome' : 'your city'}</span>
|
|
938
|
+
<h1 style="margin-top:6px">${empezando ? 'Let’s build ' + esc(E.city_name) : esc(E.city_name)}</h1>
|
|
805
939
|
<p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
|
|
806
|
-
|
|
807
|
-
|
|
940
|
+
${
|
|
941
|
+
empezando
|
|
942
|
+
? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
943
|
+
it. Two steps and it is alive: take your seat — the work domain and your role in
|
|
944
|
+
it — then add the agents, each one asked for whole: what kind of work it does,
|
|
945
|
+
its role, everything it works on, the engine that runs it and the skills it
|
|
946
|
+
starts with. Everything below is a plain file in
|
|
947
|
+
<code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>`
|
|
948
|
+
: `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
|
|
949
|
+
counted with <code class="mono">${esc(E.grow || 'nothing yet')}</code>.</p>`
|
|
950
|
+
}</div>
|
|
808
951
|
<div class="cifras">
|
|
809
952
|
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
810
953
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
@@ -1909,7 +2052,8 @@ window.addEventListener('message', (message) => {
|
|
|
1909
2052
|
function temaActual(): 'light' | 'dark' {
|
|
1910
2053
|
const elegido = document.documentElement.getAttribute('data-tema');
|
|
1911
2054
|
if (elegido) return elegido === 'claro' ? 'light' : 'dark';
|
|
1912
|
-
|
|
2055
|
+
// Day is the default now, so only a machine that asks for dark gets it.
|
|
2056
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
1913
2057
|
}
|
|
1914
2058
|
|
|
1915
2059
|
function tema(): void {
|
|
@@ -1921,14 +2065,16 @@ function tema(): void {
|
|
|
1921
2065
|
return ''; // a browser with site data blocked still gets a working page
|
|
1922
2066
|
}
|
|
1923
2067
|
})();
|
|
1924
|
-
const
|
|
2068
|
+
const oscuroDelSistema = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
1925
2069
|
|
|
1926
2070
|
const pon = (elegido: string): void => {
|
|
1927
2071
|
const raiz = document.documentElement;
|
|
1928
2072
|
if (elegido) raiz.setAttribute('data-tema', elegido);
|
|
1929
2073
|
else raiz.removeAttribute('data-tema');
|
|
1930
|
-
const claro = elegido ? elegido === 'claro' :
|
|
1931
|
-
|
|
2074
|
+
const claro = elegido ? elegido === 'claro' : !oscuroDelSistema;
|
|
2075
|
+
// The button says where a click TAKES you, not where you are: a control
|
|
2076
|
+
// labelled with the state you can already see is a riddle.
|
|
2077
|
+
if (boton) boton.textContent = claro ? '☾ Night' : '☀ Day';
|
|
1932
2078
|
const frame = document.querySelector<HTMLIFrameElement>('#cityMapFrame');
|
|
1933
2079
|
frame?.contentWindow?.postMessage(
|
|
1934
2080
|
{ type: 'agents-city-map-theme/1', theme: claro ? 'light' : 'dark' },
|
|
@@ -1941,7 +2087,7 @@ function tema(): void {
|
|
|
1941
2087
|
boton.onclick = () => {
|
|
1942
2088
|
const claroAhora = document.documentElement.getAttribute('data-tema')
|
|
1943
2089
|
? document.documentElement.getAttribute('data-tema') === 'claro'
|
|
1944
|
-
:
|
|
2090
|
+
: !oscuroDelSistema;
|
|
1945
2091
|
const elegido = claroAhora ? 'oscuro' : 'claro';
|
|
1946
2092
|
try {
|
|
1947
2093
|
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.
|
|
4
|
+
"version": "0.2.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",
|
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.
|
|
20103
|
+
{ name: "agents-city-bus", version: "0.2.0" },
|
|
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.
|
|
27
|
+
{ name: 'agents-city-bus', version: '0.2.0' },
|
|
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.
|
|
70
|
+
clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.2.0' },
|
|
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.
|
|
4897
|
+
clientInfo: { name: "agents-city", title: "Agents City", version: "0.2.0" },
|
|
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()]
|