trantor 0.18.1 → 0.18.3

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.1",
3
+ "version": "0.18.3",
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": {
@@ -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
@@ -153,6 +153,24 @@ if (!installed) warn("no crew CLIs found", "install at least one of: codex, gemi
153
153
  // 3. ~/.token-scrooge/.env — the SCROOGE layer (cheap-model grunt routing)
154
154
  section("provider keys (who spends on what)");
155
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
+ // Only some of these are a CREW credential. A seat that authenticates through its own config never
157
+ // reads the env var at all, so a shared value there costs nothing and flagging it is noise:
158
+ // glm → opencode.json provider["zai-coding-plan"].options.apiKey (and a coding plan is flat
159
+ // rate, so there is no per-token bill to attribute even in principle)
160
+ // kimi → ~/.kimi/credentials codex → its own login gemini → retired
161
+ // The opencode-driven seats that DO take their key from the env layer are deepseek and openrouter,
162
+ // and even then only when opencode.json has not already given that provider its own key.
163
+ // Getting this wrong cries wolf: the first version flagged all seven and five were false positives,
164
+ // which is how a doctor becomes something you skip.
165
+ const OPENCODE_CFG = read(join(H, ".config", "opencode", "opencode.json")) || {};
166
+ const SEAT_ENV_VARS = { DEEPSEEK_API_KEY: "deepseek", OPENROUTER_API_KEY: "openrouter" };
167
+ const crewUsesVar = (v) => {
168
+ const provider = SEAT_ENV_VARS[v];
169
+ if (!provider) return false;
170
+ const configured = OPENCODE_CFG?.provider?.[provider]?.options?.apiKey;
171
+ if (typeof configured === "string" && configured.includes("{env:")) return true; // resolves from env at run time
172
+ return !configured; // a LITERAL key bypasses the env layer
173
+ };
156
174
  const CREW_ENV = join(H, ".agent-bus", ".env");
157
175
  const SCROOGE_ENV = join(H, ".token-scrooge", ".env");
158
176
  const readEnvFile = (f) => {
@@ -177,7 +195,9 @@ for (const v of KEY_VARS) {
177
195
  const crewSrc = process.env[v] ? "process env" : crewEnv[v] ? "~/.agent-bus/.env (crew)" : scroogeEnv[v] ? "~/.token-scrooge/.env (FALLBACK)" : "none";
178
196
  const crewKey = crew || scrooge;
179
197
  const scroogeFileKey = scroogeEnv[v] || "";
180
- if (crewKey && scroogeFileKey && crewKey === scroogeFileKey) {
198
+ if (!crewUsesVar(v)) {
199
+ ok(`${v}: Scrooge only ${mask(scroogeFileKey || crewKey)} — no crew seat reads this var`);
200
+ } else if (crewKey && scroogeFileKey && crewKey === scroogeFileKey) {
181
201
  shared++; sharedVars.push(v);
182
202
  note(`${v}: crew + Scrooge share ONE key ${mask(crewKey)} — spend is indistinguishable on the bill`);
183
203
  } else {
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.1",
3
+ "version": "0.18.3",
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": [