trantor 0.18.105 → 0.18.107
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/bin/balances.mjs +10 -14
- package/hooks/lib/balance-check.mjs +3 -2
- package/hooks/lib/handoff.mjs +3 -0
- package/hooks/statusline.mjs +2 -0
- package/hub/routes/admin.mjs +37 -10
- package/hub/store.mjs +2 -1
- package/lib/balances.mjs +8 -0
- package/lib/store-pg.mjs +2 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.107",
|
|
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/bin/balances.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* oxlint-disable anti-slop/no-runtime-typeof -- SAFETY: config.json is user-editable external input; thresholds validates its optional number/object fields at that I/O boundary. */
|
|
3
|
-
// trantor balances
|
|
4
|
-
//
|
|
5
|
-
// balance API, prints them, and pushes the snapshot to the hub so the dashboard + other sessions see it.
|
|
6
|
-
//
|
|
3
|
+
// trantor balances: how much credit is left on each provider, so you can refill before a build
|
|
4
|
+
// stalls. Queries each provider with the env keys and pushes the snapshot to the hub.
|
|
7
5
|
// Usage: trantor balances [--json] [--no-push]
|
|
8
6
|
import { readFileSync, existsSync } from "node:fs";
|
|
9
7
|
import { join } from "node:path";
|
|
10
8
|
import { homedir } from "node:os";
|
|
11
|
-
import { fetchBalances, isLow, fmtBalance, DEFAULT_LOW, DEFAULT_LOW_QUOTA_PCT } from "../lib/balances.mjs";
|
|
9
|
+
import { fetchBalances, isLow, fmtBalance, snapshotPayload, DEFAULT_LOW, DEFAULT_LOW_QUOTA_PCT } from "../lib/balances.mjs";
|
|
10
|
+
import { hostId } from "../lib/project.mjs";
|
|
12
11
|
import { detectedCliBalanceRows } from "../lib/providers.mjs";
|
|
13
12
|
import { loadProfile } from "./profile.mjs";
|
|
14
13
|
import { resolveKeys } from "../lib/provider-keys.mjs";
|
|
@@ -19,9 +18,10 @@ const noPush = args.includes("--no-push");
|
|
|
19
18
|
|
|
20
19
|
// API-key providers stay profile-scoped so ambient keys are never scraped. Claude and Codex are
|
|
21
20
|
// machine CLI logins: the registry admits them from binary + credential + live-probe detection.
|
|
22
|
-
const
|
|
21
|
+
const profileProviders = loadProfile().providers || {};
|
|
22
|
+
const configured = Object.keys(profileProviders);
|
|
23
23
|
|
|
24
|
-
// Signed via the shared client
|
|
24
|
+
// Signed via the shared client: an unsigned POST is rejected under enforce.
|
|
25
25
|
import { signedPost } from "../hooks/lib/api.mjs";
|
|
26
26
|
let _qpct = DEFAULT_LOW_QUOTA_PCT;
|
|
27
27
|
function thresholds() {
|
|
@@ -35,14 +35,10 @@ const profileScoped = await fetchBalances(env, { only: configured.filter((provid
|
|
|
35
35
|
const balances = [...detected, ...profileScoped];
|
|
36
36
|
const low = thresholds();
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// the desktop app reads balances from 127.0.0.1 because the data is machine-local, and the
|
|
41
|
-
// project-resolved push goes to the REMOTE hub for pinned projects. That mismatch left the
|
|
42
|
-
// local snapshot 11 days stale on 2026-08-28: the header chips faithfully rendered Aug-16
|
|
43
|
-
// numbers, dimmed, while fresh pushes landed on a hub the app never asks.
|
|
38
|
+
// Push the snapshot to the project hub and the LOCAL hub (best-effort); the machine key and
|
|
39
|
+
// profile let a shared hub keep one snapshot per machine.
|
|
44
40
|
if (!noPush) {
|
|
45
|
-
const snap =
|
|
41
|
+
const snap = snapshotPayload(balances, { machine: hostId(), profile: profileProviders });
|
|
46
42
|
try { await signedPost("/balances", snap, { timeoutMs: 2500 }); } catch {}
|
|
47
43
|
try { await signedPost("http://127.0.0.1:4477/balances", snap, { timeoutMs: 2500 }); } catch {}
|
|
48
44
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { fetchBalances, isLow, fmtBalance, DEFAULT_LOW, DEFAULT_LOW_QUOTA_PCT } from "../../lib/balances.mjs";
|
|
8
|
+
import { fetchBalances, isLow, fmtBalance, snapshotPayload, DEFAULT_LOW, DEFAULT_LOW_QUOTA_PCT } from "../../lib/balances.mjs";
|
|
9
|
+
import { hostId } from "../../lib/project.mjs";
|
|
9
10
|
import { loadProfile } from "../../bin/profile.mjs";
|
|
10
11
|
import { resolveKeys } from "../../lib/provider-keys.mjs";
|
|
11
12
|
import { signedPost } from "./api.mjs";
|
|
@@ -38,7 +39,7 @@ export async function maybeCheckBalances() {
|
|
|
38
39
|
if (!Array.isArray(balances) || !balances.length) { try { writeFileSync(STAMP, JSON.stringify({ ts: Date.now(), low: [] })); } catch {} return { low: [] }; }
|
|
39
40
|
|
|
40
41
|
// push the fresh snapshot to the hub for the dashboard + other sessions (best-effort, signed)
|
|
41
|
-
const snap = {
|
|
42
|
+
const snap = snapshotPayload(balances, { machine: hostId(), profile: loadProfile().providers, by: process.env.TRANTOR_SESSION || "" });
|
|
42
43
|
await signedPost("/balances", snap, { timeoutMs: 2000 });
|
|
43
44
|
// The desktop reads balances from the LOCAL hub (machine-local data); a pinned project's
|
|
44
45
|
// default push lands on the REMOTE one — the 11-day-stale-header bug. Push both.
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export function contextUsage(transcriptPath, conf = readConfig()) {
|
|
|
59
59
|
let model = "";
|
|
60
60
|
for (const line of lines) {
|
|
61
61
|
let r; try { r = JSON.parse(line); } catch { continue; }
|
|
62
|
+
if (r?.type === "system" && r?.subtype === "compact_boundary") { rows.push("compact"); continue; }
|
|
62
63
|
const u = r?.message?.usage;
|
|
63
64
|
if (r?.type !== "assistant" || !u) continue;
|
|
64
65
|
const tokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
@@ -78,6 +79,8 @@ export function guardContextTokens(rows) {
|
|
|
78
79
|
let max = 0;
|
|
79
80
|
const recent = [];
|
|
80
81
|
for (const t of rows) {
|
|
82
|
+
// a compaction starts the session over: readings before it say nothing about the window now
|
|
83
|
+
if (t === "compact") { max = 0; recent.length = 0; continue; }
|
|
81
84
|
if (!t || t <= 0) continue;
|
|
82
85
|
recent.push(t);
|
|
83
86
|
if (recent.length > 5) recent.shift();
|
package/hooks/statusline.mjs
CHANGED
|
@@ -40,6 +40,8 @@ async function main() {
|
|
|
40
40
|
try { writeFileSync(join(dir, "usage-claude-live.json"), JSON.stringify({ ts: Date.now(), ...payload })); } catch {}
|
|
41
41
|
// SECONDARY, best-effort: the hub copy, for dashboard surfaces that read hub state.
|
|
42
42
|
const { signedPost } = await import("./lib/api.mjs");
|
|
43
|
+
const { hostId } = await import("../lib/project.mjs");
|
|
44
|
+
payload.machine = hostId();
|
|
43
45
|
await signedPost("/usage/claude", payload, { timeoutMs: 2500 });
|
|
44
46
|
}
|
|
45
47
|
|
package/hub/routes/admin.mjs
CHANGED
|
@@ -387,8 +387,21 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
387
387
|
const b = await body(req);
|
|
388
388
|
const entries = Array.isArray(b.balances) ? b.balances.slice(0, 30) : [];
|
|
389
389
|
const ts = (Number.isFinite(b.ts) && b.ts > 0) ? Math.floor(b.ts) : now();
|
|
390
|
+
const by = String(b.by || "").slice(0, 120);
|
|
390
391
|
// only accept a newer snapshot (avoid an older session clobbering a fresh push)
|
|
391
|
-
if (ts >= (state.balances?.ts || 0)) { state.balances = { ts, by
|
|
392
|
+
if (ts >= (state.balances?.ts || 0)) { state.balances = { ts, by, entries }; markDirty(); }
|
|
393
|
+
// Per-machine copy: two Macs on one hub each keep their own providers and profile.
|
|
394
|
+
const machine = String(b.machine || "").trim().slice(0, 60);
|
|
395
|
+
if (machine) {
|
|
396
|
+
const byM = (state.balancesByMachine ||= {});
|
|
397
|
+
if (ts >= (byM[machine]?.ts || 0)) {
|
|
398
|
+
const prof = (b.profile && typeof b.profile === "object" && !Array.isArray(b.profile)) ? Object.fromEntries(Object.entries(b.profile).slice(0, 30)) : {};
|
|
399
|
+
byM[machine] = { ts, by, entries, profile: prof };
|
|
400
|
+
const keys = Object.keys(byM);
|
|
401
|
+
if (keys.length > 20) for (const k of keys.sort((x, y) => byM[x].ts - byM[y].ts).slice(0, keys.length - 20)) delete byM[k];
|
|
402
|
+
markDirty();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
392
405
|
return json(res, 200, { ok: true });
|
|
393
406
|
}
|
|
394
407
|
// USAGE v2: the Claude statusline sidechannel. Claude Code >=2.1.80 pipes rate_limits into the
|
|
@@ -402,14 +415,23 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
402
415
|
const wins = [["fiveHour", "5h"], ["sevenDay", "7d"], ["fable", "Fable"]]
|
|
403
416
|
.map(([k, n]) => win(b[k], n)).filter(Boolean);
|
|
404
417
|
if (!wins.length) return json(res, 400, { error: "no usable windows" });
|
|
405
|
-
state.balances ||= { ts: 0, by: "", entries: [] };
|
|
406
|
-
|
|
407
|
-
if (
|
|
418
|
+
const patchSnaps = [(state.balances ||= { ts: 0, by: "", entries: [] })];
|
|
419
|
+
const machine = String(b.machine || "").trim().slice(0, 60);
|
|
420
|
+
if (machine) patchSnaps.push(((state.balancesByMachine ||= {})[machine] ||= { ts: now(), by: "", entries: [], profile: {} }));
|
|
421
|
+
const claudeEntry = (snap) => {
|
|
422
|
+
let c = snap.entries.find(x => x.provider === "claude");
|
|
423
|
+
if (!c) { c = { provider: "claude", label: "Claude", kind: "windows", ok: true, windows: [] }; snap.entries.push(c); }
|
|
424
|
+
return c;
|
|
425
|
+
};
|
|
426
|
+
const e = claudeEntry(patchSnaps[patchSnaps.length - 1]); // dedupe against the most specific copy
|
|
408
427
|
// Same-value posts inside 30s are dropped (the statusline ticks ~3x/sec while streaming).
|
|
409
428
|
const sig = JSON.stringify(wins);
|
|
410
429
|
if (e._liveSig === sig && now() - (e.liveTs || 0) < 30_000) return json(res, 200, { ok: true, deduped: true });
|
|
411
|
-
for (const
|
|
412
|
-
|
|
430
|
+
for (const snap of patchSnaps) {
|
|
431
|
+
const t = claudeEntry(snap);
|
|
432
|
+
for (const w of wins) { const cur = (t.windows ||= []).find(x => x.name === w.name); if (cur) Object.assign(cur, w); else t.windows.push({ ...w }); }
|
|
433
|
+
t.ok = true; t.liveTs = now(); t._liveSig = sig; t.liveSource = "statusline";
|
|
434
|
+
}
|
|
413
435
|
markDirty();
|
|
414
436
|
return json(res, 200, { ok: true, windows: wins.length });
|
|
415
437
|
}
|
|
@@ -422,7 +444,12 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
422
444
|
: (e.remaining != null && e.remaining < (low[e.currency] ?? low.USD ?? 5)));
|
|
423
445
|
const ALIAS = { kimi: "moonshot", moonshot: "moonshot", glm: "zhipu", zai: "zhipu", zhipu: "zhipu" };
|
|
424
446
|
const canonP = p => ALIAS[p] || p;
|
|
425
|
-
|
|
447
|
+
// ?machine=X serves that machine's own snapshot and profile; otherwise the legacy latest.
|
|
448
|
+
const mSnap = q.machine ? state.balancesByMachine?.[String(q.machine).trim().slice(0, 60)] : null;
|
|
449
|
+
const snap = mSnap || state.balances;
|
|
450
|
+
let prof = {};
|
|
451
|
+
if (mSnap?.profile && Object.keys(mSnap.profile).length) prof = mSnap.profile;
|
|
452
|
+
else { try { prof = JSON.parse(readFileSync(join(homedir(), ".agent-bus", "profile.json"), "utf8")).providers || {}; } catch {} }
|
|
426
453
|
const profByCanon = {}; for (const [p, v] of Object.entries(prof)) profByCanon[canonP(p)] = v;
|
|
427
454
|
const detectedCli = new Set(["claude", "codex"]);
|
|
428
455
|
// API-key rows remain profile-scoped so a stray ambient key never appears. Claude and Codex
|
|
@@ -430,7 +457,7 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
430
457
|
// missing quota declaration must not hide those machine-login rows from the bottom bar. A
|
|
431
458
|
// prepaid entry that ERRORED but is a subscription per profile is really one (401 expected).
|
|
432
459
|
const isSub = (t) => !!t && t !== "api"; // capped-sub / high-sub → a subscription (nothing to refill)
|
|
433
|
-
const entries = (
|
|
460
|
+
const entries = (snap?.entries || []).filter(e => detectedCli.has(e.provider) || profByCanon[canonP(e.provider)]).map(e => {
|
|
434
461
|
const pv = profByCanon[canonP(e.provider)];
|
|
435
462
|
if (!e.ok && isSub(pv?.tier)) return { provider: e.provider, label: e.label, kind: "subscription", plan: pv.plan, ok: true, remaining: null, low: false };
|
|
436
463
|
return { ...e, low: lowOf(e) };
|
|
@@ -441,8 +468,8 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
441
468
|
const subs = Object.entries(prof)
|
|
442
469
|
.filter(([p, v]) => !detectedCli.has(p) && isSub(v?.tier) && !known.has(canonP(p)))
|
|
443
470
|
.map(([p, v]) => ({ provider: p, label: p, kind: "subscription", plan: v.plan, ok: true, remaining: null, low: false }));
|
|
444
|
-
return json(res, 200, { ts:
|
|
445
|
-
entries: [...entries, ...subs], lowCount: entries.filter(e => e.low).length, stale: (now() - (
|
|
471
|
+
return json(res, 200, { ts: snap?.ts || 0, by: snap?.by || "", thresholds: low,
|
|
472
|
+
entries: [...entries, ...subs], lowCount: entries.filter(e => e.low).length, stale: (now() - (snap?.ts || 0)) > 6 * 3600e3 });
|
|
446
473
|
}
|
|
447
474
|
// Rebuild cc-subagent notional cards for a project from recomputed on-disk transcript costs
|
|
448
475
|
// (`trantor recost`). REPLACES the project's existing cc-subagent cards with the supplied set so the
|
package/hub/store.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { DEFAULT_ORG } from "../lib/store-contract.mjs";
|
|
|
6
6
|
|
|
7
7
|
export async function createStoreRuntime({ STORE_KIND, PG_URL, ORG_ID, DATA }) {
|
|
8
8
|
function emptyState() {
|
|
9
|
-
return { messages: [], peers: {}, seq: 0, tasks: [], taskSeq: 0, projectMeta: {}, lessons: [], events: [], cardEventsBackfilled: false, aliases: {}, phaseMeta: {}, verifyGates: [], verifyGateSeq: 0, proposals: [], proposalSeq: 0, balances: { ts: 0, by: "", entries: [] }, subagentCostReset: false, handoffLog: [], identities: {}, inviteTokens: {}, focus: {}, orgPolicy: {}, overseerState: {}, instances: {}, dutySession: "", contractReap: {}, eventSeq: 0 };
|
|
9
|
+
return { messages: [], peers: {}, seq: 0, tasks: [], taskSeq: 0, projectMeta: {}, lessons: [], events: [], cardEventsBackfilled: false, aliases: {}, phaseMeta: {}, verifyGates: [], verifyGateSeq: 0, proposals: [], proposalSeq: 0, balances: { ts: 0, by: "", entries: [] }, balancesByMachine: {}, subagentCostReset: false, handoffLog: [], identities: {}, inviteTokens: {}, focus: {}, orgPolicy: {}, overseerState: {}, instances: {}, dutySession: "", contractReap: {}, eventSeq: 0 };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const CARD_LOG_MAX = 40;
|
|
@@ -103,6 +103,7 @@ function normalizeState(loaded = {}) {
|
|
|
103
103
|
s.proposals = Array.isArray(loaded.proposals) ? loaded.proposals : [];
|
|
104
104
|
s.proposalSeq = Number(loaded.proposalSeq || Math.max(0, ...s.proposals.map(p => Number(p.id) || 0))) || 0;
|
|
105
105
|
s.balances = loaded.balances && typeof loaded.balances === "object" ? loaded.balances : { ts: 0, by: "", entries: [] };
|
|
106
|
+
s.balancesByMachine = loaded.balancesByMachine && typeof loaded.balancesByMachine === "object" ? loaded.balancesByMachine : {};
|
|
106
107
|
s.subagentCostReset = !!loaded.subagentCostReset;
|
|
107
108
|
s.handoffLog = Array.isArray(loaded.handoffLog) ? loaded.handoffLog : [];
|
|
108
109
|
s.identities = loaded.identities && typeof loaded.identities === "object" ? loaded.identities : {};
|
package/lib/balances.mjs
CHANGED
|
@@ -313,3 +313,11 @@ export function fmtReset(t) {
|
|
|
313
313
|
if (hrs < 48) return `${Math.round(hrs)}h`;
|
|
314
314
|
return `${Math.round(hrs / 24)}d`;
|
|
315
315
|
}
|
|
316
|
+
|
|
317
|
+
// The snapshot a machine pushes to a hub: its balances plus the machine key and the provider
|
|
318
|
+
// profile, so a shared hub can keep one snapshot per machine and filter by that machine's profile.
|
|
319
|
+
export function snapshotPayload(balances, { machine, profile, by, ts = Date.now() }) {
|
|
320
|
+
const snap = { balances, ts, machine: String(machine || ""), profile: profile ?? {} };
|
|
321
|
+
if (by) snap.by = by;
|
|
322
|
+
return snap;
|
|
323
|
+
}
|
package/lib/store-pg.mjs
CHANGED
|
@@ -162,6 +162,7 @@ function kvFromState(state) {
|
|
|
162
162
|
eventSeq: Number(state.eventSeq || 0),
|
|
163
163
|
inviteTokens: state.inviteTokens || {},
|
|
164
164
|
instances: state.instances || {},
|
|
165
|
+
balancesByMachine: state.balancesByMachine || {},
|
|
165
166
|
},
|
|
166
167
|
subagentCostReset: !!state.subagentCostReset,
|
|
167
168
|
seq: Number(state.seq || 0),
|
|
@@ -731,6 +732,7 @@ export class PgStore {
|
|
|
731
732
|
identities,
|
|
732
733
|
inviteTokens: meta.inviteTokens && typeof meta.inviteTokens === "object" ? meta.inviteTokens : {},
|
|
733
734
|
instances: meta.instances && typeof meta.instances === "object" ? meta.instances : {},
|
|
735
|
+
balancesByMachine: meta.balancesByMachine && typeof meta.balancesByMachine === "object" ? meta.balancesByMachine : {},
|
|
734
736
|
focus: kv.focus && typeof kv.focus === "object" ? kv.focus : {},
|
|
735
737
|
overseerState: meta.overseerState || {},
|
|
736
738
|
orgPolicy: kv.orgPolicy && typeof kv.orgPolicy === "object" ? kv.orgPolicy : {},
|