@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
|
@@ -1,48 +1,48 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ProviderPlugin,
|
|
3
|
-
ProviderFetchContext,
|
|
4
|
-
ProviderUsageData,
|
|
5
|
-
ProviderAuth,
|
|
6
|
-
PluginContext,
|
|
7
2
|
CredentialResult,
|
|
8
3
|
Credentials,
|
|
9
|
-
|
|
4
|
+
PluginContext,
|
|
5
|
+
ProviderAuth,
|
|
6
|
+
ProviderFetchContext,
|
|
7
|
+
ProviderPlugin,
|
|
8
|
+
ProviderUsageData,
|
|
9
|
+
} from "../types/provider.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Perplexity Shadow API Discovery (January 2026):
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
14
|
* Internal endpoints that power the billing dashboard (require session cookies, not API key):
|
|
15
15
|
* - GET /rest/pplx-api/v2/groups → { orgs: [{ api_org_id, display_name, usage_tier }] }
|
|
16
16
|
* - GET /rest/pplx-api/v2/groups/{orgId} → { customerInfo: { balance, auto_top_up_amount, ... } }
|
|
17
17
|
* - GET /rest/pplx-api/v2/groups/{orgId}/usage-analytics?time_bucket=day&time_range=past_month
|
|
18
18
|
* → Array of meter data (api_requests, input_tokens, output_tokens, etc.) with costs
|
|
19
19
|
* - GET /rest/pplx-api/v2/groups/{orgId}/invoices → Invoice history
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
21
|
* Unfortunately, these endpoints use web session auth, not API key auth.
|
|
22
22
|
* GitHub issue #266 requests a public usage API endpoint.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
export const perplexityPlugin: ProviderPlugin = {
|
|
26
26
|
apiVersion: 2,
|
|
27
|
-
id:
|
|
28
|
-
type:
|
|
29
|
-
name:
|
|
30
|
-
version:
|
|
27
|
+
id: "perplexity",
|
|
28
|
+
type: "provider",
|
|
29
|
+
name: "Perplexity",
|
|
30
|
+
version: "1.0.0",
|
|
31
31
|
|
|
32
32
|
meta: {
|
|
33
|
-
description:
|
|
34
|
-
homepage:
|
|
35
|
-
brandColor:
|
|
33
|
+
description: "Perplexity AI API usage (credit-based)",
|
|
34
|
+
homepage: "https://www.perplexity.ai",
|
|
35
|
+
brandColor: "#20b2aa",
|
|
36
36
|
},
|
|
37
37
|
|
|
38
38
|
permissions: {
|
|
39
39
|
network: {
|
|
40
40
|
enabled: true,
|
|
41
|
-
allowedDomains: [
|
|
41
|
+
allowedDomains: ["api.perplexity.ai"],
|
|
42
42
|
},
|
|
43
43
|
env: {
|
|
44
44
|
read: true,
|
|
45
|
-
vars: [
|
|
45
|
+
vars: ["PERPLEXITY_API_KEY"],
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
|
|
@@ -56,23 +56,30 @@ export const perplexityPlugin: ProviderPlugin = {
|
|
|
56
56
|
auth: {
|
|
57
57
|
async discover(ctx: PluginContext): Promise<CredentialResult> {
|
|
58
58
|
// 1. Try OpenCode auth first (wellknown type, has token or key)
|
|
59
|
-
const entry = await ctx.authSources.opencode.getProviderEntry(
|
|
59
|
+
const entry = await ctx.authSources.opencode.getProviderEntry("perplexity");
|
|
60
60
|
if (entry) {
|
|
61
|
-
if (entry.type ===
|
|
62
|
-
return { ok: true, credentials: { apiKey: entry.key, source:
|
|
61
|
+
if (entry.type === "api" && entry.key) {
|
|
62
|
+
return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
|
|
63
63
|
}
|
|
64
|
-
if (entry.type ===
|
|
65
|
-
return {
|
|
64
|
+
if (entry.type === "wellknown" && (entry.token || entry.key)) {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
credentials: { apiKey: (entry.token || entry.key)!, source: "opencode" },
|
|
68
|
+
};
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
// 2. Try env vars
|
|
70
|
-
const apiKey = ctx.authSources.env.get(
|
|
73
|
+
const apiKey = ctx.authSources.env.get("PERPLEXITY_API_KEY");
|
|
71
74
|
if (apiKey) {
|
|
72
|
-
return { ok: true, credentials: { apiKey, source:
|
|
75
|
+
return { ok: true, credentials: { apiKey, source: "env" } };
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: "missing",
|
|
81
|
+
message: "No Perplexity API key found. Set PERPLEXITY_API_KEY or configure in OpenCode.",
|
|
82
|
+
};
|
|
76
83
|
},
|
|
77
84
|
|
|
78
85
|
isConfigured(credentials: Credentials): boolean {
|
|
@@ -86,22 +93,22 @@ export const perplexityPlugin: ProviderPlugin = {
|
|
|
86
93
|
if (!credentials.apiKey) {
|
|
87
94
|
return {
|
|
88
95
|
fetchedAt: Date.now(),
|
|
89
|
-
error:
|
|
96
|
+
error: "API key required. Set PERPLEXITY_API_KEY environment variable.",
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
log.info(
|
|
100
|
+
log.info("Checking Perplexity API", { keyPrefix: credentials.apiKey.slice(0, 8) });
|
|
94
101
|
|
|
95
102
|
try {
|
|
96
|
-
const response = await http.fetch(
|
|
97
|
-
method:
|
|
103
|
+
const response = await http.fetch("https://api.perplexity.ai/chat/completions", {
|
|
104
|
+
method: "POST",
|
|
98
105
|
headers: {
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
Authorization: `Bearer ${credentials.apiKey}`,
|
|
107
|
+
"Content-Type": "application/json",
|
|
101
108
|
},
|
|
102
109
|
body: JSON.stringify({
|
|
103
|
-
model:
|
|
104
|
-
messages: [{ role:
|
|
110
|
+
model: "sonar",
|
|
111
|
+
messages: [{ role: "user", content: "ping" }],
|
|
105
112
|
max_tokens: 1,
|
|
106
113
|
}),
|
|
107
114
|
});
|
|
@@ -109,39 +116,41 @@ export const perplexityPlugin: ProviderPlugin = {
|
|
|
109
116
|
if (response.status === 401) {
|
|
110
117
|
return {
|
|
111
118
|
fetchedAt: Date.now(),
|
|
112
|
-
error:
|
|
119
|
+
error: "Invalid API key",
|
|
113
120
|
};
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
if (response.status === 402) {
|
|
117
124
|
return {
|
|
118
|
-
planType:
|
|
125
|
+
planType: "API",
|
|
119
126
|
allowed: false,
|
|
120
127
|
fetchedAt: Date.now(),
|
|
121
128
|
credits: {
|
|
122
129
|
hasCredits: false,
|
|
123
130
|
unlimited: false,
|
|
124
|
-
balance:
|
|
131
|
+
balance: "$0.00",
|
|
125
132
|
},
|
|
126
|
-
error:
|
|
133
|
+
error: "Insufficient credits",
|
|
127
134
|
};
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
if (response.status === 429) {
|
|
131
138
|
return {
|
|
132
|
-
planType:
|
|
139
|
+
planType: "API",
|
|
133
140
|
allowed: true,
|
|
134
141
|
fetchedAt: Date.now(),
|
|
135
|
-
error:
|
|
142
|
+
error: "Rate limited",
|
|
136
143
|
};
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
if (response.ok) {
|
|
140
|
-
const data = await response.json().catch(() => null) as {
|
|
147
|
+
const data = (await response.json().catch(() => null)) as {
|
|
148
|
+
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
|
149
|
+
} | null;
|
|
141
150
|
const usage = data?.usage;
|
|
142
|
-
|
|
151
|
+
|
|
143
152
|
return {
|
|
144
|
-
planType:
|
|
153
|
+
planType: "API",
|
|
145
154
|
allowed: true,
|
|
146
155
|
fetchedAt: Date.now(),
|
|
147
156
|
credits: {
|
|
@@ -157,17 +166,17 @@ export const perplexityPlugin: ProviderPlugin = {
|
|
|
157
166
|
};
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
const errorText = await response.text().catch(() =>
|
|
169
|
+
const errorText = await response.text().catch(() => "");
|
|
161
170
|
return {
|
|
162
|
-
planType:
|
|
171
|
+
planType: "API",
|
|
163
172
|
allowed: true,
|
|
164
173
|
fetchedAt: Date.now(),
|
|
165
174
|
error: `API error: ${response.status} ${errorText.slice(0, 100)}`,
|
|
166
175
|
};
|
|
167
176
|
} catch (err) {
|
|
168
177
|
const msg = err instanceof Error ? err.message : String(err);
|
|
169
|
-
log.error(
|
|
170
|
-
|
|
178
|
+
log.error("Perplexity fetch error", { error: msg });
|
|
179
|
+
|
|
171
180
|
return {
|
|
172
181
|
fetchedAt: Date.now(),
|
|
173
182
|
error: `Connection error: ${msg}`,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
CredentialResult,
|
|
3
|
+
Credentials,
|
|
4
|
+
PluginContext,
|
|
5
|
+
ProviderAuth,
|
|
3
6
|
ProviderFetchContext,
|
|
7
|
+
ProviderPlugin,
|
|
4
8
|
ProviderUsageData,
|
|
5
9
|
UsageLimit,
|
|
6
|
-
|
|
7
|
-
PluginContext,
|
|
8
|
-
CredentialResult,
|
|
9
|
-
Credentials,
|
|
10
|
-
} from '../types/provider.ts';
|
|
10
|
+
} from "../types/provider.ts";
|
|
11
11
|
|
|
12
|
-
const ZAI_USAGE_URL =
|
|
12
|
+
const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
13
13
|
|
|
14
14
|
interface ZaiLimitEntry {
|
|
15
15
|
type?: string;
|
|
@@ -28,29 +28,29 @@ interface ZaiUsageResponse {
|
|
|
28
28
|
|
|
29
29
|
export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
30
30
|
apiVersion: 2,
|
|
31
|
-
id:
|
|
32
|
-
type:
|
|
33
|
-
name:
|
|
34
|
-
version:
|
|
31
|
+
id: "zai-coding-plan",
|
|
32
|
+
type: "provider",
|
|
33
|
+
name: "Z.ai Coding Plan",
|
|
34
|
+
version: "1.0.0",
|
|
35
35
|
|
|
36
36
|
meta: {
|
|
37
|
-
description:
|
|
38
|
-
homepage:
|
|
39
|
-
brandColor:
|
|
37
|
+
description: "Z.ai coding plan usage and quota tracking",
|
|
38
|
+
homepage: "https://z.ai",
|
|
39
|
+
brandColor: "#10b981",
|
|
40
40
|
},
|
|
41
41
|
|
|
42
42
|
permissions: {
|
|
43
43
|
network: {
|
|
44
44
|
enabled: true,
|
|
45
|
-
allowedDomains: [
|
|
45
|
+
allowedDomains: ["api.z.ai"],
|
|
46
46
|
},
|
|
47
47
|
filesystem: {
|
|
48
48
|
read: true,
|
|
49
|
-
paths: [
|
|
49
|
+
paths: ["~/.local/share/opencode"],
|
|
50
50
|
},
|
|
51
51
|
env: {
|
|
52
52
|
read: true,
|
|
53
|
-
vars: [
|
|
53
|
+
vars: ["ZAI_API_KEY"],
|
|
54
54
|
},
|
|
55
55
|
},
|
|
56
56
|
|
|
@@ -64,26 +64,33 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
64
64
|
auth: {
|
|
65
65
|
async discover(ctx: PluginContext): Promise<CredentialResult> {
|
|
66
66
|
// 1. Try OpenCode auth (api, wellknown, or oauth)
|
|
67
|
-
const entry = await ctx.authSources.opencode.getProviderEntry(
|
|
67
|
+
const entry = await ctx.authSources.opencode.getProviderEntry("zai-coding-plan");
|
|
68
68
|
if (entry) {
|
|
69
|
-
if (entry.type ===
|
|
70
|
-
return { ok: true, credentials: { apiKey: entry.key, source:
|
|
69
|
+
if (entry.type === "api" && entry.key) {
|
|
70
|
+
return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
|
|
71
71
|
}
|
|
72
|
-
if (entry.type ===
|
|
73
|
-
return {
|
|
72
|
+
if (entry.type === "wellknown" && (entry.token || entry.key)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
credentials: { apiKey: (entry.token || entry.key)!, source: "opencode" },
|
|
76
|
+
};
|
|
74
77
|
}
|
|
75
|
-
if (entry.type ===
|
|
76
|
-
return { ok: true, credentials: { apiKey: entry.access, source:
|
|
78
|
+
if (entry.type === "oauth" && entry.access) {
|
|
79
|
+
return { ok: true, credentials: { apiKey: entry.access, source: "opencode" } };
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
// 2. Try env vars
|
|
81
|
-
const apiKey = ctx.authSources.env.get(
|
|
84
|
+
const apiKey = ctx.authSources.env.get("ZAI_API_KEY");
|
|
82
85
|
if (apiKey) {
|
|
83
|
-
return { ok: true, credentials: { apiKey, source:
|
|
86
|
+
return { ok: true, credentials: { apiKey, source: "env" } };
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: "missing",
|
|
92
|
+
message: "No Z.ai API key found. Configure Z.ai in OpenCode.",
|
|
93
|
+
};
|
|
87
94
|
},
|
|
88
95
|
|
|
89
96
|
isConfigured(credentials: Credentials): boolean {
|
|
@@ -99,29 +106,29 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
99
106
|
if (!token) {
|
|
100
107
|
return {
|
|
101
108
|
fetchedAt: Date.now(),
|
|
102
|
-
error:
|
|
109
|
+
error: "API key required. Configure Z.ai in OpenCode.",
|
|
103
110
|
};
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
try {
|
|
107
114
|
const response = await http.fetch(ZAI_USAGE_URL, {
|
|
108
|
-
method:
|
|
115
|
+
method: "GET",
|
|
109
116
|
headers: {
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
Authorization: `Bearer ${token}`,
|
|
118
|
+
Accept: "application/json",
|
|
112
119
|
},
|
|
113
120
|
});
|
|
114
121
|
|
|
115
122
|
if (!response.ok) {
|
|
116
|
-
log.warn(
|
|
117
|
-
|
|
123
|
+
log.warn("Failed to fetch Z.ai usage", { status: response.status });
|
|
124
|
+
|
|
118
125
|
if (response.status === 401 || response.status === 403) {
|
|
119
126
|
return {
|
|
120
127
|
fetchedAt: Date.now(),
|
|
121
|
-
error:
|
|
128
|
+
error: "Authorization failed. Check your Z.ai API key.",
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
|
-
|
|
131
|
+
|
|
125
132
|
return {
|
|
126
133
|
fetchedAt: Date.now(),
|
|
127
134
|
error: `API error: ${response.status} ${response.statusText}`,
|
|
@@ -133,7 +140,7 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
133
140
|
if (!data || data.success !== true) {
|
|
134
141
|
return {
|
|
135
142
|
fetchedAt: Date.now(),
|
|
136
|
-
planType:
|
|
143
|
+
planType: "Z.ai",
|
|
137
144
|
allowed: true,
|
|
138
145
|
};
|
|
139
146
|
}
|
|
@@ -142,18 +149,18 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
142
149
|
if (limits.length === 0) {
|
|
143
150
|
return {
|
|
144
151
|
fetchedAt: Date.now(),
|
|
145
|
-
planType:
|
|
152
|
+
planType: "Z.ai",
|
|
146
153
|
allowed: true,
|
|
147
154
|
};
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
const parsedLimits = limits
|
|
151
|
-
.map(limit => ({
|
|
158
|
+
.map((limit) => ({
|
|
152
159
|
type: limit.type,
|
|
153
160
|
usedPercent: resolveUsedPercent(limit),
|
|
154
161
|
remaining: limit.remaining,
|
|
155
162
|
}))
|
|
156
|
-
.filter(l => l.usedPercent !== null);
|
|
163
|
+
.filter((l) => l.usedPercent !== null);
|
|
157
164
|
|
|
158
165
|
let primary: UsageLimit | undefined;
|
|
159
166
|
for (const limit of parsedLimits) {
|
|
@@ -166,13 +173,14 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
const limitReached = parsedLimits.some(
|
|
170
|
-
(l
|
|
171
|
-
|
|
176
|
+
const limitReached = parsedLimits.some(
|
|
177
|
+
(l) =>
|
|
178
|
+
(l.usedPercent !== null && l.usedPercent >= 100) ||
|
|
179
|
+
(typeof l.remaining === "number" && l.remaining <= 0),
|
|
172
180
|
);
|
|
173
181
|
|
|
174
182
|
const result: ProviderUsageData = {
|
|
175
|
-
planType:
|
|
183
|
+
planType: "Z.ai Coding Plan",
|
|
176
184
|
allowed: !limitReached,
|
|
177
185
|
limitReached,
|
|
178
186
|
fetchedAt: Date.now(),
|
|
@@ -184,26 +192,27 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
184
192
|
|
|
185
193
|
return result;
|
|
186
194
|
} catch (err) {
|
|
187
|
-
log.error(
|
|
195
|
+
log.error("Failed to fetch Z.ai usage", { error: err });
|
|
188
196
|
return {
|
|
189
197
|
fetchedAt: Date.now(),
|
|
190
|
-
error: err instanceof Error ? err.message :
|
|
198
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
191
199
|
};
|
|
192
200
|
}
|
|
193
201
|
},
|
|
194
202
|
};
|
|
195
203
|
|
|
196
204
|
function resolveUsedPercent(limit: ZaiLimitEntry): number | null {
|
|
197
|
-
if (typeof limit.percentage ===
|
|
205
|
+
if (typeof limit.percentage === "number") {
|
|
198
206
|
return Math.round(Math.max(0, Math.min(100, limit.percentage)));
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
const total = limit.usage;
|
|
202
|
-
const used =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
const used =
|
|
211
|
+
typeof limit.currentValue === "number"
|
|
212
|
+
? limit.currentValue
|
|
213
|
+
: total !== undefined && typeof limit.remaining === "number"
|
|
214
|
+
? Math.max(0, total - limit.remaining)
|
|
215
|
+
: undefined;
|
|
207
216
|
|
|
208
217
|
if (total && used !== undefined && total > 0) {
|
|
209
218
|
return Math.round((used / total) * 100);
|
|
@@ -213,13 +222,13 @@ function resolveUsedPercent(limit: ZaiLimitEntry): number | null {
|
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
function formatLimitType(type?: string): string {
|
|
216
|
-
if (!type) return
|
|
225
|
+
if (!type) return "Quota";
|
|
217
226
|
switch (type) {
|
|
218
|
-
case
|
|
219
|
-
return
|
|
220
|
-
case
|
|
221
|
-
return
|
|
227
|
+
case "TIME_LIMIT":
|
|
228
|
+
return "Time";
|
|
229
|
+
case "TOKENS_LIMIT":
|
|
230
|
+
return "Tokens";
|
|
222
231
|
default:
|
|
223
|
-
return type.replace(/_/g,
|
|
232
|
+
return type.replace(/_/g, " ").toLowerCase();
|
|
224
233
|
}
|
|
225
234
|
}
|
package/src/plugins/registry.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
import type { PluginsConfig } from "@/config/schema.ts";
|
|
2
|
+
import {
|
|
3
|
+
discoverLocalPlugins,
|
|
4
|
+
loadLocalPlugin,
|
|
5
|
+
loadNpmPlugin,
|
|
6
|
+
resolvePluginPath,
|
|
7
|
+
} from "./loader.ts";
|
|
8
|
+
import { installAllNpmPlugins, resolveNpmPluginPath } from "./npm-installer.ts";
|
|
1
9
|
import type {
|
|
10
|
+
AgentPlugin,
|
|
2
11
|
AnyPlugin,
|
|
3
|
-
|
|
12
|
+
NotificationPlugin,
|
|
4
13
|
PluginByType,
|
|
14
|
+
PluginType,
|
|
5
15
|
ProviderPlugin,
|
|
6
|
-
AgentPlugin,
|
|
7
16
|
ThemePlugin,
|
|
8
|
-
|
|
9
|
-
} from './types/index.ts';
|
|
10
|
-
import { loadLocalPlugin, loadNpmPlugin, discoverLocalPlugins, resolvePluginPath } from './loader.ts';
|
|
11
|
-
import { installAllNpmPlugins, resolveNpmPluginPath } from './npm-installer.ts';
|
|
12
|
-
import type { PluginsConfig } from '@/config/schema.ts';
|
|
17
|
+
} from "./types/index.ts";
|
|
13
18
|
|
|
14
|
-
export type PluginSource =
|
|
19
|
+
export type PluginSource = "builtin" | "local" | "npm";
|
|
15
20
|
|
|
16
21
|
type PluginStore = {
|
|
17
22
|
provider: Map<string, ProviderPlugin>;
|
|
@@ -35,35 +40,35 @@ class PluginRegistryImpl {
|
|
|
35
40
|
return `${type}-${id}`;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
register(plugin: AnyPlugin, source: PluginSource =
|
|
43
|
+
register(plugin: AnyPlugin, source: PluginSource = "builtin"): void {
|
|
39
44
|
const key = this.sourceKey(plugin.type, plugin.id);
|
|
40
45
|
const existingSource = this.sources.get(key);
|
|
41
46
|
|
|
42
|
-
if (existingSource ===
|
|
47
|
+
if (existingSource === "builtin" && source !== "builtin") {
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
this.sources.set(key, source);
|
|
47
52
|
switch (plugin.type) {
|
|
48
|
-
case
|
|
53
|
+
case "provider":
|
|
49
54
|
if (this.plugins.provider.has(plugin.id)) {
|
|
50
55
|
console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
|
|
51
56
|
}
|
|
52
57
|
this.plugins.provider.set(plugin.id, plugin);
|
|
53
58
|
break;
|
|
54
|
-
case
|
|
59
|
+
case "agent":
|
|
55
60
|
if (this.plugins.agent.has(plugin.id)) {
|
|
56
61
|
console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
|
|
57
62
|
}
|
|
58
63
|
this.plugins.agent.set(plugin.id, plugin);
|
|
59
64
|
break;
|
|
60
|
-
case
|
|
65
|
+
case "theme":
|
|
61
66
|
if (this.plugins.theme.has(plugin.id)) {
|
|
62
67
|
console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
|
|
63
68
|
}
|
|
64
69
|
this.plugins.theme.set(plugin.id, plugin);
|
|
65
70
|
break;
|
|
66
|
-
case
|
|
71
|
+
case "notification":
|
|
67
72
|
if (this.plugins.notification.has(plugin.id)) {
|
|
68
73
|
console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
|
|
69
74
|
}
|
|
@@ -115,38 +120,38 @@ class PluginRegistryImpl {
|
|
|
115
120
|
|
|
116
121
|
async loadBuiltinPlugins(): Promise<void> {
|
|
117
122
|
const [providers, agents, themes, notifications] = await Promise.all([
|
|
118
|
-
import(
|
|
119
|
-
import(
|
|
120
|
-
import(
|
|
121
|
-
import(
|
|
123
|
+
import("./providers/index.ts"),
|
|
124
|
+
import("./agents/index.ts"),
|
|
125
|
+
import("./themes/index.ts"),
|
|
126
|
+
import("./notifications/index.ts"),
|
|
122
127
|
]);
|
|
123
128
|
|
|
124
129
|
for (const plugin of Object.values(providers)) {
|
|
125
|
-
if (isProviderPlugin(plugin)) this.register(plugin,
|
|
130
|
+
if (isProviderPlugin(plugin)) this.register(plugin, "builtin");
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
for (const plugin of Object.values(agents)) {
|
|
129
|
-
if (isAgentPlugin(plugin)) this.register(plugin,
|
|
134
|
+
if (isAgentPlugin(plugin)) this.register(plugin, "builtin");
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
for (const plugin of Object.values(themes)) {
|
|
133
|
-
if (isThemePlugin(plugin)) this.register(plugin,
|
|
138
|
+
if (isThemePlugin(plugin)) this.register(plugin, "builtin");
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
for (const plugin of Object.values(notifications)) {
|
|
137
|
-
if (isNotificationPlugin(plugin)) this.register(plugin,
|
|
142
|
+
if (isNotificationPlugin(plugin)) this.register(plugin, "builtin");
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
async loadLocalPlugins(extraPaths: string[] = []): Promise<void> {
|
|
142
147
|
const discoveredPaths = await discoverLocalPlugins();
|
|
143
|
-
const resolvedExtraPaths = extraPaths.map(p => resolvePluginPath(p));
|
|
148
|
+
const resolvedExtraPaths = extraPaths.map((p) => resolvePluginPath(p));
|
|
144
149
|
const allPaths = [...discoveredPaths, ...resolvedExtraPaths];
|
|
145
150
|
|
|
146
151
|
for (const pluginPath of allPaths) {
|
|
147
152
|
const result = await loadLocalPlugin(pluginPath);
|
|
148
153
|
if (result.success && result.plugin) {
|
|
149
|
-
this.register(result.plugin,
|
|
154
|
+
this.register(result.plugin, "local");
|
|
150
155
|
console.info(`Loaded local plugin: ${result.plugin.name} (${result.plugin.id})`);
|
|
151
156
|
} else {
|
|
152
157
|
console.warn(`Failed to load plugin from ${pluginPath}: ${result.error}`);
|
|
@@ -170,7 +175,7 @@ class PluginRegistryImpl {
|
|
|
170
175
|
const resolvedPath = resolveNpmPluginPath(packageName);
|
|
171
176
|
const result = await loadNpmPlugin(packageName, resolvedPath);
|
|
172
177
|
if (result.success && result.plugin) {
|
|
173
|
-
this.register(result.plugin,
|
|
178
|
+
this.register(result.plugin, "npm");
|
|
174
179
|
console.info(`Loaded npm plugin: ${result.plugin.name} (${packageName})`);
|
|
175
180
|
} else {
|
|
176
181
|
console.warn(`Failed to load npm plugin ${packageName}: ${result.error}`);
|
|
@@ -190,10 +195,7 @@ class PluginRegistryImpl {
|
|
|
190
195
|
|
|
191
196
|
await this.loadBuiltinPlugins();
|
|
192
197
|
|
|
193
|
-
const localPaths = [
|
|
194
|
-
...(config?.plugins?.local ?? []),
|
|
195
|
-
...(config?.cliPlugins ?? []),
|
|
196
|
-
];
|
|
198
|
+
const localPaths = [...(config?.plugins?.local ?? []), ...(config?.cliPlugins ?? [])];
|
|
197
199
|
await this.loadLocalPlugins(localPaths);
|
|
198
200
|
|
|
199
201
|
const npmPackages = config?.plugins?.npm ?? [];
|
|
@@ -203,7 +205,7 @@ class PluginRegistryImpl {
|
|
|
203
205
|
|
|
204
206
|
const disabled = config?.plugins?.disabled ?? [];
|
|
205
207
|
for (const pluginId of disabled) {
|
|
206
|
-
for (const type of [
|
|
208
|
+
for (const type of ["provider", "agent", "theme", "notification"] as PluginType[]) {
|
|
207
209
|
if (this.has(type, pluginId)) {
|
|
208
210
|
this.disablePlugin(type, pluginId);
|
|
209
211
|
console.info(`Disabled plugin: ${pluginId}`);
|
|
@@ -213,38 +215,40 @@ class PluginRegistryImpl {
|
|
|
213
215
|
|
|
214
216
|
this.initialized = true;
|
|
215
217
|
console.info(
|
|
216
|
-
`Plugin registry initialized: ${this.count(
|
|
217
|
-
`${this.count(
|
|
218
|
-
`${this.count(
|
|
218
|
+
`Plugin registry initialized: ${this.count("provider")} providers, ` +
|
|
219
|
+
`${this.count("agent")} agents, ${this.count("theme")} themes, ` +
|
|
220
|
+
`${this.count("notification")} notifications`,
|
|
219
221
|
);
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
function hasPluginShape(
|
|
225
|
+
function hasPluginShape(
|
|
226
|
+
obj: unknown,
|
|
227
|
+
): obj is { id: string; type: string; name: string; version: string } {
|
|
224
228
|
return (
|
|
225
229
|
obj !== null &&
|
|
226
|
-
typeof obj ===
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
typeof obj === "object" &&
|
|
231
|
+
"id" in obj &&
|
|
232
|
+
"type" in obj &&
|
|
233
|
+
"name" in obj &&
|
|
234
|
+
"version" in obj
|
|
231
235
|
);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
function isProviderPlugin(obj: unknown): obj is ProviderPlugin {
|
|
235
|
-
return hasPluginShape(obj) && obj.type ===
|
|
239
|
+
return hasPluginShape(obj) && obj.type === "provider";
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
function isAgentPlugin(obj: unknown): obj is AgentPlugin {
|
|
239
|
-
return hasPluginShape(obj) && obj.type ===
|
|
243
|
+
return hasPluginShape(obj) && obj.type === "agent";
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
function isThemePlugin(obj: unknown): obj is ThemePlugin {
|
|
243
|
-
return hasPluginShape(obj) && obj.type ===
|
|
247
|
+
return hasPluginShape(obj) && obj.type === "theme";
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
function isNotificationPlugin(obj: unknown): obj is NotificationPlugin {
|
|
247
|
-
return hasPluginShape(obj) && obj.type ===
|
|
251
|
+
return hasPluginShape(obj) && obj.type === "notification";
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
export const pluginRegistry = new PluginRegistryImpl();
|