@dropalltables/yacu 0.1.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/yacu.js +1782 -0
  4. package/package.json +62 -0
  5. package/src/data/jsonl.ts +81 -0
  6. package/src/data/load.ts +52 -0
  7. package/src/data/pricing.test.ts +22 -0
  8. package/src/data/pricing.ts +49 -0
  9. package/src/data/sources/claude.test.ts +40 -0
  10. package/src/data/sources/claude.ts +118 -0
  11. package/src/data/sources/codex.ts +73 -0
  12. package/src/data/sources/cursor.test.ts +16 -0
  13. package/src/data/sources/cursor.ts +83 -0
  14. package/src/data/sources/gemini.test.ts +27 -0
  15. package/src/data/sources/gemini.ts +112 -0
  16. package/src/data/sources/grok.ts +60 -0
  17. package/src/data/sources/opencode.ts +53 -0
  18. package/src/data/types.ts +23 -0
  19. package/src/domain/aggregate.test.ts +62 -0
  20. package/src/domain/aggregate.ts +143 -0
  21. package/src/domain/dates.ts +24 -0
  22. package/src/domain/types.ts +48 -0
  23. package/src/index.tsx +15 -0
  24. package/src/tui/App.tsx +170 -0
  25. package/src/tui/ThemeContext.tsx +12 -0
  26. package/src/tui/chart.test.ts +48 -0
  27. package/src/tui/chart.ts +130 -0
  28. package/src/tui/components/Breakdown.tsx +85 -0
  29. package/src/tui/components/Chart.test.tsx +67 -0
  30. package/src/tui/components/Chart.tsx +159 -0
  31. package/src/tui/components/Footer.tsx +11 -0
  32. package/src/tui/components/Header.tsx +52 -0
  33. package/src/tui/components/PointerButton.tsx +40 -0
  34. package/src/tui/components/ScanBoot.test.tsx +30 -0
  35. package/src/tui/components/ScanBoot.tsx +30 -0
  36. package/src/tui/components/Segmented.test.tsx +32 -0
  37. package/src/tui/components/Segmented.tsx +59 -0
  38. package/src/tui/components/Summary.tsx +67 -0
  39. package/src/tui/components/Totals.tsx +27 -0
  40. package/src/tui/format.ts +44 -0
  41. package/src/tui/theme.ts +99 -0
  42. package/src/tui/usePointer.ts +11 -0
  43. package/tsconfig.json +15 -0
@@ -0,0 +1,112 @@
1
+ import { homedir } from "node:os"
2
+ import { basename, join } from "node:path"
3
+ import { localDate } from "../../domain/dates"
4
+ import type { UsageRecord, UsageSession } from "../../domain/types"
5
+ import { estimateCacheSavings, estimateCost } from "../pricing"
6
+ import { asObject, globFiles, numberValue, stringValue } from "../jsonl"
7
+ import type { SourceLoadResult } from "../types"
8
+
9
+ type ParsedConversation = {
10
+ sessionId: string
11
+ startTime: string | null
12
+ messages: Array<Record<string, unknown>>
13
+ }
14
+
15
+ export async function loadGeminiUsage(): Promise<SourceLoadResult> {
16
+ const root = process.env.GEMINI_HOME ?? join(homedir(), ".gemini")
17
+ const files = [
18
+ ...await globFiles(join(root, "tmp"), "**/chats/*.json"),
19
+ ...await globFiles(join(root, "tmp"), "**/chats/*.jsonl"),
20
+ ]
21
+ const records: UsageRecord[] = []
22
+ const sessions = new Map<string, UsageSession>()
23
+
24
+ for (const path of files) {
25
+ let parsed: ParsedConversation | null = null
26
+ try {
27
+ parsed = parseGeminiConversation(await Bun.file(path).text(), basename(path))
28
+ } catch {
29
+ continue
30
+ }
31
+ if (parsed == null) continue
32
+ const sessionId = `gemini:${parsed.sessionId}`
33
+
34
+ for (const message of parsed.messages) {
35
+ if (message.type !== "gemini") continue
36
+ const usage = asObject(message.tokens) ?? asObject(message.usageMetadata)
37
+ if (usage == null) continue
38
+ const cached = numberValue(usage.cached) || numberValue(usage.cachedContentTokenCount)
39
+ const prompt = numberValue(usage.input) || numberValue(usage.promptTokenCount)
40
+ const candidates = numberValue(usage.output) || numberValue(usage.candidatesTokenCount)
41
+ const thoughts = numberValue(usage.thoughts) || numberValue(usage.thoughtsTokenCount)
42
+ const tool = numberValue(usage.tool) || numberValue(usage.toolUsePromptTokenCount)
43
+ const tokens = {
44
+ inputTokens: Math.max(0, prompt - cached) + tool,
45
+ outputTokens: candidates + thoughts,
46
+ cacheCreationTokens: 0,
47
+ cacheReadTokens: cached,
48
+ }
49
+ if (Object.values(tokens).every((value) => value === 0)) continue
50
+ const model = stringValue(message.model) ?? "gemini"
51
+ const timestamp = stringValue(message.timestamp) ?? parsed.startTime ?? new Date().toISOString()
52
+ const date = localDate(timestamp)
53
+ records.push({
54
+ date,
55
+ source: "gemini",
56
+ model,
57
+ sessionId,
58
+ ...tokens,
59
+ costUsd: estimateCost(model, tokens),
60
+ cacheSavingsUsd: estimateCacheSavings(model, cached),
61
+ })
62
+ sessions.set(sessionId, { id: sessionId, source: "gemini", date })
63
+ }
64
+ }
65
+
66
+ return { records, sessions: [...sessions.values()], files: files.length }
67
+ }
68
+
69
+ export function parseGeminiConversation(text: string, fallbackId = "session"): ParsedConversation | null {
70
+ const trimmed = text.trim()
71
+ if (trimmed === "") return null
72
+
73
+ try {
74
+ const full = asObject(JSON.parse(trimmed))
75
+ if (full != null && Array.isArray(full.messages)) {
76
+ return {
77
+ sessionId: stringValue(full.sessionId) ?? fallbackId,
78
+ startTime: stringValue(full.startTime),
79
+ messages: full.messages.flatMap((message) => {
80
+ const row = asObject(message)
81
+ return row == null ? [] : [row]
82
+ }),
83
+ }
84
+ }
85
+ } catch {
86
+ // JSONL is parsed below.
87
+ }
88
+
89
+ let sessionId = fallbackId
90
+ let startTime: string | null = null
91
+ const messages: Array<Record<string, unknown>> = []
92
+ for (const line of trimmed.split("\n")) {
93
+ let row: Record<string, unknown> | null = null
94
+ try {
95
+ row = asObject(JSON.parse(line))
96
+ } catch {
97
+ continue
98
+ }
99
+ if (row == null) continue
100
+ const metadata = asObject(row.$set) ?? row
101
+ sessionId = stringValue(metadata.sessionId) ?? sessionId
102
+ startTime = stringValue(metadata.startTime) ?? startTime
103
+ const rewindTo = stringValue(row.$rewindTo)
104
+ if (rewindTo != null) {
105
+ const index = messages.findIndex((message) => message.id === rewindTo)
106
+ if (index >= 0) messages.splice(index + 1)
107
+ } else if (stringValue(row.type) != null) {
108
+ messages.push(row)
109
+ }
110
+ }
111
+ return { sessionId, startTime, messages }
112
+ }
@@ -0,0 +1,60 @@
1
+ import { homedir } from "node:os"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { localDate } from "../../domain/dates"
4
+ import type { UsageRecord, UsageSession } from "../../domain/types"
5
+ import { estimateCacheSavings, estimateCost } from "../pricing"
6
+ import { asObject, forEachJsonLine, globFiles, numberValue, stringValue } from "../jsonl"
7
+ import type { SourceLoadResult } from "../types"
8
+
9
+ export async function loadGrokUsage(): Promise<SourceLoadResult> {
10
+ const root = process.env.GROK_HOME ?? join(homedir(), ".grok")
11
+ const files = await globFiles(join(root, "sessions"), "**/updates.jsonl")
12
+ const records: UsageRecord[] = []
13
+ const sessions = new Map<string, UsageSession>()
14
+
15
+ for (const path of files) {
16
+ const sessionId = `grok:${basename(dirname(path))}`
17
+ await forEachJsonLine(path, (unknownValue) => {
18
+ const value = asObject(unknownValue)
19
+ const update = asObject(value?.update)
20
+ const usage = asObject(update?.usage)
21
+ if (update?.sessionUpdate !== "turn_completed" || usage == null) return
22
+ const metadata = asObject(value?._meta)
23
+ const timestamp = stringValue(value?.timestamp) ?? (numberValue(metadata?.agentTimestampMs) || Date.now())
24
+ const date = localDate(timestamp)
25
+ const models = asObject(usage.modelUsage)
26
+ const entries = models == null ? [["grok-build", usage] as const] : Object.entries(models)
27
+ const cost = numberValue(usage.costUsdTicks) / 1_000_000_000
28
+ const tokenWeights = entries.map(([, entry]) => {
29
+ const tokens = asObject(entry)
30
+ return numberValue(tokens?.inputTokens) + numberValue(tokens?.outputTokens)
31
+ })
32
+ const totalWeight = tokenWeights.reduce((sum, value) => sum + value, 0)
33
+
34
+ entries.forEach(([model, entry], index) => {
35
+ const raw = asObject(entry)
36
+ const cached = numberValue(raw?.cachedReadTokens)
37
+ const input = Math.max(0, numberValue(raw?.inputTokens) - cached)
38
+ const tokens = {
39
+ inputTokens: input,
40
+ outputTokens: numberValue(raw?.outputTokens),
41
+ cacheCreationTokens: 0,
42
+ cacheReadTokens: cached,
43
+ }
44
+ const allocatedCost = totalWeight > 0 ? cost * (tokenWeights[index]! / totalWeight) : 0
45
+ records.push({
46
+ date,
47
+ source: "grok",
48
+ model,
49
+ sessionId,
50
+ ...tokens,
51
+ costUsd: allocatedCost || estimateCost(model, tokens),
52
+ cacheSavingsUsd: estimateCacheSavings(model, cached),
53
+ })
54
+ })
55
+ sessions.set(sessionId, { id: sessionId, source: "grok", date })
56
+ })
57
+ }
58
+
59
+ return { records, sessions: [...sessions.values()], files: files.length }
60
+ }
@@ -0,0 +1,53 @@
1
+ import { homedir } from "node:os"
2
+ import { join } from "node:path"
3
+ import { localDate } from "../../domain/dates"
4
+ import type { UsageRecord, UsageSession } from "../../domain/types"
5
+ import { estimateCacheSavings, estimateCost } from "../pricing"
6
+ import { asObject, globFiles, numberValue, stringValue } from "../jsonl"
7
+ import type { SourceLoadResult } from "../types"
8
+
9
+ export async function loadOpenCodeUsage(): Promise<SourceLoadResult> {
10
+ const root = process.env.OPENCODE_DATA_DIR ?? join(homedir(), ".local", "share", "opencode")
11
+ const files = await globFiles(join(root, "storage", "message"), "**/*.json")
12
+ const seen = new Set<string>()
13
+ const records: UsageRecord[] = []
14
+ const sessions = new Map<string, UsageSession>()
15
+
16
+ for (const path of files) {
17
+ try {
18
+ const message = asObject(await Bun.file(path).json())
19
+ const id = stringValue(message?.id)
20
+ const model = stringValue(message?.modelID)
21
+ const tokensValue = asObject(message?.tokens)
22
+ if (id == null || model == null || tokensValue == null || seen.has(id)) continue
23
+ seen.add(id)
24
+ const cache = asObject(tokensValue.cache)
25
+ const tokens = {
26
+ inputTokens: numberValue(tokensValue.input),
27
+ outputTokens: numberValue(tokensValue.output),
28
+ cacheCreationTokens: numberValue(cache?.write),
29
+ cacheReadTokens: numberValue(cache?.read),
30
+ }
31
+ if (Object.values(tokens).every((value) => value === 0)) continue
32
+ const time = asObject(message?.time)
33
+ const date = localDate(numberValue(time?.created) || Date.now())
34
+ const rawSession = stringValue(message?.sessionID) ?? id
35
+ const sessionId = `opencode:${rawSession}`
36
+ const localCost = typeof message?.cost === "number" ? message.cost : null
37
+ records.push({
38
+ date,
39
+ source: "opencode",
40
+ model,
41
+ sessionId,
42
+ ...tokens,
43
+ costUsd: localCost ?? estimateCost(model, tokens),
44
+ cacheSavingsUsd: estimateCacheSavings(model, tokens.cacheReadTokens),
45
+ })
46
+ sessions.set(sessionId, { id: sessionId, source: "opencode", date })
47
+ } catch {
48
+ continue
49
+ }
50
+ }
51
+
52
+ return { records, sessions: [...sessions.values()], files: files.length }
53
+ }
@@ -0,0 +1,23 @@
1
+ import type { SourceId, UsageRecord, UsageSession } from "../domain/types"
2
+
3
+ export type SourceLoadResult = {
4
+ records: UsageRecord[]
5
+ sessions: UsageSession[]
6
+ files: number
7
+ }
8
+
9
+ export type ScanStatus = "pending" | "scanning" | "done" | "error"
10
+
11
+ export type ScanProgress = {
12
+ source: SourceId
13
+ label: string
14
+ status: ScanStatus
15
+ completed: number
16
+ total: number
17
+ files?: number
18
+ records?: number
19
+ sessions?: number
20
+ error?: string
21
+ }
22
+
23
+ export type ScanProgressHandler = (progress: ScanProgress) => void
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { localDate } from "./dates"
3
+ import { buildDashboard } from "./aggregate"
4
+ import type { UsageDataset } from "./types"
5
+
6
+ const today = localDate(new Date())
7
+ const dataset: UsageDataset = {
8
+ scannedAt: new Date(),
9
+ errors: [],
10
+ sessions: [
11
+ { id: "claude-1", source: "claude", date: today },
12
+ { id: "codex-1", source: "codex", date: today },
13
+ ],
14
+ records: [
15
+ {
16
+ date: today,
17
+ source: "claude",
18
+ model: "claude-opus-4-1",
19
+ sessionId: "claude-1",
20
+ inputTokens: 100,
21
+ outputTokens: 20,
22
+ cacheCreationTokens: 10,
23
+ cacheReadTokens: 70,
24
+ costUsd: 2,
25
+ cacheSavingsUsd: 0.1,
26
+ },
27
+ {
28
+ date: today,
29
+ source: "codex",
30
+ model: "gpt-5-codex",
31
+ sessionId: "codex-1",
32
+ inputTokens: 50,
33
+ outputTokens: 25,
34
+ cacheCreationTokens: 0,
35
+ cacheReadTokens: 25,
36
+ costUsd: 1,
37
+ cacheSavingsUsd: 0.05,
38
+ },
39
+ ],
40
+ }
41
+
42
+ describe("buildDashboard", () => {
43
+ test("aggregates providers, sessions, and models", () => {
44
+ const dashboard = buildDashboard(dataset, 1, "cost")
45
+
46
+ expect(dashboard.totals.processedTokens).toBe(300)
47
+ expect(dashboard.totals.costUsd).toBe(3)
48
+ expect(dashboard.sessions).toBe(2)
49
+ expect(dashboard.providers.map((provider) => provider.source)).toEqual(["codex", "claude"])
50
+ expect(dashboard.providers[0]?.share).toBeCloseTo(1 / 3)
51
+ expect(dashboard.models[0]?.label).toBe("claude-opus-4-1")
52
+ })
53
+
54
+ test("uses token share when token mode is selected", () => {
55
+ const dashboard = buildDashboard(dataset, 1, "tokens")
56
+
57
+ expect(dashboard.providers[0]?.share).toBeCloseTo(1 / 3)
58
+ expect(dashboard.providers[1]?.share).toBeCloseTo(2 / 3)
59
+ expect(dashboard.series.claude.at(-1)).toBe(200)
60
+ expect(dashboard.series.codex.at(-1)).toBe(100)
61
+ })
62
+ })
@@ -0,0 +1,143 @@
1
+ import { datesInRange } from "./dates"
2
+ import {
3
+ SOURCE_ORDER,
4
+ type Metric,
5
+ type RangeDays,
6
+ type SourceId,
7
+ type UsageDataset,
8
+ type UsageRecord,
9
+ } from "./types"
10
+
11
+ type Totals = {
12
+ inputTokens: number
13
+ outputTokens: number
14
+ cacheCreationTokens: number
15
+ cacheReadTokens: number
16
+ processedTokens: number
17
+ costUsd: number
18
+ cacheSavingsUsd: number
19
+ }
20
+
21
+ export type ProviderStat = Totals & {
22
+ source: SourceId
23
+ sessions: number
24
+ share: number
25
+ }
26
+
27
+ export type BreakdownRow = Totals & {
28
+ key: string
29
+ label: string
30
+ source?: SourceId
31
+ share: number
32
+ }
33
+
34
+ export type Dashboard = {
35
+ days: string[]
36
+ records: UsageRecord[]
37
+ totals: Totals
38
+ sessions: number
39
+ providers: ProviderStat[]
40
+ models: BreakdownRow[]
41
+ daily: BreakdownRow[]
42
+ series: Record<SourceId, number[]>
43
+ }
44
+
45
+ function emptyTotals(): Totals {
46
+ return {
47
+ inputTokens: 0,
48
+ outputTokens: 0,
49
+ cacheCreationTokens: 0,
50
+ cacheReadTokens: 0,
51
+ processedTokens: 0,
52
+ costUsd: 0,
53
+ cacheSavingsUsd: 0,
54
+ }
55
+ }
56
+
57
+ function addRecord(target: Totals, record: UsageRecord): void {
58
+ target.inputTokens += record.inputTokens
59
+ target.outputTokens += record.outputTokens
60
+ target.cacheCreationTokens += record.cacheCreationTokens
61
+ target.cacheReadTokens += record.cacheReadTokens
62
+ target.processedTokens +=
63
+ record.inputTokens + record.outputTokens + record.cacheCreationTokens + record.cacheReadTokens
64
+ target.costUsd += record.costUsd
65
+ target.cacheSavingsUsd += record.cacheSavingsUsd
66
+ }
67
+
68
+ function metricValue(totals: Totals, metric: Metric): number {
69
+ return metric === "cost" ? totals.costUsd : totals.processedTokens
70
+ }
71
+
72
+ export function buildDashboard(dataset: UsageDataset, range: RangeDays, metric: Metric): Dashboard {
73
+ const days = datesInRange(range)
74
+ const dateSet = new Set(days)
75
+ const records = dataset.records.filter((record) => dateSet.has(record.date))
76
+ const sessions = dataset.sessions.filter((session) => dateSet.has(session.date))
77
+ const totals = emptyTotals()
78
+ for (const record of records) addRecord(totals, record)
79
+
80
+ const providerMap = new Map<SourceId, Totals>()
81
+ const providerSessions = new Map<SourceId, Set<string>>()
82
+ for (const record of records) {
83
+ const aggregate = providerMap.get(record.source) ?? emptyTotals()
84
+ addRecord(aggregate, record)
85
+ providerMap.set(record.source, aggregate)
86
+ }
87
+ for (const session of sessions) {
88
+ const ids = providerSessions.get(session.source) ?? new Set<string>()
89
+ ids.add(session.id)
90
+ providerSessions.set(session.source, ids)
91
+ }
92
+ const metricTotal = metricValue(totals, metric)
93
+ const providers = SOURCE_ORDER.filter((source) => providerMap.has(source)).map((source) => {
94
+ const values = providerMap.get(source) ?? emptyTotals()
95
+ return {
96
+ ...values,
97
+ source,
98
+ sessions: providerSessions.get(source)?.size ?? 0,
99
+ share: metricTotal > 0 ? metricValue(values, metric) / metricTotal : 0,
100
+ }
101
+ })
102
+
103
+ const modelMap = new Map<string, BreakdownRow>()
104
+ for (const record of records) {
105
+ const key = `${record.source}\0${record.model}`
106
+ const row = modelMap.get(key) ?? {
107
+ ...emptyTotals(),
108
+ key,
109
+ label: record.model,
110
+ source: record.source,
111
+ share: 0,
112
+ }
113
+ addRecord(row, record)
114
+ modelMap.set(key, row)
115
+ }
116
+ const models = [...modelMap.values()]
117
+ .map((row) => ({ ...row, share: metricTotal > 0 ? metricValue(row, metric) / metricTotal : 0 }))
118
+ .sort((a, b) => metricValue(b, metric) - metricValue(a, metric))
119
+
120
+ const dailyMap = new Map<string, BreakdownRow>()
121
+ for (const day of days) {
122
+ dailyMap.set(day, { ...emptyTotals(), key: day, label: day, share: 0 })
123
+ }
124
+ for (const record of records) addRecord(dailyMap.get(record.date)!, record)
125
+ const daily = [...dailyMap.values()]
126
+ .map((row) => ({ ...row, share: metricTotal > 0 ? metricValue(row, metric) / metricTotal : 0 }))
127
+ .sort((a, b) => metricValue(b, metric) - metricValue(a, metric))
128
+
129
+ const series = Object.fromEntries(
130
+ SOURCE_ORDER.map((source) => {
131
+ const values = days.map((day) => {
132
+ const dayTotals = emptyTotals()
133
+ for (const record of records) {
134
+ if (record.source === source && record.date === day) addRecord(dayTotals, record)
135
+ }
136
+ return metricValue(dayTotals, metric)
137
+ })
138
+ return [source, values]
139
+ }),
140
+ ) as Record<SourceId, number[]>
141
+
142
+ return { days, records, totals, sessions: new Set(sessions.map((session) => session.id)).size, providers, models, daily, series }
143
+ }
@@ -0,0 +1,24 @@
1
+ export function localDate(value: string | number | Date): string {
2
+ const date = value instanceof Date ? value : new Date(value)
3
+ const year = date.getFullYear()
4
+ const month = String(date.getMonth() + 1).padStart(2, "0")
5
+ const day = String(date.getDate()).padStart(2, "0")
6
+ return `${year}-${month}-${day}`
7
+ }
8
+
9
+ export function dateDaysAgo(daysAgo: number): string {
10
+ const date = new Date()
11
+ date.setHours(0, 0, 0, 0)
12
+ date.setDate(date.getDate() - daysAgo)
13
+ return localDate(date)
14
+ }
15
+
16
+ export function datesInRange(days: number): string[] {
17
+ return Array.from({ length: days }, (_, index) => dateDaysAgo(days - index - 1))
18
+ }
19
+
20
+ export function shortDate(value: string): string {
21
+ return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(
22
+ new Date(`${value}T12:00:00`),
23
+ )
24
+ }
@@ -0,0 +1,48 @@
1
+ export type SourceId = "claude" | "codex" | "cursor" | "gemini" | "opencode" | "grok"
2
+
3
+ export type UsageRecord = {
4
+ date: string
5
+ source: SourceId
6
+ model: string
7
+ sessionId?: string
8
+ inputTokens: number
9
+ outputTokens: number
10
+ cacheCreationTokens: number
11
+ cacheReadTokens: number
12
+ costUsd: number
13
+ cacheSavingsUsd: number
14
+ }
15
+
16
+ export type UsageSession = {
17
+ id: string
18
+ source: SourceId
19
+ date: string
20
+ }
21
+
22
+ export type UsageDataset = {
23
+ records: UsageRecord[]
24
+ sessions: UsageSession[]
25
+ errors: string[]
26
+ scannedAt: Date
27
+ }
28
+
29
+ export type Metric = "cost" | "tokens"
30
+ export type RangeDays = 1 | 7 | 30 | 90
31
+ export type BreakdownMode = "model" | "day"
32
+
33
+ export type SourceMeta = {
34
+ id: SourceId
35
+ label: string
36
+ mark: string
37
+ }
38
+
39
+ export const SOURCE_META: Record<SourceId, SourceMeta> = {
40
+ claude: { id: "claude", label: "Claude Code", mark: "*" },
41
+ codex: { id: "codex", label: "Codex", mark: "o" },
42
+ cursor: { id: "cursor", label: "Cursor", mark: ">" },
43
+ gemini: { id: "gemini", label: "Gemini CLI", mark: "g" },
44
+ grok: { id: "grok", label: "Grok Build", mark: "x" },
45
+ opencode: { id: "opencode", label: "OpenCode", mark: "+" },
46
+ }
47
+
48
+ export const SOURCE_ORDER: SourceId[] = ["codex", "claude", "cursor", "gemini", "grok", "opencode"]
package/src/index.tsx ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bun
2
+ import { createCliRenderer, RGBA } from "@opentui/core"
3
+ import { createRoot } from "@opentui/react"
4
+ import { App } from "./tui/App"
5
+
6
+ const renderer = await createCliRenderer({
7
+ backgroundColor: RGBA.defaultBackground("#1e1e1e"),
8
+ exitOnCtrlC: true,
9
+ targetFps: 30,
10
+ useMouse: true,
11
+ enableMouseMovement: true,
12
+ autoFocus: false,
13
+ })
14
+
15
+ createRoot(renderer).render(<App />)