trantor 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "agent-bus",
3
+ "owner": { "name": "Sasha Bogojevic", "email": "hello@hivedigitalllc.com" },
4
+ "metadata": {
5
+ "description": "Live message bus, presence, project Kanban + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
6
+ "version": "0.14.0"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "agent-bus",
11
+ "source": "./",
12
+ "description": "Let independent AI coding sessions — Claude Code, Codex, Gemini, any MCP agent — talk to each other live: auto-register, see the live roster, message/coordinate, and watch it all on a project-grouped Kanban dashboard. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
13
+ "version": "0.14.0",
14
+ "author": { "name": "Sasha Bogojevic" },
15
+ "category": "development",
16
+ "keywords": ["multi-agent", "coordination", "mcp", "hooks", "kanban", "context-handoff", "message-bus", "claude-code", "codex", "gemini"]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "agent-bus",
3
+ "version": "0.14.0",
4
+ "description": "Live message bus, presence, project Kanban + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
5
+ "mcpServers": {
6
+ "relay": {
7
+ "command": "node",
8
+ "args": ["${CLAUDE_PLUGIN_ROOT}/mcp.mjs"]
9
+ }
10
+ },
11
+ "skills": "./skills/"
12
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sasha Bogojevic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ <div align="center">
2
+
3
+ ```
4
+ ████████╗██████╗ █████╗ ███╗ ██╗████████╗ ██████╗ ██████╗
5
+ ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗██╔══██╗
6
+ ██║ ██████╔╝███████║██╔██╗ ██║ ██║ ██║ ██║██████╔╝
7
+ ██║ ██╔══██╗██╔══██║██║╚██╗██║ ██║ ██║ ██║██╔══██╗
8
+ ██║ ██║ ██║██║ ██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║
9
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
10
+ ```
11
+
12
+ ### The hub-world for AI agent crews.
13
+
14
+ **One Advisor decides how your work runs — solo, cheap inline calls, or a live crew of
15
+ Claude Code, Codex, Gemini, Kimi & DeepSeek in their own terminal windows — routed by your
16
+ actual plans, supervised on a live board, learning from every failure.**
17
+
18
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
19
+ ![Node](https://img.shields.io/badge/node-%E2%89%A518-339933?logo=node.js&logoColor=white)
20
+ ![Agents](https://img.shields.io/badge/crew-Claude%20%C2%B7%20Codex%20%C2%B7%20Gemini%20%C2%B7%20Kimi%20%C2%B7%20DeepSeek-D97757)
21
+ ![Tests](https://img.shields.io/badge/tests-37%2F37-2DD4BF)
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install -g trantor
31
+ trantor setup # hub becomes an always-on service + config + wires your AI CLIs + doctor
32
+ ```
33
+
34
+ Then give Claude Code (the orchestrator) the plugin:
35
+
36
+ ```bash
37
+ claude plugin marketplace add sashabogi/trantor
38
+ claude plugin install agent-bus
39
+ ```
40
+
41
+ That's it. (Prefer source? `git clone https://github.com/sashabogi/trantor && cd trantor &&
42
+ npm install && bash deploy/setup.sh` — identical result.)
43
+
44
+ ## What to expect on first run
45
+
46
+ `trantor setup` ends with the **doctor** — an honest map of where you stand:
47
+
48
+ ```
49
+ TRANTOR DOCTOR
50
+
51
+ core
52
+ ✓ node 22.x
53
+ ✓ hub up at http://127.0.0.1:4477
54
+ claude (the orchestrator)
55
+ ✗ plugin not installed
56
+ → claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
57
+ crew CLIs (install any subset — seats follow the work)
58
+ ✓ codex: wired to the bus
59
+ ✗ codex: NOT authenticated — it will join the bus but fail on its first turn
60
+ → codex (sign in with your ChatGPT account on first run)
61
+ – kimi: not installed (optional)
62
+ the brain
63
+ ✗ quota profile not set → trantor profile set claude=max codex=plus deepseek=api
64
+ ```
65
+
66
+ Fix the `→` lines (each CLI's own sign-in happens once, in that CLI), re-run `trantor doctor`
67
+ until it's clean, then open a Claude session in any project and say **"fire up the crew."**
68
+
69
+ Provider API keys (e.g. `DEEPSEEK_API_KEY`) live in one file: **`~/.agent-bus/.env`** — the
70
+ crew runners source it automatically.
71
+
72
+ ## What happens when you fire up a crew
73
+
74
+ 1. **The Advisor moment.** Your Claude cuts the work into difficulty-tagged packages, calls
75
+ `relay_advise`, and shows you the full picture *before spending anything*: mode
76
+ (`solo | scrooge | crew | hybrid`), a routing table with a **reason per package**, why
77
+ that many seats ("seats follow the work, not the install list"), and a real-money estimate
78
+ with quota-pool accounting. You say go.
79
+ 2. **Windows open.** `trantor up codex gemini kimi deepseek:deepseek-v4-pro` spawns one titled
80
+ terminal window per agent (`agent:model` pins a model), **serialized and then verified on
81
+ the bus** — the launcher ends with "crew verified" or names the no-shows loudly. The
82
+ orchestrator never gets a green lie.
83
+ 3. **Work flows over the bus.** Contracts arrive as messages; each agent owns its own files;
84
+ coordination happens in <280-char messages you can read on the dashboard. Crew members
85
+ live under a **runner**: the CLI works one turn and exits, the runner long-polls the bus
86
+ for free (it's also the heartbeat) and resumes the CLI — with full context — when the next
87
+ message lands. **Idle agents cost zero tokens and never die.**
88
+ 4. **The board tells the truth.** Cards flow `todo → doing → testing → done` — `testing` is a
89
+ real gate (tests/typecheck run there); failures turn the card **pulsing red** until the
90
+ orchestrator bounces them back; demoted cards wear an "↩ bounced" mark with full history.
91
+ 5. **It learns.** Failures become lessons (`relay_lesson`), stored on the hub and **injected
92
+ into every future crew's prompts** — global or per-CLI. Your crew gets smarter every run.
93
+
94
+ ## The dashboard — `trantor ui`
95
+
96
+ A live command center at `http://127.0.0.1:4477`, grouped by **project**:
97
+
98
+ - **BOARD view** — Kanban with the testing gate, difficulty + model badges per card, agent
99
+ chips with provider logos, live status, and quota-pool tags.
100
+ - **FLOW view** — the same work as an n8n-style dependency graph: parallel streams fanning
101
+ out and converging into integration, edges lighting up as work flows (green = done feeding
102
+ forward, animated blue = active), **red ↩n loops where work bounced back**. Drag nodes to
103
+ rearrange, ⌘+scroll / pinch to zoom, FIT/AUTO controls. Pick per project; your choice sticks.
104
+ - **Per-project conversation lanes** — watch agents negotiate interfaces in context — plus a
105
+ global live feed, and a composer so *you* can message the bus (or any single agent).
106
+ - **🪙 economics pill** — live Scrooge ledger: real spend, savings vs frontier pricing.
107
+
108
+ ## The brain — plan-aware economics
109
+
110
+ Trantor's economics engine ([Scrooge](https://github.com/sashabogi/token-scrooge) — installed
111
+ automatically by `trantor setup`) knows model capabilities, per-1M costs, and keeps the
112
+ ledger; Trantor turns that into decisions:
113
+
114
+ - **Declare your plans once:** `trantor profile set claude=max codex=plus gemini=tier kimi=coding-plan deepseek=api`
115
+ - The Advisor routes by **your economics**: API-billed orchestrator → offload everything;
116
+ $20-tier plan → the crew *is* the only way a real build fits; max-tier → context horizon
117
+ decides. **Quota pooling**: one build spread across your separate subscription buckets —
118
+ measured example: a five-vendor 3D game build, API-equivalent $200–600, **actual spend $2.29**.
119
+ - **Fractal delegation** (`relay_scrooge`): the architect *and* crew members push stateless
120
+ grunt work to cheap models, with ledger receipts.
121
+
122
+ ## Context handoff — sessions that never hit the wall
123
+
124
+ A PreCompact hook writes a rich handoff before Claude Code compacts; a fresh session in the
125
+ same project **takes over with a brand-new full context window**. Works manually from any
126
+ agent via `relay_handoff`. Optional macOS auto-prompt (`autoHandoffPrompt` in
127
+ `~/.agent-bus/config.json`) offers to open the fresh session for you, with a timeout.
128
+
129
+ Why crews never exhaust the orchestrator: bus messages are **by reference** (~70 tokens),
130
+ work products stay in each agent's own context — the orchestrator burns at coordination
131
+ rate, not work rate.
132
+
133
+ ## The tools (MCP — every agent on the bus gets these)
134
+
135
+ | Tool | What it does |
136
+ |---|---|
137
+ | `relay_advise(task, packages, horizon?)` | **The Advisor** — mode + reasoned per-package routing + cost estimate + ready-to-use card args |
138
+ | `relay_send(to, text)` / `relay_inbox` / `relay_wait(t)` | Live messaging: direct, read-new, long-poll wake |
139
+ | `relay_peers` / `relay_status(text)` / `relay_whoami` | Presence: who's alive (honest, heartbeat-backed), doing what |
140
+ | `relay_project_brief(text)` | The project's what/why on the dashboard |
141
+ | `relay_task_add(title, …, difficulty, model, deps)` | Cards with difficulty/model badges + DAG edges for the flow view |
142
+ | `relay_task_move(id, status)` | `todo → doing → testing → done` (the gate), `failed`, `blocked` |
143
+ | `relay_board` | The project's full board, as text |
144
+ | `relay_scrooge(prompt, task?, difficulty?)` | Fractal cheap-model delegation, with the ledger receipt |
145
+ | `relay_lesson(text, scope?)` | Record a failure lesson — auto-injected into all future crews |
146
+ | `relay_handoff(summary)` | Full-window session succession |
147
+
148
+ ## The CLI
149
+
150
+ ```
151
+ trantor setup | doctor | connect | profile | up <agents…> | down | ui | advise | hub | watch
152
+ ```
153
+
154
+ `trantor up` notes: `agent:model` pins a model (`deepseek:deepseek-v4-pro`); spawns are
155
+ verified on the bus with one retry; geometry auto-detects the screen you're working on
156
+ (`CREW_RECT="X,Y,W,H"` to override); `trantor down` kills crew processes via their ttys and
157
+ closes windows without macOS "Terminate?" dialogs.
158
+
159
+ ## Works with any MCP agent
160
+
161
+ Claude Code, Codex CLI, Gemini CLI, Kimi Code CLI, OpenCode (DeepSeek) are wired by
162
+ `trantor connect` automatically (idempotent, backed-up, never overwrites your customizations).
163
+ Anything else that speaks MCP: point it at `mcp.mjs` with `RELAY_AGENT=<brand>` — loading the
164
+ server auto-registers the session, so presence works before the model says a word.
165
+
166
+ ## How it works
167
+
168
+ ```
169
+ Claude (architect/plugin) codex ─ runner gemini ─ runner kimi ─ runner deepseek ─ runner
170
+ │ advise/contracts │ one turn, exit; runner long-polls (free) + resumes with context
171
+ └───────────┬─────────────┴──────────────┴────────────────┴────────────────┘
172
+
173
+ hub.mjs ←— plain HTTP + SSE · presence/messages/board/lessons/economics
174
+ (Node built-ins only · state in ~/.agent-bus/bus.json · loopback by default)
175
+
176
+ dashboard (ui.html) · BOARD/FLOW · conversation lanes · 🪙 ledger
177
+ ```
178
+
179
+ Config: `RELAY_URL` env → `~/.agent-bus/config.json` → `http://127.0.0.1:4477`.
180
+ Identity: `RELAY_SESSION` → `RELAY_AGENT:<project-folder>` → `<hostname>:<project-folder>`.
181
+
182
+ **Local-first and safe:** the hub binds loopback; no accounts, no cloud, no exposure. An
183
+ always-on/remote hub (private tailnet, or public with auth) is on the roadmap — never expose
184
+ the hub publicly without auth.
185
+
186
+ *Heritage note: Trantor grew out of **agent-bus** — package internals still carry some
187
+ `agent-bus`/`relay_*` identifiers; they migrate in one planned breaking release.*
188
+
189
+ ## Honest limits
190
+
191
+ - You can't interrupt an agent **mid-turn**; messages land when its current turn ends (idle
192
+ agents wake instantly via their runner).
193
+ - Window spawning is macOS (Terminal.app); on Linux the launcher prints per-agent commands
194
+ to run in your own terminals. Hub/MCP/dashboard are cross-platform.
195
+ - The hub is deliberately tiny (in-memory + JSON file) — a coordination bus, not a message queue.
196
+ - Each CLI's sign-in is its own (ChatGPT, Google, Kimi accounts) — the doctor detects state
197
+ and names the fix, but can't log in for you.
198
+
199
+ ## Tests
200
+
201
+ ```bash
202
+ npm test # unit + 37 protocol-level scenario drills with mock agents (no LLMs, seconds, $0):
203
+ # honest presence, spawn no-shows, the testing gate, bounce trails, lessons,
204
+ # advisor decisions across plan tiers, deps validation, virgin-machine doctor
205
+ ```
206
+
207
+ ## License
208
+
209
+ [MIT](./LICENSE) © 2026 Sasha Bogojevic · Built with [Claude Code](https://claude.com/claude-code) ·
210
+ Brain by [Scrooge](https://github.com/sashabogi/token-scrooge)
package/bin/advise.mjs ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ // The Advisor — the brain's front door. Given work packages, decide HOW to execute:
3
+ // solo | scrooge | crew | hybrid — from task shape × plan economics × context horizon.
4
+ //
5
+ // echo '{"task":"build X","packages":[{"title":"engine","difficulty":"hard"},…]}' | node bin/advise.mjs
6
+ // node bin/advise.mjs --demo # canned example
7
+ //
8
+ // Reads (all read-only):
9
+ // ~/.agent-bus/profile.json — the user's declared plans (bin/profile.mjs)
10
+ // ~/.token-scrooge/registry.json — Scrooge's models {cost_in, cost_out, good_for}
11
+ // ~/.token-scrooge/capabilities.json — per-model quality scores
12
+ // Exposed to agents as the MCP tool `relay_advise`; the crew skill calls it at kickoff.
13
+ import { readFileSync, existsSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { execSync } from "node:child_process";
17
+
18
+ const H = homedir();
19
+ const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
20
+
21
+ export function loadWorld() {
22
+ const profile = read(join(H, ".agent-bus", "profile.json"), { providers: {} });
23
+ const registry = read(join(H, ".token-scrooge", "registry.json"), { models: {}, tasks: {} });
24
+ const caps = read(join(H, ".token-scrooge", "capabilities.json"), {});
25
+ const has = (c) => { try { execSync(`command -v ${c}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
26
+ const agents = ["codex", "gemini", "kimi", "deepseek"].filter(a => has(a === "deepseek" ? "opencode" : a));
27
+ return { profile, registry, caps, agents, scrooge: has("scrooge") };
28
+ }
29
+
30
+ const tierOf = (profile, prov) => profile?.providers?.[prov]?.tier || "api";
31
+
32
+ // pick the cheapest Scrooge model that clears the difficulty floor for a task kind
33
+ export function scroogeModelFor(registry, caps, kind = "code", difficulty = "easy") {
34
+ const floor = { easy: 0, medium: 35, hard: 55 }[difficulty] ?? 0;
35
+ const cands = Object.entries(registry.models || {})
36
+ .filter(([, m]) => (m.good_for || []).includes(kind))
37
+ .filter(([id]) => (caps[id]?.coding ?? caps[id]?.intelligence ?? 40) >= floor)
38
+ .sort((a, b) => (a[1].cost_in + a[1].cost_out) - (b[1].cost_in + b[1].cost_out));
39
+ return cands[0] ? { model: cands[0][0], cost_in: cands[0][1].cost_in, cost_out: cands[0][1].cost_out } : null;
40
+ }
41
+
42
+ // crude per-package token forecast (input+output through the executor)
43
+ const FORECAST = { easy: 0.3e6, medium: 1.5e6, hard: 6e6 }; // tokens
44
+ // crew agent preference per difficulty: frontier subs take hard, cheap takes easy
45
+ const CREW_PREF = { hard: ["codex", "gemini", "kimi", "deepseek"], medium: ["kimi", "gemini", "codex", "deepseek"], easy: ["deepseek", "kimi", "gemini", "codex"] };
46
+
47
+ export function advise(input, world = loadWorld()) {
48
+ const { profile, registry, caps, agents, scrooge } = world;
49
+ const pkgs = (input.packages || []).map(p => ({ title: p.title || "work", difficulty: ["easy", "medium", "hard"].includes(p.difficulty) ? p.difficulty : "medium", kind: p.kind || "code", owner: p.owner === "self" ? "self" : (/(foundation|integration|scaffold)/i.test(p.title) ? "self" : "") }));
50
+ const horizon = input.horizon || (pkgs.length >= 4 ? "long" : pkgs.length >= 2 ? "medium" : "short");
51
+ const orchTier = tierOf(profile, "claude");
52
+ const n = pkgs.length, hard = pkgs.filter(p => p.difficulty === "hard").length, easy = pkgs.filter(p => p.difficulty === "easy").length;
53
+
54
+ // ---- mode decision ----
55
+ let mode, why = [];
56
+ if (n <= 1 && hard === 0 && horizon === "short") {
57
+ if (n === 1 && pkgs[0].difficulty === "easy" && scrooge) { mode = "scrooge"; why.push("single small stateless package — a cheap inline call beats spinning up anything"); }
58
+ else { mode = "solo"; why.push("one small package, short horizon — orchestrator handles it directly"); }
59
+ } else if (orchTier === "api") {
60
+ mode = "crew"; why.push("orchestrator is API-billed: every token here is money — be a thin foreman, run all work on crew quotas + Scrooge");
61
+ } else if (orchTier === "capped-sub" && n >= 2) {
62
+ mode = "crew"; why.push("orchestrator is on a capped plan: a real build can't fit in this session — spend the scarce budget on architecture + verification only");
63
+ } else if (n >= 3 || hard >= 1 || horizon === "long") {
64
+ mode = "crew"; why.push(`${n} packages${hard ? `, ${hard} hard` : ""}, ${horizon} horizon — crew isolates work in separate contexts so this session burns at coordination rate, not work rate`);
65
+ } else {
66
+ mode = "solo"; why.push("small enough to do inline without hurting the context horizon");
67
+ }
68
+ // hybrid: crew + easy packages that should be scrooge'd instead of occupying an agent
69
+ if (mode === "crew" && easy > 0 && scrooge) { mode = "hybrid"; why.push(`${easy} easy package(s) routed to Scrooge inline — don't burn a crew seat on grunt work`); }
70
+
71
+ // ---- routing per package ----
72
+ const used = {};
73
+ const routing = pkgs.map(p => {
74
+ if ((mode === "hybrid" || mode === "scrooge") && p.difficulty === "easy" && scrooge) {
75
+ const m = scroogeModelFor(registry, caps, p.kind, p.difficulty);
76
+ const tok = FORECAST.easy;
77
+ const cost = m ? +(tok * 0.6 * m.cost_in / 1e6 + tok * 0.4 * m.cost_out / 1e6).toFixed(3) : null;
78
+ return { ...p, executor: "scrooge", model: m?.model, pool: "api", est_cost_usd: cost,
79
+ reason: `easy + stateless → cheapest capable model (${m?.model}); not worth a crew seat` };
80
+ }
81
+ if (p.owner === "self") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "architect-owned (foundation/integration doctrine) — the orchestrator keeps the shared contract in its own hands" };
82
+ if (mode === "solo") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "small enough to do inline" };
83
+ const pref = CREW_PREF[p.difficulty].filter(a => agents.includes(a));
84
+ const agent = pref.sort((a, b) => (used[a] || 0) - (used[b] || 0))[0] || "deepseek";
85
+ used[agent] = (used[agent] || 0) + 1;
86
+ const pool = tierOf(profile, agent === "deepseek" ? "deepseek" : agent);
87
+ let est = null;
88
+ if (pool === "api") { // deepseek API etc — estimate real $ via registry
89
+ const m = registry.models?.["deepseek-v4-flash"] || { cost_in: 0.14, cost_out: 0.28 };
90
+ est = +((FORECAST[p.difficulty] * 0.85 * m.cost_in + FORECAST[p.difficulty] * 0.15 * m.cost_out) / 1e6).toFixed(2);
91
+ }
92
+ const why_r = p.difficulty === "hard"
93
+ ? `hard → strongest available coder (${agent}); its ${pool} pool means ${pool === "api" ? "real $ but cheapest capable" : "$0 marginal on existing quota"}`
94
+ : p.difficulty === "medium"
95
+ ? `medium → solid mid-tier (${agent}) keeps frontier seats free for hard work; ${pool === "api" ? "metered" : "quota"} pool`
96
+ : `easy → cheapest seat (${agent})`;
97
+ return { ...p, executor: agent, pool, est_cost_usd: est, reason: why_r };
98
+ });
99
+ // crew-size rationale: seats are EMERGENT from the work, and we say so
100
+ const seats = [...new Set(routing.filter(r => !["scrooge", "orchestrator"].includes(r.executor)).map(r => r.executor))];
101
+ const crew = {
102
+ seats: seats.length, of_available: agents.length, members: seats,
103
+ why: seats.length === 0 ? "no crew needed" :
104
+ `${routing.filter(r => seats.includes(r.executor)).length} crew-bound package(s) → ${seats.length} seat(s), one per concurrent work stream (load-balanced); ${agents.length - seats.length > 0 ? `${agents.length - seats.length} installed CLI(s) left idle — seats follow the work, not the install list` : "all installed CLIs engaged because the work fans that wide"}`
105
+ };
106
+
107
+ const apiCost = +(routing.reduce((s, r) => s + (r.est_cost_usd || 0), 0)).toFixed(2);
108
+ const pools = [...new Set(routing.map(r => `${r.executor}:${r.pool}`))];
109
+ const summary =
110
+ `Recommendation: ${mode.toUpperCase()}. ${why.join("; ")}. ` +
111
+ (mode === "crew" || mode === "hybrid"
112
+ ? `Routing: ${routing.map(r => `${r.title}→${r.executor}${r.model ? `(${r.model})` : ""}`).join(", ")}. ` +
113
+ `Estimated real-money cost ≈ $${apiCost} (everything on a subscription pool is $0 marginal — quota pooling across ${pools.length} pools).`
114
+ : "");
115
+ const table = ["| package | diff | executor (model) | pool | est $ | reason |", "|---|---|---|---|---|---|",
116
+ ...routing.map(r => `| ${r.title} | ${r.difficulty} | ${r.executor}${r.model ? ` (${r.model})` : ""} | ${r.pool} | ${r.est_cost_usd ?? "—"} | ${r.reason} |`)].join("\n");
117
+ // card_args are born as a DAG: crew cards depend on architect-owned foundation card(s);
118
+ // integration-titled architect cards depend on every crew card. (Indices are 1-based card
119
+ // creation order — the architect substitutes real ids as it creates them.)
120
+ const selfPkgs = routing.filter(r => r.executor === "orchestrator");
121
+ const foundationIdx = selfPkgs.length ? [1] : [];
122
+ const cards = routing.map((r, i) => ({
123
+ order: i + 1, title: r.title, difficulty: r.difficulty,
124
+ assignee: r.executor === "scrooge" || r.executor === "orchestrator" ? undefined : `${r.executor}:<project>`,
125
+ model: r.model || (["scrooge", "orchestrator"].includes(r.executor) ? undefined : `${r.executor}-default`),
126
+ via: r.executor === "scrooge" ? "relay_scrooge" : "relay_task_add",
127
+ deps_orders: r.executor === "orchestrator" && /integrat/i.test(r.title)
128
+ ? routing.map((x, j) => j + 1).filter(j => j !== i + 1)
129
+ : (r.executor !== "orchestrator" ? foundationIdx.filter(f => f !== i + 1) : []) }));
130
+ return { mode, why, crew, routing, routing_table_md: table, card_args: cards, est_api_cost_usd: apiCost, quota_pools: pools, summary, orchestrator_tier: orchTier, agents_available: agents };
131
+ }
132
+
133
+ // ---- CLI ----
134
+ if (import.meta.url === `file://${process.argv[1]}`) {
135
+ let input;
136
+ if (process.argv.includes("--demo")) {
137
+ input = { task: "neon asteroids game", packages: [
138
+ { title: "engine+integration", difficulty: "hard" }, { title: "render/bloom", difficulty: "hard" },
139
+ { title: "combat+ui", difficulty: "medium" }, { title: "audio", difficulty: "medium" },
140
+ { title: "readme+badges", difficulty: "easy" } ] };
141
+ } else {
142
+ const stdin = readFileSync(0, "utf8").trim();
143
+ input = stdin ? JSON.parse(stdin) : { packages: [] };
144
+ }
145
+ const out = advise(input);
146
+ console.log(JSON.stringify(out, null, 2));
147
+ }
package/bin/cli.mjs ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ // trantor — the command. Thin dispatcher over the toolkit so a global npm install
3
+ // gives you everything: trantor setup | doctor | connect | profile | advise | up | down | hub | ui
4
+ import { spawn } from "node:child_process";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { readFileSync } from "node:fs";
8
+
9
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ const [, , cmd, ...args] = process.argv;
11
+ const run = (file, runner = process.execPath) => {
12
+ const child = spawn(runner, [join(ROOT, file), ...args], { stdio: "inherit", cwd: process.cwd() });
13
+ child.on("exit", (c) => process.exit(c ?? 0));
14
+ };
15
+
16
+ switch (cmd) {
17
+ case "setup": run("deploy/setup.sh", "/bin/bash"); break;
18
+ case "doctor": run("bin/doctor.mjs"); break;
19
+ case "connect": run("bin/connect.mjs"); break;
20
+ case "profile": run("bin/profile.mjs"); break;
21
+ case "advise": run("bin/advise.mjs"); break;
22
+ case "verify": run("bin/crew-verify.mjs"); break;
23
+ case "up": process.argv.splice(2, 1); spawn("/bin/bash", [join(ROOT, "bin/crew.sh"), "up", ...args], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
24
+ case "down": spawn("/bin/bash", [join(ROOT, "bin/crew.sh"), "down"], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
25
+ case "hub": run("hub.mjs"); break;
26
+ case "watch": run("bin/relay-watch.mjs"); break;
27
+ case "ui": {
28
+ let url = "http://127.0.0.1:4477";
29
+ try { url = JSON.parse(readFileSync(join(process.env.HOME || "", ".agent-bus", "config.json"), "utf8")).url || url; } catch {}
30
+ spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).unref();
31
+ console.log(`dashboard → ${url}`);
32
+ break;
33
+ }
34
+ case "version": case "-v": case "--version":
35
+ console.log(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version); break;
36
+ default:
37
+ console.log(`trantor — the hub-world for AI agent crews
38
+
39
+ trantor setup one-shot install: hub as an always-on service + config + CLI wiring + doctor
40
+ trantor doctor where do I stand? hub/plugin/CLIs/auth/keys/profile, with copy-paste fixes
41
+ trantor connect (re)wire every installed AI CLI to the bus
42
+ trantor profile declare your plans: trantor profile set claude=max codex=plus deepseek=api
43
+ trantor up … spawn a crew here: trantor up codex gemini kimi deepseek:deepseek-v4-pro
44
+ trantor down tear the crew down (kills processes, closes windows, no dialogs)
45
+ trantor ui open the live dashboard (board + flow views)
46
+ trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
47
+ trantor hub run the hub in the foreground (setup installs it as a service instead)
48
+ trantor watch live bus feed in the terminal
49
+
50
+ Claude Code plugin (the orchestrator side):
51
+ claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
52
+ Docs: https://github.com/sashabogi/trantor`);
53
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // agent-bus connect — wire every AI coding CLI on this machine to the bus, in one shot.
3
+ //
4
+ // node bin/connect.mjs # detect installed CLIs, patch each one's MCP config (idempotent)
5
+ // node bin/connect.mjs --dry-run # show what would change, touch nothing
6
+ //
7
+ // Each CLI keeps its own MCP config file/format; this writes the one "relay" entry into each
8
+ // (with a timestamped .bak backup the first time it changes a file). Claude Code is handled by
9
+ // the plugin (claude plugin install agent-bus), so it's only verified here, not patched.
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { execSync } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const DRY = process.argv.includes("--dry-run");
17
+ const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
18
+ const URL_ = process.env.RELAY_URL || "http://127.0.0.1:4477";
19
+ const has = (cmd) => { try { execSync(`command -v ${cmd}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
20
+ const stamp = new Date().toISOString().slice(0, 10);
21
+ const backup = (p) => { const b = `${p}.bak-${stamp}`; if (!existsSync(b)) copyFileSync(p, b); return b; };
22
+ const out = [];
23
+ const report = (cli, status, detail = "") => out.push({ cli, status, detail });
24
+
25
+ function patchJson(path, mutate) {
26
+ const exists = existsSync(path);
27
+ const d = exists ? JSON.parse(readFileSync(path, "utf8")) : {};
28
+ const before = JSON.stringify(d);
29
+ mutate(d);
30
+ if (JSON.stringify(d) === before) return "already wired";
31
+ if (!DRY) {
32
+ if (exists) backup(path); else mkdirSync(dirname(path), { recursive: true });
33
+ writeFileSync(path, JSON.stringify(d, null, 2) + "\n");
34
+ }
35
+ return exists ? "wired" : "wired (new config)";
36
+ }
37
+
38
+ const relayEnv = (agent) => ({ RELAY_URL: URL_, RELAY_AGENT: agent });
39
+
40
+ // ---- Claude Code: plugin handles it; verify only ----
41
+ if (has("claude")) {
42
+ let st = "plugin not detected — run: claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus";
43
+ try {
44
+ const s = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
45
+ if (Object.keys(s.enabledPlugins || {}).some(k => k.startsWith("agent-bus@"))) st = "plugin installed ✓";
46
+ } catch {}
47
+ report("claude", st);
48
+ }
49
+
50
+ // ---- Codex (TOML append — no TOML lib needed) ----
51
+ if (has("codex")) {
52
+ const p = join(homedir(), ".codex", "config.toml");
53
+ const cur = existsSync(p) ? readFileSync(p, "utf8") : "";
54
+ if (cur.includes("[mcp_servers.relay]")) report("codex", "already wired");
55
+ else {
56
+ const block = `\n# agent-bus — auto-registers each Codex session on the bus + adds relay_* tools\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\nenv = { RELAY_URL = "${URL_}", RELAY_AGENT = "codex" }\n`;
57
+ if (!DRY) { if (existsSync(p)) backup(p); else mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, cur + block); }
58
+ report("codex", cur ? "wired" : "wired (new config)", p);
59
+ }
60
+ }
61
+
62
+ // ---- Gemini CLI ---- (existing relay entries are never overwritten — user customization wins)
63
+ if (has("gemini")) {
64
+ const p = join(homedir(), ".gemini", "settings.json");
65
+ report("gemini", patchJson(p, d => {
66
+ d.mcpServers ||= {};
67
+ d.mcpServers.relay ||= { command: "node", args: [MCP], env: relayEnv("gemini") };
68
+ }), p);
69
+ }
70
+
71
+ // ---- Kimi CLI ----
72
+ if (has("kimi")) {
73
+ const p = join(homedir(), ".kimi", "mcp.json");
74
+ report("kimi", patchJson(p, d => {
75
+ d.mcpServers ||= {};
76
+ d.mcpServers.relay ||= { command: "node", args: [MCP], env: relayEnv("kimi") };
77
+ }), p);
78
+ }
79
+
80
+ // ---- OpenCode ----
81
+ if (has("opencode")) {
82
+ const p = join(homedir(), ".config", "opencode", "opencode.json");
83
+ report("opencode", patchJson(p, d => {
84
+ d.$schema ||= "https://opencode.ai/config.json";
85
+ d.mcp ||= {};
86
+ d.mcp.relay ||= { type: "local", command: ["node", MCP], enabled: true, environment: relayEnv("opencode") };
87
+ }), p);
88
+ }
89
+
90
+ const found = out.length;
91
+ console.log(`agent-bus connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
92
+ for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
93
+ if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode)");
94
+ console.log(DRY ? "\nRun without --dry-run to apply." : "\nDone. New sessions of each CLI auto-join the bus.");