trantor 0.18.0 → 0.18.1
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/plugin.json +1 -1
- package/README.md +9 -1
- package/bin/doctor.mjs +52 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
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
|
|
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"))
|