trantor 0.17.23 → 0.17.25
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +35 -0
- package/bin/advise.mjs +4 -1
- package/bin/profile.mjs +9 -3
- package/hooks/lib/handoff.mjs +28 -9
- package/hooks/sessionstart.mjs +11 -1
- package/package.json +1 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.25"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, Gemini, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. 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.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.25",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.25",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/README.md
CHANGED
|
@@ -42,6 +42,41 @@ claude plugin install trantor
|
|
|
42
42
|
That's it. (Prefer source? `git clone https://github.com/sashabogi/trantor && cd trantor &&
|
|
43
43
|
npm install && bash deploy/setup.sh` — identical result.)
|
|
44
44
|
|
|
45
|
+
## What gets installed — footprint & safety
|
|
46
|
+
|
|
47
|
+
Trantor is a **local-first multi-agent orchestrator with a built-in cost router** — not a cloud
|
|
48
|
+
service, and not an agent that runs off on its own. Here is *exactly* what the two steps above put
|
|
49
|
+
on your machine, so you (or an agent installing it for you) can see the whole footprint up front:
|
|
50
|
+
|
|
51
|
+
**`npm install -g trantor` + `trantor setup`**
|
|
52
|
+
- The `trantor` CLI — one global npm package.
|
|
53
|
+
- `~/.agent-bus/` — a single local directory holding **all** state: `config.json`, the board data
|
|
54
|
+
(`bus.json`), and `.env` for any **provider API keys you choose to add** (e.g. `DEEPSEEK_API_KEY`).
|
|
55
|
+
Nothing in here ever leaves your machine.
|
|
56
|
+
- A local **hub** at `http://127.0.0.1:4477` — **loopback only**, not reachable from the network. On
|
|
57
|
+
macOS it's a launchd agent (`com.trantor.hub`) so it restarts at login; on Linux you run it yourself.
|
|
58
|
+
- The economics engine (Scrooge) into `~/.local/bin` — the cost ledger and cheap-model router.
|
|
59
|
+
|
|
60
|
+
**`claude plugin install trantor`** adds, inside Claude Code only:
|
|
61
|
+
- An MCP server (`relay`) exposing the `relay_*` tools, plus the `/trantor:*` skills.
|
|
62
|
+
- Hooks on four events — local Node scripts that only POST to the loopback hub: **SessionStart**
|
|
63
|
+
(register the session + show the live roster), **PostToolUse** (presence heartbeat + mirror your
|
|
64
|
+
TodoWrite list onto the board), **PreCompact** (write a handoff before the context window compacts),
|
|
65
|
+
**SubagentStop** (record each sub-agent's notional cost on the board).
|
|
66
|
+
|
|
67
|
+
**What it does *not* do:** no cloud, no accounts, no telemetry, nothing phones home; it never uploads
|
|
68
|
+
your code or keys; it doesn't touch other CLIs' credentials — Codex, Gemini, Kimi and DeepSeek are
|
|
69
|
+
ones *you* already installed and signed into, and Trantor just coordinates them locally. The optional
|
|
70
|
+
API keys in `~/.agent-bus/.env` are used only to call the cheap models *you* opted into for routing.
|
|
71
|
+
|
|
72
|
+
**Remove everything, anytime:**
|
|
73
|
+
```bash
|
|
74
|
+
claude plugin uninstall trantor # drop the MCP tools, skills, and hooks
|
|
75
|
+
launchctl bootout gui/$(id -u)/com.trantor.hub # stop the hub service (macOS)
|
|
76
|
+
rm -f ~/Library/LaunchAgents/com.trantor.hub.plist
|
|
77
|
+
rm -rf ~/.agent-bus # delete all local state + keys
|
|
78
|
+
```
|
|
79
|
+
|
|
45
80
|
## What to expect on first run
|
|
46
81
|
|
|
47
82
|
`trantor setup` ends with the **doctor** — an honest map of where you stand:
|
package/bin/advise.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { execSync } from "node:child_process";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
17
18
|
|
|
18
19
|
const H = homedir();
|
|
19
20
|
const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
|
|
@@ -134,7 +135,9 @@ export function advise(input, world = loadWorld()) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// ---- CLI ----
|
|
137
|
-
|
|
138
|
+
// is-main guard via PROPERLY ENCODED file URL — a hand-built `file://${argv[1]}` silently no-ops when the
|
|
139
|
+
// install path contains a URL-reserved char (e.g. a SPACE in ".../Application Support/..."). See profile.mjs.
|
|
140
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
138
141
|
let input;
|
|
139
142
|
if (process.argv.includes("--demo")) {
|
|
140
143
|
input = { task: "neon asteroids game", packages: [
|
package/bin/profile.mjs
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// for real builds; the plan's quota is a scarce budget)
|
|
13
13
|
// max | max-5x | max-20x | ultra — high-tier subscription (cost moot; context horizon decides)
|
|
14
14
|
// none — provider not available on this machine
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
18
19
|
|
|
19
20
|
const FILE = join(homedir(), ".agent-bus", "profile.json");
|
|
20
21
|
const KNOWN = ["claude", "codex", "gemini", "kimi", "deepseek", "opencode"];
|
|
@@ -32,7 +33,11 @@ export function loadProfile() {
|
|
|
32
33
|
export function tierOf(profile, provider) { return TIER(profile?.providers?.[provider]?.plan); }
|
|
33
34
|
|
|
34
35
|
const [, , cmd, ...args] = process.argv;
|
|
35
|
-
|
|
36
|
+
// is-main guard: compare against a PROPERLY ENCODED file URL. A hand-built `file://${argv[1]}` is raw
|
|
37
|
+
// text, but import.meta.url is percent-encoded — so any URL-reserved char in the install path (most
|
|
38
|
+
// commonly a SPACE, e.g. ".../Application Support/...") made this false and silently skipped main (exit 0,
|
|
39
|
+
// no write). pathToFileURL is the canonical Node idiom. See regression in test-handoff.mjs / test.mjs.
|
|
40
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
36
41
|
const prof = loadProfile();
|
|
37
42
|
prof.providers ||= {};
|
|
38
43
|
if (cmd === "set") {
|
|
@@ -42,6 +47,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
42
47
|
prof.providers[prov.toLowerCase()] = { plan: plan.toLowerCase(), tier: TIER(plan) };
|
|
43
48
|
}
|
|
44
49
|
prof.updated = new Date().toISOString().slice(0, 10);
|
|
50
|
+
mkdirSync(dirname(FILE), { recursive: true }); // `set` creates its own ~/.agent-bus if absent
|
|
45
51
|
writeFileSync(FILE, JSON.stringify(prof, null, 2) + "\n");
|
|
46
52
|
console.log("profile saved →", FILE);
|
|
47
53
|
} else if (cmd && cmd !== "show") {
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { homedir, hostname } from "node:os";
|
|
|
16
16
|
import { execSync, spawn } from "node:child_process";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
-
export const HANDOFF_DIR = join(homedir(), ".agent-bus", "handoffs");
|
|
19
|
+
export const HANDOFF_DIR = join(process.env.RELAY_DATA_DIR || join(homedir(), ".agent-bus"), "handoffs");
|
|
20
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
22
|
export function readConfig() {
|
|
@@ -229,7 +229,12 @@ export function terminalWindowForTty(tty) {
|
|
|
229
229
|
end repeat
|
|
230
230
|
return ""
|
|
231
231
|
end tell`;
|
|
232
|
-
|
|
232
|
+
// Pass the MULTI-LINE script via stdin, NOT `-e ${JSON.stringify(osa)}`: a single -e arg keeps the
|
|
233
|
+
// newlines as literal "\n" (JSON escapes them, the shell's double-quotes don't expand them), so
|
|
234
|
+
// osascript saw `…"Terminal"\n repeat…` and died with "27:28: Expected end of line but found unknown
|
|
235
|
+
// token". stdin gives it real newlines. (This silently returned "" for years → callers always fell
|
|
236
|
+
// through to frontTerminalWindow, which after a spawn grabs the WRONG window.)
|
|
237
|
+
try { return execSync(`osascript`, { input: osa, encoding: "utf8", timeout: 3000 }).trim(); } catch { return ""; }
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
// Arm the baton-close watcher: a DETACHED process that waits until the fresh session consumes the
|
|
@@ -301,14 +306,28 @@ export function frontTerminalWindow() {
|
|
|
301
306
|
} catch { return { id: "", tty: "" }; }
|
|
302
307
|
}
|
|
303
308
|
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
// Resolve the ORIGINAL window (id + tty) to close on takeover. Controlling tty first (if we were
|
|
310
|
+
// invoked with one — heartbeat/precompact have it), else the CURRENT front window (a manual baton
|
|
311
|
+
// runs through the headless Bash tool with no tty; the session you're looking at is frontmost).
|
|
312
|
+
// MUST be called BEFORE spawning the fresh session — once the new window opens it becomes frontmost
|
|
313
|
+
// and this would capture IT instead.
|
|
314
|
+
export function resolveOriginalWindow() {
|
|
310
315
|
let tty = controllingTty(), windowId = tty ? terminalWindowForTty(tty) : "";
|
|
311
316
|
if (!windowId) { const f = frontTerminalWindow(); windowId = f.id; tty = f.tty; }
|
|
312
|
-
|
|
317
|
+
return { windowId, tty };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// MANUAL one-command baton: spawn the fresh session (no dialog) + arm the close of THIS window once the
|
|
321
|
+
// fresh one consumes the handoff. Returns { spawned, armed, windowId }.
|
|
322
|
+
// ORDER IS LOAD-BEARING: resolve the original window BEFORE spawning. Reversing it is the
|
|
323
|
+
// "successor closes ITSELF" bug — the just-opened window is frontmost, the front-window fallback
|
|
324
|
+
// captures it, and baton-close then kills the FRESH session the moment it takes over. The seams
|
|
325
|
+
// (_resolveWindow/_spawnFresh/_armClose) exist so the ordering can be regression-tested headlessly.
|
|
326
|
+
export function spawnBaton({ projectDir, handoffFile, conf = readConfig(),
|
|
327
|
+
_resolveWindow = resolveOriginalWindow, _spawnFresh = spawnFresh, _armClose = armBatonClose }) {
|
|
328
|
+
const { windowId, tty } = _resolveWindow(); // original window FIRST, while it's still frontmost
|
|
329
|
+
const spawned = _spawnFresh(projectDir);
|
|
330
|
+
if (!spawned) return { spawned: false, armed: false, windowId: "" };
|
|
331
|
+
const armed = windowId ? _armClose(handoffFile, windowId, tty, conf) : false;
|
|
313
332
|
return { spawned, armed, windowId };
|
|
314
333
|
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -21,7 +21,17 @@ function loadPendingHandoff(projectName, { claim = true } = {}) {
|
|
|
21
21
|
try {
|
|
22
22
|
const dir = join(homedir(), ".agent-bus", "handoffs");
|
|
23
23
|
if (!existsSync(dir)) return null;
|
|
24
|
-
|
|
24
|
+
// Match ONLY this project's handoffs: "<projectName>-<numeric stamp>.json".
|
|
25
|
+
// NOT a loose startsWith() — that also caught leaked test fixtures like
|
|
26
|
+
// "trantor-handoff-61385-….json" for project "trantor". And sort by the numeric
|
|
27
|
+
// stamp (newest first), NOT lexicographically — string sort ranks a letter prefix
|
|
28
|
+
// ("…-handoff-…") above a digit one, so it could pick a stale/wrong handoff.
|
|
29
|
+
const re = new RegExp("^" + projectName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "-(\\d+)\\.json$");
|
|
30
|
+
const files = readdirSync(dir)
|
|
31
|
+
.map(f => { const m = re.exec(f); return m ? { f, stamp: Number(m[1]) } : null; })
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.sort((a, b) => b.stamp - a.stamp)
|
|
34
|
+
.map(x => x.f);
|
|
25
35
|
for (const f of files) {
|
|
26
36
|
const p = join(dir, f);
|
|
27
37
|
const rec = JSON.parse(readFileSync(p, "utf8"));
|