@tokentop/ttop 0.2.2 → 0.4.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 +42 -38
- 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 +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -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 +68 -57
- package/src/plugins/registry.ts +80 -71
- 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/persistence-service.ts +206 -0
- 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/retention.ts +146 -0
- 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 +314 -183
- 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 +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- 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 +164 -151
- 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 +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- 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 +7 -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 +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- 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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as
|
|
2
|
-
export { default as
|
|
1
|
+
export { default as claudeCodeAgentPlugin } from "@tokentop/agent-claude-code";
|
|
2
|
+
export { default as opencodeAgentPlugin } from "@tokentop/agent-opencode";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import type { PluginPermissions } from
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import type { PluginPermissions } from "./types/base.ts";
|
|
5
|
+
import { PluginPermissionError } from "./types/base.ts";
|
|
6
|
+
import type { AuthSources, OpenCodeAuthEntry } from "./types/provider.ts";
|
|
7
7
|
|
|
8
|
-
const OPENCODE_AUTH_PATH = path.join(os.homedir(),
|
|
9
|
-
const OPENCODE_CONFIG_PATH = path.join(os.homedir(),
|
|
8
|
+
const OPENCODE_AUTH_PATH = path.join(os.homedir(), ".local/share/opencode/auth.json");
|
|
9
|
+
const OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".config/opencode/opencode.json");
|
|
10
10
|
|
|
11
11
|
function expandHome(filePath: string): string {
|
|
12
|
-
if (filePath.startsWith(
|
|
12
|
+
if (filePath.startsWith("~")) {
|
|
13
13
|
return path.join(os.homedir(), filePath.slice(1));
|
|
14
14
|
}
|
|
15
15
|
return filePath;
|
|
@@ -35,7 +35,7 @@ function isEnvVarAllowed(name: string, permissions: PluginPermissions): boolean
|
|
|
35
35
|
|
|
36
36
|
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
37
37
|
try {
|
|
38
|
-
const content = await fs.readFile(filePath,
|
|
38
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
39
39
|
return JSON.parse(content) as T;
|
|
40
40
|
} catch {
|
|
41
41
|
return null;
|
|
@@ -43,14 +43,17 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface OpenCodeConfig {
|
|
46
|
-
provider?: Record<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
provider?: Record<
|
|
47
|
+
string,
|
|
48
|
+
{
|
|
49
|
+
name?: string;
|
|
50
|
+
key?: string;
|
|
51
|
+
options?: {
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
>;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function resolveEnvValue(value: string | undefined): string | undefined {
|
|
@@ -69,8 +72,8 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
69
72
|
if (!isEnvVarAllowed(name, permissions)) {
|
|
70
73
|
throw new PluginPermissionError(
|
|
71
74
|
pluginId,
|
|
72
|
-
|
|
73
|
-
`Environment variable "${name}" not in allowlist
|
|
75
|
+
"env",
|
|
76
|
+
`Environment variable "${name}" not in allowlist`,
|
|
74
77
|
);
|
|
75
78
|
}
|
|
76
79
|
return process.env[name];
|
|
@@ -83,19 +86,19 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
83
86
|
if (!permissions.filesystem?.read) {
|
|
84
87
|
throw new PluginPermissionError(
|
|
85
88
|
pluginId,
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
"filesystem",
|
|
90
|
+
"Filesystem read access not permitted",
|
|
88
91
|
);
|
|
89
92
|
}
|
|
90
93
|
if (!isPathAllowed(filePath, permissions)) {
|
|
91
94
|
throw new PluginPermissionError(
|
|
92
95
|
pluginId,
|
|
93
|
-
|
|
94
|
-
`Path "${filePath}" not in allowlist
|
|
96
|
+
"filesystem",
|
|
97
|
+
`Path "${filePath}" not in allowlist`,
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
try {
|
|
98
|
-
return await fs.readFile(resolved,
|
|
101
|
+
return await fs.readFile(resolved, "utf-8");
|
|
99
102
|
} catch {
|
|
100
103
|
return null;
|
|
101
104
|
}
|
|
@@ -106,19 +109,19 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
106
109
|
if (!permissions.filesystem?.read) {
|
|
107
110
|
throw new PluginPermissionError(
|
|
108
111
|
pluginId,
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
"filesystem",
|
|
113
|
+
"Filesystem read access not permitted",
|
|
111
114
|
);
|
|
112
115
|
}
|
|
113
116
|
if (!isPathAllowed(filePath, permissions)) {
|
|
114
117
|
throw new PluginPermissionError(
|
|
115
118
|
pluginId,
|
|
116
|
-
|
|
117
|
-
`Path "${filePath}" not in allowlist
|
|
119
|
+
"filesystem",
|
|
120
|
+
`Path "${filePath}" not in allowlist`,
|
|
118
121
|
);
|
|
119
122
|
}
|
|
120
123
|
try {
|
|
121
|
-
const content = await fs.readFile(resolved,
|
|
124
|
+
const content = await fs.readFile(resolved, "utf-8");
|
|
122
125
|
return JSON.parse(content) as T;
|
|
123
126
|
} catch {
|
|
124
127
|
return null;
|
|
@@ -130,15 +133,15 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
130
133
|
if (!permissions.filesystem?.read) {
|
|
131
134
|
throw new PluginPermissionError(
|
|
132
135
|
pluginId,
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
"filesystem",
|
|
137
|
+
"Filesystem read access not permitted",
|
|
135
138
|
);
|
|
136
139
|
}
|
|
137
140
|
if (!isPathAllowed(filePath, permissions)) {
|
|
138
141
|
throw new PluginPermissionError(
|
|
139
142
|
pluginId,
|
|
140
|
-
|
|
141
|
-
`Path "${filePath}" not in allowlist
|
|
143
|
+
"filesystem",
|
|
144
|
+
`Path "${filePath}" not in allowlist`,
|
|
142
145
|
);
|
|
143
146
|
}
|
|
144
147
|
try {
|
|
@@ -162,11 +165,12 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
162
165
|
if (config?.provider) {
|
|
163
166
|
const providerConfig = config.provider[key];
|
|
164
167
|
if (providerConfig) {
|
|
165
|
-
const apiKey =
|
|
166
|
-
|
|
168
|
+
const apiKey =
|
|
169
|
+
resolveEnvValue(providerConfig.key) ??
|
|
170
|
+
resolveEnvValue(providerConfig.options?.apiKey as string);
|
|
167
171
|
if (apiKey) {
|
|
168
172
|
return {
|
|
169
|
-
type:
|
|
173
|
+
type: "api",
|
|
170
174
|
key: apiKey,
|
|
171
175
|
};
|
|
172
176
|
}
|
|
@@ -178,7 +182,7 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
|
|
|
178
182
|
},
|
|
179
183
|
|
|
180
184
|
platform: {
|
|
181
|
-
os: process.platform as
|
|
185
|
+
os: process.platform as "darwin" | "linux" | "win32",
|
|
182
186
|
homedir: os.homedir(),
|
|
183
187
|
arch: process.arch,
|
|
184
188
|
},
|
package/src/plugins/lifecycle.ts
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
* with safeInvoke for error isolation and circuit breaker protection.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import { pluginRegistry } from
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
8
|
+
import { safeInvoke } from "./plugin-host.ts";
|
|
9
|
+
import { pluginRegistry } from "./registry.ts";
|
|
10
|
+
import { createPluginLogger } from "./sandbox.ts";
|
|
11
|
+
import { runInPluginGuard } from "./sandbox-guard.ts";
|
|
12
|
+
import type { BasePlugin, PluginLifecycleContext, PluginLogger } from "./types/base.ts";
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// State machine
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
18
|
export type PluginLifecycleState =
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
19
|
+
| "loaded"
|
|
20
|
+
| "initialized"
|
|
21
|
+
| "started"
|
|
22
|
+
| "stopped"
|
|
23
|
+
| "destroyed"
|
|
24
|
+
| "failed";
|
|
25
25
|
|
|
26
26
|
interface PluginLifecycleEntry {
|
|
27
27
|
pluginId: string;
|
|
@@ -44,7 +44,7 @@ class PluginLifecycleManager {
|
|
|
44
44
|
if (!entry) {
|
|
45
45
|
entry = {
|
|
46
46
|
pluginId: plugin.id,
|
|
47
|
-
state:
|
|
47
|
+
state: "loaded",
|
|
48
48
|
config: { ...(plugin.defaultConfig ?? {}) },
|
|
49
49
|
logger: createPluginLogger(plugin.id),
|
|
50
50
|
};
|
|
@@ -59,7 +59,7 @@ class PluginLifecycleManager {
|
|
|
59
59
|
|
|
60
60
|
private async invokeHook(
|
|
61
61
|
plugin: BasePlugin,
|
|
62
|
-
hook:
|
|
62
|
+
hook: "initialize" | "start" | "stop" | "destroy",
|
|
63
63
|
): Promise<boolean> {
|
|
64
64
|
const fn = plugin[hook];
|
|
65
65
|
if (!fn) return true;
|
|
@@ -72,7 +72,10 @@ class PluginLifecycleManager {
|
|
|
72
72
|
Promise.race([
|
|
73
73
|
fn.call(plugin, ctx),
|
|
74
74
|
new Promise<never>((_, reject) =>
|
|
75
|
-
setTimeout(
|
|
75
|
+
setTimeout(
|
|
76
|
+
() => reject(new Error(`${hook} timed out after ${HOOK_TIMEOUT_MS}ms`)),
|
|
77
|
+
HOOK_TIMEOUT_MS,
|
|
78
|
+
),
|
|
76
79
|
),
|
|
77
80
|
]),
|
|
78
81
|
),
|
|
@@ -88,25 +91,29 @@ class PluginLifecycleManager {
|
|
|
88
91
|
async initializeAll(): Promise<void> {
|
|
89
92
|
const plugins = pluginRegistry.getAllPlugins();
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
await Promise.all(
|
|
95
|
+
plugins.map(async (plugin) => {
|
|
96
|
+
const entry = this.getOrCreate(plugin);
|
|
97
|
+
if (entry.state !== "loaded") return;
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
const ok = await this.invokeHook(plugin, "initialize");
|
|
100
|
+
entry.state = ok ? "initialized" : "failed";
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
async startAll(): Promise<void> {
|
|
101
106
|
const plugins = pluginRegistry.getAllPlugins();
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
await Promise.all(
|
|
109
|
+
plugins.map(async (plugin) => {
|
|
110
|
+
const entry = this.getOrCreate(plugin);
|
|
111
|
+
if (entry.state !== "initialized") return;
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
const ok = await this.invokeHook(plugin, "start");
|
|
114
|
+
entry.state = ok ? "started" : "failed";
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
async stopAll(): Promise<void> {
|
|
@@ -114,10 +121,10 @@ class PluginLifecycleManager {
|
|
|
114
121
|
|
|
115
122
|
for (const plugin of plugins) {
|
|
116
123
|
const entry = this.entries.get(plugin.id);
|
|
117
|
-
if (!entry || entry.state !==
|
|
124
|
+
if (!entry || entry.state !== "started") continue;
|
|
118
125
|
|
|
119
|
-
const ok = await this.invokeHook(plugin,
|
|
120
|
-
entry.state = ok ?
|
|
126
|
+
const ok = await this.invokeHook(plugin, "stop");
|
|
127
|
+
entry.state = ok ? "stopped" : "failed";
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -127,27 +134,24 @@ class PluginLifecycleManager {
|
|
|
127
134
|
for (const plugin of plugins) {
|
|
128
135
|
const entry = this.entries.get(plugin.id);
|
|
129
136
|
if (!entry) continue;
|
|
130
|
-
if (entry.state ===
|
|
137
|
+
if (entry.state === "destroyed") continue;
|
|
131
138
|
|
|
132
|
-
await this.invokeHook(plugin,
|
|
133
|
-
entry.state =
|
|
139
|
+
await this.invokeHook(plugin, "destroy");
|
|
140
|
+
entry.state = "destroyed";
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
async notifyConfigChange(
|
|
138
|
-
pluginId: string,
|
|
139
|
-
newConfig: Record<string, unknown>,
|
|
140
|
-
): Promise<void> {
|
|
144
|
+
async notifyConfigChange(pluginId: string, newConfig: Record<string, unknown>): Promise<void> {
|
|
141
145
|
const plugin = pluginRegistry.getAllPlugins().find((p) => p.id === pluginId);
|
|
142
146
|
if (!plugin?.onConfigChange) return;
|
|
143
147
|
|
|
144
148
|
const entry = this.entries.get(pluginId);
|
|
145
|
-
if (!entry || entry.state ===
|
|
149
|
+
if (!entry || entry.state === "destroyed" || entry.state === "failed") return;
|
|
146
150
|
|
|
147
151
|
entry.config = { ...newConfig };
|
|
148
152
|
|
|
149
153
|
const ctx = this.makeCtx(entry);
|
|
150
|
-
await safeInvoke(pluginId,
|
|
154
|
+
await safeInvoke(pluginId, "onConfigChange", () =>
|
|
151
155
|
runInPluginGuard(pluginId, plugin.permissions, () =>
|
|
152
156
|
Promise.resolve(plugin.onConfigChange!(newConfig, ctx)),
|
|
153
157
|
),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { describe, expect, test } from
|
|
2
|
-
import { validatePlugin } from
|
|
3
|
-
import { CURRENT_API_VERSION } from
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { validatePlugin } from "./loader.ts";
|
|
3
|
+
import { CURRENT_API_VERSION } from "./types/base.ts";
|
|
4
4
|
|
|
5
5
|
const validPlugin = {
|
|
6
|
-
id:
|
|
7
|
-
type:
|
|
8
|
-
name:
|
|
9
|
-
version:
|
|
6
|
+
id: "test-plugin",
|
|
7
|
+
type: "notification",
|
|
8
|
+
name: "Test Plugin",
|
|
9
|
+
version: "1.0.0",
|
|
10
10
|
apiVersion: CURRENT_API_VERSION,
|
|
11
11
|
permissions: {},
|
|
12
12
|
initialize: async () => {},
|
|
@@ -17,18 +17,18 @@ function hasError(errors: string[], fragment: string): boolean {
|
|
|
17
17
|
return errors.some((error) => error.includes(fragment));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
describe(
|
|
21
|
-
test(
|
|
20
|
+
describe("validatePlugin", () => {
|
|
21
|
+
test("accepts a valid minimal plugin object", async () => {
|
|
22
22
|
const result = await validatePlugin(validPlugin);
|
|
23
23
|
|
|
24
24
|
expect(result.valid).toBe(true);
|
|
25
25
|
expect(result.errors).toHaveLength(0);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test(
|
|
28
|
+
test("rejects plugin missing id", async () => {
|
|
29
29
|
const plugin = {
|
|
30
30
|
...validPlugin,
|
|
31
|
-
id:
|
|
31
|
+
id: "",
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const result = await validatePlugin(plugin);
|
|
@@ -36,46 +36,46 @@ describe('validatePlugin', () => {
|
|
|
36
36
|
expect(hasError(result.errors, 'non-empty string "id"')).toBe(true);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test(
|
|
39
|
+
test("rejects plugin with invalid id format", async () => {
|
|
40
40
|
const plugin = {
|
|
41
41
|
...validPlugin,
|
|
42
|
-
id:
|
|
42
|
+
id: "Bad Plugin",
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
const result = await validatePlugin(plugin);
|
|
46
46
|
expect(result.valid).toBe(false);
|
|
47
|
-
expect(hasError(result.errors,
|
|
47
|
+
expect(hasError(result.errors, "kebab-case")).toBe(true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
test(
|
|
50
|
+
test("rejects plugin missing type", async () => {
|
|
51
51
|
const plugin = {
|
|
52
|
-
id:
|
|
53
|
-
name:
|
|
54
|
-
version:
|
|
52
|
+
id: "test-plugin",
|
|
53
|
+
name: "Test Plugin",
|
|
54
|
+
version: "1.0.0",
|
|
55
55
|
apiVersion: CURRENT_API_VERSION,
|
|
56
56
|
permissions: {},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const result = await validatePlugin(plugin);
|
|
60
60
|
expect(result.valid).toBe(false);
|
|
61
|
-
expect(hasError(result.errors,
|
|
61
|
+
expect(hasError(result.errors, "must be one of")).toBe(true);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test(
|
|
64
|
+
test("rejects plugin with invalid type", async () => {
|
|
65
65
|
const plugin = {
|
|
66
66
|
...validPlugin,
|
|
67
|
-
type:
|
|
67
|
+
type: "invalid",
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
const result = await validatePlugin(plugin);
|
|
71
71
|
expect(result.valid).toBe(false);
|
|
72
|
-
expect(hasError(result.errors,
|
|
72
|
+
expect(hasError(result.errors, "provider, agent, theme, notification")).toBe(true);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test(
|
|
75
|
+
test("rejects plugin missing name", async () => {
|
|
76
76
|
const plugin = {
|
|
77
77
|
...validPlugin,
|
|
78
|
-
name:
|
|
78
|
+
name: "",
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
const result = await validatePlugin(plugin);
|
|
@@ -83,10 +83,10 @@ describe('validatePlugin', () => {
|
|
|
83
83
|
expect(hasError(result.errors, 'non-empty string "name"')).toBe(true);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test(
|
|
86
|
+
test("rejects plugin missing version", async () => {
|
|
87
87
|
const plugin = {
|
|
88
88
|
...validPlugin,
|
|
89
|
-
version:
|
|
89
|
+
version: "",
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
const result = await validatePlugin(plugin);
|
|
@@ -94,10 +94,10 @@ describe('validatePlugin', () => {
|
|
|
94
94
|
expect(hasError(result.errors, 'valid semver "version"')).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
test(
|
|
97
|
+
test("rejects plugin with invalid version format", async () => {
|
|
98
98
|
const plugin = {
|
|
99
99
|
...validPlugin,
|
|
100
|
-
version:
|
|
100
|
+
version: "v1",
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const result = await validatePlugin(plugin);
|
|
@@ -105,12 +105,12 @@ describe('validatePlugin', () => {
|
|
|
105
105
|
expect(hasError(result.errors, 'valid semver "version"')).toBe(true);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test(
|
|
108
|
+
test("rejects plugin missing permissions", async () => {
|
|
109
109
|
const plugin = {
|
|
110
|
-
id:
|
|
111
|
-
type:
|
|
112
|
-
name:
|
|
113
|
-
version:
|
|
110
|
+
id: "test-plugin",
|
|
111
|
+
type: "notification",
|
|
112
|
+
name: "Test Plugin",
|
|
113
|
+
version: "1.0.0",
|
|
114
114
|
apiVersion: CURRENT_API_VERSION,
|
|
115
115
|
};
|
|
116
116
|
|
|
@@ -119,7 +119,7 @@ describe('validatePlugin', () => {
|
|
|
119
119
|
expect(hasError(result.errors, 'declare "permissions" object')).toBe(true);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
test(
|
|
122
|
+
test("rejects plugin with wrong apiVersion", async () => {
|
|
123
123
|
const plugin = {
|
|
124
124
|
...validPlugin,
|
|
125
125
|
apiVersion: CURRENT_API_VERSION + 1,
|
|
@@ -130,10 +130,10 @@ describe('validatePlugin', () => {
|
|
|
130
130
|
expect(hasError(result.errors, `expected ${CURRENT_API_VERSION}`)).toBe(true);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
test(
|
|
133
|
+
test("rejects provider plugin missing auth", async () => {
|
|
134
134
|
const providerPlugin = {
|
|
135
135
|
...validPlugin,
|
|
136
|
-
type:
|
|
136
|
+
type: "provider",
|
|
137
137
|
fetchUsage: async () => [],
|
|
138
138
|
};
|
|
139
139
|
|
|
@@ -142,10 +142,10 @@ describe('validatePlugin', () => {
|
|
|
142
142
|
expect(hasError(result.errors, 'declare "auth" object')).toBe(true);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
test(
|
|
145
|
+
test("accepts valid provider plugin with auth and fetchUsage", async () => {
|
|
146
146
|
const providerPlugin = {
|
|
147
147
|
...validPlugin,
|
|
148
|
-
type:
|
|
148
|
+
type: "provider",
|
|
149
149
|
auth: {
|
|
150
150
|
discover: async () => undefined,
|
|
151
151
|
isConfigured: () => true,
|
|
@@ -157,11 +157,11 @@ describe('validatePlugin', () => {
|
|
|
157
157
|
expect(result.valid).toBe(true);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test(
|
|
160
|
+
test("rejects theme plugin missing colors object", async () => {
|
|
161
161
|
const themePlugin = {
|
|
162
162
|
...validPlugin,
|
|
163
|
-
type:
|
|
164
|
-
colorScheme:
|
|
163
|
+
type: "theme",
|
|
164
|
+
colorScheme: "dark",
|
|
165
165
|
};
|
|
166
166
|
|
|
167
167
|
const result = await validatePlugin(themePlugin);
|
|
@@ -169,24 +169,24 @@ describe('validatePlugin', () => {
|
|
|
169
169
|
expect(hasError(result.errors, '"colors" object')).toBe(true);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
test(
|
|
172
|
+
test("rejects theme plugin missing colorScheme", async () => {
|
|
173
173
|
const themePlugin = {
|
|
174
174
|
...validPlugin,
|
|
175
|
-
type:
|
|
176
|
-
colors: { primary:
|
|
175
|
+
type: "theme",
|
|
176
|
+
colors: { primary: "#fff" },
|
|
177
177
|
};
|
|
178
178
|
|
|
179
179
|
const result = await validatePlugin(themePlugin);
|
|
180
180
|
expect(result.valid).toBe(false);
|
|
181
|
-
expect(hasError(result.errors,
|
|
181
|
+
expect(hasError(result.errors, "colorScheme")).toBe(true);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test(
|
|
184
|
+
test("accepts valid theme plugin", async () => {
|
|
185
185
|
const themePlugin = {
|
|
186
186
|
...validPlugin,
|
|
187
|
-
type:
|
|
188
|
-
colorScheme:
|
|
189
|
-
colors: { primary:
|
|
187
|
+
type: "theme",
|
|
188
|
+
colorScheme: "dark",
|
|
189
|
+
colors: { primary: "#fff" },
|
|
190
190
|
};
|
|
191
191
|
|
|
192
192
|
const result = await validatePlugin(themePlugin);
|