agents-city 0.3.0-beta.21
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 +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2021 -0
- package/README.md +1999 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +116 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +426 -0
- package/bin/logs +3 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1428 -0
- package/bin/setup.py +753 -0
- package/bin/skills +3 -0
- package/bin/test +131 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +206 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +544 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +106 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1386 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1057 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +188 -0
- package/bin/testlib.py +95 -0
- package/bin/tokens.py +14 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +264 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1867 -0
- package/city/web/src/main.ts +2225 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +99 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +231 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +252 -0
- package/plugin/scripts/capabilities.py +197 -0
- package/plugin/scripts/card.py +312 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +526 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +172 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +51 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1349 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +364 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- package/templates/product.md +45 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The local catalogue and identity of Agents City cities.
|
|
3
|
+
|
|
4
|
+
A user owns any number of cities. A managed city lives at::
|
|
5
|
+
|
|
6
|
+
~/.agents-city/<user>/<city>/
|
|
7
|
+
|
|
8
|
+
The application root is a container; it is never a city itself. Each city has
|
|
9
|
+
one stable id, one immutable address slug, one seat/card, its repos, and its own
|
|
10
|
+
roads. The same city can be moved without changing its identity.
|
|
11
|
+
|
|
12
|
+
Old releases wrote the first city directly into ``~/.agents-city``. The first
|
|
13
|
+
v2 command migrates that flat folder to ``<user>/home`` after making a complete
|
|
14
|
+
copy under ``~/.agents-city/.backups``. State and runtime directories stay at
|
|
15
|
+
the application root.
|
|
16
|
+
|
|
17
|
+
This module owns every path and naming decision. Python callers import it; bash
|
|
18
|
+
callers use the CLI, so there is no second implementation to drift.
|
|
19
|
+
"""
|
|
20
|
+
import hashlib
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
import uuid
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Kept as a public variable because tests and embedded callers redirect it. New
|
|
31
|
+
# entries are ``user<TAB>path``; legacy one-column entries remain readable.
|
|
32
|
+
REGISTRO = os.path.expanduser('~/.config/agents-city/cities')
|
|
33
|
+
RESERVADOS = {'state', '.state', '.runtime', '.backups', '.layout-v2'}
|
|
34
|
+
MARCADORES = ('city.yml', 'units.yml', 'parcels.yml')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _real(ruta):
|
|
38
|
+
return os.path.realpath(os.path.expanduser(ruta))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _slug(valor, defecto='city'):
|
|
42
|
+
limpio = re.sub(r'[^a-z0-9-]+', '-', str(valor).lower()).strip('-')
|
|
43
|
+
return limpio or defecto
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def usuario_actual():
|
|
47
|
+
"""The local owner. Explicit env wins; git is the useful default."""
|
|
48
|
+
if os.environ.get('AGENTS_CITY_USER'):
|
|
49
|
+
return _slug(os.environ['AGENTS_CITY_USER'], 'me')
|
|
50
|
+
try:
|
|
51
|
+
correo = subprocess.run(
|
|
52
|
+
['git', 'config', 'user.email'], capture_output=True, text=True,
|
|
53
|
+
timeout=5).stdout.strip()
|
|
54
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
55
|
+
correo = ''
|
|
56
|
+
local = correo.split('@', 1)[0].lower()
|
|
57
|
+
# GitHub's noreply form: 12345678+alice@users.noreply.github.com.
|
|
58
|
+
if '+' in local and local.split('+', 1)[0].isdigit():
|
|
59
|
+
local = local.split('+', 1)[1]
|
|
60
|
+
return _slug(local or os.environ.get('USER', 'me'), 'me')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def raiz():
|
|
64
|
+
return _real(os.environ.get('AGENTS_CITY_HOME', '~/.agents-city'))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def carpeta_usuario(usuario=''):
|
|
68
|
+
return os.path.join(raiz(), _slug(usuario or usuario_actual(), 'me'))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _lee_clave(datos, clave, defecto=''):
|
|
72
|
+
try:
|
|
73
|
+
texto = open(os.path.join(datos, 'city.yml'), encoding='utf-8').read()
|
|
74
|
+
except OSError:
|
|
75
|
+
return defecto
|
|
76
|
+
m = re.search(rf'^{re.escape(clave)}:[ \t]*(.+)$', texto, re.M)
|
|
77
|
+
return m.group(1).strip().strip('"\'') if m else defecto
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def lee_clave(datos, clave, defecto=''):
|
|
81
|
+
"""Read one scalar from city.yml through the shared tiny parser."""
|
|
82
|
+
return _lee_clave(datos, clave, defecto)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _atomico(ruta, texto):
|
|
86
|
+
os.makedirs(os.path.dirname(ruta), exist_ok=True)
|
|
87
|
+
temporal = ruta + f'.tmp-{os.getpid()}'
|
|
88
|
+
with open(temporal, 'w', encoding='utf-8') as f:
|
|
89
|
+
f.write(texto)
|
|
90
|
+
os.replace(temporal, ruta)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def escribe_atomico(ruta, texto):
|
|
94
|
+
"""Write one of the city's small text files atomically."""
|
|
95
|
+
_atomico(ruta, texto)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def pon_clave(datos, clave, valor):
|
|
99
|
+
"""Upsert one scalar in city.yml, atomically, preserving everything else.
|
|
100
|
+
|
|
101
|
+
The city's little truths (seat_yolo, grow_command…) live here rather than
|
|
102
|
+
scattered across shells: one reader, one writer, no drift.
|
|
103
|
+
"""
|
|
104
|
+
ruta = os.path.join(_real(datos), 'city.yml')
|
|
105
|
+
try:
|
|
106
|
+
texto = open(ruta, encoding='utf-8').read()
|
|
107
|
+
except OSError:
|
|
108
|
+
texto = '# Agents City identity. Plain text, safe to version.\n'
|
|
109
|
+
linea = f'{clave}: {valor}'
|
|
110
|
+
patron = re.compile(rf'^{re.escape(clave)}:.*$', re.M)
|
|
111
|
+
if patron.search(texto):
|
|
112
|
+
texto = patron.sub(linea, texto)
|
|
113
|
+
else:
|
|
114
|
+
if texto and not texto.endswith('\n'):
|
|
115
|
+
texto += '\n'
|
|
116
|
+
texto += linea + '\n'
|
|
117
|
+
_atomico(ruta, texto)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def es_ciudad(ruta):
|
|
121
|
+
"""A folder with city metadata, a seat card, or the old map files."""
|
|
122
|
+
if not os.path.isdir(ruta):
|
|
123
|
+
return False
|
|
124
|
+
if any(os.path.exists(os.path.join(ruta, f)) for f in MARCADORES):
|
|
125
|
+
return True
|
|
126
|
+
try:
|
|
127
|
+
return any(f.endswith('.md') and not f.startswith('README')
|
|
128
|
+
for f in os.listdir(ruta))
|
|
129
|
+
except OSError:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def gestionada(datos, usuario=''):
|
|
134
|
+
"""Whether ``datos`` is one of this user's managed city folders."""
|
|
135
|
+
return os.path.dirname(_real(datos)) == _real(carpeta_usuario(usuario))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def asegura_metadata(datos, usuario='', nombre_ciudad='', ident=''):
|
|
139
|
+
"""Give a city a stable id/address without overwriting existing metadata."""
|
|
140
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
141
|
+
datos = _real(datos)
|
|
142
|
+
os.makedirs(datos, exist_ok=True)
|
|
143
|
+
ruta = os.path.join(datos, 'city.yml')
|
|
144
|
+
try:
|
|
145
|
+
texto = open(ruta, encoding='utf-8').read()
|
|
146
|
+
except OSError:
|
|
147
|
+
texto = '# Agents City identity. Plain text, safe to version.\n'
|
|
148
|
+
existente_id = _lee_clave(datos, 'id')
|
|
149
|
+
nombre_ciudad = (nombre_ciudad or _lee_clave(datos, 'name')
|
|
150
|
+
or os.path.basename(datos))
|
|
151
|
+
nombre_ciudad = ' '.join(str(nombre_ciudad).split()) or 'city'
|
|
152
|
+
valores = {
|
|
153
|
+
# An identity already on disk is immutable. ``ident`` is only a seed for
|
|
154
|
+
# a new city (not authority to rewrite an existing one).
|
|
155
|
+
'id': existente_id or ident or f'city_{uuid.uuid4().hex}',
|
|
156
|
+
'name': nombre_ciudad,
|
|
157
|
+
'slug': _lee_clave(datos, 'slug') or _slug(nombre_ciudad),
|
|
158
|
+
'owner': _lee_clave(datos, 'owner') or usuario,
|
|
159
|
+
'layout': _lee_clave(datos, 'layout') or 'personal-v2',
|
|
160
|
+
}
|
|
161
|
+
faltan = [f'{k}: {v}' for k, v in valores.items()
|
|
162
|
+
if not re.search(rf'^{re.escape(k)}:', texto, re.M)]
|
|
163
|
+
if faltan:
|
|
164
|
+
if texto and not texto.endswith('\n'):
|
|
165
|
+
texto += '\n'
|
|
166
|
+
texto += '\n'.join(faltan) + '\n'
|
|
167
|
+
_atomico(ruta, texto)
|
|
168
|
+
caminos = os.path.join(datos, 'roads.json')
|
|
169
|
+
if not os.path.exists(caminos):
|
|
170
|
+
_atomico(caminos, '{\n "version": 1,\n "roads": []\n}\n')
|
|
171
|
+
return valores
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _legacy_plana():
|
|
175
|
+
base = raiz()
|
|
176
|
+
if not os.path.isdir(base):
|
|
177
|
+
return False
|
|
178
|
+
return (any(os.path.exists(os.path.join(base, f)) for f in MARCADORES)
|
|
179
|
+
or any(f.endswith('.md') and not f.startswith('README')
|
|
180
|
+
for f in os.listdir(base)))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _respaldo_libre(base, prefijo):
|
|
184
|
+
"""Return a timestamped backup path that cannot collide in one second."""
|
|
185
|
+
sello = time.strftime('%Y%m%d-%H%M%S')
|
|
186
|
+
candidato = os.path.join(base, f'{prefijo}-{sello}')
|
|
187
|
+
n = 2
|
|
188
|
+
while os.path.exists(candidato):
|
|
189
|
+
candidato = os.path.join(base, f'{prefijo}-{sello}-{n}')
|
|
190
|
+
n += 1
|
|
191
|
+
return candidato, sello
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def migra_legacy(usuario='', anunciar=False):
|
|
195
|
+
"""Move the old flat city to ``<user>/home``, after a full backup.
|
|
196
|
+
|
|
197
|
+
Returns the destination, or ``''`` when there was nothing to migrate. The
|
|
198
|
+
operation is idempotent: once the flat markers are gone it is a no-op.
|
|
199
|
+
"""
|
|
200
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
201
|
+
base = raiz()
|
|
202
|
+
if not _legacy_plana():
|
|
203
|
+
return ''
|
|
204
|
+
# A v2 user folder can already exist after an interrupted first run. It is a
|
|
205
|
+
# container, not part of the old flat city; moving it below itself would be
|
|
206
|
+
# recursive and could swallow another city.
|
|
207
|
+
entradas = [n for n in os.listdir(base)
|
|
208
|
+
if n not in RESERVADOS and n != usuario]
|
|
209
|
+
# A previous partial migration must never overwrite its destination.
|
|
210
|
+
destino = os.path.join(carpeta_usuario(usuario), 'home')
|
|
211
|
+
if os.path.exists(destino) and os.listdir(destino):
|
|
212
|
+
return ''
|
|
213
|
+
|
|
214
|
+
copia, sello = _respaldo_libre(os.path.join(base, '.backups'), 'legacy-flat')
|
|
215
|
+
os.makedirs(copia, exist_ok=False)
|
|
216
|
+
for nombre_ in entradas:
|
|
217
|
+
origen = os.path.join(base, nombre_)
|
|
218
|
+
respaldo = os.path.join(copia, nombre_)
|
|
219
|
+
if os.path.isdir(origen) and not os.path.islink(origen):
|
|
220
|
+
shutil.copytree(origen, respaldo, symlinks=True)
|
|
221
|
+
else:
|
|
222
|
+
shutil.copy2(origen, respaldo, follow_symlinks=False)
|
|
223
|
+
|
|
224
|
+
os.makedirs(destino, exist_ok=True)
|
|
225
|
+
for nombre_ in entradas:
|
|
226
|
+
shutil.move(os.path.join(base, nombre_), os.path.join(destino, nombre_))
|
|
227
|
+
asegura_metadata(destino, usuario, 'home')
|
|
228
|
+
_atomico(os.path.join(base, '.layout-v2'),
|
|
229
|
+
f'migrated: {sello}\nuser: {usuario}\ncity: home\nbackup: {copia}\n')
|
|
230
|
+
if anunciar:
|
|
231
|
+
corto_d = destino.replace(os.path.expanduser('~'), '~')
|
|
232
|
+
corto_b = copia.replace(os.path.expanduser('~'), '~')
|
|
233
|
+
print(f'\n Migrated the old flat city to {corto_d}')
|
|
234
|
+
print(f' Recovery copy: {corto_b}\n')
|
|
235
|
+
return destino
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def crea(usuario, nombre_ciudad, usar=True):
|
|
239
|
+
"""Create a managed city. Existing cities are returned untouched."""
|
|
240
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
241
|
+
slug_ = _slug(nombre_ciudad)
|
|
242
|
+
destino = os.path.join(carpeta_usuario(usuario), slug_)
|
|
243
|
+
if os.path.exists(destino) and os.listdir(destino) and not es_ciudad(destino):
|
|
244
|
+
raise ValueError(f'{destino} exists and is not a city')
|
|
245
|
+
asegura_metadata(destino, usuario, str(nombre_ciudad).strip() or slug_)
|
|
246
|
+
if usar:
|
|
247
|
+
selecciona(usuario, destino)
|
|
248
|
+
return _real(destino)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _registro_para(usuario):
|
|
252
|
+
try:
|
|
253
|
+
lineas = [l.strip() for l in open(REGISTRO, encoding='utf-8') if l.strip()]
|
|
254
|
+
except OSError:
|
|
255
|
+
return []
|
|
256
|
+
fuera = []
|
|
257
|
+
for linea in lineas:
|
|
258
|
+
if '\t' in linea:
|
|
259
|
+
dueno, ruta = linea.split('\t', 1)
|
|
260
|
+
if dueno != usuario:
|
|
261
|
+
continue
|
|
262
|
+
else:
|
|
263
|
+
ruta = linea # v1 registry: implicitly this user
|
|
264
|
+
fuera.append(ruta)
|
|
265
|
+
return fuera
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def registra(datos, usuario=''):
|
|
269
|
+
"""Remember an external city; managed cities are discovered from folders."""
|
|
270
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
271
|
+
r = _real(datos)
|
|
272
|
+
owner = _slug(_lee_clave(r, 'owner'), '') if _lee_clave(r, 'owner') else ''
|
|
273
|
+
if gestionada(r, usuario) or not es_ciudad(r) or (owner and owner != usuario):
|
|
274
|
+
return
|
|
275
|
+
actuales = {_real(x) for x in _registro_para(usuario)}
|
|
276
|
+
if r in actuales:
|
|
277
|
+
return
|
|
278
|
+
os.makedirs(os.path.dirname(REGISTRO), exist_ok=True)
|
|
279
|
+
with open(REGISTRO, 'a', encoding='utf-8') as f:
|
|
280
|
+
f.write(f'{usuario}\t{r}\n')
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def lista(usuario=''):
|
|
284
|
+
"""Every city this user owns or has explicitly registered."""
|
|
285
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
286
|
+
migra_legacy(usuario)
|
|
287
|
+
candidatas = []
|
|
288
|
+
propia = carpeta_usuario(usuario)
|
|
289
|
+
try:
|
|
290
|
+
candidatas += [os.path.join(propia, n) for n in sorted(os.listdir(propia))
|
|
291
|
+
if not n.startswith('.')]
|
|
292
|
+
except OSError:
|
|
293
|
+
pass
|
|
294
|
+
candidatas += _registro_para(usuario)
|
|
295
|
+
fuera, vistos = [], set()
|
|
296
|
+
for candidata in candidatas:
|
|
297
|
+
r = _real(candidata)
|
|
298
|
+
if r in vistos or not es_ciudad(r):
|
|
299
|
+
continue
|
|
300
|
+
owner_en_disco = _lee_clave(r, 'owner')
|
|
301
|
+
if owner_en_disco and _slug(owner_en_disco, 'me') != usuario:
|
|
302
|
+
continue
|
|
303
|
+
vistos.add(r)
|
|
304
|
+
meta = asegura_metadata(r, usuario) if gestionada(r, usuario) else {}
|
|
305
|
+
fuera.append({
|
|
306
|
+
'ruta': r,
|
|
307
|
+
'nombre': nombre(r),
|
|
308
|
+
'slug': slug_ciudad(r),
|
|
309
|
+
'id': meta.get('id') or identidad(r),
|
|
310
|
+
'owner': meta.get('owner') or _lee_clave(r, 'owner', usuario),
|
|
311
|
+
'managed': gestionada(r, usuario),
|
|
312
|
+
})
|
|
313
|
+
return fuera
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _fichero_actual(usuario):
|
|
317
|
+
return os.path.join(carpeta_usuario(usuario), '.current')
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def selecciona(usuario, datos):
|
|
321
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
322
|
+
datos = _real(datos)
|
|
323
|
+
os.makedirs(carpeta_usuario(usuario), exist_ok=True)
|
|
324
|
+
_atomico(_fichero_actual(usuario), datos + '\n')
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def actual(usuario='', crear=True, migrar=True):
|
|
328
|
+
"""The selected city. Explicit ``AGENTS_CITY_DATA`` always wins."""
|
|
329
|
+
explicita = os.environ.get('AGENTS_CITY_DATA')
|
|
330
|
+
if explicita and os.path.isdir(os.path.expanduser(explicita)):
|
|
331
|
+
return _real(explicita)
|
|
332
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
333
|
+
if migrar:
|
|
334
|
+
migra_legacy(usuario)
|
|
335
|
+
try:
|
|
336
|
+
elegida = open(_fichero_actual(usuario), encoding='utf-8').read().strip()
|
|
337
|
+
except OSError:
|
|
338
|
+
elegida = ''
|
|
339
|
+
if elegida and es_ciudad(elegida):
|
|
340
|
+
owner = _lee_clave(elegida, 'owner')
|
|
341
|
+
if not owner or _slug(owner, 'me') == usuario:
|
|
342
|
+
return _real(elegida)
|
|
343
|
+
ciudades = lista(usuario)
|
|
344
|
+
home = next((c for c in ciudades if c['slug'] == 'home'), None)
|
|
345
|
+
if home:
|
|
346
|
+
selecciona(usuario, home['ruta'])
|
|
347
|
+
return home['ruta']
|
|
348
|
+
if ciudades:
|
|
349
|
+
selecciona(usuario, ciudades[0]['ruta'])
|
|
350
|
+
return ciudades[0]['ruta']
|
|
351
|
+
return crea(usuario, 'home') if crear else ''
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def nombre(datos):
|
|
355
|
+
return _lee_clave(_real(datos), 'name') or os.path.basename(_real(datos))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def slug_ciudad(datos):
|
|
359
|
+
return _slug(_lee_clave(_real(datos), 'slug') or nombre(datos))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def identidad(datos):
|
|
363
|
+
valor = _lee_clave(_real(datos), 'id')
|
|
364
|
+
if valor:
|
|
365
|
+
return valor
|
|
366
|
+
return 'legacy_' + hashlib.sha256(_real(datos).encode()).hexdigest()[:16]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def slug(datos):
|
|
370
|
+
"""Stable, filesystem-safe state key; path moves do not change it in v2."""
|
|
371
|
+
return f'{slug_ciudad(datos)}-{identidad(datos).split("_")[-1][:8]}'
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def sesion(usuario, datos):
|
|
375
|
+
"""Every session names both owner and city, including the first one."""
|
|
376
|
+
return f'{_slug(usuario or usuario_actual(), "me")}-{slug_ciudad(datos)}'
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def direccion(usuario, datos):
|
|
380
|
+
"""The city seat's stable road/bus address."""
|
|
381
|
+
owner = _lee_clave(_real(datos), 'owner') or usuario or usuario_actual()
|
|
382
|
+
return f'{_slug(owner, "me")}/{slug_ciudad(datos)}'
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def resuelve(cual, usuario=''):
|
|
386
|
+
"""A path, display name, slug or stable id -> city folder."""
|
|
387
|
+
usuario = _slug(usuario or usuario_actual(), 'me')
|
|
388
|
+
if cual and os.path.isdir(os.path.expanduser(cual)):
|
|
389
|
+
candidata = _real(cual)
|
|
390
|
+
return candidata if es_ciudad(candidata) else ''
|
|
391
|
+
buscada = str(cual or '').lower()
|
|
392
|
+
for ciudad in lista(usuario):
|
|
393
|
+
if buscada in {ciudad['nombre'].lower(), ciudad['slug'].lower(),
|
|
394
|
+
ciudad['id'].lower(), os.path.basename(ciudad['ruta']).lower()}:
|
|
395
|
+
return ciudad['ruta']
|
|
396
|
+
return ''
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _uso():
|
|
400
|
+
return """usage:
|
|
401
|
+
agents-city cities list
|
|
402
|
+
agents-city cities current
|
|
403
|
+
agents-city cities create <name>
|
|
404
|
+
agents-city cities use <name-or-path>
|
|
405
|
+
|
|
406
|
+
Managed cities live at ~/.agents-city/<owner>/<city>/. Creating or selecting one
|
|
407
|
+
never starts its tmux session; use `agents-city seat --city <name>` for that."""
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _cli_help(_args):
|
|
411
|
+
print(_uso())
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _cli_user(_args):
|
|
416
|
+
print(usuario_actual())
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _cli_list(args):
|
|
421
|
+
usuario = args[1] if len(args) > 1 else usuario_actual()
|
|
422
|
+
actual_ = actual(usuario, crear=False)
|
|
423
|
+
for ciudad in lista(usuario):
|
|
424
|
+
marca = '*' if _real(ciudad['ruta']) == _real(actual_) else ' '
|
|
425
|
+
print(f"{marca}\t{ciudad['slug']}\t{ciudad['id']}\t{ciudad['ruta']}")
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _cli_current(args):
|
|
430
|
+
print(actual(args[1] if len(args) > 1 else usuario_actual()))
|
|
431
|
+
return True
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _cli_create(args):
|
|
435
|
+
if len(args) < 2:
|
|
436
|
+
return False
|
|
437
|
+
try:
|
|
438
|
+
print(crea(args[2] if len(args) > 2 else usuario_actual(), args[1]))
|
|
439
|
+
except (OSError, ValueError) as error:
|
|
440
|
+
sys.exit(str(error))
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _cli_use(args):
|
|
445
|
+
if len(args) < 2:
|
|
446
|
+
return False
|
|
447
|
+
usuario = args[2] if len(args) > 2 else usuario_actual()
|
|
448
|
+
datos = resuelve(args[1], usuario)
|
|
449
|
+
if not datos:
|
|
450
|
+
sys.exit(f'No city called {args[1]!r}.')
|
|
451
|
+
selecciona(usuario, datos)
|
|
452
|
+
print(datos)
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _cli_session(args):
|
|
457
|
+
if len(args) != 3:
|
|
458
|
+
return False
|
|
459
|
+
print(sesion(args[1], args[2]))
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _cli_slug(args):
|
|
464
|
+
if len(args) != 2:
|
|
465
|
+
return False
|
|
466
|
+
print(slug(args[1]))
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _cli_id(args):
|
|
471
|
+
if len(args) != 2:
|
|
472
|
+
return False
|
|
473
|
+
print(identidad(args[1]))
|
|
474
|
+
return True
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _cli_address(args):
|
|
478
|
+
if len(args) != 3:
|
|
479
|
+
return False
|
|
480
|
+
print(direccion(args[1], args[2]))
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _cli_key(args):
|
|
485
|
+
# clave <path> <key> — read one city.yml scalar; add <value> to upsert it.
|
|
486
|
+
if len(args) == 3:
|
|
487
|
+
print(_lee_clave(args[1], args[2]))
|
|
488
|
+
return True
|
|
489
|
+
if len(args) == 4:
|
|
490
|
+
pon_clave(args[1], args[2], args[3])
|
|
491
|
+
return True
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _cli_resolve(args):
|
|
496
|
+
if len(args) < 2:
|
|
497
|
+
return False
|
|
498
|
+
print(resuelve(args[1], args[2] if len(args) > 2 else usuario_actual()))
|
|
499
|
+
return True
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def main():
|
|
503
|
+
args = sys.argv[1:]
|
|
504
|
+
orden = args[0] if args else 'list'
|
|
505
|
+
handlers = {
|
|
506
|
+
'-h': _cli_help, '--help': _cli_help, 'help': _cli_help,
|
|
507
|
+
'user': _cli_user, 'usuario': _cli_user,
|
|
508
|
+
'list': _cli_list, 'lista': _cli_list,
|
|
509
|
+
'current': _cli_current, 'create': _cli_create, 'use': _cli_use,
|
|
510
|
+
'sesion': _cli_session, 'session': _cli_session,
|
|
511
|
+
'slug': _cli_slug, 'id': _cli_id, 'address': _cli_address,
|
|
512
|
+
'clave': _cli_key, 'key': _cli_key,
|
|
513
|
+
'resolve': _cli_resolve, 'resuelve': _cli_resolve,
|
|
514
|
+
}
|
|
515
|
+
if not handlers.get(orden, lambda _args: False)(args):
|
|
516
|
+
sys.exit(_uso())
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == '__main__':
|
|
520
|
+
main()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Where this seat's settings come from. Sourced by every hook and script, so
|
|
3
|
+
# there is one answer to "which city, which token, which data" instead of five.
|
|
4
|
+
#
|
|
5
|
+
# Order: the environment wins (that is how the MCP server passes user config),
|
|
6
|
+
# then an optional transport .env, then the keychain for the
|
|
7
|
+
# token alone. Nothing here fails: a hook with no settings has to stay quiet, not
|
|
8
|
+
# break somebody's turn.
|
|
9
|
+
#
|
|
10
|
+
# CITY_BUS_URL / CITY_BUS_TOKEN / CITY_BUS_AGENT the bus
|
|
11
|
+
# AGENTS_CITY_URL the map (optional)
|
|
12
|
+
# AGENTS_CITY_DATA the selected city folder
|
|
13
|
+
# AGENTS_CITY_HOME / AGENTS_CITY_USER local city namespace
|
|
14
|
+
# AGENTS_CITY_ORG only report repos of this org
|
|
15
|
+
#
|
|
16
|
+
# AGENTS_CITY_ORG is a filter, not a requirement: unset means every repo with a
|
|
17
|
+
# remote counts. It exists when one machine holds repos from more than one domain
|
|
18
|
+
# and a city should recognise only one of those sets.
|
|
19
|
+
|
|
20
|
+
CITY_DIR="${CITY_DIR:-$HOME/.claude/channels/city-bus}"
|
|
21
|
+
|
|
22
|
+
if [ -f "$CITY_DIR/.env" ]; then
|
|
23
|
+
# Only the keys we know, and only when not already set: a .env should not be
|
|
24
|
+
# able to redefine PATH.
|
|
25
|
+
while IFS='=' read -r k v; do
|
|
26
|
+
case "$k" in
|
|
27
|
+
CITY_BUS_URL|CITY_BUS_TOKEN|AGENTS_CITY_URL|AGENTS_CITY_DATA|AGENTS_CITY_DATA_DEFAULT|AGENTS_CITY_ORG|AGENTS_CITY_HOME|AGENTS_CITY_USER|CITY_ADDRESS|CITY_SEAT_NAME|CITY_HOOKS)
|
|
28
|
+
v="${v%\"}"; v="${v#\"}"
|
|
29
|
+
[ -z "$(eval "printf '%s' \"\${$k:-}\"")" ] && export "$k=$v" ;;
|
|
30
|
+
esac
|
|
31
|
+
done < "$CITY_DIR/.env"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [ -z "${CITY_BUS_TOKEN:-}" ] && command -v security >/dev/null 2>&1; then
|
|
35
|
+
CITY_BUS_TOKEN="$(security find-generic-password -s city@agents-city -w 2>/dev/null || true)"
|
|
36
|
+
export CITY_BUS_TOKEN
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Delegate the selected-city decision to cities.py, the same owner used by the
|
|
40
|
+
# seat and the hall. A shell copy of the folder rule is exactly how v1 drifted.
|
|
41
|
+
if [ -z "${AGENTS_CITY_DATA:-}" ]; then
|
|
42
|
+
CITY_SCRIPTS="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts"
|
|
43
|
+
if [ -n "${AGENTS_CITY_DATA_DEFAULT:-}" ] && [ -d "${AGENTS_CITY_DATA_DEFAULT}" ]; then
|
|
44
|
+
AGENTS_CITY_DATA="$AGENTS_CITY_DATA_DEFAULT"
|
|
45
|
+
else
|
|
46
|
+
AGENTS_CITY_DATA="$(python3 "$CITY_SCRIPTS/cities.py" current "${AGENTS_CITY_USER:-}" 2>/dev/null || true)"
|
|
47
|
+
fi
|
|
48
|
+
export AGENTS_CITY_DATA
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# The installed plugin is shared; the active city identity is resolved for each
|
|
52
|
+
# seat at runtime instead of being stored as one package-global user/role.
|
|
53
|
+
CITY_SCRIPTS="${CITY_SCRIPTS:-${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts}"
|
|
54
|
+
if [ -z "${AGENTS_CITY_USER:-}" ]; then
|
|
55
|
+
AGENTS_CITY_USER="$(python3 "$CITY_SCRIPTS/cities.py" user 2>/dev/null || true)"
|
|
56
|
+
export AGENTS_CITY_USER
|
|
57
|
+
fi
|
|
58
|
+
if [ -z "${CITY_ADDRESS:-}" ] && [ -n "${AGENTS_CITY_DATA:-}" ]; then
|
|
59
|
+
CITY_ADDRESS="$(python3 "$CITY_SCRIPTS/cities.py" address \
|
|
60
|
+
"${AGENTS_CITY_USER:-me}" "$AGENTS_CITY_DATA" 2>/dev/null || true)"
|
|
61
|
+
export CITY_ADDRESS
|
|
62
|
+
fi
|
|
63
|
+
if [ -n "${CITY_ADDRESS:-}" ]; then
|
|
64
|
+
# A v1 .env may still carry one global CITY_BUS_AGENT. It cannot identify two
|
|
65
|
+
# concurrently running cities, so a resolved v2 city always overrides it.
|
|
66
|
+
CITY_BUS_AGENT="$CITY_ADDRESS"
|
|
67
|
+
export CITY_BUS_AGENT
|
|
68
|
+
fi
|
|
69
|
+
if [ -z "${CITY_SEAT_NAME:-}" ] && [ -n "${AGENTS_CITY_DATA:-}" ]; then
|
|
70
|
+
CITY_SEAT_NAME="$(python3 "$CITY_SCRIPTS/cities.py" session \
|
|
71
|
+
"${AGENTS_CITY_USER:-me}" "$AGENTS_CITY_DATA" 2>/dev/null || true)"
|
|
72
|
+
export CITY_SEAT_NAME
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# The map defaults to the bus: they are the same worker unless somebody split them.
|
|
76
|
+
[ -z "${AGENTS_CITY_URL:-}" ] && export AGENTS_CITY_URL="${CITY_BUS_URL:-}"
|
|
77
|
+
|
|
78
|
+
# Is this repo one this seat reports about? Prints the repo name and returns 0,
|
|
79
|
+
# or returns 1 and prints nothing.
|
|
80
|
+
repo_de_la_ciudad() {
|
|
81
|
+
local remoto repo
|
|
82
|
+
remoto="$(git remote get-url origin 2>/dev/null || true)"
|
|
83
|
+
[ -z "$remoto" ] && return 1
|
|
84
|
+
if [ -n "${AGENTS_CITY_ORG:-}" ]; then
|
|
85
|
+
case "$remoto" in *"/${AGENTS_CITY_ORG}/"*|*":${AGENTS_CITY_ORG}/"*) ;; *) return 1 ;; esac
|
|
86
|
+
fi
|
|
87
|
+
repo="${remoto##*/}"; repo="${repo%.git}"
|
|
88
|
+
[ -z "$repo" ] && return 1
|
|
89
|
+
printf '%s' "$repo"
|
|
90
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Own the city bus processes without adding transport details to city-session.sh.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPTS="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
ROOT="$(cd "$SCRIPTS/.." && pwd)"
|
|
7
|
+
. "$SCRIPTS/city-env.sh"
|
|
8
|
+
CLIENT="$ROOT/channel/client.js"
|
|
9
|
+
ADAPTER="$ROOT/channel/adapter.js"
|
|
10
|
+
GATEWAY="$ROOT/channel/runtime-gateway.js"
|
|
11
|
+
|
|
12
|
+
case "${1:-}" in
|
|
13
|
+
ensure)
|
|
14
|
+
exec node "$CLIENT" ensure
|
|
15
|
+
;;
|
|
16
|
+
gateway)
|
|
17
|
+
actor="${2:-}"
|
|
18
|
+
workdir="${3:-}"
|
|
19
|
+
command_line="${4:-}"
|
|
20
|
+
auto="${5:-1}"
|
|
21
|
+
[ -n "$actor" ] && [ -n "$workdir" ] && [ -n "$command_line" ] || {
|
|
22
|
+
echo "usage: city-runtime.sh gateway <actor> <workdir> <runtime-command> [auto]" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
}
|
|
25
|
+
node "$CLIENT" ensure >/dev/null
|
|
26
|
+
if [ "$actor" = seat ]; then
|
|
27
|
+
road_url="${CITY_BUS_URL:-}"
|
|
28
|
+
road_token="${CITY_BUS_TOKEN:-}"
|
|
29
|
+
runtime_kind="seat"
|
|
30
|
+
else
|
|
31
|
+
road_url=""
|
|
32
|
+
road_token=""
|
|
33
|
+
runtime_kind="repo"
|
|
34
|
+
fi
|
|
35
|
+
exec env \
|
|
36
|
+
CITY_BUS_ACTOR="$actor" \
|
|
37
|
+
CITY_RUNTIME_KIND="$runtime_kind" \
|
|
38
|
+
CITY_RUNTIME_AUTO="$auto" \
|
|
39
|
+
CITY_BUS_URL="$road_url" \
|
|
40
|
+
CITY_BUS_TOKEN="$road_token" \
|
|
41
|
+
node "$GATEWAY" \
|
|
42
|
+
--data "$AGENTS_CITY_DATA" \
|
|
43
|
+
--actor "$actor" \
|
|
44
|
+
--cwd "$workdir" \
|
|
45
|
+
--command "$command_line" \
|
|
46
|
+
--auto "$auto" \
|
|
47
|
+
--interactive 1
|
|
48
|
+
;;
|
|
49
|
+
fallback)
|
|
50
|
+
actor="${2:-}"
|
|
51
|
+
target="${3:-}"
|
|
52
|
+
runtime="${4:-unknown}"
|
|
53
|
+
[ -n "$actor" ] && [ -n "$target" ] || {
|
|
54
|
+
echo "usage: city-runtime.sh fallback <actor> <tmux-target> <runtime>" >&2
|
|
55
|
+
exit 2
|
|
56
|
+
}
|
|
57
|
+
echo "WARNING: $actor uses the explicit terminal fallback; native delivery is unavailable." >&2
|
|
58
|
+
node "$CLIENT" ensure >/dev/null
|
|
59
|
+
runtime_dir="$(node "$CLIENT" runtime-dir)"
|
|
60
|
+
mkdir -p "$runtime_dir/adapters"
|
|
61
|
+
if [ "$actor" = seat ]; then
|
|
62
|
+
road_url="${CITY_BUS_URL:-}"
|
|
63
|
+
road_token="${CITY_BUS_TOKEN:-}"
|
|
64
|
+
runtime_kind="seat"
|
|
65
|
+
else
|
|
66
|
+
road_url=""
|
|
67
|
+
road_token=""
|
|
68
|
+
runtime_kind="repo"
|
|
69
|
+
fi
|
|
70
|
+
nohup env \
|
|
71
|
+
CITY_BUS_ACTOR="$actor" \
|
|
72
|
+
CITY_RUNTIME_KIND="$runtime_kind" \
|
|
73
|
+
CITY_AGENT_RUNTIME="$runtime" \
|
|
74
|
+
CITY_BUS_URL="$road_url" \
|
|
75
|
+
CITY_BUS_TOKEN="$road_token" \
|
|
76
|
+
node "$ADAPTER" \
|
|
77
|
+
--data "$AGENTS_CITY_DATA" \
|
|
78
|
+
--actor "$actor" \
|
|
79
|
+
--target "$target" \
|
|
80
|
+
--runtime "$runtime" \
|
|
81
|
+
>>"$runtime_dir/adapters/$actor.log" 2>&1 </dev/null &
|
|
82
|
+
;;
|
|
83
|
+
*)
|
|
84
|
+
echo "usage: city-runtime.sh <ensure | gateway actor workdir command [auto] | fallback actor tmux-target runtime>" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
;;
|
|
87
|
+
esac
|