@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,14 +1,14 @@
1
- import * as fs from 'fs/promises';
2
- import * as os from 'os';
3
- import * as path from 'path';
1
+ import * as fs from "fs/promises";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { PATHS } from "@/storage/paths.ts";
5
+ import { CURRENT_API_VERSION } from "./types/base.ts";
4
6
  import type {
5
- PluginType,
7
+ AnyPlugin,
6
8
  PluginLoadResult,
9
+ PluginType,
7
10
  PluginValidationResult,
8
- AnyPlugin,
9
- } from './types/index.ts';
10
- import { CURRENT_API_VERSION } from './types/base.ts';
11
- import { PATHS } from '@/storage/paths.ts';
11
+ } from "./types/index.ts";
12
12
 
13
13
  const CUSTOM_PLUGINS_DIR = PATHS.config.plugins;
14
14
 
@@ -16,90 +16,92 @@ export async function validatePlugin(plugin: unknown): Promise<PluginValidationR
16
16
  const errors: string[] = [];
17
17
  const warnings: string[] = [];
18
18
 
19
- if (!plugin || typeof plugin !== 'object') {
20
- errors.push('Plugin must be an object');
19
+ if (!plugin || typeof plugin !== "object") {
20
+ errors.push("Plugin must be an object");
21
21
  return { valid: false, errors, warnings };
22
22
  }
23
23
 
24
24
  const p = plugin as Record<string, unknown>;
25
25
 
26
- if (typeof p.id !== 'string' || p.id.length === 0) {
26
+ if (typeof p.id !== "string" || p.id.length === 0) {
27
27
  errors.push('Plugin must have a non-empty string "id"');
28
28
  } else if (!/^[a-z][a-z0-9-]*$/.test(p.id)) {
29
- errors.push('Plugin id must be kebab-case (lowercase letters, numbers, hyphens)');
29
+ errors.push("Plugin id must be kebab-case (lowercase letters, numbers, hyphens)");
30
30
  }
31
31
 
32
- const validTypes: PluginType[] = ['provider', 'agent', 'theme', 'notification'];
32
+ const validTypes: PluginType[] = ["provider", "agent", "theme", "notification"];
33
33
  if (!validTypes.includes(p.type as PluginType)) {
34
- errors.push(`Plugin type must be one of: ${validTypes.join(', ')}`);
34
+ errors.push(`Plugin type must be one of: ${validTypes.join(", ")}`);
35
35
  }
36
36
 
37
- if (typeof p.name !== 'string' || p.name.length === 0) {
37
+ if (typeof p.name !== "string" || p.name.length === 0) {
38
38
  errors.push('Plugin must have a non-empty string "name"');
39
39
  }
40
40
 
41
- if (typeof p.version !== 'string' || !/^\d+\.\d+\.\d+/.test(p.version)) {
41
+ if (typeof p.version !== "string" || !/^\d+\.\d+\.\d+/.test(p.version)) {
42
42
  errors.push('Plugin must have a valid semver "version" (e.g., "1.0.0")');
43
43
  }
44
44
 
45
- if (!p.permissions || typeof p.permissions !== 'object') {
45
+ if (!p.permissions || typeof p.permissions !== "object") {
46
46
  errors.push('Plugin must declare "permissions" object');
47
47
  }
48
48
 
49
49
  if (p.apiVersion !== CURRENT_API_VERSION) {
50
- if (typeof p.apiVersion === 'number') {
51
- errors.push(`Plugin apiVersion ${p.apiVersion} is not compatible (expected ${CURRENT_API_VERSION})`);
50
+ if (typeof p.apiVersion === "number") {
51
+ errors.push(
52
+ `Plugin apiVersion ${p.apiVersion} is not compatible (expected ${CURRENT_API_VERSION})`,
53
+ );
52
54
  } else {
53
55
  errors.push(`Plugin must declare "apiVersion: ${CURRENT_API_VERSION}"`);
54
56
  }
55
57
  }
56
58
 
57
- if (p.type === 'provider') {
58
- if (!p.auth || typeof p.auth !== 'object') {
59
+ if (p.type === "provider") {
60
+ if (!p.auth || typeof p.auth !== "object") {
59
61
  errors.push('Provider plugin must declare "auth" object with discover() and isConfigured()');
60
62
  } else {
61
63
  const auth = p.auth as Record<string, unknown>;
62
- if (typeof auth.discover !== 'function') {
64
+ if (typeof auth.discover !== "function") {
63
65
  errors.push('Provider plugin auth must implement "discover" method');
64
66
  }
65
- if (typeof auth.isConfigured !== 'function') {
67
+ if (typeof auth.isConfigured !== "function") {
66
68
  errors.push('Provider plugin auth must implement "isConfigured" method');
67
69
  }
68
70
  }
69
- if (typeof p.fetchUsage !== 'function') {
71
+ if (typeof p.fetchUsage !== "function") {
70
72
  errors.push('Provider plugin must implement "fetchUsage" method');
71
73
  }
72
74
  }
73
75
 
74
- if (p.type === 'agent') {
75
- if (typeof p.isInstalled !== 'function') {
76
+ if (p.type === "agent") {
77
+ if (typeof p.isInstalled !== "function") {
76
78
  errors.push('Agent plugin must implement "isInstalled" method');
77
79
  }
78
- if (typeof p.parseSessions !== 'function') {
80
+ if (typeof p.parseSessions !== "function") {
79
81
  errors.push('Agent plugin must implement "parseSessions" method');
80
82
  }
81
- if (p.readCredentials !== undefined && typeof p.readCredentials !== 'function') {
83
+ if (p.readCredentials !== undefined && typeof p.readCredentials !== "function") {
82
84
  errors.push('Agent plugin "readCredentials" must be a function if provided');
83
85
  }
84
- if (p.getProviders !== undefined && typeof p.getProviders !== 'function') {
86
+ if (p.getProviders !== undefined && typeof p.getProviders !== "function") {
85
87
  errors.push('Agent plugin "getProviders" must be a function if provided');
86
88
  }
87
89
  }
88
90
 
89
- if (p.type === 'theme') {
90
- if (!['light', 'dark'].includes(p.colorScheme as string)) {
91
+ if (p.type === "theme") {
92
+ if (!["light", "dark"].includes(p.colorScheme as string)) {
91
93
  errors.push('Theme plugin must specify colorScheme as "light" or "dark"');
92
94
  }
93
- if (!p.colors || typeof p.colors !== 'object') {
95
+ if (!p.colors || typeof p.colors !== "object") {
94
96
  errors.push('Theme plugin must provide "colors" object');
95
97
  }
96
98
  }
97
99
 
98
- if (p.type === 'notification') {
99
- if (typeof p.initialize !== 'function') {
100
+ if (p.type === "notification") {
101
+ if (typeof p.initialize !== "function") {
100
102
  errors.push('Notification plugin must implement "initialize" method');
101
103
  }
102
- if (typeof p.notify !== 'function') {
104
+ if (typeof p.notify !== "function") {
103
105
  errors.push('Notification plugin must implement "notify" method');
104
106
  }
105
107
  }
@@ -108,7 +110,7 @@ export async function validatePlugin(plugin: unknown): Promise<PluginValidationR
108
110
  }
109
111
 
110
112
  function expandTilde(filePath: string): string {
111
- if (filePath.startsWith('~/') || filePath === '~') {
113
+ if (filePath.startsWith("~/") || filePath === "~") {
112
114
  return path.join(os.homedir(), filePath.slice(1));
113
115
  }
114
116
  return filePath;
@@ -116,37 +118,35 @@ function expandTilde(filePath: string): string {
116
118
 
117
119
  async function resolvePluginEntryPoint(dirPath: string): Promise<string | null> {
118
120
  try {
119
- const pkgJsonPath = path.join(dirPath, 'package.json');
120
- const pkgContent = await fs.readFile(pkgJsonPath, 'utf-8');
121
+ const pkgJsonPath = path.join(dirPath, "package.json");
122
+ const pkgContent = await fs.readFile(pkgJsonPath, "utf-8");
121
123
  const pkg = JSON.parse(pkgContent) as Record<string, unknown>;
122
124
 
123
- if (typeof pkg.main === 'string') {
125
+ if (typeof pkg.main === "string") {
124
126
  return path.resolve(dirPath, pkg.main);
125
127
  }
126
128
 
127
- if (pkg.exports && typeof pkg.exports === 'object') {
129
+ if (pkg.exports && typeof pkg.exports === "object") {
128
130
  const exports = pkg.exports as Record<string, unknown>;
129
- const root = exports['.'];
130
- if (typeof root === 'string') return path.resolve(dirPath, root);
131
- if (root && typeof root === 'object') {
131
+ const root = exports["."];
132
+ if (typeof root === "string") return path.resolve(dirPath, root);
133
+ if (root && typeof root === "object") {
132
134
  const rootObj = root as Record<string, unknown>;
133
135
  const entry = rootObj.import ?? rootObj.default;
134
- if (typeof entry === 'string') return path.resolve(dirPath, entry);
136
+ if (typeof entry === "string") return path.resolve(dirPath, entry);
135
137
  }
136
138
  }
137
139
  } catch {
138
140
  // no package.json — try convention-based entry points
139
141
  }
140
142
 
141
- const candidates = ['src/index.ts', 'src/index.js', 'index.ts', 'index.js', 'dist/index.js'];
143
+ const candidates = ["src/index.ts", "src/index.js", "index.ts", "index.js", "dist/index.js"];
142
144
  for (const candidate of candidates) {
143
145
  const candidatePath = path.join(dirPath, candidate);
144
146
  try {
145
147
  await fs.access(candidatePath);
146
148
  return candidatePath;
147
- } catch {
148
- continue;
149
- }
149
+ } catch {}
150
150
  }
151
151
 
152
152
  return null;
@@ -170,7 +170,7 @@ export async function loadLocalPlugin(filePath: string): Promise<PluginLoadResul
170
170
  return {
171
171
  success: false,
172
172
  error: `Directory "${filePath}" has no recognizable entry point (checked package.json main/exports, src/index.ts, index.ts)`,
173
- source: 'local',
173
+ source: "local",
174
174
  };
175
175
  }
176
176
  importPath = entryPoint;
@@ -183,29 +183,32 @@ export async function loadLocalPlugin(filePath: string): Promise<PluginLoadResul
183
183
  if (!validation.valid) {
184
184
  return {
185
185
  success: false,
186
- error: `Validation failed: ${validation.errors.join('; ')}`,
187
- source: 'local',
186
+ error: `Validation failed: ${validation.errors.join("; ")}`,
187
+ source: "local",
188
188
  };
189
189
  }
190
190
 
191
191
  return {
192
192
  success: true,
193
193
  plugin: plugin as AnyPlugin,
194
- source: 'local',
194
+ source: "local",
195
195
  };
196
196
  } catch (err) {
197
197
  return {
198
198
  success: false,
199
199
  error: err instanceof Error ? err.message : String(err),
200
- source: 'local',
200
+ source: "local",
201
201
  };
202
202
  }
203
203
  }
204
204
 
205
- export async function loadNpmPlugin(packageName: string, resolvedPath?: string): Promise<PluginLoadResult<AnyPlugin>> {
206
- const pluginTypes = ['provider', 'agent', 'theme', 'notification'];
205
+ export async function loadNpmPlugin(
206
+ packageName: string,
207
+ resolvedPath?: string,
208
+ ): Promise<PluginLoadResult<AnyPlugin>> {
209
+ const pluginTypes = ["provider", "agent", "theme", "notification"];
207
210
 
208
- const cleanName = packageName.replace(/@[^@]*$/, '');
211
+ const cleanName = packageName.replace(/@[^@]*$/, "");
209
212
 
210
213
  const isOfficial = pluginTypes.some((t) => cleanName.startsWith(`@tokentop/${t}-`));
211
214
  const isCommunity = pluginTypes.some((t) => cleanName.startsWith(`tokentop-${t}-`));
@@ -217,11 +220,12 @@ export async function loadNpmPlugin(packageName: string, resolvedPath?: string):
217
220
  if (!isOfficial && !isCommunity && !isScopedCommunity) {
218
221
  return {
219
222
  success: false,
220
- error: `Invalid npm plugin name "${packageName}". Expected one of:\n` +
223
+ error:
224
+ `Invalid npm plugin name "${packageName}". Expected one of:\n` +
221
225
  ` Official: @tokentop/{provider,agent,theme,notification}-*\n` +
222
226
  ` Community: tokentop-{provider,agent,theme,notification}-*\n` +
223
227
  ` Scoped: @yourname/tokentop-{provider,agent,theme,notification}-*`,
224
- source: 'npm',
228
+ source: "npm",
225
229
  };
226
230
  }
227
231
 
@@ -234,21 +238,21 @@ export async function loadNpmPlugin(packageName: string, resolvedPath?: string):
234
238
  if (!validation.valid) {
235
239
  return {
236
240
  success: false,
237
- error: `Validation failed: ${validation.errors.join('; ')}`,
238
- source: 'npm',
241
+ error: `Validation failed: ${validation.errors.join("; ")}`,
242
+ source: "npm",
239
243
  };
240
244
  }
241
245
 
242
246
  return {
243
247
  success: true,
244
248
  plugin: plugin as AnyPlugin,
245
- source: 'npm',
249
+ source: "npm",
246
250
  };
247
251
  } catch (err) {
248
252
  return {
249
253
  success: false,
250
254
  error: err instanceof Error ? err.message : String(err),
251
- source: 'npm',
255
+ source: "npm",
252
256
  };
253
257
  }
254
258
  }
@@ -266,7 +270,7 @@ export async function discoverLocalPlugins(): Promise<string[]> {
266
270
  for (const entry of entries) {
267
271
  const fullPath = path.join(CUSTOM_PLUGINS_DIR, entry.name);
268
272
 
269
- if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
273
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
270
274
  results.push(fullPath);
271
275
  } else if (entry.isDirectory()) {
272
276
  const entryPoint = await resolvePluginEntryPoint(fullPath);
@@ -1,8 +1,8 @@
1
- import type { NotificationPlugin, NotificationEvent } from './types/notification.ts';
2
- import type { ProviderUsageData } from './types/provider.ts';
3
- import { safeInvoke } from './plugin-host.ts';
4
- import { createPluginLogger } from './sandbox.ts';
5
- import type { AppConfig } from '@/config/schema.ts';
1
+ import type { AppConfig } from "@/config/schema.ts";
2
+ import { safeInvoke } from "./plugin-host.ts";
3
+ import { createPluginLogger } from "./sandbox.ts";
4
+ import type { NotificationEvent, NotificationPlugin } from "./types/notification.ts";
5
+ import type { ProviderUsageData } from "./types/provider.ts";
6
6
 
7
7
  interface DedupEntry {
8
8
  key: string;
@@ -42,8 +42,8 @@ class NotificationBus {
42
42
  ): Promise<void> {
43
43
  if (usage.limitReached) {
44
44
  await this.emit(`provider.limitReached:${providerId}`, {
45
- type: 'provider.limitReached',
46
- severity: 'critical',
45
+ type: "provider.limitReached",
46
+ severity: "critical",
47
47
  title: `${providerName} Rate Limit Reached`,
48
48
  message: `Rate limit reached for ${providerName}. Requests may be throttled.`,
49
49
  timestamp: Date.now(),
@@ -52,10 +52,15 @@ class NotificationBus {
52
52
  }
53
53
 
54
54
  const primaryPercent = usage.limits?.primary?.usedPercent;
55
- if (primaryPercent !== null && primaryPercent !== undefined && primaryPercent >= 80 && !usage.limitReached) {
55
+ if (
56
+ primaryPercent !== null &&
57
+ primaryPercent !== undefined &&
58
+ primaryPercent >= 80 &&
59
+ !usage.limitReached
60
+ ) {
56
61
  await this.emit(`provider.limitReached:warning:${providerId}`, {
57
- type: 'provider.limitReached',
58
- severity: primaryPercent >= 95 ? 'critical' : 'warning',
62
+ type: "provider.limitReached",
63
+ severity: primaryPercent >= 95 ? "critical" : "warning",
59
64
  title: `${providerName} Approaching Limit`,
60
65
  message: `${providerName} usage at ${Math.round(primaryPercent)}%.`,
61
66
  timestamp: Date.now(),
@@ -67,7 +72,7 @@ class NotificationBus {
67
72
  async checkBudget(
68
73
  cost: number,
69
74
  limit: number,
70
- budgetType: 'daily' | 'weekly' | 'monthly',
75
+ budgetType: "daily" | "weekly" | "monthly",
71
76
  config: AppConfig,
72
77
  ): Promise<void> {
73
78
  if (limit <= 0) return;
@@ -77,8 +82,8 @@ class NotificationBus {
77
82
 
78
83
  if (percent >= config.alerts.criticalPercent) {
79
84
  await this.emit(`budget.limitReached:${budgetType}`, {
80
- type: 'budget.limitReached',
81
- severity: 'critical',
85
+ type: "budget.limitReached",
86
+ severity: "critical",
82
87
  title: `${label} Budget Critical`,
83
88
  message: `${label} spending at ${Math.round(percent)}% ($${cost.toFixed(2)}/$${limit.toFixed(2)}).`,
84
89
  timestamp: Date.now(),
@@ -86,8 +91,8 @@ class NotificationBus {
86
91
  });
87
92
  } else if (percent >= config.alerts.warningPercent) {
88
93
  await this.emit(`budget.thresholdCrossed:${budgetType}`, {
89
- type: 'budget.thresholdCrossed',
90
- severity: 'warning',
94
+ type: "budget.thresholdCrossed",
95
+ severity: "warning",
91
96
  title: `${label} Budget Warning`,
92
97
  message: `${label} spending at ${Math.round(percent)}% ($${cost.toFixed(2)}/$${limit.toFixed(2)}).`,
93
98
  timestamp: Date.now(),
@@ -111,7 +116,7 @@ class NotificationBus {
111
116
  matchingPlugins.map((plugin) => {
112
117
  const config = this.pluginConfigs.get(plugin.id) ?? {};
113
118
  const logger = createPluginLogger(plugin.id);
114
- return safeInvoke(plugin.id, 'notify', () =>
119
+ return safeInvoke(plugin.id, "notify", () =>
115
120
  plugin.notify({ logger, config, signal: AbortSignal.timeout(10_000) }, event),
116
121
  );
117
122
  }),
@@ -1,2 +1,2 @@
1
- export { terminalBellPlugin } from './terminal-bell.ts';
2
- export { visualFlashPlugin } from './visual-flash.ts';
1
+ export { terminalBellPlugin } from "./terminal-bell.ts";
2
+ export { visualFlashPlugin } from "./visual-flash.ts";
@@ -1,53 +1,59 @@
1
1
  import type {
2
- NotificationPlugin,
3
2
  NotificationContext,
4
3
  NotificationEvent,
5
- } from '../types/notification.ts';
4
+ NotificationPlugin,
5
+ } from "../types/notification.ts";
6
6
 
7
7
  export const terminalBellPlugin: NotificationPlugin = {
8
8
  apiVersion: 2,
9
- id: 'terminal-bell',
10
- type: 'notification',
11
- name: 'Terminal Bell',
12
- version: '1.0.0',
9
+ id: "terminal-bell",
10
+ type: "notification",
11
+ name: "Terminal Bell",
12
+ version: "1.0.0",
13
13
 
14
14
  meta: {
15
- description: 'Simple terminal bell (BEL character) for alerts',
15
+ description: "Simple terminal bell (BEL character) for alerts",
16
16
  },
17
17
 
18
18
  permissions: {},
19
19
 
20
20
  configSchema: {
21
- enabled: { type: 'boolean', default: true, description: 'Enable terminal bell notifications' },
22
- minSeverity: { type: 'select', default: 'warning', options: [
23
- { value: 'info', label: 'Info' },
24
- { value: 'warning', label: 'Warning' },
25
- { value: 'critical', label: 'Critical' },
26
- ]},
21
+ enabled: { type: "boolean", default: true, description: "Enable terminal bell notifications" },
22
+ minSeverity: {
23
+ type: "select",
24
+ default: "warning",
25
+ options: [
26
+ { value: "info", label: "Info" },
27
+ { value: "warning", label: "Warning" },
28
+ { value: "critical", label: "Critical" },
29
+ ],
30
+ },
27
31
  },
28
32
 
29
33
  supports(event: NotificationEvent): boolean {
30
- return event.type.startsWith('budget.') ||
31
- event.type.startsWith('provider.') ||
32
- event.type === 'plugin.crashed';
34
+ return (
35
+ event.type.startsWith("budget.") ||
36
+ event.type.startsWith("provider.") ||
37
+ event.type === "plugin.crashed"
38
+ );
33
39
  },
34
40
 
35
41
  async initialize(ctx: NotificationContext): Promise<void> {
36
- ctx.logger.debug('Terminal bell notification plugin initialized');
42
+ ctx.logger.debug("Terminal bell notification plugin initialized");
37
43
  },
38
44
 
39
45
  async notify(ctx: NotificationContext, event: NotificationEvent): Promise<void> {
40
- const severityOrder = ['info', 'warning', 'critical'];
41
- const minSeverity = (ctx.config.minSeverity as string) ?? 'warning';
46
+ const severityOrder = ["info", "warning", "critical"];
47
+ const minSeverity = (ctx.config.minSeverity as string) ?? "warning";
42
48
 
43
49
  if (severityOrder.indexOf(event.severity) < severityOrder.indexOf(minSeverity)) {
44
50
  return;
45
51
  }
46
52
 
47
- const bellCount = event.severity === 'critical' ? 3 : 1;
53
+ const bellCount = event.severity === "critical" ? 3 : 1;
48
54
 
49
55
  for (let i = 0; i < bellCount; i++) {
50
- process.stdout.write('\x07');
56
+ process.stdout.write("\x07");
51
57
  if (i < bellCount - 1) {
52
58
  await sleep(200);
53
59
  }
@@ -55,8 +61,8 @@ export const terminalBellPlugin: NotificationPlugin = {
55
61
  },
56
62
 
57
63
  async test(ctx: NotificationContext): Promise<boolean> {
58
- ctx.logger.info('Testing terminal bell...');
59
- process.stdout.write('\x07');
64
+ ctx.logger.info("Testing terminal bell...");
65
+ process.stdout.write("\x07");
60
66
  return true;
61
67
  },
62
68
  };
@@ -1,53 +1,55 @@
1
1
  import type {
2
- NotificationPlugin,
3
2
  NotificationContext,
4
3
  NotificationEvent,
5
- } from '../types/notification.ts';
4
+ NotificationPlugin,
5
+ } from "../types/notification.ts";
6
6
 
7
7
  export const visualFlashPlugin: NotificationPlugin = {
8
8
  apiVersion: 2,
9
- id: 'visual-flash',
10
- type: 'notification',
11
- name: 'Visual Flash',
12
- version: '1.0.0',
9
+ id: "visual-flash",
10
+ type: "notification",
11
+ name: "Visual Flash",
12
+ version: "1.0.0",
13
13
 
14
14
  meta: {
15
- description: 'Visual screen flash using ANSI escape sequences',
15
+ description: "Visual screen flash using ANSI escape sequences",
16
16
  },
17
17
 
18
18
  permissions: {},
19
19
 
20
20
  configSchema: {
21
- enabled: { type: 'boolean', default: true, description: 'Enable visual flash notifications' },
22
- duration: { type: 'number', default: 100, description: 'Flash duration in milliseconds' },
21
+ enabled: { type: "boolean", default: true, description: "Enable visual flash notifications" },
22
+ duration: { type: "number", default: 100, description: "Flash duration in milliseconds" },
23
23
  },
24
24
 
25
25
  supports(event: NotificationEvent): boolean {
26
- return event.type.startsWith('budget.') ||
27
- event.type.startsWith('provider.') ||
28
- event.type === 'plugin.crashed';
26
+ return (
27
+ event.type.startsWith("budget.") ||
28
+ event.type.startsWith("provider.") ||
29
+ event.type === "plugin.crashed"
30
+ );
29
31
  },
30
32
 
31
33
  async initialize(ctx: NotificationContext): Promise<void> {
32
- ctx.logger.debug('Visual flash notification plugin initialized');
34
+ ctx.logger.debug("Visual flash notification plugin initialized");
33
35
  },
34
36
 
35
37
  async notify(ctx: NotificationContext, event: NotificationEvent): Promise<void> {
36
38
  const duration = (ctx.config.duration as number) ?? 100;
37
39
 
38
- const colorCode = event.severity === 'critical' ? '41' :
39
- event.severity === 'warning' ? '43' : '42';
40
+ const colorCode =
41
+ event.severity === "critical" ? "41" : event.severity === "warning" ? "43" : "42";
40
42
 
41
43
  process.stdout.write(`\x1b[${colorCode}m`);
42
44
  await sleep(duration);
43
- process.stdout.write('\x1b[0m');
45
+ process.stdout.write("\x1b[0m");
44
46
  },
45
47
 
46
48
  async test(ctx: NotificationContext): Promise<boolean> {
47
- ctx.logger.info('Testing visual flash...');
48
- process.stdout.write('\x1b[44m');
49
+ ctx.logger.info("Testing visual flash...");
50
+ process.stdout.write("\x1b[44m");
49
51
  await sleep(100);
50
- process.stdout.write('\x1b[0m');
52
+ process.stdout.write("\x1b[0m");
51
53
  return true;
52
54
  },
53
55
  };
@@ -1,10 +1,10 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import * as os from 'os';
1
+ import * as fs from "fs/promises";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
4
 
5
- const CACHE_DIR = path.join(os.homedir(), '.cache/tokentop');
6
- const PACKAGE_JSON_PATH = path.join(CACHE_DIR, 'package.json');
7
- const NODE_MODULES = path.join(CACHE_DIR, 'node_modules');
5
+ const CACHE_DIR = path.join(os.homedir(), ".cache/tokentop");
6
+ const PACKAGE_JSON_PATH = path.join(CACHE_DIR, "package.json");
7
+ const NODE_MODULES = path.join(CACHE_DIR, "node_modules");
8
8
 
9
9
  interface CachePackageJson {
10
10
  name: string;
@@ -19,7 +19,7 @@ async function ensureCacheDir(): Promise<void> {
19
19
  await fs.access(PACKAGE_JSON_PATH);
20
20
  } catch {
21
21
  const skeleton: CachePackageJson = {
22
- name: 'tokentop-plugin-cache',
22
+ name: "tokentop-plugin-cache",
23
23
  private: true,
24
24
  dependencies: {},
25
25
  };
@@ -28,20 +28,20 @@ async function ensureCacheDir(): Promise<void> {
28
28
  }
29
29
 
30
30
  function parsePackageSpec(spec: string): { name: string; version: string } {
31
- const atIndex = spec.lastIndexOf('@');
31
+ const atIndex = spec.lastIndexOf("@");
32
32
  if (atIndex > 0) {
33
33
  return { name: spec.slice(0, atIndex), version: spec.slice(atIndex + 1) };
34
34
  }
35
- return { name: spec, version: 'latest' };
35
+ return { name: spec, version: "latest" };
36
36
  }
37
37
 
38
38
  async function isInstalled(name: string, version: string): Promise<boolean> {
39
39
  try {
40
- const pkgJsonPath = path.join(NODE_MODULES, name, 'package.json');
41
- const raw = await fs.readFile(pkgJsonPath, 'utf-8');
40
+ const pkgJsonPath = path.join(NODE_MODULES, name, "package.json");
41
+ const raw = await fs.readFile(pkgJsonPath, "utf-8");
42
42
  const pkg = JSON.parse(raw) as { version?: string };
43
43
 
44
- if (version === 'latest') {
44
+ if (version === "latest") {
45
45
  return !!pkg.version;
46
46
  }
47
47
  return pkg.version === version;
@@ -68,13 +68,13 @@ export async function installNpmPlugin(spec: string): Promise<InstallResult> {
68
68
  return { name, version, installed: false, resolvedPath };
69
69
  }
70
70
 
71
- const versionArg = version === 'latest' ? name : `${name}@${version}`;
71
+ const versionArg = version === "latest" ? name : `${name}@${version}`;
72
72
 
73
73
  try {
74
- const proc = Bun.spawn(
75
- ['bun', 'add', '--force', '--exact', '--cwd', CACHE_DIR, versionArg],
76
- { stdout: 'pipe', stderr: 'pipe' },
77
- );
74
+ const proc = Bun.spawn(["bun", "add", "--force", "--exact", "--cwd", CACHE_DIR, versionArg], {
75
+ stdout: "pipe",
76
+ stderr: "pipe",
77
+ });
78
78
 
79
79
  const exitCode = await proc.exited;
80
80
 
@@ -101,9 +101,7 @@ export async function installNpmPlugin(spec: string): Promise<InstallResult> {
101
101
  }
102
102
  }
103
103
 
104
- export async function installAllNpmPlugins(
105
- specs: string[],
106
- ): Promise<InstallResult[]> {
104
+ export async function installAllNpmPlugins(specs: string[]): Promise<InstallResult[]> {
107
105
  if (specs.length === 0) return [];
108
106
 
109
107
  await ensureCacheDir();
@@ -118,7 +116,7 @@ export async function installAllNpmPlugins(
118
116
  if (await isInstalled(name, version)) {
119
117
  results.push({ name, version, installed: false, resolvedPath });
120
118
  } else {
121
- toInstall.push(version === 'latest' ? name : `${name}@${version}`);
119
+ toInstall.push(version === "latest" ? name : `${name}@${version}`);
122
120
  results.push({ name, version, installed: true, resolvedPath });
123
121
  }
124
122
  }
@@ -126,10 +124,10 @@ export async function installAllNpmPlugins(
126
124
  if (toInstall.length === 0) return results;
127
125
 
128
126
  try {
129
- const proc = Bun.spawn(
130
- ['bun', 'add', '--force', '--exact', '--cwd', CACHE_DIR, ...toInstall],
131
- { stdout: 'pipe', stderr: 'pipe' },
132
- );
127
+ const proc = Bun.spawn(["bun", "add", "--force", "--exact", "--cwd", CACHE_DIR, ...toInstall], {
128
+ stdout: "pipe",
129
+ stderr: "pipe",
130
+ });
133
131
 
134
132
  const exitCode = await proc.exited;
135
133