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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "city-channel",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
7
|
+
"ws": "^8.18.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "esbuild bus.ts local-hub.ts client.ts adapter.ts runtime-gateway.ts --bundle --platform=node --target=node22 --format=esm --outdir=. --outbase=. --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); // Generated from TypeScript; do not edit. npm run build\"",
|
|
11
|
+
"typecheck": "tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler --skipLibCheck *.ts committee/*.ts hub/*.ts runtime/*.ts"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^22.0.0",
|
|
15
|
+
"@types/ws": "^8.18.0",
|
|
16
|
+
"esbuild": "^0.24.0",
|
|
17
|
+
"typescript": "^5.7.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Shared wire contract for every Agents City transport and runtime adapter. */
|
|
2
|
+
|
|
3
|
+
export const BUS_PROTOCOL = 'agents-city-bus/2' as const;
|
|
4
|
+
export const MAX_BODY = 64_000;
|
|
5
|
+
export const MESSAGE_TTL_MS = 72 * 60 * 60 * 1000;
|
|
6
|
+
export const MAX_PENDING = 200;
|
|
7
|
+
|
|
8
|
+
export const CITY_ADDRESS_RE = /^[a-z0-9][a-z0-9_-]{0,63}\/[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9
|
+
export const ACTOR_RE = /^(?:seat|[a-z0-9][a-z0-9-]{0,79})$/;
|
|
10
|
+
export const THREAD_RE = /^delib_[a-z0-9][a-z0-9_-]{5,79}$/;
|
|
11
|
+
|
|
12
|
+
export type ActorRole = 'chair' | 'member';
|
|
13
|
+
export type EnvelopeScope = 'internal' | 'road';
|
|
14
|
+
|
|
15
|
+
export interface BusEnvelope {
|
|
16
|
+
protocol: typeof BUS_PROTOCOL;
|
|
17
|
+
id: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
scope: EnvelopeScope;
|
|
20
|
+
thread: string | null;
|
|
21
|
+
from: { city: string; actor: string; role: ActorRole | 'external-seat' };
|
|
22
|
+
to: { city: string; actor: string };
|
|
23
|
+
createdAt: string;
|
|
24
|
+
payload: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Road {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
owner: string;
|
|
31
|
+
address: string;
|
|
32
|
+
local?: boolean;
|
|
33
|
+
domain?: string;
|
|
34
|
+
role?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ActorCredential {
|
|
38
|
+
actor: string;
|
|
39
|
+
role: ActorRole;
|
|
40
|
+
repo?: string;
|
|
41
|
+
token: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HubEndpoint {
|
|
45
|
+
protocol: typeof BUS_PROTOCOL;
|
|
46
|
+
cityId: string;
|
|
47
|
+
cityAddress: string;
|
|
48
|
+
dataDir: string;
|
|
49
|
+
url: string;
|
|
50
|
+
pid: number;
|
|
51
|
+
startedAt: string;
|
|
52
|
+
roadToken: string;
|
|
53
|
+
/** Read-only browser credential, rotated whenever the local hub restarts. */
|
|
54
|
+
spectatorToken: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const isoNow = (): string => new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
export function randomId(prefix: string): string {
|
|
60
|
+
return `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function safeSegment(value: string, fallback = 'actor'): string {
|
|
64
|
+
const out = String(value || '')
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
67
|
+
.replace(/^-+|-+$/g, '')
|
|
68
|
+
.slice(0, 80);
|
|
69
|
+
return out || fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function asObject(value: unknown, label = 'payload'): Record<string, unknown> {
|
|
73
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
74
|
+
throw new Error(`${label} must be a JSON object`);
|
|
75
|
+
}
|
|
76
|
+
return value as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function text(value: unknown, label: string, required = true): string {
|
|
80
|
+
const out = typeof value === 'string' ? value.trim() : '';
|
|
81
|
+
if (required && !out) throw new Error(`${label} is required`);
|
|
82
|
+
if (out.length > MAX_BODY) throw new Error(`${label} is too large`);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function strings(value: unknown, label: string, required = false): string[] {
|
|
87
|
+
const raw = value === undefined || value === null ? [] : Array.isArray(value) ? value : [value];
|
|
88
|
+
const out = raw.map((v) => text(v, label)).filter(Boolean);
|
|
89
|
+
if (required && !out.length) throw new Error(`${label} needs at least one value`);
|
|
90
|
+
return [...new Set(out)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function oneOf<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
|
|
94
|
+
if (!allowed.includes(value as T)) {
|
|
95
|
+
throw new Error(`${label} must be one of: ${allowed.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
return value as T;
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { busCommand } from './hub-client.js';
|
|
2
|
+
|
|
3
|
+
export async function roadBusCli(args: string[]): Promise<unknown> {
|
|
4
|
+
const verb = args.shift() || 'roster';
|
|
5
|
+
if (['help', '-h', '--help'].includes(verb)) {
|
|
6
|
+
return {
|
|
7
|
+
usage: 'agents-city bus <roster | inbox | send owner/city text>',
|
|
8
|
+
note: 'Only the seat can cross a road; repo agents are rejected by the bus ACL.',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (verb === 'roster') return busCommand('road.roster');
|
|
12
|
+
if (verb === 'inbox') return busCommand('road.inbox');
|
|
13
|
+
if (verb !== 'send') throw new Error(`unknown bus command: ${verb}`);
|
|
14
|
+
const to = args.shift();
|
|
15
|
+
const text = args.join(' ');
|
|
16
|
+
if (!to || !text) throw new Error('bus send needs a destination and text');
|
|
17
|
+
return busCommand('road.send', { to, text });
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start the channel with the Node runtime required by the npm package.
|
|
3
|
+
# Bun can execute the bundled MCP process, but Bun 1.2.x does not complete the
|
|
4
|
+
# `ws` server handshake used by the detached local hub. A health check then
|
|
5
|
+
# passes while every authenticated WebSocket client hangs. Do not silently
|
|
6
|
+
# select a different JavaScript runtime for this transport boundary.
|
|
7
|
+
RAIZ="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
# Resolve the selected city before starting the shared plugin channel. Two local
|
|
9
|
+
# cities therefore get two addresses and two road graphs from one installation.
|
|
10
|
+
. "${CLAUDE_PLUGIN_ROOT:-$(cd "$RAIZ/.." && pwd)}/scripts/city-env.sh"
|
|
11
|
+
if command -v node >/dev/null 2>&1; then
|
|
12
|
+
exec node "$RAIZ/bus.js"
|
|
13
|
+
fi
|
|
14
|
+
echo "[city-bus] I need Node 22 or newer on PATH to run the channel." >&2
|
|
15
|
+
exit 1
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { ChildProcess, spawn } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { BusEnvelope } from '../protocol.js';
|
|
4
|
+
import { NativeAcceptance } from '../runtime-metrics.js';
|
|
5
|
+
import { commandWords, hasOption } from './command.js';
|
|
6
|
+
import { terminate, wait } from './process.js';
|
|
7
|
+
import { ConnectorOptions, RuntimeConnector } from './types.js';
|
|
8
|
+
|
|
9
|
+
interface ClaudeTurn {
|
|
10
|
+
uuid: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
envelopeId: string;
|
|
13
|
+
thread: string;
|
|
14
|
+
assistant: string[];
|
|
15
|
+
acknowledged: boolean;
|
|
16
|
+
completed: boolean;
|
|
17
|
+
timer: NodeJS.Timeout;
|
|
18
|
+
resolveAcceptance: (at: string) => void;
|
|
19
|
+
rejectAcceptance: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One persistent Claude Code process speaking the documented Agent SDK JSONL
|
|
24
|
+
* protocol over stdin/stdout. This is deliberately not a custom Channel:
|
|
25
|
+
* personal Pro/Max accounts can use it without an organisation allowlist,
|
|
26
|
+
* managed settings, a development warning, clipboard access or tmux input.
|
|
27
|
+
*/
|
|
28
|
+
export class ClaudeConnector implements RuntimeConnector {
|
|
29
|
+
readonly runtime = 'claude' as const;
|
|
30
|
+
readonly transport = 'claude-stream-json';
|
|
31
|
+
private child: ChildProcess | null = null;
|
|
32
|
+
private stdoutBuffer = '';
|
|
33
|
+
private stderrTail = '';
|
|
34
|
+
private sessionId = '';
|
|
35
|
+
private turns: ClaudeTurn[] = [];
|
|
36
|
+
private closing = false;
|
|
37
|
+
private ready = false;
|
|
38
|
+
private fatalError: Error | null = null;
|
|
39
|
+
private startupUuid = '';
|
|
40
|
+
private resolveStartup: (() => void) | null = null;
|
|
41
|
+
private rejectStartup: ((error: Error) => void) | null = null;
|
|
42
|
+
|
|
43
|
+
constructor(private readonly options: ConnectorOptions) {}
|
|
44
|
+
|
|
45
|
+
async start(): Promise<void> {
|
|
46
|
+
const words = commandWords(this.options.command);
|
|
47
|
+
const executable = words.shift() || 'claude';
|
|
48
|
+
const args = streamArguments(words, this.options);
|
|
49
|
+
const child = spawn(executable, args, {
|
|
50
|
+
cwd: this.options.cwd,
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
CITY_CLAUDE_CHANNEL: '0',
|
|
54
|
+
CITY_CLAUDE_STREAM_GATEWAY: '1',
|
|
55
|
+
},
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
this.child = child;
|
|
59
|
+
child.stdout?.on('data', (chunk) => this.output(String(chunk)));
|
|
60
|
+
child.stderr?.on('data', (chunk) => this.errorOutput(String(chunk)));
|
|
61
|
+
child.once('close', (code, signal) => this.exited(code, signal));
|
|
62
|
+
|
|
63
|
+
await new Promise<void>((resolve, reject) => {
|
|
64
|
+
const started = () => {
|
|
65
|
+
child.off('error', failed);
|
|
66
|
+
resolve();
|
|
67
|
+
};
|
|
68
|
+
const failed = (error: Error) => {
|
|
69
|
+
child.off('spawn', started);
|
|
70
|
+
reject(new Error(`could not start Claude Code: ${error.message}`));
|
|
71
|
+
};
|
|
72
|
+
child.once('spawn', started);
|
|
73
|
+
child.once('error', failed);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// A stream-json process initializes lazily on its first input. Warm it with
|
|
77
|
+
// documented shouldQuery=false context: Claude replays the message but makes
|
|
78
|
+
// no model call. This moves CLI/plugin startup out of the first real task and
|
|
79
|
+
// proves the stdin/stdout contract before the actor is announced online.
|
|
80
|
+
this.startupUuid = randomUUID();
|
|
81
|
+
const initialized = new Promise<void>((resolve, reject) => {
|
|
82
|
+
this.resolveStartup = resolve;
|
|
83
|
+
this.rejectStartup = reject;
|
|
84
|
+
});
|
|
85
|
+
const startupTimer = setTimeout(() => {
|
|
86
|
+
this.rejectStartup?.(new Error('Claude Code stream initialization timed out'));
|
|
87
|
+
}, startupTimeoutMs());
|
|
88
|
+
try {
|
|
89
|
+
await new Promise<void>((resolve, reject) => {
|
|
90
|
+
child.stdin?.write(
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
type: 'user',
|
|
93
|
+
uuid: this.startupUuid,
|
|
94
|
+
message: {
|
|
95
|
+
role: 'user',
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text:
|
|
100
|
+
`Agents City transport is ready for ${this.options.actor}. ` +
|
|
101
|
+
'Keep this as context without acting; wait for the next message.',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
parent_tool_use_id: null,
|
|
106
|
+
isSynthetic: true,
|
|
107
|
+
shouldQuery: false,
|
|
108
|
+
}) + '\n',
|
|
109
|
+
(error) => (error ? reject(error) : resolve()),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
await initialized;
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(startupTimer);
|
|
115
|
+
this.resolveStartup = null;
|
|
116
|
+
this.rejectStartup = null;
|
|
117
|
+
}
|
|
118
|
+
if (this.fatalError) throw this.fatalError;
|
|
119
|
+
this.ready = true;
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`[city-gateway:${this.options.actor}] Claude Code ready over persistent stream-json\n`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async accept(prompt: string, envelope: BusEnvelope): Promise<NativeAcceptance> {
|
|
126
|
+
const child = this.child;
|
|
127
|
+
if (!this.ready || !child?.stdin?.writable) {
|
|
128
|
+
throw this.fatalError || new Error('Claude stream connector is not ready');
|
|
129
|
+
}
|
|
130
|
+
const uuid = randomUUID();
|
|
131
|
+
let resolveAcceptance: (at: string) => void = () => {};
|
|
132
|
+
let rejectAcceptance: (error: Error) => void = () => {};
|
|
133
|
+
const accepted = new Promise<string>((resolve, reject) => {
|
|
134
|
+
resolveAcceptance = resolve;
|
|
135
|
+
rejectAcceptance = reject;
|
|
136
|
+
});
|
|
137
|
+
const turn: ClaudeTurn = {
|
|
138
|
+
uuid,
|
|
139
|
+
prompt,
|
|
140
|
+
envelopeId: envelope.id,
|
|
141
|
+
thread: envelope.thread || envelope.id,
|
|
142
|
+
assistant: [],
|
|
143
|
+
acknowledged: false,
|
|
144
|
+
completed: false,
|
|
145
|
+
timer: setTimeout(() => {
|
|
146
|
+
const error = new Error(
|
|
147
|
+
`Claude Code did not acknowledge ${envelope.id} over stream-json in time`,
|
|
148
|
+
);
|
|
149
|
+
this.rejectTurn(turn, error);
|
|
150
|
+
this.fail(error);
|
|
151
|
+
}, acknowledgementTimeoutMs()),
|
|
152
|
+
resolveAcceptance,
|
|
153
|
+
rejectAcceptance,
|
|
154
|
+
};
|
|
155
|
+
this.turns.push(turn);
|
|
156
|
+
const input = {
|
|
157
|
+
type: 'user',
|
|
158
|
+
uuid,
|
|
159
|
+
message: {
|
|
160
|
+
role: 'user',
|
|
161
|
+
content: [{ type: 'text', text: prompt }],
|
|
162
|
+
},
|
|
163
|
+
parent_tool_use_id: null,
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
await new Promise<void>((resolve, reject) => {
|
|
167
|
+
child.stdin?.write(JSON.stringify(input) + '\n', (error) => {
|
|
168
|
+
if (error) reject(error);
|
|
169
|
+
else resolve();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.rejectTurn(turn, error as Error);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const acceptedAt = await accepted;
|
|
177
|
+
return {
|
|
178
|
+
acceptedAt,
|
|
179
|
+
runtime: this.runtime,
|
|
180
|
+
transport: this.transport,
|
|
181
|
+
providerRequestId: uuid,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async close(): Promise<void> {
|
|
186
|
+
this.closing = true;
|
|
187
|
+
const child = this.child;
|
|
188
|
+
this.child = null;
|
|
189
|
+
for (const turn of this.turns) {
|
|
190
|
+
if (!turn.completed) this.rejectTurn(turn, new Error('Claude stream connector stopped'));
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
child?.stdin?.end();
|
|
194
|
+
} catch {}
|
|
195
|
+
if (child && child.exitCode === null) {
|
|
196
|
+
await Promise.race([
|
|
197
|
+
new Promise<void>((resolve) => child.once('exit', () => resolve())),
|
|
198
|
+
wait(400),
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
await terminate(child);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private output(chunk: string): void {
|
|
205
|
+
this.stdoutBuffer += chunk.replaceAll('\r\n', '\n');
|
|
206
|
+
let boundary = this.stdoutBuffer.indexOf('\n');
|
|
207
|
+
while (boundary >= 0) {
|
|
208
|
+
const line = this.stdoutBuffer.slice(0, boundary).trim();
|
|
209
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(boundary + 1);
|
|
210
|
+
if (line) this.message(line);
|
|
211
|
+
boundary = this.stdoutBuffer.indexOf('\n');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private message(line: string): void {
|
|
216
|
+
let message: Record<string, unknown>;
|
|
217
|
+
try {
|
|
218
|
+
message = JSON.parse(line) as Record<string, unknown>;
|
|
219
|
+
} catch {
|
|
220
|
+
this.diagnostic('claude.stream.invalid-json', {
|
|
221
|
+
outcome: 'ignored',
|
|
222
|
+
message: line.slice(0, 240),
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const type = String(message.type || '');
|
|
227
|
+
if (type === 'system' && message.subtype === 'init') {
|
|
228
|
+
this.sessionId = String(message.session_id || '');
|
|
229
|
+
const model = String(message.model || 'default model');
|
|
230
|
+
this.resolveStartup?.();
|
|
231
|
+
process.stdout.write(
|
|
232
|
+
`[claude:${this.options.actor}] session ${this.sessionId || 'ready'} · ${model}\n`,
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (type === 'user') {
|
|
237
|
+
this.acknowledge(message);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (type === 'assistant') {
|
|
241
|
+
this.assistant(message);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (type === 'result') this.result(message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private acknowledge(message: Record<string, unknown>): void {
|
|
248
|
+
const uuid = String(message.uuid || '');
|
|
249
|
+
if (uuid && uuid === this.startupUuid) {
|
|
250
|
+
this.resolveStartup?.();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const replayedText = userText(message);
|
|
254
|
+
const turn =
|
|
255
|
+
this.turns.find((candidate) => !candidate.acknowledged && candidate.uuid === uuid) ||
|
|
256
|
+
this.turns.find((candidate) => !candidate.acknowledged && replayedText === candidate.prompt);
|
|
257
|
+
if (!turn) return;
|
|
258
|
+
turn.acknowledged = true;
|
|
259
|
+
clearTimeout(turn.timer);
|
|
260
|
+
const acceptedAt = new Date().toISOString();
|
|
261
|
+
turn.resolveAcceptance(acceptedAt);
|
|
262
|
+
this.diagnostic('claude.stream.acknowledged', {
|
|
263
|
+
thread: turn.thread,
|
|
264
|
+
outcome: 'accepted',
|
|
265
|
+
providerRequestId: turn.uuid,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private assistant(message: Record<string, unknown>): void {
|
|
270
|
+
const turn = this.currentTurn();
|
|
271
|
+
if (!turn) return;
|
|
272
|
+
const body = object(message.message);
|
|
273
|
+
const blocks = Array.isArray(body.content) ? body.content : [];
|
|
274
|
+
for (const raw of blocks) {
|
|
275
|
+
const block = object(raw);
|
|
276
|
+
if (block.type === 'text') {
|
|
277
|
+
const value = String(block.text || '').trim();
|
|
278
|
+
if (!value) continue;
|
|
279
|
+
turn.assistant.push(value);
|
|
280
|
+
process.stdout.write(`\n${value}\n`);
|
|
281
|
+
} else if (block.type === 'tool_use') {
|
|
282
|
+
process.stdout.write(` · ${String(block.name || 'tool')}\n`);
|
|
283
|
+
}
|
|
284
|
+
// Deliberately ignore thinking blocks: private reasoning never enters the
|
|
285
|
+
// terminal transcript, the bus logs or the spectator feed.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private result(message: Record<string, unknown>): void {
|
|
290
|
+
const turn = this.currentTurn();
|
|
291
|
+
if (!turn) return;
|
|
292
|
+
const failed = Boolean(message.is_error) || message.subtype !== 'success';
|
|
293
|
+
const direct = String(message.result || '').trim();
|
|
294
|
+
if (direct && !turn.assistant.includes(direct)) {
|
|
295
|
+
turn.assistant.push(direct);
|
|
296
|
+
process.stdout.write(`\n${direct}\n`);
|
|
297
|
+
}
|
|
298
|
+
if (!turn.acknowledged) {
|
|
299
|
+
const details = resultError(message);
|
|
300
|
+
this.rejectTurn(turn, new Error(details));
|
|
301
|
+
}
|
|
302
|
+
const summary = (direct || turn.assistant.at(-1) || '').trim().slice(0, 4_000);
|
|
303
|
+
if (failed) {
|
|
304
|
+
const details = resultError(message);
|
|
305
|
+
process.stderr.write(`[claude:${this.options.actor}] ${details}\n`);
|
|
306
|
+
this.options.onActivity?.({
|
|
307
|
+
sourceId: `claude-stream:${this.sessionId || 'session'}:${turn.envelopeId}:error`,
|
|
308
|
+
kind: 'runtime.turn.failed',
|
|
309
|
+
thread: turn.thread,
|
|
310
|
+
phase: 'failed',
|
|
311
|
+
tone: 'error',
|
|
312
|
+
title: `${this.options.actor} runtime failed`,
|
|
313
|
+
summary: details,
|
|
314
|
+
});
|
|
315
|
+
} else if (summary) {
|
|
316
|
+
this.options.onActivity?.({
|
|
317
|
+
sourceId: `claude-stream:${this.sessionId || 'session'}:${turn.envelopeId}:answer`,
|
|
318
|
+
kind: 'conversation.agent',
|
|
319
|
+
thread: turn.thread,
|
|
320
|
+
phase: 'answered',
|
|
321
|
+
tone: 'evidence',
|
|
322
|
+
title: `${this.options.actor} answered`,
|
|
323
|
+
summary,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
turn.completed = true;
|
|
327
|
+
clearTimeout(turn.timer);
|
|
328
|
+
this.turns = this.turns.filter((candidate) => candidate !== turn);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private currentTurn(): ClaudeTurn | undefined {
|
|
332
|
+
return this.turns.find((turn) => !turn.completed);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private rejectTurn(turn: ClaudeTurn, error: Error): void {
|
|
336
|
+
clearTimeout(turn.timer);
|
|
337
|
+
if (!turn.acknowledged) turn.rejectAcceptance(error);
|
|
338
|
+
turn.completed = true;
|
|
339
|
+
this.turns = this.turns.filter((candidate) => candidate !== turn);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private errorOutput(chunk: string): void {
|
|
343
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-4_000);
|
|
344
|
+
process.stderr.write(`[claude:${this.options.actor}] ${chunk}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private exited(code: number | null, signal: NodeJS.Signals | null): void {
|
|
348
|
+
if (this.closing) return;
|
|
349
|
+
const detail = this.stderrTail.trim().split('\n').slice(-2).join(' · ');
|
|
350
|
+
const error = new Error(
|
|
351
|
+
`Claude Code stream exited with ${(signal || code) ?? 'unknown'}${detail ? `: ${detail}` : ''}`,
|
|
352
|
+
);
|
|
353
|
+
this.fail(error);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fail(error: Error): void {
|
|
357
|
+
if (this.fatalError) return;
|
|
358
|
+
this.fatalError = error;
|
|
359
|
+
this.rejectStartup?.(error);
|
|
360
|
+
for (const turn of [...this.turns]) this.rejectTurn(turn, error);
|
|
361
|
+
this.options.onActivity?.({
|
|
362
|
+
sourceId: `claude-stream:${this.options.actor}:fatal:${Date.now()}`,
|
|
363
|
+
kind: 'runtime.gateway.failed',
|
|
364
|
+
phase: 'failed',
|
|
365
|
+
tone: 'error',
|
|
366
|
+
title: `${this.options.actor} disconnected`,
|
|
367
|
+
summary: error.message,
|
|
368
|
+
});
|
|
369
|
+
this.diagnostic('claude.stream.exited', { outcome: 'failed', message: error.message });
|
|
370
|
+
if (this.ready) this.options.onFatal?.(error);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private diagnostic(event: string, fields: Record<string, unknown>): void {
|
|
374
|
+
this.options.onDiagnostic?.(event, fields);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function streamArguments(configured: string[], options: ConnectorOptions): string[] {
|
|
379
|
+
const out: string[] = [];
|
|
380
|
+
const valueFlags = new Set(['--input-format', '--output-format', '--channels']);
|
|
381
|
+
for (let index = 0; index < configured.length; index += 1) {
|
|
382
|
+
const word = configured[index];
|
|
383
|
+
if (valueFlags.has(word)) {
|
|
384
|
+
index += 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (
|
|
388
|
+
[...valueFlags].some((flag) => word.startsWith(`${flag}=`)) ||
|
|
389
|
+
word === '--dangerously-load-development-channels' ||
|
|
390
|
+
word === '--replay-user-messages' ||
|
|
391
|
+
word === '--print' ||
|
|
392
|
+
word === '-p'
|
|
393
|
+
) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
out.push(word);
|
|
397
|
+
}
|
|
398
|
+
out.push(
|
|
399
|
+
'--print',
|
|
400
|
+
'--input-format',
|
|
401
|
+
'stream-json',
|
|
402
|
+
'--output-format',
|
|
403
|
+
'stream-json',
|
|
404
|
+
'--replay-user-messages',
|
|
405
|
+
'--verbose',
|
|
406
|
+
);
|
|
407
|
+
const configuredLine = configured.join(' ');
|
|
408
|
+
if (
|
|
409
|
+
options.autoApprove &&
|
|
410
|
+
!hasOption(configuredLine, ['--dangerously-skip-permissions', '--permission-mode'])
|
|
411
|
+
) {
|
|
412
|
+
out.push('--dangerously-skip-permissions');
|
|
413
|
+
}
|
|
414
|
+
if (!options.autoApprove && !hasOption(configuredLine, ['--allowedTools', '--allowed-tools'])) {
|
|
415
|
+
// The chair remains permissioned for ordinary tools. Its one coordination
|
|
416
|
+
// command is safe to run headlessly and is still authority-checked by the
|
|
417
|
+
// authenticated committee state machine.
|
|
418
|
+
out.push('--allowedTools', 'Bash(agents-city committee:*)');
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function userText(message: Record<string, unknown>): string {
|
|
424
|
+
const body = object(message.message);
|
|
425
|
+
if (typeof body.content === 'string') return body.content;
|
|
426
|
+
if (!Array.isArray(body.content)) return '';
|
|
427
|
+
return body.content
|
|
428
|
+
.map((raw) => {
|
|
429
|
+
const block = object(raw);
|
|
430
|
+
return block.type === 'text' ? String(block.text || '') : '';
|
|
431
|
+
})
|
|
432
|
+
.join('');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function resultError(message: Record<string, unknown>): string {
|
|
436
|
+
const errors = Array.isArray(message.errors) ? message.errors.map(String).filter(Boolean) : [];
|
|
437
|
+
return (
|
|
438
|
+
errors.join(' · ') ||
|
|
439
|
+
String(message.result || '') ||
|
|
440
|
+
`Claude turn ended as ${String(message.subtype || 'error')}`
|
|
441
|
+
).slice(0, 4_000);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function object(value: unknown): Record<string, unknown> {
|
|
445
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
446
|
+
? (value as Record<string, unknown>)
|
|
447
|
+
: {};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function acknowledgementTimeoutMs(): number {
|
|
451
|
+
const value = Number(process.env.CITY_CLAUDE_ACK_TIMEOUT_MS || 30_000);
|
|
452
|
+
return Number.isFinite(value) && value >= 100 ? value : 30_000;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function startupTimeoutMs(): number {
|
|
456
|
+
const value = Number(process.env.CITY_CLAUDE_STARTUP_TIMEOUT_MS || 20_000);
|
|
457
|
+
return Number.isFinite(value) && value >= 500 ? value : 20_000;
|
|
458
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { delimiter, isAbsolute, resolve } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
interface McpEntry {
|
|
6
|
+
name?: unknown;
|
|
7
|
+
enabled?: unknown;
|
|
8
|
+
transport?: {
|
|
9
|
+
type?: unknown;
|
|
10
|
+
command?: unknown;
|
|
11
|
+
cwd?: unknown;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CodexConfigOverrides {
|
|
16
|
+
args: string[];
|
|
17
|
+
disabledMcpServers: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Codex loads the owner's global MCP registry before opening a thread. A stale
|
|
22
|
+
* absolute command there otherwise produces warnings in every city agent and
|
|
23
|
+
* can derail the model into debugging an integration unrelated to its repo.
|
|
24
|
+
* Disable only entries that are enabled, stdio-based and provably absent.
|
|
25
|
+
*/
|
|
26
|
+
export function unavailableMcpOverrides(
|
|
27
|
+
executable: string,
|
|
28
|
+
cwd: string,
|
|
29
|
+
env: NodeJS.ProcessEnv,
|
|
30
|
+
): CodexConfigOverrides {
|
|
31
|
+
let entries: McpEntry[];
|
|
32
|
+
try {
|
|
33
|
+
const listed = spawnSync(executable, ['mcp', 'list', '--json'], {
|
|
34
|
+
cwd,
|
|
35
|
+
env,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
timeout: 5_000,
|
|
38
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
39
|
+
});
|
|
40
|
+
if (listed.status !== 0 || !listed.stdout) return empty();
|
|
41
|
+
const parsed = JSON.parse(listed.stdout);
|
|
42
|
+
if (!Array.isArray(parsed)) return empty();
|
|
43
|
+
entries = parsed as McpEntry[];
|
|
44
|
+
} catch {
|
|
45
|
+
return empty();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const disabledMcpServers = entries
|
|
49
|
+
.filter((entry) => entry.enabled === true && entry.transport?.type === 'stdio')
|
|
50
|
+
.filter((entry) => {
|
|
51
|
+
const command = String(entry.transport?.command || '');
|
|
52
|
+
const commandCwd = String(entry.transport?.cwd || cwd);
|
|
53
|
+
return command !== '' && !commandExists(command, commandCwd, env);
|
|
54
|
+
})
|
|
55
|
+
.map((entry) => String(entry.name || ''))
|
|
56
|
+
// Codex MCP names use this safe alphabet. Skip anything exotic instead of
|
|
57
|
+
// guessing how its dotted TOML override should be escaped.
|
|
58
|
+
.filter((name) => /^[A-Za-z0-9_-]+$/.test(name));
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
disabledMcpServers,
|
|
62
|
+
args: disabledMcpServers.flatMap((name) => ['-c', `mcp_servers.${name}.enabled=false`]),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function commandExists(command: string, cwd: string, env: NodeJS.ProcessEnv): boolean {
|
|
67
|
+
if (isAbsolute(command)) return existsSync(command);
|
|
68
|
+
if (command.includes('/')) return existsSync(resolve(cwd, command));
|
|
69
|
+
return String(env.PATH || '')
|
|
70
|
+
.split(delimiter)
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.some((directory) => existsSync(resolve(directory, command)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function empty(): CodexConfigOverrides {
|
|
76
|
+
return { args: [], disabledMcpServers: [] };
|
|
77
|
+
}
|