@tokentop/ttop 0.2.2 → 0.3.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 (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,28 +1,28 @@
1
1
  #!/usr/bin/env bun
2
- import * as readline from 'readline';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import { diffFrames, highlightDiff } from './diff.ts';
6
- import { assertSnapshot, listGoldenFiles, deleteGoldenFile, getGoldenFile } from './assertions.ts';
7
- import { createDriver, type Driver, type DriverOptions } from './driver.ts';
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ import * as readline from "readline";
5
+ import { assertSnapshot, deleteGoldenFile, getGoldenFile, listGoldenFiles } from "./assertions.ts";
6
+ import {
7
+ type CoverageTracker,
8
+ createCoverageTracker,
9
+ detectViewFromFrame,
10
+ formatCoverageReport,
11
+ } from "./coverage.ts";
12
+ import { diffFrames, highlightDiff } from "./diff.ts";
13
+ import { createDriver, type Driver, type DriverOptions } from "./driver.ts";
8
14
  import {
9
15
  createRecorder,
10
- replayRecording,
11
- saveRecording,
12
- loadRecording,
13
- listRecordings,
14
16
  deleteRecording,
17
+ listRecordings,
18
+ loadRecording,
15
19
  type Recorder,
16
20
  type Recording,
17
- } from './recorder.ts';
18
- import {
19
- createCoverageTracker,
20
- detectViewFromFrame,
21
- formatCoverageReport,
22
- type CoverageTracker,
23
- } from './coverage.ts';
21
+ replayRecording,
22
+ saveRecording,
23
+ } from "./recorder.ts";
24
24
 
25
- const DEFAULT_SNAPSHOTS_DIR = './snapshots';
25
+ const DEFAULT_SNAPSHOTS_DIR = "./snapshots";
26
26
 
27
27
  interface Command {
28
28
  action: string;
@@ -49,28 +49,29 @@ async function ensureDir(dir: string): Promise<void> {
49
49
  async function saveFrameToFile(frame: string, filePath: string): Promise<string> {
50
50
  const dir = path.dirname(filePath);
51
51
  await ensureDir(dir);
52
- await fs.writeFile(filePath, frame, 'utf-8');
52
+ await fs.writeFile(filePath, frame, "utf-8");
53
53
  return path.resolve(filePath);
54
54
  }
55
55
 
56
56
  async function handleCommand(cmd: Command): Promise<Response> {
57
57
  try {
58
58
  switch (cmd.action) {
59
- case 'launch': {
59
+ case "launch": {
60
60
  if (driver?.isRunning()) {
61
- return { ok: false, error: 'Driver already running' };
61
+ return { ok: false, error: "Driver already running" };
62
62
  }
63
- currentWidth = typeof cmd.width === 'number' ? cmd.width : 100;
64
- currentHeight = typeof cmd.height === 'number' ? cmd.height : 30;
63
+ currentWidth = typeof cmd.width === "number" ? cmd.width : 100;
64
+ currentHeight = typeof cmd.height === "number" ? cmd.height : 30;
65
65
  const options: DriverOptions = {
66
66
  width: currentWidth,
67
67
  height: currentHeight,
68
68
  appOptions: {
69
69
  debug: cmd.debug === true,
70
70
  demoMode: cmd.demo === true,
71
- ...(typeof cmd.demoSeed === 'number' ? { demoSeed: cmd.demoSeed, demoMode: true } : {}),
72
- ...(typeof cmd.demoPreset === 'string' && ['light', 'normal', 'heavy'].includes(cmd.demoPreset)
73
- ? { demoPreset: cmd.demoPreset as 'light' | 'normal' | 'heavy', demoMode: true }
71
+ ...(typeof cmd.demoSeed === "number" ? { demoSeed: cmd.demoSeed, demoMode: true } : {}),
72
+ ...(typeof cmd.demoPreset === "string" &&
73
+ ["light", "normal", "heavy"].includes(cmd.demoPreset)
74
+ ? { demoPreset: cmd.demoPreset as "light" | "normal" | "heavy", demoMode: true }
74
75
  : {}),
75
76
  },
76
77
  };
@@ -79,203 +80,207 @@ async function handleCommand(cmd: Command): Promise<Response> {
79
80
  return { ok: true };
80
81
  }
81
82
 
82
- case 'close': {
83
+ case "close": {
83
84
  if (!driver) {
84
- return { ok: false, error: 'Driver not running' };
85
+ return { ok: false, error: "Driver not running" };
85
86
  }
86
87
  await driver.close();
87
88
  driver = null;
88
89
  return { ok: true };
89
90
  }
90
91
 
91
- case 'sendKeys': {
92
+ case "sendKeys": {
92
93
  if (!driver?.isRunning()) {
93
- return { ok: false, error: 'Driver not running' };
94
+ return { ok: false, error: "Driver not running" };
94
95
  }
95
- const keys = typeof cmd.keys === 'string' ? cmd.keys : '';
96
+ const keys = typeof cmd.keys === "string" ? cmd.keys : "";
96
97
  await driver.sendKeys(keys);
97
- recorder?.addCommand('sendKeys', { keys });
98
+ recorder?.addCommand("sendKeys", { keys });
98
99
  if (recorder?.isRecording()) {
99
100
  recorder.captureFrame(await driver.capture());
100
101
  }
101
102
  return { ok: true };
102
103
  }
103
104
 
104
- case 'pressKey': {
105
+ case "pressKey": {
105
106
  if (!driver?.isRunning()) {
106
- return { ok: false, error: 'Driver not running' };
107
+ return { ok: false, error: "Driver not running" };
107
108
  }
108
- const key = typeof cmd.key === 'string' ? cmd.key : '';
109
- const modifiers = typeof cmd.modifiers === 'object' && cmd.modifiers !== null
110
- ? cmd.modifiers as Record<string, boolean>
111
- : {};
109
+ const key = typeof cmd.key === "string" ? cmd.key : "";
110
+ const modifiers =
111
+ typeof cmd.modifiers === "object" && cmd.modifiers !== null
112
+ ? (cmd.modifiers as Record<string, boolean>)
113
+ : {};
112
114
  await driver.pressKey(key, modifiers);
113
- recorder?.addCommand('pressKey', { key, ...(Object.keys(modifiers).length > 0 ? { modifiers } : {}) });
115
+ recorder?.addCommand("pressKey", {
116
+ key,
117
+ ...(Object.keys(modifiers).length > 0 ? { modifiers } : {}),
118
+ });
114
119
  if (recorder?.isRecording()) {
115
120
  recorder.captureFrame(await driver.capture());
116
121
  }
117
122
  return { ok: true };
118
123
  }
119
124
 
120
- case 'typeText': {
125
+ case "typeText": {
121
126
  if (!driver?.isRunning()) {
122
- return { ok: false, error: 'Driver not running' };
127
+ return { ok: false, error: "Driver not running" };
123
128
  }
124
- const text = typeof cmd.text === 'string' ? cmd.text : '';
125
- const delay = typeof cmd.delay === 'number' ? cmd.delay : 0;
129
+ const text = typeof cmd.text === "string" ? cmd.text : "";
130
+ const delay = typeof cmd.delay === "number" ? cmd.delay : 0;
126
131
  await driver.typeText(text, delay);
127
- recorder?.addCommand('typeText', { text, ...(delay > 0 ? { delay } : {}) });
132
+ recorder?.addCommand("typeText", { text, ...(delay > 0 ? { delay } : {}) });
128
133
  if (recorder?.isRecording()) {
129
134
  recorder.captureFrame(await driver.capture());
130
135
  }
131
136
  return { ok: true };
132
137
  }
133
138
 
134
- case 'pressTab': {
139
+ case "pressTab": {
135
140
  if (!driver?.isRunning()) {
136
- return { ok: false, error: 'Driver not running' };
141
+ return { ok: false, error: "Driver not running" };
137
142
  }
138
143
  await driver.pressTab();
139
- recorder?.addCommand('pressTab');
144
+ recorder?.addCommand("pressTab");
140
145
  if (recorder?.isRecording()) {
141
146
  recorder.captureFrame(await driver.capture());
142
147
  }
143
148
  return { ok: true };
144
149
  }
145
150
 
146
- case 'pressEnter': {
151
+ case "pressEnter": {
147
152
  if (!driver?.isRunning()) {
148
- return { ok: false, error: 'Driver not running' };
153
+ return { ok: false, error: "Driver not running" };
149
154
  }
150
155
  await driver.pressEnter();
151
- recorder?.addCommand('pressEnter');
156
+ recorder?.addCommand("pressEnter");
152
157
  if (recorder?.isRecording()) {
153
158
  recorder.captureFrame(await driver.capture());
154
159
  }
155
160
  return { ok: true };
156
161
  }
157
162
 
158
- case 'pressEscape': {
163
+ case "pressEscape": {
159
164
  if (!driver?.isRunning()) {
160
- return { ok: false, error: 'Driver not running' };
165
+ return { ok: false, error: "Driver not running" };
161
166
  }
162
167
  await driver.pressEscape();
163
- recorder?.addCommand('pressEscape');
168
+ recorder?.addCommand("pressEscape");
164
169
  if (recorder?.isRecording()) {
165
170
  recorder.captureFrame(await driver.capture());
166
171
  }
167
172
  return { ok: true };
168
173
  }
169
174
 
170
- case 'pressArrow': {
175
+ case "pressArrow": {
171
176
  if (!driver?.isRunning()) {
172
- return { ok: false, error: 'Driver not running' };
177
+ return { ok: false, error: "Driver not running" };
173
178
  }
174
- const direction = cmd.direction as 'up' | 'down' | 'left' | 'right';
175
- if (!['up', 'down', 'left', 'right'].includes(direction)) {
176
- return { ok: false, error: 'Invalid direction. Use: up, down, left, right' };
179
+ const direction = cmd.direction as "up" | "down" | "left" | "right";
180
+ if (!["up", "down", "left", "right"].includes(direction)) {
181
+ return { ok: false, error: "Invalid direction. Use: up, down, left, right" };
177
182
  }
178
183
  await driver.pressArrow(direction);
179
- recorder?.addCommand('pressArrow', { direction });
184
+ recorder?.addCommand("pressArrow", { direction });
180
185
  if (recorder?.isRecording()) {
181
186
  recorder.captureFrame(await driver.capture());
182
187
  }
183
188
  return { ok: true };
184
189
  }
185
190
 
186
- case 'capture': {
191
+ case "capture": {
187
192
  if (!driver?.isRunning()) {
188
- return { ok: false, error: 'Driver not running' };
193
+ return { ok: false, error: "Driver not running" };
189
194
  }
190
- const result = cmd.meta === true
191
- ? await driver.captureWithMeta()
192
- : { frame: await driver.capture() };
193
-
194
- if (typeof cmd.save === 'string') {
195
+ const result =
196
+ cmd.meta === true ? await driver.captureWithMeta() : { frame: await driver.capture() };
197
+
198
+ if (typeof cmd.save === "string") {
195
199
  const savedPath = await saveFrameToFile(result.frame, cmd.save);
196
200
  return { ok: true, ...result, savedTo: savedPath };
197
201
  }
198
-
202
+
199
203
  return { ok: true, ...result };
200
204
  }
201
205
 
202
- case 'snapshot': {
206
+ case "snapshot": {
203
207
  if (!driver?.isRunning()) {
204
- return { ok: false, error: 'Driver not running' };
208
+ return { ok: false, error: "Driver not running" };
205
209
  }
206
210
  const result = await driver.captureWithMeta();
207
- const dir = typeof cmd.dir === 'string' ? cmd.dir : DEFAULT_SNAPSHOTS_DIR;
208
- const name = typeof cmd.name === 'string'
209
- ? cmd.name
210
- : `frame-${String(++frameCounter).padStart(4, '0')}`;
211
-
211
+ const dir = typeof cmd.dir === "string" ? cmd.dir : DEFAULT_SNAPSHOTS_DIR;
212
+ const name =
213
+ typeof cmd.name === "string"
214
+ ? cmd.name
215
+ : `frame-${String(++frameCounter).padStart(4, "0")}`;
216
+
212
217
  const framePath = path.join(dir, `${name}.txt`);
213
218
  const metadataPath = path.join(dir, `${name}.json`);
214
-
219
+
215
220
  const metadata = {
216
221
  timestamp: new Date(result.timestamp).toISOString(),
217
222
  width: result.width,
218
223
  height: result.height,
219
224
  name,
220
225
  };
221
-
226
+
222
227
  await ensureDir(dir);
223
228
  await Promise.all([
224
- fs.writeFile(framePath, result.frame, 'utf-8'),
225
- fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'),
229
+ fs.writeFile(framePath, result.frame, "utf-8"),
230
+ fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"),
226
231
  ]);
227
-
228
- return {
229
- ok: true,
230
- savedTo: path.resolve(framePath),
232
+
233
+ return {
234
+ ok: true,
235
+ savedTo: path.resolve(framePath),
231
236
  metadataPath: path.resolve(metadataPath),
232
237
  name,
233
238
  metadata,
234
239
  };
235
240
  }
236
241
 
237
- case 'waitForText': {
242
+ case "waitForText": {
238
243
  if (!driver?.isRunning()) {
239
- return { ok: false, error: 'Driver not running' };
244
+ return { ok: false, error: "Driver not running" };
240
245
  }
241
- const text = typeof cmd.text === 'string' ? cmd.text : '';
242
- const timeout = typeof cmd.timeout === 'number' ? cmd.timeout : 5000;
246
+ const text = typeof cmd.text === "string" ? cmd.text : "";
247
+ const timeout = typeof cmd.timeout === "number" ? cmd.timeout : 5000;
243
248
  const found = await driver.waitForText(text, { timeout });
244
249
  return { ok: true, found };
245
250
  }
246
251
 
247
- case 'waitForStable': {
252
+ case "waitForStable": {
248
253
  if (!driver?.isRunning()) {
249
- return { ok: false, error: 'Driver not running' };
254
+ return { ok: false, error: "Driver not running" };
250
255
  }
251
- const maxIterations = typeof cmd.maxIterations === 'number' ? cmd.maxIterations : 10;
252
- const intervalMs = typeof cmd.intervalMs === 'number' ? cmd.intervalMs : 50;
256
+ const maxIterations = typeof cmd.maxIterations === "number" ? cmd.maxIterations : 10;
257
+ const intervalMs = typeof cmd.intervalMs === "number" ? cmd.intervalMs : 50;
253
258
  await driver.waitForStable({ maxIterations, intervalMs });
254
- recorder?.addCommand('waitForStable', { maxIterations, intervalMs });
259
+ recorder?.addCommand("waitForStable", { maxIterations, intervalMs });
255
260
  return { ok: true };
256
261
  }
257
262
 
258
- case 'sleep': {
259
- const ms = typeof cmd.ms === 'number' ? cmd.ms : 1000;
260
- await new Promise(resolve => setTimeout(resolve, ms));
261
- recorder?.addCommand('sleep', { ms });
263
+ case "sleep": {
264
+ const ms = typeof cmd.ms === "number" ? cmd.ms : 1000;
265
+ await new Promise((resolve) => setTimeout(resolve, ms));
266
+ recorder?.addCommand("sleep", { ms });
262
267
  return { ok: true, sleptMs: ms };
263
268
  }
264
269
 
265
- case 'resize': {
270
+ case "resize": {
266
271
  if (!driver?.isRunning()) {
267
- return { ok: false, error: 'Driver not running' };
272
+ return { ok: false, error: "Driver not running" };
268
273
  }
269
- const cols = typeof cmd.cols === 'number' ? cmd.cols : 100;
270
- const rows = typeof cmd.rows === 'number' ? cmd.rows : 30;
274
+ const cols = typeof cmd.cols === "number" ? cmd.cols : 100;
275
+ const rows = typeof cmd.rows === "number" ? cmd.rows : 30;
271
276
  await driver.resize(cols, rows);
272
277
  currentWidth = cols;
273
278
  currentHeight = rows;
274
- recorder?.addCommand('resize', { cols, rows });
279
+ recorder?.addCommand("resize", { cols, rows });
275
280
  return { ok: true };
276
281
  }
277
282
 
278
- case 'status': {
283
+ case "status": {
279
284
  return {
280
285
  ok: true,
281
286
  running: driver?.isRunning() ?? false,
@@ -283,29 +288,30 @@ async function handleCommand(cmd: Command): Promise<Response> {
283
288
  };
284
289
  }
285
290
 
286
- case 'diff': {
287
- const file1 = typeof cmd.file1 === 'string' ? cmd.file1 : null;
288
- const file2 = typeof cmd.file2 === 'string' ? cmd.file2 : null;
289
- const frame1 = typeof cmd.frame1 === 'string' ? cmd.frame1 : null;
290
- const frame2 = typeof cmd.frame2 === 'string' ? cmd.frame2 : null;
291
+ case "diff": {
292
+ const file1 = typeof cmd.file1 === "string" ? cmd.file1 : null;
293
+ const file2 = typeof cmd.file2 === "string" ? cmd.file2 : null;
294
+ const frame1 = typeof cmd.frame1 === "string" ? cmd.frame1 : null;
295
+ const frame2 = typeof cmd.frame2 === "string" ? cmd.frame2 : null;
291
296
  const ignoreWhitespace = cmd.ignoreWhitespace === true;
292
-
297
+
293
298
  let expected: string;
294
299
  let actual: string;
295
-
300
+
296
301
  if (file1 && file2) {
297
- expected = await fs.readFile(file1, 'utf-8');
298
- actual = await fs.readFile(file2, 'utf-8');
302
+ expected = await fs.readFile(file1, "utf-8");
303
+ actual = await fs.readFile(file2, "utf-8");
299
304
  } else if (frame1 && frame2) {
300
305
  expected = frame1;
301
306
  actual = frame2;
302
307
  } else {
303
- return { ok: false, error: 'Provide file1/file2 or frame1/frame2' };
308
+ return { ok: false, error: "Provide file1/file2 or frame1/frame2" };
304
309
  }
305
-
310
+
306
311
  const result = diffFrames(expected, actual, { ignoreWhitespace });
307
- const visual = cmd.visual === true ? highlightDiff(expected, actual, { ignoreWhitespace }) : undefined;
308
-
312
+ const visual =
313
+ cmd.visual === true ? highlightDiff(expected, actual, { ignoreWhitespace }) : undefined;
314
+
309
315
  return {
310
316
  ok: true,
311
317
  identical: result.identical,
@@ -319,21 +325,21 @@ async function handleCommand(cmd: Command): Promise<Response> {
319
325
  };
320
326
  }
321
327
 
322
- case 'assert': {
328
+ case "assert": {
323
329
  if (!driver?.isRunning()) {
324
- return { ok: false, error: 'Driver not running' };
330
+ return { ok: false, error: "Driver not running" };
325
331
  }
326
- const name = typeof cmd.name === 'string' ? cmd.name : null;
332
+ const name = typeof cmd.name === "string" ? cmd.name : null;
327
333
  if (!name) {
328
- return { ok: false, error: 'Name is required for assertion' };
334
+ return { ok: false, error: "Name is required for assertion" };
329
335
  }
330
-
336
+
331
337
  const frame = await driver.capture();
332
338
  const { width, height } = driver.getSize();
333
- const goldenDir = typeof cmd.goldenDir === 'string' ? cmd.goldenDir : './golden';
339
+ const goldenDir = typeof cmd.goldenDir === "string" ? cmd.goldenDir : "./golden";
334
340
  const updateGolden = cmd.update === true;
335
341
  const ignoreWhitespace = cmd.ignoreWhitespace === true;
336
-
342
+
337
343
  const result = await assertSnapshot(name, frame, {
338
344
  goldenDir,
339
345
  updateGolden,
@@ -341,7 +347,7 @@ async function handleCommand(cmd: Command): Promise<Response> {
341
347
  width,
342
348
  height,
343
349
  });
344
-
350
+
345
351
  return {
346
352
  ok: true,
347
353
  passed: result.passed,
@@ -351,27 +357,29 @@ async function handleCommand(cmd: Command): Promise<Response> {
351
357
  width,
352
358
  height,
353
359
  ...(result.dimensionMismatch ? { dimensionMismatch: result.dimensionMismatch } : {}),
354
- ...(result.diff ? {
355
- changedLines: result.diff.changedLines,
356
- totalLines: result.diff.totalLines,
357
- changePercentage: result.diff.changePercentage,
358
- } : {}),
360
+ ...(result.diff
361
+ ? {
362
+ changedLines: result.diff.changedLines,
363
+ totalLines: result.diff.totalLines,
364
+ changePercentage: result.diff.changePercentage,
365
+ }
366
+ : {}),
359
367
  ...(result.visual && cmd.visual === true ? { visual: result.visual } : {}),
360
368
  };
361
369
  }
362
370
 
363
- case 'listGolden': {
364
- const goldenDir = typeof cmd.goldenDir === 'string' ? cmd.goldenDir : './golden';
371
+ case "listGolden": {
372
+ const goldenDir = typeof cmd.goldenDir === "string" ? cmd.goldenDir : "./golden";
365
373
  const files = await listGoldenFiles(goldenDir);
366
374
  return { ok: true, files, count: files.length };
367
375
  }
368
376
 
369
- case 'getGolden': {
370
- const name = typeof cmd.name === 'string' ? cmd.name : null;
377
+ case "getGolden": {
378
+ const name = typeof cmd.name === "string" ? cmd.name : null;
371
379
  if (!name) {
372
- return { ok: false, error: 'Name is required' };
380
+ return { ok: false, error: "Name is required" };
373
381
  }
374
- const goldenDir = typeof cmd.goldenDir === 'string' ? cmd.goldenDir : './golden';
382
+ const goldenDir = typeof cmd.goldenDir === "string" ? cmd.goldenDir : "./golden";
375
383
  const info = await getGoldenFile(name, goldenDir);
376
384
  if (info === null) {
377
385
  return { ok: false, error: `Golden file not found: ${name}` };
@@ -388,36 +396,36 @@ async function handleCommand(cmd: Command): Promise<Response> {
388
396
  };
389
397
  }
390
398
 
391
- case 'deleteGolden': {
392
- const name = typeof cmd.name === 'string' ? cmd.name : null;
399
+ case "deleteGolden": {
400
+ const name = typeof cmd.name === "string" ? cmd.name : null;
393
401
  if (!name) {
394
- return { ok: false, error: 'Name is required' };
402
+ return { ok: false, error: "Name is required" };
395
403
  }
396
- const goldenDir = typeof cmd.goldenDir === 'string' ? cmd.goldenDir : './golden';
404
+ const goldenDir = typeof cmd.goldenDir === "string" ? cmd.goldenDir : "./golden";
397
405
  const deleted = await deleteGoldenFile(name, goldenDir);
398
406
  return { ok: true, deleted, name };
399
407
  }
400
408
 
401
- case 'startRecording': {
409
+ case "startRecording": {
402
410
  if (!driver?.isRunning()) {
403
- return { ok: false, error: 'Driver not running' };
411
+ return { ok: false, error: "Driver not running" };
404
412
  }
405
413
  if (recorder?.isRecording()) {
406
- return { ok: false, error: 'Already recording' };
414
+ return { ok: false, error: "Already recording" };
407
415
  }
408
- const name = typeof cmd.name === 'string' ? cmd.name : `recording-${Date.now()}`;
416
+ const name = typeof cmd.name === "string" ? cmd.name : `recording-${Date.now()}`;
409
417
  const captureFrames = cmd.captureFrames === true;
410
418
  recorder = createRecorder(currentWidth, currentHeight, { name, captureFrames });
411
419
  recorder.start();
412
420
  return { ok: true, name, captureFrames };
413
421
  }
414
422
 
415
- case 'stopRecording': {
423
+ case "stopRecording": {
416
424
  if (!recorder?.isRecording()) {
417
- return { ok: false, error: 'Not recording' };
425
+ return { ok: false, error: "Not recording" };
418
426
  }
419
427
  const recording = recorder.stop();
420
- const recordingsDir = typeof cmd.dir === 'string' ? cmd.dir : './recordings';
428
+ const recordingsDir = typeof cmd.dir === "string" ? cmd.dir : "./recordings";
421
429
  const savedPath = await saveRecording(recording, recordingsDir);
422
430
  recorder = null;
423
431
  return {
@@ -428,15 +436,15 @@ async function handleCommand(cmd: Command): Promise<Response> {
428
436
  };
429
437
  }
430
438
 
431
- case 'cancelRecording': {
439
+ case "cancelRecording": {
432
440
  if (!recorder?.isRecording()) {
433
- return { ok: false, error: 'Not recording' };
441
+ return { ok: false, error: "Not recording" };
434
442
  }
435
443
  recorder = null;
436
444
  return { ok: true };
437
445
  }
438
446
 
439
- case 'recordingStatus': {
447
+ case "recordingStatus": {
440
448
  return {
441
449
  ok: true,
442
450
  recording: recorder?.isRecording() ?? false,
@@ -444,26 +452,26 @@ async function handleCommand(cmd: Command): Promise<Response> {
444
452
  };
445
453
  }
446
454
 
447
- case 'replay': {
455
+ case "replay": {
448
456
  if (!driver?.isRunning()) {
449
- return { ok: false, error: 'Driver not running' };
457
+ return { ok: false, error: "Driver not running" };
450
458
  }
451
- const name = typeof cmd.name === 'string' ? cmd.name : null;
459
+ const name = typeof cmd.name === "string" ? cmd.name : null;
452
460
  let recording: Recording | null = null;
453
461
 
454
462
  if (name) {
455
- const recordingsDir = typeof cmd.dir === 'string' ? cmd.dir : './recordings';
463
+ const recordingsDir = typeof cmd.dir === "string" ? cmd.dir : "./recordings";
456
464
  recording = await loadRecording(name, recordingsDir);
457
465
  if (!recording) {
458
466
  return { ok: false, error: `Recording not found: ${name}` };
459
467
  }
460
- } else if (typeof cmd.recording === 'object' && cmd.recording !== null) {
468
+ } else if (typeof cmd.recording === "object" && cmd.recording !== null) {
461
469
  recording = cmd.recording as Recording;
462
470
  } else {
463
- return { ok: false, error: 'Provide name or recording object' };
471
+ return { ok: false, error: "Provide name or recording object" };
464
472
  }
465
473
 
466
- const speed = typeof cmd.speed === 'number' ? cmd.speed : Infinity;
474
+ const speed = typeof cmd.speed === "number" ? cmd.speed : Infinity;
467
475
  const result = await replayRecording(driver, recording, { speed });
468
476
 
469
477
  return {
@@ -475,18 +483,18 @@ async function handleCommand(cmd: Command): Promise<Response> {
475
483
  };
476
484
  }
477
485
 
478
- case 'listRecordings': {
479
- const recordingsDir = typeof cmd.dir === 'string' ? cmd.dir : './recordings';
486
+ case "listRecordings": {
487
+ const recordingsDir = typeof cmd.dir === "string" ? cmd.dir : "./recordings";
480
488
  const recordings = await listRecordings(recordingsDir);
481
489
  return { ok: true, recordings, count: recordings.length };
482
490
  }
483
491
 
484
- case 'getRecording': {
485
- const name = typeof cmd.name === 'string' ? cmd.name : null;
492
+ case "getRecording": {
493
+ const name = typeof cmd.name === "string" ? cmd.name : null;
486
494
  if (!name) {
487
- return { ok: false, error: 'Name is required' };
495
+ return { ok: false, error: "Name is required" };
488
496
  }
489
- const recordingsDir = typeof cmd.dir === 'string' ? cmd.dir : './recordings';
497
+ const recordingsDir = typeof cmd.dir === "string" ? cmd.dir : "./recordings";
490
498
  const recording = await loadRecording(name, recordingsDir);
491
499
  if (!recording) {
492
500
  return { ok: false, error: `Recording not found: ${name}` };
@@ -494,26 +502,26 @@ async function handleCommand(cmd: Command): Promise<Response> {
494
502
  return { ok: true, recording };
495
503
  }
496
504
 
497
- case 'deleteRecording': {
498
- const name = typeof cmd.name === 'string' ? cmd.name : null;
505
+ case "deleteRecording": {
506
+ const name = typeof cmd.name === "string" ? cmd.name : null;
499
507
  if (!name) {
500
- return { ok: false, error: 'Name is required' };
508
+ return { ok: false, error: "Name is required" };
501
509
  }
502
- const recordingsDir = typeof cmd.dir === 'string' ? cmd.dir : './recordings';
510
+ const recordingsDir = typeof cmd.dir === "string" ? cmd.dir : "./recordings";
503
511
  const deleted = await deleteRecording(name, recordingsDir);
504
512
  return { ok: true, deleted, name };
505
513
  }
506
514
 
507
- case 'startCoverage': {
515
+ case "startCoverage": {
508
516
  if (!driver?.isRunning()) {
509
- return { ok: false, error: 'Driver not running' };
517
+ return { ok: false, error: "Driver not running" };
510
518
  }
511
519
  if (coverageTracker?.isTracking()) {
512
- return { ok: false, error: 'Already tracking coverage' };
520
+ return { ok: false, error: "Already tracking coverage" };
513
521
  }
514
522
  const knownViews =
515
523
  Array.isArray(cmd.knownViews) &&
516
- cmd.knownViews.every((v): v is string => typeof v === 'string')
524
+ cmd.knownViews.every((v): v is string => typeof v === "string")
517
525
  ? cmd.knownViews
518
526
  : undefined;
519
527
  coverageTracker = createCoverageTracker(knownViews);
@@ -521,9 +529,9 @@ async function handleCommand(cmd: Command): Promise<Response> {
521
529
  return { ok: true };
522
530
  }
523
531
 
524
- case 'stopCoverage': {
532
+ case "stopCoverage": {
525
533
  if (!coverageTracker?.isTracking()) {
526
- return { ok: false, error: 'Not tracking coverage' };
534
+ return { ok: false, error: "Not tracking coverage" };
527
535
  }
528
536
  const report = coverageTracker.stop();
529
537
  const visual = cmd.visual === true ? formatCoverageReport(report) : undefined;
@@ -535,12 +543,12 @@ async function handleCommand(cmd: Command): Promise<Response> {
535
543
  };
536
544
  }
537
545
 
538
- case 'recordViewFromFrame': {
546
+ case "recordViewFromFrame": {
539
547
  if (!driver?.isRunning()) {
540
- return { ok: false, error: 'Driver not running' };
548
+ return { ok: false, error: "Driver not running" };
541
549
  }
542
550
  if (!coverageTracker?.isTracking()) {
543
- return { ok: false, error: 'Not tracking coverage' };
551
+ return { ok: false, error: "Not tracking coverage" };
544
552
  }
545
553
  const frame = await driver.capture();
546
554
  const view = detectViewFromFrame(frame);
@@ -550,9 +558,9 @@ async function handleCommand(cmd: Command): Promise<Response> {
550
558
  return { ok: true, detectedView: view };
551
559
  }
552
560
 
553
- case 'getCoverage': {
561
+ case "getCoverage": {
554
562
  if (!coverageTracker) {
555
- return { ok: false, error: 'No coverage tracker' };
563
+ return { ok: false, error: "No coverage tracker" };
556
564
  }
557
565
  const report = coverageTracker.getReport();
558
566
  const visual = cmd.visual === true ? formatCoverageReport(report) : undefined;
@@ -563,43 +571,43 @@ async function handleCommand(cmd: Command): Promise<Response> {
563
571
  };
564
572
  }
565
573
 
566
- case 'help': {
574
+ case "help": {
567
575
  return {
568
576
  ok: true,
569
577
  commands: [
570
- 'launch - Start the app (options: width, height, debug; demo mode: demo, demoSeed, demoPreset)',
571
- 'close - Stop the app',
572
- 'sendKeys - Send key sequence (options: keys)',
573
- 'pressKey - Press single key (options: key, modifiers)',
574
- 'pressTab - Press Tab key',
575
- 'pressEnter - Press Enter key',
576
- 'pressEscape - Press Escape key',
577
- 'pressArrow - Press arrow key (options: direction)',
578
- 'typeText - Type text (options: text, delay)',
579
- 'capture - Get current frame (options: meta, save)',
580
- 'snapshot - Save frame to file (options: dir, name)',
581
- 'waitForText - Wait for text (options: text, timeout)',
582
- 'waitForStable - Wait for stable frame (options: maxIterations, intervalMs)',
583
- 'sleep - Pause for duration (options: ms)',
584
- 'resize - Resize terminal (options: cols, rows)',
585
- 'status - Get driver status',
586
- 'diff - Compare two frames (options: file1, file2 OR frame1, frame2, ignoreWhitespace, visual)',
587
- 'assert - Assert snapshot matches golden file (options: name, goldenDir, update, ignoreWhitespace, visual)',
588
- 'listGolden - List all golden files (options: goldenDir)',
589
- 'getGolden - Get content of golden file (options: name, goldenDir)',
590
- 'deleteGolden - Delete a golden file (options: name, goldenDir)',
591
- 'startRecording - Start recording commands (options: name, captureFrames)',
592
- 'stopRecording - Stop recording and save (options: dir)',
593
- 'cancelRecording - Cancel recording without saving',
594
- 'recordingStatus - Get current recording status',
595
- 'replay - Replay a recording (options: name, dir, speed)',
596
- 'listRecordings - List all recordings (options: dir)',
597
- 'getRecording - Get a recording (options: name, dir)',
598
- 'deleteRecording - Delete a recording (options: name, dir)',
599
- 'startCoverage - Start tracking view coverage (options: knownViews)',
600
- 'stopCoverage - Stop tracking and get report (options: visual)',
601
- 'recordViewFromFrame - Detect and record current view',
602
- 'getCoverage - Get current coverage report (options: visual)',
578
+ "launch - Start the app (options: width, height, debug; demo mode: demo, demoSeed, demoPreset)",
579
+ "close - Stop the app",
580
+ "sendKeys - Send key sequence (options: keys)",
581
+ "pressKey - Press single key (options: key, modifiers)",
582
+ "pressTab - Press Tab key",
583
+ "pressEnter - Press Enter key",
584
+ "pressEscape - Press Escape key",
585
+ "pressArrow - Press arrow key (options: direction)",
586
+ "typeText - Type text (options: text, delay)",
587
+ "capture - Get current frame (options: meta, save)",
588
+ "snapshot - Save frame to file (options: dir, name)",
589
+ "waitForText - Wait for text (options: text, timeout)",
590
+ "waitForStable - Wait for stable frame (options: maxIterations, intervalMs)",
591
+ "sleep - Pause for duration (options: ms)",
592
+ "resize - Resize terminal (options: cols, rows)",
593
+ "status - Get driver status",
594
+ "diff - Compare two frames (options: file1, file2 OR frame1, frame2, ignoreWhitespace, visual)",
595
+ "assert - Assert snapshot matches golden file (options: name, goldenDir, update, ignoreWhitespace, visual)",
596
+ "listGolden - List all golden files (options: goldenDir)",
597
+ "getGolden - Get content of golden file (options: name, goldenDir)",
598
+ "deleteGolden - Delete a golden file (options: name, goldenDir)",
599
+ "startRecording - Start recording commands (options: name, captureFrames)",
600
+ "stopRecording - Stop recording and save (options: dir)",
601
+ "cancelRecording - Cancel recording without saving",
602
+ "recordingStatus - Get current recording status",
603
+ "replay - Replay a recording (options: name, dir, speed)",
604
+ "listRecordings - List all recordings (options: dir)",
605
+ "getRecording - Get a recording (options: name, dir)",
606
+ "deleteRecording - Delete a recording (options: name, dir)",
607
+ "startCoverage - Start tracking view coverage (options: knownViews)",
608
+ "stopCoverage - Stop tracking and get report (options: visual)",
609
+ "recordViewFromFrame - Detect and record current view",
610
+ "getCoverage - Get current coverage report (options: visual)",
603
611
  ],
604
612
  };
605
613
  }
@@ -617,8 +625,8 @@ async function handleCommand(cmd: Command): Promise<Response> {
617
625
 
618
626
  async function main() {
619
627
  const args = process.argv.slice(2);
620
-
621
- if (args.includes('--help') || args.includes('-h')) {
628
+
629
+ if (args.includes("--help") || args.includes("-h")) {
622
630
  console.log(`
623
631
  Tokentop Driver CLI
624
632
 
@@ -667,7 +675,7 @@ Example:
667
675
  async function processQueue(): Promise<void> {
668
676
  if (processing) return;
669
677
  processing = true;
670
-
678
+
671
679
  while (commandQueue.length > 0) {
672
680
  const line = commandQueue.shift()!;
673
681
  try {
@@ -675,15 +683,17 @@ Example:
675
683
  const response = await handleCommand(cmd);
676
684
  console.log(JSON.stringify(response));
677
685
  } catch (err) {
678
- console.log(JSON.stringify({
679
- ok: false,
680
- error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
681
- }));
686
+ console.log(
687
+ JSON.stringify({
688
+ ok: false,
689
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
690
+ }),
691
+ );
682
692
  }
683
693
  }
684
-
694
+
685
695
  processing = false;
686
-
696
+
687
697
  if (closed && commandQueue.length === 0) {
688
698
  if (driver?.isRunning()) {
689
699
  await driver.close();
@@ -692,14 +702,14 @@ Example:
692
702
  }
693
703
  }
694
704
 
695
- rl.on('line', (line) => {
705
+ rl.on("line", (line) => {
696
706
  const trimmed = line.trim();
697
707
  if (!trimmed) return;
698
708
  commandQueue.push(trimmed);
699
709
  processQueue();
700
710
  });
701
711
 
702
- rl.on('close', () => {
712
+ rl.on("close", () => {
703
713
  closed = true;
704
714
  if (!processing && commandQueue.length === 0) {
705
715
  if (driver?.isRunning()) {
@@ -712,6 +722,6 @@ Example:
712
722
  }
713
723
 
714
724
  main().catch((err) => {
715
- console.error('Driver CLI failed:', err);
725
+ console.error("Driver CLI failed:", err);
716
726
  process.exit(1);
717
727
  });