@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
package/src/plugins/loader.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
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
|
-
|
|
7
|
+
AnyPlugin,
|
|
6
8
|
PluginLoadResult,
|
|
9
|
+
PluginType,
|
|
7
10
|
PluginValidationResult,
|
|
8
|
-
|
|
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 !==
|
|
20
|
-
errors.push(
|
|
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 !==
|
|
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(
|
|
29
|
+
errors.push("Plugin id must be kebab-case (lowercase letters, numbers, hyphens)");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const validTypes: PluginType[] = [
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
51
|
-
errors.push(
|
|
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 ===
|
|
58
|
-
if (!p.auth || typeof p.auth !==
|
|
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 !==
|
|
64
|
+
if (typeof auth.discover !== "function") {
|
|
63
65
|
errors.push('Provider plugin auth must implement "discover" method');
|
|
64
66
|
}
|
|
65
|
-
if (typeof auth.isConfigured !==
|
|
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 !==
|
|
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 ===
|
|
75
|
-
if (typeof p.isInstalled !==
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
90
|
-
if (![
|
|
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 !==
|
|
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 ===
|
|
99
|
-
if (typeof p.initialize !==
|
|
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 !==
|
|
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(
|
|
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,
|
|
120
|
-
const pkgContent = await fs.readFile(pkgJsonPath,
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
131
|
-
if (root && typeof root ===
|
|
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 ===
|
|
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 = [
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
200
|
+
source: "local",
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
export async function loadNpmPlugin(
|
|
206
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
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:
|
|
46
|
-
severity:
|
|
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 (
|
|
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:
|
|
58
|
-
severity: primaryPercent >= 95 ?
|
|
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:
|
|
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:
|
|
81
|
-
severity:
|
|
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:
|
|
90
|
-
severity:
|
|
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,
|
|
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
|
|
2
|
-
export { visualFlashPlugin } from
|
|
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
|
-
|
|
4
|
+
NotificationPlugin,
|
|
5
|
+
} from "../types/notification.ts";
|
|
6
6
|
|
|
7
7
|
export const terminalBellPlugin: NotificationPlugin = {
|
|
8
8
|
apiVersion: 2,
|
|
9
|
-
id:
|
|
10
|
-
type:
|
|
11
|
-
name:
|
|
12
|
-
version:
|
|
9
|
+
id: "terminal-bell",
|
|
10
|
+
type: "notification",
|
|
11
|
+
name: "Terminal Bell",
|
|
12
|
+
version: "1.0.0",
|
|
13
13
|
|
|
14
14
|
meta: {
|
|
15
|
-
description:
|
|
15
|
+
description: "Simple terminal bell (BEL character) for alerts",
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
permissions: {},
|
|
19
19
|
|
|
20
20
|
configSchema: {
|
|
21
|
-
enabled: { type:
|
|
22
|
-
minSeverity: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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 = [
|
|
41
|
-
const minSeverity = (ctx.config.minSeverity as string) ??
|
|
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 ===
|
|
53
|
+
const bellCount = event.severity === "critical" ? 3 : 1;
|
|
48
54
|
|
|
49
55
|
for (let i = 0; i < bellCount; i++) {
|
|
50
|
-
process.stdout.write(
|
|
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(
|
|
59
|
-
process.stdout.write(
|
|
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
|
-
|
|
4
|
+
NotificationPlugin,
|
|
5
|
+
} from "../types/notification.ts";
|
|
6
6
|
|
|
7
7
|
export const visualFlashPlugin: NotificationPlugin = {
|
|
8
8
|
apiVersion: 2,
|
|
9
|
-
id:
|
|
10
|
-
type:
|
|
11
|
-
name:
|
|
12
|
-
version:
|
|
9
|
+
id: "visual-flash",
|
|
10
|
+
type: "notification",
|
|
11
|
+
name: "Visual Flash",
|
|
12
|
+
version: "1.0.0",
|
|
13
13
|
|
|
14
14
|
meta: {
|
|
15
|
-
description:
|
|
15
|
+
description: "Visual screen flash using ANSI escape sequences",
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
permissions: {},
|
|
19
19
|
|
|
20
20
|
configSchema: {
|
|
21
|
-
enabled: { type:
|
|
22
|
-
duration: { type:
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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 =
|
|
39
|
-
|
|
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(
|
|
45
|
+
process.stdout.write("\x1b[0m");
|
|
44
46
|
},
|
|
45
47
|
|
|
46
48
|
async test(ctx: NotificationContext): Promise<boolean> {
|
|
47
|
-
ctx.logger.info(
|
|
48
|
-
process.stdout.write(
|
|
49
|
+
ctx.logger.info("Testing visual flash...");
|
|
50
|
+
process.stdout.write("\x1b[44m");
|
|
49
51
|
await sleep(100);
|
|
50
|
-
process.stdout.write(
|
|
52
|
+
process.stdout.write("\x1b[0m");
|
|
51
53
|
return true;
|
|
52
54
|
},
|
|
53
55
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
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(),
|
|
6
|
-
const PACKAGE_JSON_PATH = path.join(CACHE_DIR,
|
|
7
|
-
const NODE_MODULES = path.join(CACHE_DIR,
|
|
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:
|
|
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:
|
|
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,
|
|
41
|
-
const raw = await fs.readFile(pkgJsonPath,
|
|
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 ===
|
|
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 ===
|
|
71
|
+
const versionArg = version === "latest" ? name : `${name}@${version}`;
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
const proc = Bun.spawn(
|
|
75
|
-
|
|
76
|
-
|
|
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 ===
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|