@scrylog/cli 0.3.0 → 0.3.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.
Files changed (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
@@ -1,40 +0,0 @@
1
- import { TextAttributes, type MouseEvent } from "@opentui/core";
2
- import type { DashboardView } from "../model";
3
- import { BRAND } from "../theme";
4
-
5
- const ACTIVE_BACKGROUND = BRAND.primary;
6
- const ACTIVE_FOREGROUND = BRAND.bgDark;
7
- const INACTIVE_FOREGROUND = BRAND.grey;
8
-
9
- const TABS: { view: DashboardView; label: string }[] = [
10
- { view: "sessions", label: " Sessions " },
11
- { view: "usage", label: " Usage " },
12
- ];
13
-
14
- export function TabBar({ view, onSelect }: { view: DashboardView; onSelect?(next: DashboardView): void }) {
15
- return (
16
- <box height={1} flexDirection="row">
17
- {TABS.map((tab) => {
18
- const active = tab.view === view;
19
- return (
20
- <box
21
- key={tab.view}
22
- backgroundColor={active ? ACTIVE_BACKGROUND : undefined}
23
- flexDirection="row"
24
- height={1}
25
- onMouseUp={onSelect ? (event: MouseEvent) => {
26
- if (event.button !== 0) return;
27
- if (!active) onSelect(tab.view);
28
- } : undefined}
29
- >
30
- <text
31
- fg={active ? ACTIVE_FOREGROUND : INACTIVE_FOREGROUND}
32
- attributes={active ? TextAttributes.BOLD : TextAttributes.NONE}
33
- content={tab.label}
34
- />
35
- </box>
36
- );
37
- })}
38
- </box>
39
- );
40
- }
@@ -1,272 +0,0 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import type { TuiConfig } from "../../shared/config";
3
- import type { UsageProviderEntry, UsageState, UsageWindow } from "../../usage/types";
4
- import { clampPercent, compactCostLabel, providerAccentColor, providerVisual, renderUsageBar, usageColor, ProviderIcon } from "./UsageView";
5
- import { MINI_ICON_SIZE, isImageIconMode } from "./tool-icons";
6
- import { BRAND } from "../theme";
7
-
8
- export interface ProviderUsageSummary {
9
- provider: string;
10
- short: string;
11
- name: string;
12
- percent: number;
13
- windowLabel: string | null;
14
- costLabel: string | null;
15
- color: string;
16
- hasWindows: boolean;
17
- status: UsageProviderEntry["status"];
18
- }
19
-
20
- export function usageWindowLabel(window: UsageWindow): string {
21
- const haystack = `${window.id} ${window.title}`.toLowerCase();
22
- if (/\bsession\b/.test(haystack)) return "S";
23
- if (/week/.test(haystack)) return "W";
24
- if (/month/.test(haystack)) return "M";
25
- if (/\bday\b|\bdaily\b/.test(haystack)) return "D";
26
- if (/hour/.test(haystack)) return "H";
27
- const first = window.title.trim()[0] ?? window.id.trim()[0];
28
- return first ? first.toUpperCase() : "?";
29
- }
30
-
31
- export function summarizeProvider(entry: UsageProviderEntry): ProviderUsageSummary {
32
- const worst = (entry.windows ?? [])
33
- .filter((window) => Number.isFinite(window.usedPercent))
34
- .reduce<UsageWindow | null>((acc, window) =>
35
- !acc || clampPercent(window.usedPercent) > clampPercent(acc.usedPercent) ? window : acc, null);
36
- const visual = providerVisual(entry.provider);
37
- return {
38
- provider: entry.provider,
39
- short: visual.short,
40
- name: visual.name,
41
- percent: clampPercent(worst?.usedPercent ?? 0),
42
- windowLabel: worst ? usageWindowLabel(worst) : null,
43
- costLabel: compactCostLabel(entry.cost),
44
- color: usageColor(worst?.usedPercent ?? 0, entry.provider),
45
- hasWindows: Boolean(worst),
46
- status: entry.status,
47
- };
48
- }
49
-
50
- const NARROW_WIDTH = 110;
51
- /** border (2) + paddingX (2) — chrome the pane consumes from the width prop. */
52
- const PANE_CHROME = 4;
53
- const CELL_GAP = 2;
54
- const INNER_GAP = 1;
55
- const MIN_BAR_WIDTH = 4;
56
- const MAX_COST_WIDTH = 6;
57
- const MIN_STATUS_WIDTH = 6;
58
- /** outer gap (1) + separator (1) + gap to next cell (1) in compact mode. */
59
- const COMPACT_JUNCTION = INNER_GAP + 1 + INNER_GAP;
60
-
61
- export type SummaryLayoutMode = "bar" | "compact";
62
-
63
- export interface SummaryLayout {
64
- mode: SummaryLayoutMode;
65
- /** Shared bar width in bar mode; 0 in compact mode. */
66
- barWidth: number;
67
- visible: ProviderUsageSummary[];
68
- hiddenCount: number;
69
- }
70
-
71
- /** Highest-usage providers first, but only on narrow terminals. */
72
- export function orderSummaries(summaries: ProviderUsageSummary[], width: number): ProviderUsageSummary[] {
73
- if (width >= NARROW_WIDTH) return summaries;
74
- return [...summaries].sort((a, b) => b.percent - a.percent);
75
- }
76
-
77
- interface CellSegment {
78
- content: string;
79
- width: number;
80
- }
81
-
82
- function pctText(summary: ProviderUsageSummary): string {
83
- return `${Math.round(summary.percent)}%`;
84
- }
85
-
86
- function statusText(summary: ProviderUsageSummary): string {
87
- return summary.status.slice(0, MIN_STATUS_WIDTH);
88
- }
89
-
90
- function trimCost(label: string | null | undefined): string | null {
91
- if (!label || label.length <= MAX_COST_WIDTH) return label ?? null;
92
- return `${label.slice(0, MAX_COST_WIDTH - 1)}…`;
93
- }
94
-
95
- function summarySegments(summary: ProviderUsageSummary, costLabel: string | null, barWidth: number): CellSegment[] {
96
- const segments: CellSegment[] = [
97
- { content: summary.short.slice(0, 2), width: 2 },
98
- { content: renderUsageBar(summary.percent, barWidth), width: barWidth },
99
- ];
100
- if (summary.windowLabel) {
101
- segments.push({ content: ` ${summary.windowLabel}`, width: summary.windowLabel.length + 1 });
102
- }
103
- const pct = pctText(summary);
104
- segments.push({ content: pct, width: pct.length });
105
- if (costLabel) {
106
- segments.push({ content: costLabel, width: costLabel.length });
107
- }
108
- return segments;
109
- }
110
-
111
- function segmentsWidth(segments: CellSegment[]): number {
112
- const widths = segments.reduce((acc, segment) => acc + segment.width, 0);
113
- return widths + (segments.length - 1) * INNER_GAP;
114
- }
115
-
116
- /** Zero-bar cell width, honored by the budget solver for ok summaries. */
117
- function nonBarWidth(summary: ProviderUsageSummary, costLabel: string | null): number {
118
- return cellWidth({ ...summary, costLabel }, 0);
119
- }
120
-
121
- function cellWidth(summary: ProviderUsageSummary, barWidth: number): number {
122
- if (!summary.hasWindows || summary.status !== "ok") {
123
- return 2 + INNER_GAP + statusText(summary).length;
124
- }
125
- return segmentsWidth(summarySegments(summary, summary.costLabel, barWidth));
126
- }
127
-
128
- /**
129
- * Fit preference: maximize visible providers first (compact separators fit
130
- * more providers than bars), falling back to bar rows when both show the same
131
- * count. Bars share one width; costs are truncated before rows are dropped.
132
- */
133
- export function layoutSummaries(summaries: ProviderUsageSummary[], width: number): SummaryLayout {
134
- const ordered = orderSummaries(summaries, width);
135
- const available = Math.max(0, width - PANE_CHROME);
136
- if (ordered.length === 0) {
137
- return { mode: "bar", barWidth: MIN_BAR_WIDTH, visible: [], hiddenCount: 0 };
138
- }
139
-
140
- const trimmed = ordered.map((s) => ({ ...s, costLabel: trimCost(s.costLabel) }));
141
- let barLayout: SummaryLayout | null = null;
142
- {
143
- const allBudget = Math.floor(
144
- (available - trimmed.reduce((acc, s) => acc + nonBarWidth(s, s.costLabel), 0) - (trimmed.length - 1) * CELL_GAP) / trimmed.length,
145
- );
146
- if (allBudget >= MIN_BAR_WIDTH) {
147
- barLayout = { mode: "bar", barWidth: allBudget, visible: trimmed, hiddenCount: 0 };
148
- } else {
149
- for (let count = trimmed.length - 1; count >= 1; count--) {
150
- const cells = trimmed.slice(0, count);
151
- const gaps = (count - 1) * CELL_GAP;
152
- const budget = Math.floor((available - cells.reduce((acc, s) => acc + nonBarWidth(s, s.costLabel), 0) - gaps) / count);
153
- if (budget >= MIN_BAR_WIDTH) {
154
- barLayout = { mode: "bar", barWidth: budget, visible: cells, hiddenCount: trimmed.length - count };
155
- break;
156
- }
157
- }
158
- }
159
- }
160
-
161
- // Compact: without bars, separators fit the most providers.
162
- const widths = ordered.map(compactSegmentWidth);
163
- let visibleCount = 1;
164
- let total = widths[0];
165
- for (let i = 1; i < widths.length; i++) {
166
- const next = total + COMPACT_JUNCTION + widths[i];
167
- if (next > available) break;
168
- total = next;
169
- visibleCount++;
170
- }
171
- const compactLayout: SummaryLayout = {
172
- mode: "compact",
173
- barWidth: 0,
174
- visible: ordered.slice(0, visibleCount),
175
- hiddenCount: ordered.length - visibleCount,
176
- };
177
-
178
- if (!barLayout) {
179
- return compactLayout;
180
- }
181
- return barLayout.visible.length >= compactLayout.visible.length ? barLayout : compactLayout;
182
- }
183
-
184
- function compactSegmentWidth(summary: ProviderUsageSummary): number {
185
- if (!summary.hasWindows && summary.status !== "ok") {
186
- return 2 + INNER_GAP + statusText(summary).length;
187
- }
188
- return 2 + INNER_GAP + pctText(summary).length;
189
- }
190
-
191
- function BarCell({ summary, width, barWidth, iconMode }: { summary: ProviderUsageSummary; width: number; barWidth: number; iconMode: TuiConfig["providerIcons"] }) {
192
- const shortCell = isImageIconMode(iconMode)
193
- ? <ProviderIcon provider={summary.provider} mode={iconMode} size={MINI_ICON_SIZE} />
194
- : <text width={2} fg={providerAccentColor(summary.provider)} attributes={TextAttributes.BOLD} content={summary.short.slice(0, 2)} />;
195
- if (!summary.hasWindows || summary.status !== "ok") {
196
- return (
197
- <box width={width} height={1} flexDirection="row" gap={INNER_GAP}>
198
- {shortCell}
199
- <text fg={BRAND.grey} content={statusText(summary)} />
200
- </box>
201
- );
202
- }
203
- const segments = summarySegments(summary, summary.costLabel, barWidth);
204
- const accent = providerAccentColor(summary.provider);
205
- return (
206
- <box width={width} height={1} flexDirection="row" gap={INNER_GAP}>
207
- {shortCell}
208
- <text width={barWidth} fg={accent} content={renderUsageBar(summary.percent, barWidth)} />
209
- {summary.windowLabel && (
210
- <text width={summary.windowLabel.length + 1} fg={BRAND.grey} attributes={TextAttributes.BOLD} content={` ${summary.windowLabel}`} />
211
- )}
212
- <text width={pctText(summary).length} fg={usageColor(summary.percent, summary.provider)} attributes={TextAttributes.BOLD} content={pctText(summary)} />
213
- {summary.costLabel && <text width={summary.costLabel.length} fg={BRAND.grey} content={summary.costLabel} />}
214
- </box>
215
- );
216
- }
217
-
218
- function CompactCell({ summary, leadingSeparator }: { summary: ProviderUsageSummary; leadingSeparator: boolean }) {
219
- const shortCell = (
220
- <text width={2} fg={providerAccentColor(summary.provider)} attributes={TextAttributes.BOLD} content={summary.short.slice(0, 2)} />
221
- );
222
- const dim = summary.hasWindows && summary.status === "ok" ? usageColor(summary.percent, summary.provider) : BRAND.grey;
223
- const label = !summary.hasWindows && summary.status !== "ok"
224
- ? statusText(summary)
225
- : pctText(summary);
226
- return (
227
- <box height={1} flexDirection="row" gap={INNER_GAP}>
228
- {leadingSeparator && <text fg={BRAND.border} content="│" />}
229
- {shortCell}
230
- <text fg={dim} attributes={TextAttributes.BOLD} content={label} />
231
- </box>
232
- );
233
- }
234
-
235
- export function UsageSummaryPane({ usage, width, iconMode }: { usage: UsageState; width: number; iconMode: TuiConfig["providerIcons"] }) {
236
- const summaries = usage.providers.filter((entry) => entry.available)
237
- .map((entry) => summarizeProvider(entry));
238
- const layout = layoutSummaries(summaries, width);
239
- return (
240
- <box
241
- height={3}
242
- border
243
- borderColor={BRAND.border}
244
- titleColor={BRAND.grey}
245
- title="Usage"
246
- paddingX={1}
247
- flexDirection="row"
248
- gap={layout.mode === "bar" ? CELL_GAP : INNER_GAP}
249
- >
250
- {summaries.length === 0 ? (
251
- <text fg={BRAND.grey} content="no usage data" />
252
- ) : layout.mode === "bar" ? (
253
- layout.visible.map((summary) => (
254
- <BarCell
255
- key={summary.provider}
256
- summary={summary}
257
- width={cellWidth(summary, layout.barWidth)}
258
- barWidth={layout.barWidth}
259
- iconMode={iconMode}
260
- />
261
- ))
262
- ) : (
263
- layout.visible.map((summary, index) => (
264
- <CompactCell key={summary.provider} summary={summary} leadingSeparator={index > 0} />
265
- ))
266
- )}
267
- {layout.mode === "bar" && layout.hiddenCount > 0 && (
268
- <text fg={BRAND.grey} content={`+${layout.hiddenCount}`} />
269
- )}
270
- </box>
271
- );
272
- }
@@ -1,242 +0,0 @@
1
- import { NativeImage, TextAttributes } from "@opentui/core";
2
- import { useEffect, useState } from "react";
3
- import type { TuiConfig } from "../../shared/config";
4
- import type { ProviderCostSnapshot, UsageProviderEntry, UsageState, UsageWindow } from "../../usage/types";
5
- import { BRAND, PROVIDER_BRANDS, type ProviderVisual } from "../theme";
6
-
7
- export type { ProviderVisual };
8
-
9
- export const PROVIDER_ICON_SIZES = {
10
- blocks: { width: 8, height: 4 },
11
- graphics: { width: 6, height: 3 },
12
- kitty: { width: 6, height: 3 },
13
- sixel: { width: 6, height: 3 },
14
- text: { width: 4, height: 3 },
15
- mini: { width: 2, height: 1 },
16
- } as const;
17
-
18
- export function iconImageProtocol(mode: TuiConfig["providerIcons"]): "blocks" | "auto" | "kitty" | "sixel" {
19
- if (mode === "blocks") return "blocks";
20
- if (mode === "kitty" || mode === "sixel") return mode;
21
- return "auto";
22
- }
23
-
24
- const PROVIDERS = PROVIDER_BRANDS;
25
-
26
- export function providerVisual(provider: string): ProviderVisual {
27
- return PROVIDERS[provider] ?? { name: provider, short: "AI", brandColor: BRAND.grey, borderColor: BRAND.grey };
28
- }
29
-
30
- export function providerAccentColor(provider: string): string {
31
- return providerVisual(provider).onDarkColor ?? providerVisual(provider).brandColor;
32
- }
33
-
34
- export function clampPercent(value: number): number {
35
- return Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
36
- }
37
-
38
- export function usageColor(value: number, provider?: string): string {
39
- const percent = clampPercent(value);
40
- if (percent >= 90) return BRAND.usageHigh;
41
- if (percent >= 70) return BRAND.stateWorking;
42
- if (provider) return providerAccentColor(provider);
43
- return BRAND.stateIdle;
44
- }
45
-
46
- export function renderUsageBar(value: number, width: number): string {
47
- const size = Math.max(4, width);
48
- const filled = Math.round((clampPercent(value) / 100) * size);
49
- return `${"█".repeat(filled)}${"░".repeat(size - filled)}`;
50
- }
51
-
52
- function formatUsageNumber(value: number, compact: boolean): string {
53
- if (compact && Math.abs(value) >= 1_000) {
54
- return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1).replace(/\.0$/, "")}k`;
55
- }
56
- return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
57
- }
58
-
59
- export function formatCostAmount(amount: number, currency: string, compact = false): string {
60
- const value = formatUsageNumber(amount, compact);
61
- const normalized = currency.trim().toUpperCase();
62
- if (normalized === "USD") return `$${value}`;
63
- if (normalized === "EUR") return `€${value}`;
64
- if (normalized === "GBP") return `£${value}`;
65
- if (normalized === "CREDITS") return compact ? `${value}cr` : `${value} credits`;
66
- return compact ? `${value} ${currency}` : `${currency} ${value}`;
67
- }
68
-
69
- export function compactCostLabel(cost: ProviderCostSnapshot | null | undefined): string | null {
70
- if (!cost) return null;
71
- if (cost.limit > 0) return formatCostAmount(cost.used, cost.currency, true);
72
- if (cost.balance !== undefined) return `${formatCostAmount(cost.balance, cost.currency, true)} left`;
73
- return null;
74
- }
75
-
76
- export function formatCostRange(cost: ProviderCostSnapshot): string {
77
- const normalized = cost.currency.trim().toUpperCase();
78
- if (normalized === "CREDITS") {
79
- return `${formatUsageNumber(cost.used, false)} / ${formatUsageNumber(cost.limit, false)} credits`;
80
- }
81
- return `${formatCostAmount(cost.used, cost.currency)} / ${formatCostAmount(cost.limit, cost.currency)}`;
82
- }
83
-
84
- export function formatRelativeReset(seconds: number): string {
85
- const days = Math.floor(seconds / 86_400);
86
- const hours = Math.floor((seconds % 86_400) / 3_600);
87
- const minutes = Math.floor((seconds % 3_600) / 60);
88
- if (seconds <= 0) return "now";
89
- if (days > 0) return `${days}d ${hours}h`;
90
- if (hours > 0) return `${hours}h ${minutes}m`;
91
- return `${minutes}m`;
92
- }
93
-
94
- export function formatResetTime(timestamp: number): string {
95
- return new Date(timestamp).toLocaleString("en-US", {
96
- month: "short",
97
- day: "numeric",
98
- hour: "numeric",
99
- minute: "2-digit",
100
- hour12: true,
101
- }).replace(":00 ", " ").replace(" AM", "am").replace(" PM", "pm");
102
- }
103
-
104
- export function formatReset(window: UsageWindow, now = Date.now()): string {
105
- const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : undefined;
106
- const seconds = resetsAt !== undefined
107
- ? Math.max(0, Math.round((resetsAt - now) / 1000))
108
- : Math.max(0, window.resetInSec ?? 0);
109
- if (resetsAt === undefined && window.resetInSec === undefined) return "reset unknown";
110
- const timestamp = resetsAt ?? now + seconds * 1000;
111
- return `${formatResetTime(timestamp)} (${formatRelativeReset(seconds)})`;
112
- }
113
-
114
- export function ProviderIcon({ provider, mode, size }: { provider: string; mode: TuiConfig["providerIcons"]; size?: { width: number; height: number } }) {
115
- const visual = providerVisual(provider);
116
- const iconSize = size ?? PROVIDER_ICON_SIZES[mode];
117
- const [image, setImage] = useState<NativeImage | null>(null);
118
-
119
- useEffect(() => {
120
- const asset = mode === "blocks" ? visual.terminalAsset : visual.graphicsAsset;
121
- if (mode === "text" || !asset) return;
122
- let active = true;
123
- let loaded: NativeImage | null = null;
124
- void NativeImage.load(asset).then((next) => {
125
- loaded = next;
126
- if (active) setImage(next);
127
- else next.dispose();
128
- }).catch(() => setImage(null));
129
- return () => {
130
- active = false;
131
- loaded?.dispose();
132
- };
133
- }, [mode, provider]);
134
-
135
- if (image) {
136
- return <image source={image} protocol={iconImageProtocol(mode)} fit="fit" {...iconSize} />;
137
- }
138
- return (
139
- <box {...iconSize} backgroundColor={visual.tileColor} alignItems="center" justifyContent="center">
140
- <text fg={visual.brandColor} attributes={TextAttributes.BOLD} content={visual.short} />
141
- </box>
142
- );
143
- }
144
-
145
- function UsageLine({ provider, window, width }: { provider: string; window: UsageWindow; width: number }) {
146
- const percent = clampPercent(window.usedPercent);
147
- return (
148
- <box flexDirection="column">
149
- <box flexDirection="row" justifyContent="space-between">
150
- <text content={window.title} />
151
- <text fg={usageColor(percent, provider)} attributes={TextAttributes.BOLD} content={`${Math.round(percent)}%`} />
152
- </box>
153
- <text fg={usageColor(percent, provider)} content={renderUsageBar(percent, width)} />
154
- <text fg={BRAND.grey} content={formatReset(window)} />
155
- </box>
156
- );
157
- }
158
-
159
- function ProviderCard({ provider, width, iconMode }: {
160
- provider: UsageProviderEntry;
161
- width: number;
162
- iconMode: TuiConfig["providerIcons"];
163
- }) {
164
- const visual = providerVisual(provider.provider);
165
- const windows = provider.windows ?? [];
166
- const subtitle = provider.metadata?.subscriptionType
167
- ?? provider.metadata?.workspaceName
168
- ?? provider.metadata?.accountEmail
169
- ?? provider.status;
170
- return (
171
- <box title={visual.name} border borderColor={visual.borderColor} width={width} flexShrink={0} flexDirection="column" paddingX={1} paddingBottom={1} paddingTop={1}>
172
- <box height={PROVIDER_ICON_SIZES.blocks.height} flexDirection="row" gap={1}>
173
- <ProviderIcon provider={provider.provider} mode={iconMode} />
174
- <box flexDirection="column" justifyContent="center">
175
- <text attributes={TextAttributes.BOLD} content={visual.name} />
176
- <text fg={BRAND.grey} content={subtitle ?? provider.status} />
177
- </box>
178
- </box>
179
- <box height={1} />
180
- {provider.status !== "ok" && (
181
- <text
182
- fg={provider.status === "error" ? BRAND.danger : BRAND.warning}
183
- content={provider.errors?.[0] ?? (provider.status === "pending" ? "Waiting for first refresh" : "Credentials not available")}
184
- wrapMode="word"
185
- />
186
- )}
187
- {provider.status === "ok" && windows.map((window) => (
188
- <UsageLine key={window.id} provider={provider.provider} window={window} width={width - 4} />
189
- ))}
190
- {provider.cost && (
191
- <box flexDirection="column">
192
- {provider.cost.limit > 0 && (
193
- <text
194
- fg={BRAND.grey}
195
- content={`${provider.cost.period}: ${formatCostRange(provider.cost)}`}
196
- />
197
- )}
198
- {provider.cost.balance !== undefined && (
199
- <text fg={BRAND.grey} content={`Balance: ${formatCostAmount(provider.cost.balance, provider.cost.currency)}`} />
200
- )}
201
- </box>
202
- )}
203
- </box>
204
- );
205
- }
206
-
207
- export function UsageView({ usage, width, iconMode, hidden = false }: { usage: UsageState; width: number; iconMode: TuiConfig["providerIcons"]; hidden?: boolean }) {
208
- const rich = width >= 110;
209
- const columns = rich ? Math.min(3, Math.max(1, usage.providers.length)) : 1;
210
- // Narrow single-column cards reserve extra width for border + padding so the
211
- // outer pane border is never painted over by scrollbox content.
212
- const cardWidth = rich
213
- ? Math.max(32, Math.floor((width - 4 - (columns - 1)) / columns))
214
- : Math.max(20, width - 6);
215
- return (
216
- <box
217
- border
218
- borderColor={BRAND.border}
219
- flexDirection="column"
220
- flexGrow={1}
221
- paddingX={1}
222
- >
223
- {usage.providers.length === 0 ? (
224
- <box flexGrow={1} alignItems="center" justifyContent="center">
225
- <text fg={BRAND.grey} content="No usage providers configured" />
226
- </box>
227
- ) : (
228
- <scrollbox
229
- focused={!hidden}
230
- scrollX={false}
231
- scrollY
232
- flexGrow={1}
233
- contentOptions={{ flexDirection: rich ? "row" : "column", flexWrap: rich ? "wrap" : "no-wrap", gap: 1 }}
234
- >
235
- {usage.providers.map((provider) => (
236
- <ProviderCard key={provider.provider} provider={provider} width={cardWidth} iconMode={iconMode} />
237
- ))}
238
- </scrollbox>
239
- )}
240
- </box>
241
- );
242
- }
@@ -1,24 +0,0 @@
1
- import type { SessionTool } from "../../shared/types";
2
- import type { TuiConfig } from "../../shared/config";
3
- import { PROVIDER_ICON_SIZES } from "./UsageView";
4
-
5
- type ToolProviderKey = "claude" | "codex" | "opencode-go" | "copilot" | "shell" | "unknown";
6
-
7
- const TOOL_PROVIDER_KEYS: Record<SessionTool, ToolProviderKey> = {
8
- claude: "claude",
9
- codex: "codex",
10
- opencode: "opencode-go",
11
- copilotcli: "copilot",
12
- shell: "shell",
13
- unknown: "unknown",
14
- };
15
-
16
- export function toolProviderKey(tool: string): ToolProviderKey | null {
17
- return (TOOL_PROVIDER_KEYS as Record<string, ToolProviderKey>)[tool] ?? null;
18
- }
19
-
20
- export function isImageIconMode(mode: TuiConfig["providerIcons"]): boolean {
21
- return mode === "graphics" || mode === "kitty" || mode === "sixel";
22
- }
23
-
24
- export const MINI_ICON_SIZE = PROVIDER_ICON_SIZES.mini;
package/src/tui/index.tsx DELETED
@@ -1,106 +0,0 @@
1
- import { createCliRenderer } from "@opentui/core";
2
- import type { CliRenderer } from "@opentui/core";
3
- import { createRoot } from "@opentui/react";
4
- import type { ReactNode } from "react";
5
- import { loadConfig } from "../shared/config";
6
- import type { ScrylogConfig } from "../shared/config";
7
- import { getServiceManagerOrThrow } from "../service/manager";
8
- import type { ServiceStatus } from "../service/manager";
9
- import { getDaemonVersionMismatch, getInstalledVersion } from "../shared/version";
10
- import type { DaemonVersionMismatch } from "../shared/version";
11
- import { App } from "./App";
12
- import { DaemonClient } from "./client";
13
-
14
- export async function launchTui(configPath?: string, includeUnknown = false): Promise<void> {
15
- const config = loadConfig(configPath);
16
- const client = new DaemonClient(config);
17
- const health = await client.getHealth();
18
- if (!health) {
19
- printDaemonNotRunning(config, getServiceManagerOrThrow().platform);
20
- process.exitCode = 1;
21
- return;
22
- }
23
-
24
- const versionMismatch = getDaemonVersionMismatch(getInstalledVersion(), health.version);
25
- if (versionMismatch) printDaemonVersionMismatch(versionMismatch);
26
-
27
- const renderer = await createCliRenderer({ exitOnCtrlC: false });
28
- await mountDashboard(renderer, (close) => {
29
- const attach = (command: string[], sessionId: string) => {
30
- close();
31
- const result = Bun.spawnSync(command, { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
32
- if (result.exitCode !== 0) {
33
- process.stderr.write(`Could not enter session ${sessionId}\n`);
34
- process.exitCode = 1;
35
- }
36
- };
37
-
38
- return (
39
- <App
40
- client={client}
41
- options={{ refreshMs: config.tui.refreshMs, includeUnknown }}
42
- config={config}
43
- versionMismatch={versionMismatch}
44
- onExit={close}
45
- onAttach={attach}
46
- />
47
- );
48
- });
49
- }
50
-
51
- export function printDaemonVersionMismatch(mismatch: DaemonVersionMismatch): void {
52
- if (mismatch.daemonVersion) {
53
- process.stderr.write(
54
- `scrylog: daemon version mismatch (CLI ${mismatch.cliVersion}, daemon ${mismatch.daemonVersion}).\n` +
55
- "Restart it to activate the installed version: scrylog daemon restart\n" +
56
- "For a foreground or unsupported service, stop it and run: scrylogd\n" +
57
- `Release notes: https://github.com/scrylog/scrylog/releases/tag/v${mismatch.cliVersion}\n\n`,
58
- );
59
- return;
60
- }
61
-
62
- process.stderr.write(
63
- "scrylog: the running daemon is from an older version.\n" +
64
- "Restart it to activate the installed version: scrylog daemon restart\n" +
65
- "For a foreground or unsupported service, stop it and run: scrylogd\n" +
66
- "If integrations misbehave after updating, run: scrylog setup\n\n",
67
- );
68
- }
69
-
70
- export async function mountDashboard(renderer: CliRenderer, render: (close: () => void) => ReactNode): Promise<void> {
71
- const root = createRoot(renderer);
72
- await new Promise<void>((resolve) => {
73
- let closed = false;
74
- const close = () => {
75
- if (closed) return;
76
- closed = true;
77
- root.unmount();
78
- renderer.destroy();
79
- resolve();
80
- };
81
- root.render(render(close));
82
- });
83
- }
84
-
85
- export function printDaemonNotRunning(config: ScrylogConfig, platform: ServiceStatus["platform"]): void {
86
- const logPath = config.daemon.logFilePath;
87
- const hasService = platform === "macos" || platform === "linux";
88
-
89
- process.stderr.write("scrylog: daemon is not running.\n\n");
90
- process.stderr.write("Verify:\n scrylog daemon status\n scrylog doctor\n\n");
91
- process.stderr.write("Start it (pick one):\n");
92
- if (hasService) {
93
- process.stderr.write(" scrylog daemon start # user service\n");
94
- process.stderr.write(" scrylog daemon install # if the service is not installed yet\n");
95
- }
96
- process.stderr.write(` scrylogd # foreground\n\nLogs:\n scrylog logs\n ${logPath}\n`);
97
-
98
- if (!hasService) {
99
- const note = platform === "wsl"
100
- ? "Service management is unavailable (WSL without systemd). Run scrylogd directly or enable systemd in /etc/wsl.conf."
101
- : platform === "linux-no-systemd"
102
- ? "Service management is unavailable (no systemd user session). Run scrylogd directly."
103
- : "Service management is unavailable on this platform. Run scrylogd directly or use Task Scheduler.";
104
- process.stderr.write(`\n${note}\n`);
105
- }
106
- }