@dotobokuri/fleet-console 1.59.0 → 1.61.0

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.
Files changed (69) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/cli.d.ts +13 -2
  3. package/dist/cli.mjs +126 -30
  4. package/dist/client/assets/{_baseUniq-CKkDoWU8.js → _baseUniq-9W447fNf.js} +1 -1
  5. package/dist/client/assets/{arc-CcRdX8oF.js → arc-hoitnVWi.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-Q4EWVU46-gaFzE1W7.js → architectureDiagram-Q4EWVU46-DYd58Vfc.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-DXYQGD6D-Bzybih30.js → blockDiagram-DXYQGD6D-kuFqPdP_.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-AHTNJAMY-Dz8bwV9j.js → c4Diagram-AHTNJAMY-C7n41zCn.js} +1 -1
  9. package/dist/client/assets/channel-D2TsEFjE.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-DmgZtGyV.js → chunk-4BX2VUAB-DrHeb54-.js} +1 -1
  11. package/dist/client/assets/{chunk-4TB4RGXK-DX1WxV3U.js → chunk-4TB4RGXK-DFjT6kwT.js} +1 -1
  12. package/dist/client/assets/{chunk-55IACEB6-Bk4b3T9u.js → chunk-55IACEB6-DxEzuA7W.js} +1 -1
  13. package/dist/client/assets/{chunk-EDXVE4YY-BzGadfvT.js → chunk-EDXVE4YY-aAlF0YEo.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-44f7YyvW.js → chunk-FMBD7UC4-DwqRMFL1.js} +1 -1
  15. package/dist/client/assets/{chunk-OYMX7WX6-CyUAky9W.js → chunk-OYMX7WX6-DCkUlaRi.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-C11OUntR.js → chunk-QZHKN3VN-B1Dx8Zhl.js} +1 -1
  17. package/dist/client/assets/{chunk-YZCP3GAM-DVTmWawU.js → chunk-YZCP3GAM-J2bgJmn-.js} +1 -1
  18. package/dist/client/assets/classDiagram-6PBFFD2Q-DhjJiPic.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DhjJiPic.js +1 -0
  20. package/dist/client/assets/clone-DwVC2-Mi.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-CEatGsZv.js → cose-bilkent-S5V4N54A-BxzLzyJp.js} +1 -1
  22. package/dist/client/assets/{dagre-KV5264BT-DlrgN02H.js → dagre-KV5264BT-D1ixDJxZ.js} +1 -1
  23. package/dist/client/assets/{diagram-5BDNPKRD-BZ3hRO5t.js → diagram-5BDNPKRD-DZPIz1Pa.js} +1 -1
  24. package/dist/client/assets/{diagram-G4DWMVQ6-B-izS46x.js → diagram-G4DWMVQ6-Pbcr-a5H.js} +1 -1
  25. package/dist/client/assets/{diagram-MMDJMWI5-Cw-wnJGs.js → diagram-MMDJMWI5-CBssBmPY.js} +1 -1
  26. package/dist/client/assets/{diagram-TYMM5635-BoWnyd4_.js → diagram-TYMM5635-cVQUs6jp.js} +1 -1
  27. package/dist/client/assets/{erDiagram-SMLLAGMA-BUmyXZbP.js → erDiagram-SMLLAGMA-ChMSAXos.js} +1 -1
  28. package/dist/client/assets/{flowDiagram-DWJPFMVM-COS3kjS0.js → flowDiagram-DWJPFMVM-FEIrU5mu.js} +1 -1
  29. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-BTr1fdio.js → ganttDiagram-T4ZO3ILL-D6ELHTJY.js} +1 -1
  30. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-BkGkaosN.js → gitGraphDiagram-UUTBAWPF-DTJ6HgV4.js} +1 -1
  31. package/dist/client/assets/{graph-DmbgK-mH.js → graph-uscjDdHm.js} +1 -1
  32. package/dist/client/assets/index-DMjY1OQ0.js +465 -0
  33. package/dist/client/assets/index-Ec4RaE0g.css +1 -0
  34. package/dist/client/assets/{infoDiagram-42DDH7IO-B8zI6k1l.js → infoDiagram-42DDH7IO-C2AlA___.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-cIcEzrcK.js → ishikawaDiagram-UXIWVN3A-BZGfIhBp.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-VCZTEJTY-DhuabHCk.js → journeyDiagram-VCZTEJTY-DnGNMdvj.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-6JOO6SKY-uHTkqxhX.js → kanban-definition-6JOO6SKY-BkuplZGR.js} +1 -1
  38. package/dist/client/assets/{layout-Cfgk_duA.js → layout-BBayH8iU.js} +1 -1
  39. package/dist/client/assets/{linear-BKvV0sr5.js → linear-BQvaHed0.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-DozmrCuG.js → mermaid.core-CO0f3d6Y.js} +4 -4
  41. package/dist/client/assets/{min-ke46otlW.js → min-B610GdeY.js} +1 -1
  42. package/dist/client/assets/{mindmap-definition-QFDTVHPH-d7xzxhzf.js → mindmap-definition-QFDTVHPH-gwkhgCyj.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-DEJITSTG-C4S9Jy5A.js → pieDiagram-DEJITSTG-DYU2zgk8.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CoQnj3kv.js → quadrantDiagram-34T5L4WZ-B5Vkk65i.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-MS252O5E-CdopMDtg.js → requirementDiagram-MS252O5E-D4EcF7kq.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-XADWPNL6-DdR4zdh4.js → sankeyDiagram-XADWPNL6-DFk-RMV4.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-FGHM5R23-NeYZWVQm.js → sequenceDiagram-FGHM5R23-B-beo9e0.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-FHFEXIEX-D0CQGKV_.js → stateDiagram-FHFEXIEX-VDT0TOd7.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-wHPBl_p1.js +1 -0
  50. package/dist/client/assets/{timeline-definition-GMOUNBTQ-Bz2b3nfQ.js → timeline-definition-GMOUNBTQ-By4DMNqG.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-DHZGUBPP-CVO2Ja80.js → vennDiagram-DHZGUBPP-DzlBrijd.js} +1 -1
  52. package/dist/client/assets/{wardley-RL74JXVD-Ba_GKs5u.js → wardley-RL74JXVD-BHSHTMU6.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-yP3xvrPy.js → wardleyDiagram-NUSXRM2D-COB-cWHQ.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-5P7HB3ND-BLSs6w4C.js → xychartDiagram-5P7HB3ND-C-GVSBHd.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/fleet-plugins/file-explorer/routes.mjs +132 -13
  57. package/dist/fleet-plugins/ledger/routes.mjs +223 -16
  58. package/dist/fleet-plugins/quota/routes.mjs +33 -2
  59. package/dist/fleet-plugins/scuttlebutt/routes.mjs +5 -5
  60. package/dist/fleet-plugins/terminal/routes.mjs +653 -79
  61. package/dist/fleet.mjs +383 -66
  62. package/package.json +1 -1
  63. package/dist/client/assets/channel-CAhLkt5l.js +0 -1
  64. package/dist/client/assets/classDiagram-6PBFFD2Q-BSvLoE2t.js +0 -1
  65. package/dist/client/assets/classDiagram-v2-HSJHXN6E-BSvLoE2t.js +0 -1
  66. package/dist/client/assets/clone-o26VS_sk.js +0 -1
  67. package/dist/client/assets/index-C-3spb_b.js +0 -465
  68. package/dist/client/assets/index-FU1PGEfG.css +0 -1
  69. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-hM1GV37r.js +0 -1
@@ -567,6 +567,7 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs2.watch(p, op
567
567
  try {
568
568
  startWatcher(theaterId, entry, relativePath, target.path, target.identity);
569
569
  entry.pendingDirectories.delete(relativePath);
570
+ scheduleChange(entry, relativePath);
570
571
  } catch {
571
572
  for (const notify of entry.stateSubscribers) notify("degraded");
572
573
  }
@@ -614,6 +615,8 @@ var FolderBrowserError = class extends Error {
614
615
  }
615
616
  };
616
617
  var DIRECTORY_ENTRY_CAP = 500;
618
+ var CLASSIFY_CONCURRENCY = 16;
619
+ var VCS_INTERNAL_NAMES = /* @__PURE__ */ new Set([".git", ".svn", ".hg"]);
617
620
  async function listTheaterContents(theaterPath, relativePath, deps = {}) {
618
621
  const opendir = deps.opendir ?? fs2.promises.opendir;
619
622
  const stat = deps.stat ?? fs2.promises.stat;
@@ -636,10 +639,12 @@ async function listTheaterContents(theaterPath, relativePath, deps = {}) {
636
639
  if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
637
640
  throw new FolderBrowserError("forbidden");
638
641
  }
642
+ if (vcsSegmentOf(realRoot, realTargetAbs)) throw new FolderBrowserError("forbidden");
639
643
  const targetStat = await statDirectory(realTargetAbs, stat);
640
644
  if (!targetStat.isDirectory()) throw new FolderBrowserError("invalid_path");
641
645
  const entries = [];
642
- const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries);
646
+ const hiddenVcs = [];
647
+ const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries, hiddenVcs);
643
648
  entries.sort((a, b) => {
644
649
  if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
645
650
  return a.name.localeCompare(b.name);
@@ -650,7 +655,8 @@ async function listTheaterContents(theaterPath, relativePath, deps = {}) {
650
655
  relativePath: rel,
651
656
  parentRelativePath: parentRel === "" ? null : parentRel ?? null,
652
657
  entries,
653
- ...truncated ? { truncated: true } : {}
658
+ ...truncated ? { truncated: true, cap: DIRECTORY_ENTRY_CAP } : {},
659
+ ...hiddenVcs.length > 0 ? { hiddenVcsInternals: [...new Set(hiddenVcs)].sort() } : {}
654
660
  };
655
661
  }
656
662
  async function statDirectory(targetPath, stat) {
@@ -660,17 +666,100 @@ async function statDirectory(targetPath, stat) {
660
666
  throw mapFolderBrowserFsError(error);
661
667
  }
662
668
  }
663
- async function collectContentsEntries(targetPath, theaterPath, opendir, stat, entries) {
669
+ async function collectContentsEntries(targetPath, theaterPath, opendir, stat, entries, hiddenVcs) {
664
670
  const directory = await openDirectory(targetPath, opendir);
665
671
  try {
672
+ const pending = [];
673
+ let truncated = false;
674
+ const accept = (entry) => {
675
+ if (entry === null) return false;
676
+ if ("vcsInternal" in entry) {
677
+ hiddenVcs.push(entry.vcsInternal);
678
+ return false;
679
+ }
680
+ if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
681
+ entries.push(entry);
682
+ return false;
683
+ };
684
+ const flushPending = async () => {
685
+ while (pending.length > 0) {
686
+ if (entries.length >= DIRECTORY_ENTRY_CAP) {
687
+ pending.length = 0;
688
+ return true;
689
+ }
690
+ const remaining = DIRECTORY_ENTRY_CAP - entries.length;
691
+ const batch = pending.splice(0, Math.min(CLASSIFY_CONCURRENCY, remaining));
692
+ const classified = await Promise.all(
693
+ batch.map((item) => toContentsEntry(targetPath, theaterPath, item, stat))
694
+ );
695
+ let overflowed = false;
696
+ for (const entry of classified) {
697
+ if (overflowed) {
698
+ if (entry && "vcsInternal" in entry) hiddenVcs.push(entry.vcsInternal);
699
+ continue;
700
+ }
701
+ if (accept(entry)) overflowed = true;
702
+ }
703
+ if (overflowed) {
704
+ pending.length = 0;
705
+ return true;
706
+ }
707
+ }
708
+ return false;
709
+ };
710
+ const finishCheapTail = async (first) => {
711
+ pending.length = 0;
712
+ let current = first;
713
+ let omitted = false;
714
+ while (current !== null) {
715
+ if (VCS_INTERNAL_NAMES.has(current.name)) hiddenVcs.push(current.name);
716
+ else {
717
+ omitted = true;
718
+ break;
719
+ }
720
+ current = await directory.read();
721
+ }
722
+ return omitted;
723
+ };
666
724
  let dirent = await directory.read();
667
725
  while (dirent !== null) {
668
- if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
669
- const entry = await toContentsEntry(targetPath, theaterPath, dirent, stat);
670
- if (entry !== null) entries.push(entry);
726
+ if (VCS_INTERNAL_NAMES.has(dirent.name)) {
727
+ hiddenVcs.push(dirent.name);
728
+ dirent = await directory.read();
729
+ continue;
730
+ }
731
+ if (entries.length >= DIRECTORY_ENTRY_CAP) {
732
+ truncated = await finishCheapTail(dirent);
733
+ dirent = null;
734
+ break;
735
+ }
736
+ if (dirent.isDirectory() || dirent.isFile()) {
737
+ if (pending.length > 0 && await flushPending()) {
738
+ truncated = true;
739
+ await finishCheapTail(dirent);
740
+ dirent = null;
741
+ break;
742
+ }
743
+ const rel = path5.relative(theaterPath, path5.join(targetPath, dirent.name));
744
+ if (accept({ name: dirent.name, relativePath: rel, kind: dirent.isDirectory() ? "dir" : "file" })) {
745
+ truncated = true;
746
+ await finishCheapTail(await directory.read());
747
+ dirent = null;
748
+ break;
749
+ }
750
+ } else {
751
+ pending.push(dirent);
752
+ if (pending.length >= CLASSIFY_CONCURRENCY && await flushPending()) {
753
+ truncated = true;
754
+ await finishCheapTail(await directory.read());
755
+ dirent = null;
756
+ break;
757
+ }
758
+ }
671
759
  dirent = await directory.read();
672
760
  }
673
- return false;
761
+ if (!truncated && await flushPending()) truncated = true;
762
+ return truncated;
674
763
  } catch (error) {
675
764
  throw mapFolderBrowserFsError(error);
676
765
  } finally {
@@ -684,6 +773,14 @@ async function openDirectory(targetPath, opendir) {
684
773
  throw mapFolderBrowserFsError(error);
685
774
  }
686
775
  }
776
+ function vcsSegmentOf(realRoot, realPath) {
777
+ const rel = path5.relative(realRoot, realPath);
778
+ if (!rel) return null;
779
+ for (const segment of rel.split(path5.sep)) {
780
+ if (VCS_INTERNAL_NAMES.has(segment)) return segment;
781
+ }
782
+ return null;
783
+ }
687
784
  async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
688
785
  const entryPath = path5.join(targetPath, dirent.name);
689
786
  const rel = path5.relative(theaterPath, entryPath);
@@ -694,6 +791,8 @@ async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
694
791
  const realEntryPath = await fs2.promises.realpath(entryPath);
695
792
  const realNormalizedRoot = theaterPath.endsWith(path5.sep) ? theaterPath : theaterPath + path5.sep;
696
793
  if (realEntryPath !== theaterPath && !realEntryPath.startsWith(realNormalizedRoot)) return null;
794
+ const vcsSegment = vcsSegmentOf(theaterPath, realEntryPath);
795
+ if (vcsSegment) return { vcsInternal: vcsSegment };
697
796
  const s = await stat(realEntryPath);
698
797
  if (s.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
699
798
  if (s.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
@@ -756,7 +855,7 @@ async function readTheaterGitStatus(theaterPath, deps = {}) {
756
855
  ok: true,
757
856
  gitAvailable: true,
758
857
  statuses: statuses.slice(0, GIT_STATUS_CAP),
759
- ...truncated ? { truncated: true } : {}
858
+ ...truncated ? { truncated: true, cap: GIT_STATUS_CAP } : {}
760
859
  };
761
860
  } catch {
762
861
  return { ok: true, gitAvailable: false, statuses: [] };
@@ -859,7 +958,12 @@ async function searchTheaterFiles(theaterPath, query, limit) {
859
958
  const matches = [];
860
959
  let directoryCount = 0;
861
960
  let entryCount = 0;
862
- while (pending.length > 0 && directoryCount < SEARCH_DIRECTORY_CAP && entryCount < SEARCH_ENTRY_CAP) {
961
+ let walkCapped = false;
962
+ while (pending.length > 0) {
963
+ if (directoryCount >= SEARCH_DIRECTORY_CAP || entryCount >= SEARCH_ENTRY_CAP) {
964
+ walkCapped = true;
965
+ break;
966
+ }
863
967
  const directory = pending.shift();
864
968
  if (!directory || visited.has(directory.absolutePath)) continue;
865
969
  visited.add(directory.absolutePath);
@@ -872,8 +976,12 @@ async function searchTheaterFiles(theaterPath, query, limit) {
872
976
  }
873
977
  entries.sort((left, right) => left.name.localeCompare(right.name));
874
978
  for (const entry of entries) {
875
- if (entryCount >= SEARCH_ENTRY_CAP) break;
979
+ if (entryCount >= SEARCH_ENTRY_CAP) {
980
+ walkCapped = true;
981
+ break;
982
+ }
876
983
  entryCount += 1;
984
+ if (VCS_INTERNAL_NAMES.has(entry.name)) continue;
877
985
  const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
878
986
  const absolutePath = path5.join(directory.absolutePath, entry.name);
879
987
  let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
@@ -882,6 +990,7 @@ async function searchTheaterFiles(theaterPath, query, limit) {
882
990
  try {
883
991
  realPath = await fsp.realpath(absolutePath);
884
992
  if (!isContained(realRoot, realPath)) continue;
993
+ if (vcsSegmentOf(realRoot, realPath)) continue;
885
994
  const stat = await fsp.stat(realPath);
886
995
  kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
887
996
  } catch {
@@ -896,8 +1005,14 @@ async function searchTheaterFiles(theaterPath, query, limit) {
896
1005
  const low = relativePath.toLocaleLowerCase();
897
1006
  if (tokens.every((token) => low.includes(token))) matches.push({ relativePath });
898
1007
  }
1008
+ if (walkCapped) break;
899
1009
  }
900
- return matches.sort((left, right) => compareFileSearchItem(left, right, query)).slice(0, limit);
1010
+ const sorted = matches.sort((left, right) => compareFileSearchItem(left, right, query));
1011
+ return {
1012
+ files: sorted.slice(0, limit),
1013
+ totalMatches: sorted.length,
1014
+ ...walkCapped ? { walkCapped: true } : {}
1015
+ };
901
1016
  }
902
1017
  async function handleFilesSearch(req, res, ctx) {
903
1018
  if (req.method !== "POST") {
@@ -919,8 +1034,11 @@ async function handleFilesSearch(req, res, ctx) {
919
1034
  return;
920
1035
  }
921
1036
  try {
1037
+ const outcome = await searchTheaterFiles(theaterPath, body.query, body.limit);
922
1038
  ctx.host.http.writeJson(res, 200, {
923
- files: await searchTheaterFiles(theaterPath, body.query, body.limit)
1039
+ files: outcome.files,
1040
+ totalMatches: outcome.totalMatches,
1041
+ ...outcome.walkCapped ? { walkCapped: true } : {}
924
1042
  });
925
1043
  } catch {
926
1044
  ctx.host.http.writeJson(res, 500, { error: "search_failed" });
@@ -978,7 +1096,8 @@ async function handleFilesList(req, res, ctx) {
978
1096
  }
979
1097
  try {
980
1098
  const result = await listTheaterContents(theaterPath, relPath);
981
- await watcherRegistry.trackDirectory(rawTheaterId, theaterPath, relPath);
1099
+ void watcherRegistry.trackDirectory(rawTheaterId, theaterPath, relPath).catch(() => {
1100
+ });
982
1101
  ctx.host.http.writeJson(res, 200, result);
983
1102
  } catch (error) {
984
1103
  if (error instanceof FolderBrowserError) {
@@ -338,7 +338,7 @@ async function handleSummary(req, res, ctx, service) {
338
338
 
339
339
  // ../fleet-plugins/ledger/server/parser.ts
340
340
  var CLIENT_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
341
- var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}$/;
341
+ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}(?:\[1[mM]\])?$/;
342
342
  var UUID_FRAGMENT_RE = /(?:^|[^0-9a-f])[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:$|[^0-9a-f])/i;
343
343
  function isRecord(value) {
344
344
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -398,8 +398,112 @@ function parseTokscaleOutput(stdout) {
398
398
  if (parsed.length > 0 && sessions.length === 0) return { status: "unreadable", sessions: [], skippedSessions };
399
399
  return { status: skippedSessions > 0 ? "degraded" : "ok", sessions, skippedSessions };
400
400
  }
401
+ function parseModelEntry(value) {
402
+ if (!isRecord(value)) return null;
403
+ const models = sanitizeModels(typeof value.model === "string" ? [value.model] : []);
404
+ if (models.length !== 1 || !isNonNegativeSafeInteger(value.input) || !isNonNegativeSafeInteger(value.output) || !isNonNegativeSafeInteger(value.cacheRead) || !isNonNegativeFinite(value.cost) || !isNonNegativeSafeInteger(value.messageCount)) return null;
405
+ return {
406
+ modelId: models[0],
407
+ input: value.input,
408
+ output: value.output,
409
+ cacheRead: value.cacheRead,
410
+ costUsd: value.cost,
411
+ messages: value.messageCount
412
+ };
413
+ }
414
+ function parseTokscaleModelsOutput(stdout) {
415
+ let parsed;
416
+ try {
417
+ parsed = JSON.parse(stdout);
418
+ } catch {
419
+ return { status: "unreadable", entries: [], skippedEntries: 0 };
420
+ }
421
+ if (!isRecord(parsed) || !Array.isArray(parsed.entries)) {
422
+ return { status: "unreadable", entries: [], skippedEntries: 0 };
423
+ }
424
+ const entries = parsed.entries.map(parseModelEntry).filter((entry) => entry !== null);
425
+ const skippedEntries = parsed.entries.length - entries.length;
426
+ if (parsed.entries.length > 0 && entries.length === 0) {
427
+ return { status: "unreadable", entries: [], skippedEntries };
428
+ }
429
+ return { status: skippedEntries > 0 ? "degraded" : "ok", entries, skippedEntries };
430
+ }
431
+
432
+ // ../fleet-plugins/ledger/server/identity.ts
433
+ var LEDGER_LAUNCH_PROVIDERS = ["claude", "codex", "cursor", "kimi", "opencode", "xai"];
434
+ var GATEWAY_MODEL_PREFIX = "claude-gateway--";
435
+ var NATIVE_SUPPLIER = "native";
436
+ var PROVIDER_SET = new Set(LEDGER_LAUNCH_PROVIDERS);
437
+ function isLaunchProvider(value) {
438
+ return typeof value === "string" && PROVIDER_SET.has(value);
439
+ }
440
+ function readLaunchProvider(payload) {
441
+ return isLaunchProvider(payload.launchProvider) ? payload.launchProvider : null;
442
+ }
443
+ function parseModelIdentity(modelId) {
444
+ let rest = modelId;
445
+ let supplier = NATIVE_SUPPLIER;
446
+ if (rest.startsWith(GATEWAY_MODEL_PREFIX)) {
447
+ rest = rest.slice(GATEWAY_MODEL_PREFIX.length);
448
+ const separator = rest.indexOf("--");
449
+ if (separator > 0) {
450
+ const candidate = rest.slice(0, separator);
451
+ if (PROVIDER_SET.has(candidate)) {
452
+ supplier = candidate;
453
+ rest = rest.slice(separator + 2);
454
+ }
455
+ }
456
+ } else {
457
+ const inferred = inferUnprefixedSupplier(rest);
458
+ supplier = inferred.supplier;
459
+ rest = inferred.bare;
460
+ }
461
+ return {
462
+ modelId,
463
+ supplier,
464
+ bare: rest,
465
+ label: humanizeBareModel(rest)
466
+ };
467
+ }
468
+ function inferUnprefixedSupplier(modelId) {
469
+ const slash = modelId.indexOf("/");
470
+ if (slash > 0) {
471
+ const candidate = modelId.slice(0, slash).toLowerCase();
472
+ if (PROVIDER_SET.has(candidate)) {
473
+ return { supplier: candidate, bare: modelId.slice(slash + 1) };
474
+ }
475
+ }
476
+ const lower = modelId.toLowerCase();
477
+ if (lower.startsWith("claude")) return { supplier: "claude", bare: modelId };
478
+ if (lower.startsWith("gpt-")) return { supplier: "codex", bare: modelId };
479
+ if (lower.startsWith("grok")) return { supplier: "xai", bare: modelId };
480
+ if (lower.startsWith("kimi")) return { supplier: "kimi", bare: modelId };
481
+ return { supplier: NATIVE_SUPPLIER, bare: modelId };
482
+ }
483
+ function normalizeModelKey(modelId) {
484
+ return parseModelIdentity(modelId).bare.toLowerCase().replace(/\bgpt-(\d)-(\d)\b/g, "gpt-$1.$2");
485
+ }
486
+ function humanizeBareModel(bare) {
487
+ const bracketMillion = /\[1m\]$/i.test(bare);
488
+ const tokens = bare.replace(/\[1m\]$/i, "").split(/[-_]/).filter(Boolean);
489
+ const trailingMillion = tokens.at(-1)?.toLowerCase() === "1m";
490
+ const nameTokens = trailingMillion ? tokens.slice(0, -1) : tokens;
491
+ const words = nameTokens.map((token) => {
492
+ if (/^\d/.test(token)) return token;
493
+ const lower = token.toLowerCase();
494
+ if (lower === "gpt") return "GPT";
495
+ return `${token.charAt(0).toUpperCase()}${token.slice(1)}`;
496
+ });
497
+ const joined = words.join(" ").replace(/\bGPT (\d) (\d)\b/g, "GPT $1.$2");
498
+ return bracketMillion || trailingMillion ? `${joined} (1M)` : joined;
499
+ }
401
500
 
402
501
  // ../fleet-plugins/ledger/server/summary.ts
502
+ var EMPTY_MODEL_BREAKDOWN = {
503
+ entries: [],
504
+ status: "ok",
505
+ skippedEntries: 0
506
+ };
403
507
  var AggregateOverflowError = class extends Error {
404
508
  };
405
509
  var MAX_DAILY_DAYS = 366;
@@ -475,7 +579,8 @@ function readClaim(operation) {
475
579
  sessionId: candidate.sessionId,
476
580
  provider: candidate.provider,
477
581
  cliId: stringField(operation.payload, "cliId"),
478
- cliLabel: stringField(operation.payload, "cliLabel")
582
+ cliLabel: stringField(operation.payload, "cliLabel"),
583
+ launchProvider: readLaunchProvider(operation.payload)
479
584
  };
480
585
  }
481
586
  function nativeSessionId(session) {
@@ -489,9 +594,17 @@ function nativeSessionId(session) {
489
594
  const match = session.sessionId.match(new RegExp(`^rollout-${timestamp}-(${uuid})$`, "i"));
490
595
  return match?.[1]?.toLowerCase() ?? null;
491
596
  }
492
- function buildSummary(sessions, operations, scope, status = "ok", generatedAtMs = Date.now(), skippedSessions = 0) {
597
+ function emptyModelFields(status = "ok", skippedEntries = 0) {
598
+ return {
599
+ suppliers: [],
600
+ modelRows: [],
601
+ modelTotals: { costUsd: 0, input: 0, output: 0, cacheRead: 0, messages: 0, models: 0 },
602
+ modelSource: { status, skippedEntries }
603
+ };
604
+ }
605
+ function buildSummary(sessions, operations, scope, status = "ok", generatedAtMs = Date.now(), skippedSessions = 0, modelBreakdown = EMPTY_MODEL_BREAKDOWN) {
493
606
  try {
494
- return buildSummaryUnchecked(sessions, operations, scope, status, generatedAtMs, skippedSessions);
607
+ return buildSummaryUnchecked(sessions, operations, scope, status, generatedAtMs, skippedSessions, modelBreakdown);
495
608
  } catch (error) {
496
609
  if (!(error instanceof AggregateOverflowError)) throw error;
497
610
  return {
@@ -505,6 +618,7 @@ function buildSummary(sessions, operations, scope, status = "ok", generatedAtMs
505
618
  otherTheaterTotals: { costUsd: 0, input: 0, output: 0, cacheRead: 0, messages: 0 },
506
619
  deviceTotals: { costUsd: 0, input: 0, output: 0, cacheRead: 0, messages: 0, sessions: 0 },
507
620
  clients: [],
621
+ ...emptyModelFields("unreadable", modelBreakdown.skippedEntries + modelBreakdown.entries.length),
508
622
  daily: [],
509
623
  dailyAttributed: [],
510
624
  source: {
@@ -514,7 +628,7 @@ function buildSummary(sessions, operations, scope, status = "ok", generatedAtMs
514
628
  };
515
629
  }
516
630
  }
517
- function buildSummaryUnchecked(sessions, operations, scope, status, generatedAtMs, skippedSessions) {
631
+ function buildSummaryUnchecked(sessions, operations, scope, status, generatedAtMs, skippedSessions, modelBreakdown) {
518
632
  const operationClaims = operations.map(readClaim).filter((claim) => claim !== null).sort((a, b) => b.operation.ts.updatedAt - a.operation.ts.updatedAt || (a.operation.id < b.operation.id ? -1 : a.operation.id > b.operation.id ? 1 : 0));
519
633
  const claims = /* @__PURE__ */ new Map();
520
634
  for (const claim of operationClaims) {
@@ -547,6 +661,7 @@ function buildSummaryUnchecked(sessions, operations, scope, status, generatedAtM
547
661
  title: claim.operation.title,
548
662
  cliId: claim.cliId,
549
663
  cliLabel: claim.cliLabel,
664
+ launchProvider: claim.launchProvider,
550
665
  client: claimed[0]?.client ?? claim.provider,
551
666
  messages: totals.messages,
552
667
  usage: usageOf(totals),
@@ -560,6 +675,7 @@ function buildSummaryUnchecked(sessions, operations, scope, status, generatedAtM
560
675
  title: claim.operation.title,
561
676
  cliId: claim.cliId,
562
677
  cliLabel: claim.cliLabel,
678
+ launchProvider: claim.launchProvider,
563
679
  lastActivityAtMs: claim.operation.ts.updatedAt
564
680
  }));
565
681
  const unmatched = unmatchedAll.slice(0, MAX_UNMATCHED);
@@ -608,11 +724,77 @@ function buildSummaryUnchecked(sessions, operations, scope, status, generatedAtM
608
724
  otherTheaterTotals: { ...usageOf(otherTheaterValues), costUsd: otherTheaterValues.costUsd, messages: otherTheaterValues.messages },
609
725
  deviceTotals: { ...usageOf(deviceValues), costUsd: deviceValues.costUsd, messages: deviceValues.messages, sessions: deviceSessions },
610
726
  clients,
727
+ ...buildModelFields(modelBreakdown),
611
728
  daily,
612
729
  dailyAttributed,
613
730
  source: { status, skippedSessions }
614
731
  };
615
732
  }
733
+ var MAX_MODEL_ROWS = 80;
734
+ function buildModelFields(modelBreakdown) {
735
+ if (modelBreakdown.status === "unavailable" || modelBreakdown.status === "unreadable" || modelBreakdown.status === "bootstrapping") {
736
+ return emptyModelFields(modelBreakdown.status, modelBreakdown.skippedEntries);
737
+ }
738
+ try {
739
+ return buildModelFieldsUnchecked(modelBreakdown);
740
+ } catch (error) {
741
+ if (!(error instanceof AggregateOverflowError)) throw error;
742
+ return emptyModelFields("unreadable", modelBreakdown.skippedEntries + modelBreakdown.entries.length);
743
+ }
744
+ }
745
+ function buildModelFieldsUnchecked(modelBreakdown) {
746
+ const modelMap = /* @__PURE__ */ new Map();
747
+ const supplierMap = /* @__PURE__ */ new Map();
748
+ const grand = emptyAccumulator();
749
+ for (const entry of modelBreakdown.entries) {
750
+ const identity = parseModelIdentity(entry.modelId);
751
+ const key = `${identity.supplier}:${normalizeModelKey(entry.modelId)}`;
752
+ const model = modelMap.get(key) ?? { identity, totals: emptyAccumulator() };
753
+ addModelEntry(model.totals, entry);
754
+ modelMap.set(key, model);
755
+ const supplier = supplierMap.get(identity.supplier) ?? { models: /* @__PURE__ */ new Set(), totals: emptyAccumulator() };
756
+ supplier.models.add(key);
757
+ addModelEntry(supplier.totals, entry);
758
+ supplierMap.set(identity.supplier, supplier);
759
+ addModelEntry(grand, entry);
760
+ }
761
+ const modelRows = [...modelMap.values()].map(({ identity, totals }) => ({
762
+ modelId: identity.modelId,
763
+ label: identity.label,
764
+ supplier: identity.supplier,
765
+ usage: usageOf(totals),
766
+ costUsd: totals.costUsd,
767
+ messages: totals.messages
768
+ })).sort((a, b) => b.costUsd - a.costUsd || b.messages - a.messages || (a.label < b.label ? -1 : 1)).slice(0, MAX_MODEL_ROWS);
769
+ const suppliers = [...supplierMap.entries()].map(([supplier, value]) => ({
770
+ supplier,
771
+ models: value.models.size,
772
+ usage: usageOf(value.totals),
773
+ costUsd: value.totals.costUsd,
774
+ messages: value.totals.messages
775
+ })).sort((a, b) => b.costUsd - a.costUsd || (a.supplier < b.supplier ? -1 : 1));
776
+ return {
777
+ suppliers,
778
+ modelRows,
779
+ modelTotals: {
780
+ ...usageOf(grand),
781
+ costUsd: grand.costUsd,
782
+ messages: grand.messages,
783
+ models: modelMap.size
784
+ },
785
+ modelSource: {
786
+ status: modelBreakdown.status,
787
+ skippedEntries: modelBreakdown.skippedEntries
788
+ }
789
+ };
790
+ }
791
+ function addModelEntry(target, entry) {
792
+ target.input = addFinite(target.input, entry.input);
793
+ target.output = addFinite(target.output, entry.output);
794
+ target.cacheRead = addFinite(target.cacheRead, entry.cacheRead);
795
+ target.costUsd = addFinite(target.costUsd, entry.costUsd);
796
+ target.messages = addFinite(target.messages, entry.messages);
797
+ }
616
798
 
617
799
  // ../fleet-plugins/ledger/server/service.ts
618
800
  var CACHE_TTL_MS = 15e3;
@@ -626,14 +808,17 @@ function createLedgerService(deps) {
626
808
  function cacheKey(request) {
627
809
  return request.window;
628
810
  }
629
- function summaryFromReport(request, report) {
630
- return buildSummary(report.sessions, request.operations, {
811
+ function summaryFromWindow(request, window) {
812
+ return buildSummary(window.report.sessions, request.operations, {
631
813
  theaterId: request.theaterId,
632
814
  window: request.window
633
- }, report.status, report.generatedAtMs, report.skippedSessions);
815
+ }, window.report.status, window.report.generatedAtMs, window.report.skippedSessions, window.models);
634
816
  }
635
817
  function statusDto(request, status) {
636
- return summaryFromReport(request, { sessions: [], status, skippedSessions: 0, generatedAtMs: now() });
818
+ return summaryFromWindow(request, {
819
+ report: { sessions: [], status, skippedSessions: 0, generatedAtMs: now() },
820
+ models: { ...EMPTY_MODEL_BREAKDOWN, status }
821
+ });
637
822
  }
638
823
  async function executeReport(window) {
639
824
  const args = ["report", "--json", "--no-summarize", `--${window}`];
@@ -654,18 +839,40 @@ function createLedgerService(deps) {
654
839
  return { sessions: [], status: "unavailable", skippedSessions: 0, generatedAtMs: now() };
655
840
  }
656
841
  }
842
+ async function executeModels(window) {
843
+ const args = ["models", "--json", "--no-spinner", "--group-by", "model", `--${window}`];
844
+ try {
845
+ const result = await deps.executor(args, {
846
+ cwd: os.homedir(),
847
+ timeout: TOKSCALE_TIMEOUT_MS
848
+ });
849
+ if (result.exitCode !== 0) return { entries: [], status: "unavailable", skippedEntries: 0 };
850
+ const parsed = parseTokscaleModelsOutput(result.stdout);
851
+ return {
852
+ entries: parsed.entries,
853
+ status: parsed.status,
854
+ skippedEntries: parsed.skippedEntries
855
+ };
856
+ } catch {
857
+ return { entries: [], status: "unavailable", skippedEntries: 0 };
858
+ }
859
+ }
860
+ async function executeWindow(window) {
861
+ const [report, models] = await Promise.all([executeReport(window), executeModels(window)]);
862
+ return { report, models };
863
+ }
657
864
  function getOrStartReport(key, window) {
658
865
  const existing = reportInFlight.get(key);
659
866
  if (existing) return existing;
660
- const task = executeReport(window).finally(() => {
867
+ const task = executeWindow(window).finally(() => {
661
868
  reportInFlight.delete(key);
662
869
  });
663
870
  reportInFlight.set(key, task);
664
871
  return task;
665
872
  }
666
873
  function startBootstrapAndReport(key, request) {
667
- const task = bootstrap(deps.cliHome).then(() => getOrStartReport(key, request.window)).then((report) => {
668
- cache.set(key, { report, expiresAt: now() + CACHE_TTL_MS });
874
+ const task = bootstrap(deps.cliHome).then(() => getOrStartReport(key, request.window)).then((window) => {
875
+ cache.set(key, { window, expiresAt: now() + CACHE_TTL_MS });
669
876
  }).catch(() => {
670
877
  }).finally(() => {
671
878
  bootstrapPending.delete(key);
@@ -677,7 +884,7 @@ function createLedgerService(deps) {
677
884
  const key = cacheKey(request);
678
885
  if (request.refresh) cache.delete(key);
679
886
  const cached = cache.get(key);
680
- if (cached && cached.expiresAt > now()) return summaryFromReport(request, cached.report);
887
+ if (cached && cached.expiresAt > now()) return summaryFromWindow(request, cached.window);
681
888
  cache.delete(key);
682
889
  if (bootstrapPending.has(key)) return statusDto(request, "bootstrapping");
683
890
  if (!await isInstalled(deps.cliHome)) {
@@ -685,9 +892,9 @@ function createLedgerService(deps) {
685
892
  startBootstrapAndReport(key, request);
686
893
  return statusDto(request, "bootstrapping");
687
894
  }
688
- const report = await getOrStartReport(key, request.window);
689
- cache.set(key, { report, expiresAt: now() + CACHE_TTL_MS });
690
- return summaryFromReport(request, report);
895
+ const window = await getOrStartReport(key, request.window);
896
+ cache.set(key, { window, expiresAt: now() + CACHE_TTL_MS });
897
+ return summaryFromWindow(request, window);
691
898
  }
692
899
  };
693
900
  }