trantor 0.18.0 → 0.18.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Trantor \u2014 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
@@ -151,7 +151,15 @@ Fix the `→` lines (each CLI's own sign-in happens once, in that CLI) and re-ru
151
151
  until it's clean.
152
152
 
153
153
  Provider API keys (e.g. `DEEPSEEK_API_KEY`) live in one file: **`~/.agent-bus/.env`** — the
154
- crew runners source it automatically.
154
+ crew runners source it automatically, and it wins over anything Scrooge has.
155
+
156
+ That precedence is the point. Scrooge (the cheap-model router) keeps its own keys in
157
+ `~/.token-scrooge/.env`, and if the crew has no key of its own it falls through to Scrooge's. That
158
+ still works, but then one key authenticates both and your provider bill cannot tell them apart —
159
+ a crew seat and a batch of grunt summaries land on the same line item. Give the crew **separate
160
+ keys**, minted in the provider console rather than copied, and each shows up on its own line and
161
+ can be capped independently. `trantor doctor` reports which key each surface resolves to, masked,
162
+ under "provider keys".
155
163
 
156
164
  ## Your first build
157
165
 
@@ -12,7 +12,7 @@ import { execSync, spawnSync } from "node:child_process";
12
12
  import { readFileSync, writeFileSync, unlinkSync, existsSync, appendFileSync } from "node:fs";
13
13
  import { join, basename } from "node:path";
14
14
  import { homedir } from "node:os";
15
- import { resolveProject, resolveHub } from "../lib/project.mjs";
15
+ import { resolveProject, resolveHub, withEnvFiles } from "../lib/project.mjs";
16
16
  import { loadOrCreate } from "../lib/identity.mjs";
17
17
  import { signedHeaders } from "../lib/signed-fetch.mjs";
18
18
  import { ensureEnrolled } from "../lib/enroll.mjs";
@@ -301,8 +301,16 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
301
301
  let cmd = (isFirst || (cli.sid && !sid)) ? cli.first : cli.next;
302
302
  const mfrag = MODEL && cli.mflag ? `${cli.mflag}${MODEL}` : "";
303
303
  cmd = cmd.replaceAll("{M}", mfrag).replaceAll("{P}", pf).replaceAll("{SID}", sid);
304
+ // PRECEDENCE, and it is easy to get backwards — this is the second time.
305
+ // Each file is PREPENDED, so the one prepended LAST runs FIRST, and in shell the file that runs
306
+ // LAST wins. To make ~/.agent-bus/.env (the CREW layer) win it must be prepended FIRST, i.e.
307
+ // iterate the list in its written order — highest priority first. A `.reverse()` here inverted it
308
+ // and handed every seat Scrooge's key instead of the crew's, which is why one key was paying for
309
+ // both and no provider bill could tell them apart. `.reverse()` also mutated the array in place.
310
+ // Verified by test-crew-env.mjs, which runs the real shell rather than reading this comment.
311
+ // Priority order: the CREW layer first, the agent's own fallback (Scrooge's .env) after it.
304
312
  const envs = [join(homedir(), ".agent-bus", ".env"), cli.env].filter(f => f && existsSync(f));
305
- for (const f of envs.reverse()) cmd = `set -a; source ${f}; set +a; ${cmd}`; // ~/.agent-bus/.env wins
313
+ cmd = withEnvFiles(cmd, envs);
306
314
  log(`turn starting (${isFirst ? "fresh session" : "resume"})${MODEL ? ` · model=${MODEL}` : ""}`);
307
315
  cmuxStatus("building", "#4a90d9", "hammer", { priority: 50 });
308
316
  // inherit stdio so the window shows the agent working live; also capture for sid-parsing.
package/bin/doctor.mjs CHANGED
@@ -139,6 +139,58 @@ for (const c of CLIS) {
139
139
  }
140
140
  if (!installed) warn("no crew CLIs found", "install at least one of: codex, gemini, kimi, opencode — Trantor orchestrates whatever you have");
141
141
 
142
+ // ---- key attribution: WHICH key does each surface actually spend on? ------------------------
143
+ // Provider keys resolve through a LAYERED lookup and nothing ever showed which layer won. On
144
+ // 2026-08-25 a $14 DeepSeek day could not be explained: ~/.token-scrooge/.env held the only
145
+ // DEEPSEEK_API_KEY, so Scrooge's `dev-infra` key was ALSO authenticating every crew seat (the
146
+ // runner sources that file). Scrooge turned out to be 0.15% of the tokens on that key and the
147
+ // crew was the other 99.85%, but the bill could not say so — one key, two jobs, one line item.
148
+ //
149
+ // The layers, highest priority first — this MIRRORS bin/crew-runner.mjs, which sources
150
+ // ~/.agent-bus/.env last so it wins:
151
+ // 1. the process environment
152
+ // 2. ~/.agent-bus/.env — the CREW layer (seats: opencode/deepseek/openrouter/dsh)
153
+ // 3. ~/.token-scrooge/.env — the SCROOGE layer (cheap-model grunt routing)
154
+ section("provider keys (who spends on what)");
155
+ const KEY_VARS = ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "MOONSHOT_API_KEY", "ZAI_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "XAI_API_KEY"];
156
+ const CREW_ENV = join(H, ".agent-bus", ".env");
157
+ const SCROOGE_ENV = join(H, ".token-scrooge", ".env");
158
+ const readEnvFile = (f) => {
159
+ const out = {};
160
+ try {
161
+ for (const line of readFileSync(f, "utf8").split("\n")) {
162
+ const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
163
+ if (m) out[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
164
+ }
165
+ } catch {}
166
+ return out;
167
+ };
168
+ // Never print a key. The suffix is enough to match a line item in a provider console.
169
+ const mask = (v) => (!v ? "" : v.length <= 12 ? "****" : `${v.slice(0, 5)}…${v.slice(-4)}`);
170
+ const crewEnv = readEnvFile(CREW_ENV), scroogeEnv = readEnvFile(SCROOGE_ENV);
171
+ let anyKey = false, shared = 0; const sharedVars = [];
172
+ for (const v of KEY_VARS) {
173
+ const crew = process.env[v] || crewEnv[v] || "";
174
+ const scrooge = process.env[v] || scroogeEnv[v] || "";
175
+ if (!crew && !scrooge) continue;
176
+ anyKey = true;
177
+ const crewSrc = process.env[v] ? "process env" : crewEnv[v] ? "~/.agent-bus/.env (crew)" : scroogeEnv[v] ? "~/.token-scrooge/.env (FALLBACK)" : "none";
178
+ const crewKey = crew || scrooge;
179
+ const scroogeFileKey = scroogeEnv[v] || "";
180
+ if (crewKey && scroogeFileKey && crewKey === scroogeFileKey) {
181
+ shared++; sharedVars.push(v);
182
+ note(`${v}: crew + Scrooge share ONE key ${mask(crewKey)} — spend is indistinguishable on the bill`);
183
+ } else {
184
+ ok(`${v}: crew ${mask(crewKey)} via ${crewSrc}${scroogeFileKey && scroogeFileKey !== crewKey ? ` · scrooge ${mask(scroogeFileKey)} via ~/.token-scrooge/.env` : ""}`);
185
+ }
186
+ }
187
+ if (!anyKey) note("no provider API keys found in env, ~/.agent-bus/.env or ~/.token-scrooge/.env");
188
+ else if (!shared) ok("crew and Scrooge spend on separate keys — each shows up as its own line item");
189
+ else {
190
+ warn(`${shared} provider key(s) do double duty (${sharedVars.join(", ")}) — a spike on the bill cannot be attributed to the crew or to Scrooge`,
191
+ `mint a second key per provider and give the CREW its own, e.g.: echo 'DEEPSEEK_API_KEY=<new-crew-key>' >> ~/.agent-bus/.env (Scrooge keeps ~/.token-scrooge/.env; the runner sources ~/.agent-bus/.env last, so it wins)`);
192
+ }
193
+
142
194
  // brain
143
195
  section("the brain");
144
196
  has("scrooge") || existsSync(join(H, ".local", "bin", "scrooge"))
package/lib/project.mjs CHANGED
@@ -152,6 +152,21 @@ export function unsetProjectHub(project) {
152
152
  // to ~/.agent-bus/machine-id so it never drifts: RELAY_HOST_ID > persisted id > macOS LocalHostName
153
153
  // (stable, no domain) > hostname() without its domain suffix.
154
154
  let _hostId = null;
155
+ // Wrap a command so it runs with the seat's provider keys loaded, highest-priority file FIRST.
156
+ //
157
+ // The ordering is the whole point and it has been wrong twice. Each file is PREPENDED, so the one
158
+ // prepended LAST runs FIRST — and in shell, whichever runs LAST wins. Therefore the highest-priority
159
+ // file must be prepended FIRST. Callers pass files in priority order (crew layer, then fallbacks).
160
+ //
161
+ // Getting it backwards silently handed every crew seat Scrooge's API key, so one key authenticated
162
+ // both and no provider bill could attribute a spend spike to either. test-crew-env.mjs proves this
163
+ // against a REAL shell, because the last time it was wrong the comment above it said it was right.
164
+ export function withEnvFiles(cmd, files = []) {
165
+ let out = cmd;
166
+ for (const f of files) if (f) out = `set -a; source ${f}; set +a; ${out}`;
167
+ return out;
168
+ }
169
+
155
170
  export function hostId() {
156
171
  if (_hostId) return _hostId;
157
172
  if (process.env.RELAY_HOST_ID) return (_hostId = process.env.RELAY_HOST_ID.slice(0, 60));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"
@@ -11,7 +11,7 @@
11
11
  "zod": "^4.4.3"
12
12
  },
13
13
  "scripts": {
14
- "test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
14
+ "test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-crew-env.mjs && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
15
15
  },
16
16
  "description": "The hub-world for AI agent crews \u2014 orchestrate Claude Code, Codex, GLM, Kimi, DeepSeek & any OpenRouter model as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
17
17
  "files": [