trantor 0.17.20 → 0.17.21

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.
@@ -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.20"
9
+ "version": "0.17.21"
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.20",
16
+ "version": "0.17.21",
17
17
  "author": {
18
18
  "name": "Sasha Bogojevic"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.20",
3
+ "version": "0.17.21",
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/hub.mjs CHANGED
@@ -228,7 +228,7 @@ function derivePhases(tasks) {
228
228
  const phases = [...byPhase.entries()].map(([key, cards]) => {
229
229
  const counts = { todo:0, doing:0, testing:0, failed:0, done:0, blocked:0 };
230
230
  for (const c of cards) counts[c.status] = (counts[c.status] || 0) + 1;
231
- const node = (c) => ({ id: c.id, title: c.title, assignee: c.assignee || "", agent: agentBrand(c.assignee), model: c.model || "", status: c.status, difficulty: c.difficulty || "", ts: c.ts || 0, updated: c.updated || c.ts || 0, deps: Array.isArray(c.deps) ? c.deps : [] });
231
+ const node = (c) => ({ id: c.id, title: c.title, assignee: c.assignee || "", agent: agentBrand(c.assignee), model: c.model || "", status: c.status, difficulty: c.difficulty || "", ts: c.ts || 0, updated: c.updated || c.ts || 0, deps: Array.isArray(c.deps) ? c.deps : [], costKind: c.costKind || "", costUsd: (typeof c.costUsd === "number") ? c.costUsd : null, source: c.source || "" });
232
232
  const crew = cards.filter(c => !isOrchAssignee(c.assignee)).map(node);
233
233
  const orchestrators = cards.filter(c => isOrchAssignee(c.assignee)).map(node);
234
234
  return {
@@ -526,6 +526,39 @@ const server = http.createServer(async (req, res) => {
526
526
  // back-compat: `scrooge` is the window older dashboards read (honor ?hours= if passed)
527
527
  out.scrooge = q.hours ? rollup(rows.filter(c => c.ts >= nowS - Number(q.hours) * 3600)) : out.windows["24h"];
528
528
  } catch {}
529
+ // --- card-based costs (FLOW v2): the orchestrator's OWN work, by costKind ---
530
+ // NOTIONAL (Claude sub-agents/orchestrator — plan-covered) is kept STRICTLY SEPARATE from REAL
531
+ // spend (Scrooge). We never sum them into one headline — that would imply we paid for plan-covered
532
+ // tokens. Crew is subscription (no per-task $). Card ts is in ms (the scrooge ledger is in seconds).
533
+ try {
534
+ const WINDOWS_MS = { "24h": 864e5, week: 7 * 864e5, month: 30 * 864e5, quarter: 90 * 864e5, year: 365 * 864e5 };
535
+ const costCards = state.tasks.filter(t => t.costKind || t.costUsd != null);
536
+ const rollupCards = cards => {
537
+ const byKind = {};
538
+ for (const t of cards) {
539
+ const k = t.costKind || "other";
540
+ const e = byKind[k] ||= { count: 0, usd: 0, tokens_in: 0, tokens_out: 0, cache_read: 0, cache_write: 0, by_model: {}, hasUsd: false };
541
+ e.count++;
542
+ if (typeof t.costUsd === "number") { e.usd += t.costUsd; e.hasUsd = true; }
543
+ if (t.tokens) { e.tokens_in += t.tokens.input || 0; e.tokens_out += t.tokens.output || 0; e.cache_read += t.tokens.cacheRead || 0; e.cache_write += t.tokens.cacheWrite || 0; }
544
+ if (t.model) { const m = e.by_model[t.model] ||= { count: 0, usd: 0 }; m.count++; m.usd += t.costUsd || 0; }
545
+ }
546
+ for (const e of Object.values(byKind)) { e.usd = +e.usd.toFixed(4); e.usd = e.hasUsd ? e.usd : null; }
547
+ return byKind;
548
+ };
549
+ out.costKinds = {};
550
+ const nowMs = now();
551
+ for (const [k, ms] of Object.entries(WINDOWS_MS)) out.costKinds[k] = rollupCards(costCards.filter(t => (t.ts || 0) >= nowMs - ms));
552
+ out.costKinds.lifetime = rollupCards(costCards);
553
+ // per-project notional totals (subagent+orchestrator) so the dashboard can scope it like reliability
554
+ const perProject = {};
555
+ for (const t of costCards) {
556
+ if (typeof t.costUsd !== "number") continue;
557
+ if (t.costKind !== "subagent-notional" && t.costKind !== "orchestrator-notional") continue;
558
+ perProject[canon(t.project)] = +((perProject[canon(t.project)] || 0) + t.costUsd).toFixed(4);
559
+ }
560
+ out.notionalByProject = perProject;
561
+ } catch {}
529
562
  return json(res, 200, out);
530
563
  }
531
564
  if (req.method === "GET" && P === "/lessons") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.20",
3
+ "version": "0.17.21",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"
package/ui.html CHANGED
@@ -189,6 +189,7 @@ main:not(.learn-open) .learn-body{display:none}
189
189
  .gnode.failed .gnbox,.gnode.blocked .gnbox{stroke:var(--red)}
190
190
  .gntext{fill:#eaf1fa;font-size:11px;font-family:ui-sans-serif,system-ui;pointer-events:none}
191
191
  .gnsub{fill:var(--mut);font-size:9.5px;font-family:ui-sans-serif,system-ui;pointer-events:none}
192
+ .gncost{fill:var(--blu);font-size:9px;font-family:ui-sans-serif,system-ui;text-anchor:end;pointer-events:none}
192
193
  .pfband{fill:#0c1320;opacity:.45}
193
194
  .pfband.alt{fill:#0e1626;opacity:.55}
194
195
  .pfbandtop{opacity:.12}
@@ -318,8 +319,16 @@ let ECON=null;
318
319
  const ECON_WINS=[['24h','last 24h'],['week','last week'],['month','last month'],['quarter','last quarter'],['year','last year']];
319
320
  let econWin=localStorage.getItem('abEconWin')||'24h';
320
321
  const econSaved=x=>x?(x.saved_usd!=null?(+x.saved_usd):Math.max(0,(x.opus_equiv_usd||0)-(x.cost_usd||0))):0;
322
+ // notional (plan-covered) cost of the orchestrator's OWN Claude work for a window — subagent +
323
+ // orchestrator costKinds, summed ONLY with each other (never with real Scrooge spend).
324
+ function econNotional(win){
325
+ const ck=ECON&&ECON.costKinds&&ECON.costKinds[win]; if(!ck)return 0;
326
+ return ((ck['subagent-notional']&&ck['subagent-notional'].usd)||0)+((ck['orchestrator-notional']&&ck['orchestrator-notional'].usd)||0);
327
+ }
321
328
  function renderEcon(){
322
- if(!ECON||!ECON.lifetime||!ECON.lifetime.calls)return;
329
+ const lifeCalls=(ECON&&ECON.lifetime&&ECON.lifetime.calls)||0;
330
+ const notionalLife=ECON?econNotional('lifetime'):0;
331
+ if(!ECON||(!lifeCalls&&!(notionalLife>0)))return; // show once we have EITHER real savings OR notional CC cost
323
332
  const el=$('#econ'); el.style.display='';
324
333
  // lifetime running total is the fixed headline; the dropdown picks the comparison window.
325
334
  if(!el.dataset.built){
@@ -327,7 +336,8 @@ function renderEcon(){
327
336
  `<span style="opacity:.55"> · lifetime · </span>`+
328
337
  `<select id="econsel" title="comparison window" style="background:#1a2030;color:inherit;border:1px solid rgba(255,255,255,.18);border-radius:4px;font:inherit;font-size:11px;padding:1px 3px;cursor:pointer;outline:none">`+
329
338
  ECON_WINS.map(([k,l])=>`<option value="${k}">${l}</option>`).join('')+`</select>`+
330
- `<span style="opacity:.85" id="econwinval"></span>`;
339
+ `<span style="opacity:.85" id="econwinval"></span>`+
340
+ `<span id="econnotional" title="Claude's OWN sub-agent work (Agent/Workflow/ultracode) at API rates — plan-covered on a subscription, NOT real spend. Shown separately so it's never confused with the real Scrooge \$ saved." style="opacity:.9;margin-left:2px"></span>`;
331
341
  const sel=$('#econsel'); sel.value=econWin;
332
342
  sel.onchange=()=>{econWin=sel.value; localStorage.setItem('abEconWin',econWin); renderEcon();};
333
343
  el.dataset.built='1';
@@ -335,6 +345,11 @@ function renderEcon(){
335
345
  const w=(ECON.windows&&ECON.windows[econWin])||ECON.scrooge, wc=w?w.calls:0;
336
346
  $('#econlife').textContent='$'+econSaved(ECON.lifetime).toFixed(2);
337
347
  $('#econwinval').textContent=' $'+econSaved(w).toFixed(2)+' ('+wc+' call'+(wc===1?'':'s')+')';
348
+ // separate notional line — distinct icon + "notional" wording, never added to the savings number
349
+ const nEl=$('#econnotional');
350
+ if(nEl) nEl.innerHTML = notionalLife>0
351
+ ? `<span style="opacity:.5"> · </span>🤖 CC sub-agents <b style="color:var(--blu)">$${notionalLife.toFixed(2)}</b> <span style="opacity:.7">notional</span>`
352
+ : '';
338
353
  const sel=$('#econsel'); if(sel)sel.value=econWin;
339
354
  }
340
355
  async function econ(){
@@ -395,14 +410,16 @@ function flowHTML(pt, proj){
395
410
  const maxColCount = Math.max(1, ...layouts.flatMap(L => L.cols.map(c => c.length)));
396
411
  const Y0 = MT + stackH(maxColCount)/2; // shared horizontal spine midline
397
412
  const totalH = MT + stackH(maxColCount) + MB;
398
- const gnode = (x, y, title, status, id, orch, agent) => {
413
+ const gnode = (x, y, title, status, id, orch, agent, cost) => {
399
414
  const stripe = SCOL[status] || '#3a4458';
415
+ const c = (typeof cost === 'number') ? (cost < 0.005 ? '$' + cost.toFixed(4) : '$' + cost.toFixed(2)) : '';
400
416
  return `<g class="gnode ${status}${orch?' orch':''}"${id?` data-id="${id}"`:''}>`
401
417
  + `<rect class="gnbox" x="${x}" y="${y}" width="${NW}" height="${NH}" rx="8"/>`
402
418
  + `<rect x="${x}" y="${y}" width="4" height="${NH}" rx="2" fill="${stripe}"/>`
403
419
  + `<text class="gntext" x="${x+11}" y="${y+(agent?15:23)}">${esc(title.slice(0,24))}${title.length>24?'…':''}</text>`
404
420
  + (agent?`<text class="gnsub" x="${x+11}" y="${y+29}">@${esc(agent)}</text>`:'')
405
- + `<title>${esc(title)}${agent?' · @'+esc(agent):''} — ${status}</title></g>`;
421
+ + (c?`<text class="gncost" x="${x+NW-8}" y="${y+(agent?15:23)}">${c}</text>`:'')
422
+ + `<title>${esc(title)}${agent?' · @'+esc(agent):''}${c?` · ${c} notional`:''} — ${status}</title></g>`;
406
423
  };
407
424
  const gedge = (x1,y1,x2,y2,done) => { const mx=(x1+x2)/2;
408
425
  return `<path class="gedge${done?' done':''}" d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}"/>`; };
@@ -435,7 +452,7 @@ function flowHTML(pt, proj){
435
452
  const p = pos[c.id], parents = parentsOf(c);
436
453
  if (parents.length) for (const pid of parents){ const pp = pos[pid]; if (pp) svg += gedge(pp.x+NW, pp.y+NH/2, p.x, p.y+NH/2, byId[pid].status==='done'); }
437
454
  else svg += gedge(planX+NW, planY+NH/2, p.x, p.y+NH/2, ph.status==='done'); // root → plan
438
- svg += gnode(p.x, p.y, (c.id?'#'+c.id+' ':'')+c.title, c.status, c.synthetic?null:c.id, !!c._orch, c.agent||'');
455
+ svg += gnode(p.x, p.y, (c.id?'#'+c.id+' ':'')+c.title, c.status, c.synthetic?null:c.id, !!c._orch, c.agent||'', c.costUsd);
439
456
  if (!hasChild(c)) svg += gedge(p.x+NW, p.y+NH/2, intX, intY+NH/2, c.status==='done'); // leaf → integrate
440
457
  }
441
458
  svg += gnode(intX, intY, '◆ integrate', ph.status, null, true, '');