@tokentop/ttop 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +7 -6
  2. package/package.json +12 -5
  3. package/src/agents/aggregator.test.ts +306 -0
  4. package/src/agents/aggregator.ts +44 -2
  5. package/src/agents/types.ts +3 -0
  6. package/src/cli.ts +16 -2
  7. package/src/demo/simulator.ts +2 -2
  8. package/src/plugins/agents/index.ts +3 -0
  9. package/src/plugins/auth-sources.ts +2 -3
  10. package/src/plugins/lifecycle.ts +2 -2
  11. package/src/plugins/loader.test.ts +1 -1
  12. package/src/plugins/loader.ts +15 -7
  13. package/src/plugins/notification-bus.ts +5 -2
  14. package/src/plugins/notifications/terminal-bell.ts +1 -1
  15. package/src/plugins/notifications/visual-flash.ts +1 -1
  16. package/src/plugins/plugin-context-factory.ts +4 -19
  17. package/src/plugins/plugin-host.test.ts +197 -0
  18. package/src/plugins/plugin-host.ts +45 -1
  19. package/src/plugins/providers/anthropic.test.ts +202 -0
  20. package/src/plugins/providers/anthropic.ts +21 -2
  21. package/src/plugins/providers/antigravity.ts +17 -1
  22. package/src/plugins/providers/chutes.ts +205 -0
  23. package/src/plugins/providers/codex.ts +17 -1
  24. package/src/plugins/providers/gemini.ts +17 -1
  25. package/src/plugins/providers/github-copilot.ts +17 -1
  26. package/src/plugins/providers/index.ts +1 -0
  27. package/src/plugins/providers/minimax.ts +17 -1
  28. package/src/plugins/providers/openai-api.ts +53 -1
  29. package/src/plugins/providers/opencode-zen.ts +1 -1
  30. package/src/plugins/providers/perplexity.ts +11 -2
  31. package/src/plugins/providers/zai.ts +17 -1
  32. package/src/plugins/registry.ts +9 -9
  33. package/src/plugins/sandbox-guard.test.ts +1 -1
  34. package/src/plugins/sandbox-guard.ts +1 -1
  35. package/src/plugins/sandbox.ts +1 -1
  36. package/src/plugins/themes/catppuccin-latte.ts +1 -1
  37. package/src/plugins/themes/catppuccin-mocha.ts +1 -1
  38. package/src/plugins/themes/claude-code.ts +1 -1
  39. package/src/plugins/themes/dracula.ts +1 -1
  40. package/src/plugins/themes/github-light.ts +1 -1
  41. package/src/plugins/themes/gruvbox-dark.ts +1 -1
  42. package/src/plugins/themes/gruvbox-light.ts +1 -1
  43. package/src/plugins/themes/kanagawa.ts +1 -1
  44. package/src/plugins/themes/nord.ts +1 -1
  45. package/src/plugins/themes/one-dark.ts +1 -1
  46. package/src/plugins/themes/opencode.ts +1 -1
  47. package/src/plugins/themes/rose-pine-dawn.ts +1 -1
  48. package/src/plugins/themes/rose-pine.ts +1 -1
  49. package/src/plugins/themes/solarized-light.ts +1 -1
  50. package/src/plugins/themes/tokyo-night.ts +1 -1
  51. package/src/plugins/update-checker.ts +2 -2
  52. package/src/pricing/estimator.test.ts +1 -1
  53. package/src/pricing/estimator.ts +1 -1
  54. package/src/pricing/fallback.ts +5 -1
  55. package/src/pricing/index.ts +1 -1
  56. package/src/pricing/models-dev.ts +1 -1
  57. package/src/storage/index.ts +6 -0
  58. package/src/storage/migrations/index.ts +17 -0
  59. package/src/storage/repos/pluginStorage.ts +35 -0
  60. package/src/tui/App.tsx +17 -9
  61. package/src/tui/components/HelpOverlay.tsx +1 -1
  62. package/src/tui/components/LimitGauge.tsx +12 -4
  63. package/src/tui/components/ProviderCard.tsx +39 -23
  64. package/src/tui/components/ProviderDetailPanel.tsx +1 -1
  65. package/src/tui/components/ProviderLimitsPanel.tsx +5 -0
  66. package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
  67. package/src/tui/components/SessionsTable.tsx +62 -37
  68. package/src/tui/components/SettingsModal.tsx +1 -1
  69. package/src/tui/components/Skeleton.tsx +9 -6
  70. package/src/tui/components/SortOverlay.tsx +167 -0
  71. package/src/tui/components/Spinner.tsx +16 -11
  72. package/src/tui/components/ThemePicker.tsx +1 -1
  73. package/src/tui/contexts/AgentSessionContext.tsx +70 -17
  74. package/src/tui/contexts/PluginContext.tsx +72 -14
  75. package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
  76. package/src/tui/contexts/ThemeContext.tsx +7 -4
  77. package/src/tui/createApp.tsx +2 -0
  78. package/src/tui/hooks/index.ts +1 -0
  79. package/src/tui/hooks/useAnimatedValue.ts +29 -26
  80. package/src/tui/hooks/useAnimationTick.ts +93 -0
  81. package/src/tui/hooks/useDashboardKeyboard.ts +11 -4
  82. package/src/tui/hooks/useEntranceAnimation.ts +16 -10
  83. package/src/tui/hooks/usePulse.ts +13 -7
  84. package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
  85. package/src/tui/hooks/useValueFlash.ts +25 -33
  86. package/src/tui/index.tsx +4 -0
  87. package/src/tui/types/projectSort.ts +35 -0
  88. package/src/tui/types/sort.ts +61 -0
  89. package/src/tui/views/ProjectsView.tsx +104 -38
  90. package/src/tui/views/RealTimeDashboard.tsx +91 -8
  91. package/src/tui/views/SettingsView.tsx +1 -1
  92. package/src/utils/__tests__/error-category.test.ts +147 -0
  93. package/src/utils/currency.ts +4 -2
  94. package/src/utils/error-category.ts +91 -0
  95. package/src/version.ts +1 -1
  96. package/src/plugins/types/agent.ts +0 -108
  97. package/src/plugins/types/base.ts +0 -278
  98. package/src/plugins/types/index.ts +0 -19
  99. package/src/plugins/types/notification.ts +0 -51
  100. package/src/plugins/types/provider.ts +0 -237
  101. package/src/plugins/types/theme.ts +0 -78
  102. package/src/storage/database.ts +0 -623
@@ -0,0 +1,205 @@
1
+ import type {
2
+ CredentialResult,
3
+ Credentials,
4
+ PluginContext,
5
+ ProviderAuth,
6
+ ProviderFetchContext,
7
+ ProviderPlugin,
8
+ ProviderUsageData,
9
+ } from "@tokentop/plugin-sdk";
10
+
11
+ const CHUTES_QUOTA_URL = "https://api.chutes.ai/users/me/quota_usage/me";
12
+
13
+ interface ChutesQuotaResponse {
14
+ quota?: number;
15
+ used?: number;
16
+ }
17
+
18
+ export const chutesPlugin: ProviderPlugin = {
19
+ apiVersion: 2,
20
+ id: "chutes",
21
+ type: "provider",
22
+ name: "Chutes",
23
+ version: "1.0.0",
24
+
25
+ meta: {
26
+ description: "Chutes AI API quota and usage tracking",
27
+ homepage: "https://chutes.ai",
28
+ brandColor: "#6366f1",
29
+ },
30
+
31
+ permissions: {
32
+ network: {
33
+ enabled: true,
34
+ allowedDomains: ["api.chutes.ai"],
35
+ },
36
+ filesystem: {
37
+ read: true,
38
+ paths: ["~/.local/share/opencode"],
39
+ },
40
+ env: {
41
+ read: true,
42
+ vars: ["CHUTES_API_KEY"],
43
+ },
44
+ },
45
+
46
+ capabilities: {
47
+ usageLimits: true,
48
+ apiRateLimits: false,
49
+ tokenUsage: false,
50
+ actualCosts: false,
51
+ },
52
+
53
+ auth: {
54
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
55
+ const entry = await ctx.authSources.opencode.getProviderEntry("chutes");
56
+ if (entry) {
57
+ if (entry.type === "api" && entry.key) {
58
+ return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
59
+ }
60
+ if (entry.type === "wellknown" && (entry.token || entry.key)) {
61
+ return {
62
+ ok: true,
63
+ credentials: { apiKey: (entry.token || entry.key)!, source: "opencode" },
64
+ };
65
+ }
66
+ if (entry.type === "oauth" && entry.access) {
67
+ return { ok: true, credentials: { apiKey: entry.access, source: "opencode" } };
68
+ }
69
+ }
70
+
71
+ const apiKey = ctx.authSources.env.get("CHUTES_API_KEY");
72
+ if (apiKey) {
73
+ return { ok: true, credentials: { apiKey, source: "env" } };
74
+ }
75
+
76
+ return {
77
+ ok: false,
78
+ reason: "missing",
79
+ message: "No Chutes API key found. Configure Chutes in OpenCode.",
80
+ };
81
+ },
82
+
83
+ isConfigured(credentials: Credentials): boolean {
84
+ return !!(credentials.apiKey || credentials.oauth?.accessToken);
85
+ },
86
+ } satisfies ProviderAuth,
87
+
88
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
89
+ const { credentials, http, logger: log } = ctx;
90
+
91
+ const token = credentials.apiKey || credentials.oauth?.accessToken;
92
+
93
+ if (!token) {
94
+ return {
95
+ fetchedAt: Date.now(),
96
+ error: "API key required. Configure Chutes in OpenCode.",
97
+ };
98
+ }
99
+
100
+ try {
101
+ const response = await http.fetch(CHUTES_QUOTA_URL, {
102
+ method: "GET",
103
+ headers: {
104
+ Authorization: `Bearer ${token}`,
105
+ Accept: "application/json",
106
+ },
107
+ });
108
+
109
+ if (!response.ok) {
110
+ log.warn("Failed to fetch Chutes usage", { status: response.status });
111
+
112
+ if (response.status === 429) {
113
+ const retryAfterHeader = response.headers.get("retry-after");
114
+ const parsedRetryAfter = retryAfterHeader
115
+ ? Number.parseInt(retryAfterHeader, 10)
116
+ : Number.NaN;
117
+ const retrySec =
118
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
119
+ log.warn("Rate limited by Chutes API", { retryAfterHeader, retryAfterSec: retrySec });
120
+ return {
121
+ fetchedAt: Date.now(),
122
+ error: `Rate limited. Retry after ${retrySec}s.`,
123
+ rateLimited: true,
124
+ retryAfterMs: retrySec * 1000,
125
+ };
126
+ }
127
+
128
+ if (response.status === 401 || response.status === 403) {
129
+ return {
130
+ fetchedAt: Date.now(),
131
+ error: "Authorization failed. Check your Chutes API key.",
132
+ };
133
+ }
134
+
135
+ return {
136
+ fetchedAt: Date.now(),
137
+ error: `API error: ${response.status} ${response.statusText}`,
138
+ };
139
+ }
140
+
141
+ const data = (await response.json()) as ChutesQuotaResponse;
142
+
143
+ if (!data || typeof data.quota !== "number" || typeof data.used !== "number") {
144
+ return {
145
+ fetchedAt: Date.now(),
146
+ planType: "Chutes",
147
+ allowed: true,
148
+ };
149
+ }
150
+
151
+ const quota = data.quota;
152
+ const used = data.used;
153
+ const usedPercent = quota > 0 ? Math.round((used / quota) * 100) : 0;
154
+
155
+ const planType = getTierFromQuota(quota);
156
+ const resetAt = calculateNextReset();
157
+ const limitReached = usedPercent >= 100;
158
+
159
+ const result: ProviderUsageData = {
160
+ planType,
161
+ allowed: !limitReached,
162
+ limitReached,
163
+ fetchedAt: Date.now(),
164
+ };
165
+
166
+ if (usedPercent !== null) {
167
+ result.limits = {
168
+ primary: {
169
+ usedPercent,
170
+ label: "Daily quota",
171
+ resetsAt: resetAt,
172
+ windowMinutes: 1440,
173
+ },
174
+ };
175
+ }
176
+
177
+ return result;
178
+ } catch (err) {
179
+ log.error("Failed to fetch Chutes usage", { error: err });
180
+ return {
181
+ fetchedAt: Date.now(),
182
+ error: err instanceof Error ? err.message : "Unknown error",
183
+ };
184
+ }
185
+ },
186
+ };
187
+
188
+ function getTierFromQuota(quota: number): string {
189
+ const tierMap: Record<number, string> = {
190
+ 200: "Legacy",
191
+ 300: "Base",
192
+ 2000: "Plus",
193
+ 5000: "Pro",
194
+ };
195
+
196
+ return tierMap[quota] ?? `Custom (${quota})`;
197
+ }
198
+
199
+ function calculateNextReset(): number {
200
+ const now = new Date();
201
+ const tomorrow = new Date(now);
202
+ tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
203
+ tomorrow.setUTCHours(0, 0, 0, 0);
204
+ return tomorrow.getTime();
205
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  ProviderFetchContext,
8
8
  ProviderPlugin,
9
9
  ProviderUsageData,
10
- } from "../types/provider.ts";
10
+ } from "@tokentop/plugin-sdk";
11
11
 
12
12
  interface CodexCliCredentials {
13
13
  access_token?: string;
@@ -178,6 +178,22 @@ export const codexPlugin: ProviderPlugin = {
178
178
  const bodyText = await response.text().catch(() => "");
179
179
  log.warn("Failed to fetch ChatGPT usage", { status: response.status, body: bodyText });
180
180
 
181
+ if (response.status === 429) {
182
+ const retryAfterHeader = response.headers.get("retry-after");
183
+ const parsedRetryAfter = retryAfterHeader
184
+ ? Number.parseInt(retryAfterHeader, 10)
185
+ : Number.NaN;
186
+ const retrySec =
187
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
188
+ log.warn("Rate limited by ChatGPT API", { retryAfterHeader, retryAfterSec: retrySec });
189
+ return {
190
+ fetchedAt: Date.now(),
191
+ error: `Rate limited. Retry after ${retrySec}s.`,
192
+ rateLimited: true,
193
+ retryAfterMs: retrySec * 1000,
194
+ };
195
+ }
196
+
181
197
  if (response.status === 401) {
182
198
  return {
183
199
  fetchedAt: Date.now(),
@@ -9,7 +9,7 @@ import type {
9
9
  ProviderUsageData,
10
10
  RefreshedCredentials,
11
11
  UsageLimit,
12
- } from "../types/provider.ts";
12
+ } from "@tokentop/plugin-sdk";
13
13
 
14
14
  const GOOGLE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
15
15
 
@@ -212,6 +212,22 @@ export const geminiPlugin: ProviderPlugin = {
212
212
  });
213
213
 
214
214
  if (!response.ok) {
215
+ if (response.status === 429) {
216
+ const retryAfterHeader = response.headers.get("retry-after");
217
+ const parsedRetryAfter = retryAfterHeader
218
+ ? Number.parseInt(retryAfterHeader, 10)
219
+ : Number.NaN;
220
+ const retrySec =
221
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
222
+ log.warn("Rate limited by Google API", { retryAfterHeader, retryAfterSec: retrySec });
223
+ return {
224
+ fetchedAt: Date.now(),
225
+ error: `Rate limited. Retry after ${retrySec}s.`,
226
+ rateLimited: true,
227
+ retryAfterMs: retrySec * 1000,
228
+ };
229
+ }
230
+
215
231
  const errorText = await response.text().catch(() => "");
216
232
  if (response.status === 401) {
217
233
  return {
@@ -7,7 +7,7 @@ import type {
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
9
  UsageLimit,
10
- } from "../types/provider.ts";
10
+ } from "@tokentop/plugin-sdk";
11
11
 
12
12
  interface CopilotUserResponse {
13
13
  access_type_sku?: string;
@@ -134,6 +134,22 @@ export const githubCopilotPlugin: ProviderPlugin = {
134
134
  });
135
135
 
136
136
  if (!response.ok) {
137
+ if (response.status === 429) {
138
+ const retryAfterHeader = response.headers.get("retry-after");
139
+ const parsedRetryAfter = retryAfterHeader
140
+ ? Number.parseInt(retryAfterHeader, 10)
141
+ : Number.NaN;
142
+ const retrySec =
143
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
144
+ log.warn("Rate limited by GitHub API", { retryAfterHeader, retryAfterSec: retrySec });
145
+ return {
146
+ fetchedAt: Date.now(),
147
+ error: `Rate limited. Retry after ${retrySec}s.`,
148
+ rateLimited: true,
149
+ retryAfterMs: retrySec * 1000,
150
+ };
151
+ }
152
+
137
153
  if (response.status === 404) {
138
154
  return {
139
155
  planType: "None",
@@ -1,5 +1,6 @@
1
1
  export { anthropicPlugin } from "./anthropic.ts";
2
2
  export { antigravityPlugin } from "./antigravity.ts";
3
+ export { chutesPlugin } from "./chutes.ts";
3
4
  export { codexPlugin } from "./codex.ts";
4
5
  export { geminiPlugin } from "./gemini.ts";
5
6
  export { githubCopilotPlugin } from "./github-copilot.ts";
@@ -6,7 +6,7 @@ import type {
6
6
  ProviderFetchContext,
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
- } from "../types/provider.ts";
9
+ } from "@tokentop/plugin-sdk";
10
10
 
11
11
  const MINIMAX_USAGE_URL = "https://platform.minimax.io/v1/api/openplatform/coding_plan/remains";
12
12
 
@@ -151,6 +151,22 @@ function createMinimaxPlugin(id: string, name: string): ProviderPlugin {
151
151
  if (!response.ok) {
152
152
  log.warn("Failed to fetch MiniMax usage", { status: response.status });
153
153
 
154
+ if (response.status === 429) {
155
+ const retryAfterHeader = response.headers.get("retry-after");
156
+ const parsedRetryAfter = retryAfterHeader
157
+ ? Number.parseInt(retryAfterHeader, 10)
158
+ : Number.NaN;
159
+ const retrySec =
160
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
161
+ log.warn("Rate limited by MiniMax API", { retryAfterHeader, retryAfterSec: retrySec });
162
+ return {
163
+ fetchedAt: Date.now(),
164
+ error: `Rate limited. Retry after ${retrySec}s.`,
165
+ rateLimited: true,
166
+ retryAfterMs: retrySec * 1000,
167
+ };
168
+ }
169
+
154
170
  if (response.status === 401 || response.status === 403) {
155
171
  return {
156
172
  fetchedAt: Date.now(),
@@ -6,7 +6,7 @@ import type {
6
6
  ProviderFetchContext,
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
- } from "../types/provider.ts";
9
+ } from "@tokentop/plugin-sdk";
10
10
 
11
11
  interface UsageBucket {
12
12
  object: string;
@@ -49,6 +49,15 @@ interface CostsResponse {
49
49
  has_more: boolean;
50
50
  }
51
51
 
52
+ class RateLimitError extends Error {
53
+ readonly retryAfterMs: number;
54
+ constructor(retryAfterMs: number) {
55
+ super(`Rate limited. Retry after ${retryAfterMs / 1000}s.`);
56
+ this.name = "RateLimitError";
57
+ this.retryAfterMs = retryAfterMs;
58
+ }
59
+ }
60
+
52
61
  export const openaiApiPlugin: ProviderPlugin = {
53
62
  apiVersion: 2,
54
63
  id: "openai-api",
@@ -145,6 +154,21 @@ export const openaiApiPlugin: ProviderPlugin = {
145
154
  fetchCosts(http, adminKey, oneDayAgo),
146
155
  ]);
147
156
 
157
+ // If either helper hit a 429, surface rate-limit info immediately
158
+ for (const settled of [usageResult, costsResult]) {
159
+ if (settled.status === "rejected" && settled.reason instanceof RateLimitError) {
160
+ log.warn("Rate limited by OpenAI API", { retryAfterMs: settled.reason.retryAfterMs });
161
+ return {
162
+ planType: "API",
163
+ allowed: true,
164
+ fetchedAt: Date.now(),
165
+ error: settled.reason.message,
166
+ rateLimited: true,
167
+ retryAfterMs: settled.reason.retryAfterMs,
168
+ };
169
+ }
170
+ }
171
+
148
172
  const result: ProviderUsageData = {
149
173
  planType: "API",
150
174
  allowed: true,
@@ -208,6 +232,16 @@ export const openaiApiPlugin: ProviderPlugin = {
208
232
 
209
233
  return result;
210
234
  } catch (err) {
235
+ if (err instanceof RateLimitError) {
236
+ return {
237
+ planType: "API",
238
+ allowed: true,
239
+ fetchedAt: Date.now(),
240
+ error: err.message,
241
+ rateLimited: true,
242
+ retryAfterMs: err.retryAfterMs,
243
+ };
244
+ }
211
245
  const msg = err instanceof Error ? err.message : String(err);
212
246
  log.error("OpenAI fetch error", { error: msg });
213
247
 
@@ -236,6 +270,15 @@ async function fetchCompletionsUsage(
236
270
  });
237
271
 
238
272
  if (!response.ok) {
273
+ if (response.status === 429) {
274
+ const retryAfterHeader = response.headers.get("retry-after");
275
+ const parsedRetryAfter = retryAfterHeader
276
+ ? Number.parseInt(retryAfterHeader, 10)
277
+ : Number.NaN;
278
+ const retrySec =
279
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
280
+ throw new RateLimitError(retrySec * 1000);
281
+ }
239
282
  if (response.status === 403) {
240
283
  return null;
241
284
  }
@@ -260,6 +303,15 @@ async function fetchCosts(
260
303
  });
261
304
 
262
305
  if (!response.ok) {
306
+ if (response.status === 429) {
307
+ const retryAfterHeader = response.headers.get("retry-after");
308
+ const parsedRetryAfter = retryAfterHeader
309
+ ? Number.parseInt(retryAfterHeader, 10)
310
+ : Number.NaN;
311
+ const retrySec =
312
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
313
+ throw new RateLimitError(retrySec * 1000);
314
+ }
263
315
  if (response.status === 403) {
264
316
  return null;
265
317
  }
@@ -6,7 +6,7 @@ import type {
6
6
  ProviderFetchContext,
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
- } from "../types/provider.ts";
9
+ } from "@tokentop/plugin-sdk";
10
10
 
11
11
  export const opencodeZenPlugin: ProviderPlugin = {
12
12
  apiVersion: 2,
@@ -6,7 +6,7 @@ import type {
6
6
  ProviderFetchContext,
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
- } from "../types/provider.ts";
9
+ } from "@tokentop/plugin-sdk";
10
10
 
11
11
  /**
12
12
  * Perplexity Shadow API Discovery (January 2026):
@@ -135,11 +135,20 @@ export const perplexityPlugin: ProviderPlugin = {
135
135
  }
136
136
 
137
137
  if (response.status === 429) {
138
+ const retryAfterHeader = response.headers.get("retry-after");
139
+ const parsedRetryAfter = retryAfterHeader
140
+ ? Number.parseInt(retryAfterHeader, 10)
141
+ : Number.NaN;
142
+ const retrySec =
143
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
144
+ log.warn("Rate limited by Perplexity API", { retryAfterHeader, retryAfterSec: retrySec });
138
145
  return {
139
146
  planType: "API",
140
147
  allowed: true,
141
148
  fetchedAt: Date.now(),
142
- error: "Rate limited",
149
+ error: `Rate limited. Retry after ${retrySec}s.`,
150
+ rateLimited: true,
151
+ retryAfterMs: retrySec * 1000,
143
152
  };
144
153
  }
145
154
 
@@ -7,7 +7,7 @@ import type {
7
7
  ProviderPlugin,
8
8
  ProviderUsageData,
9
9
  UsageLimit,
10
- } from "../types/provider.ts";
10
+ } from "@tokentop/plugin-sdk";
11
11
 
12
12
  const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
13
13
 
@@ -124,6 +124,22 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
124
124
  if (!response.ok) {
125
125
  log.warn("Failed to fetch Z.ai usage", { status: response.status });
126
126
 
127
+ if (response.status === 429) {
128
+ const retryAfterHeader = response.headers.get("retry-after");
129
+ const parsedRetryAfter = retryAfterHeader
130
+ ? Number.parseInt(retryAfterHeader, 10)
131
+ : Number.NaN;
132
+ const retrySec =
133
+ Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
134
+ log.warn("Rate limited by Z.ai API", { retryAfterHeader, retryAfterSec: retrySec });
135
+ return {
136
+ fetchedAt: Date.now(),
137
+ error: `Rate limited. Retry after ${retrySec}s.`,
138
+ rateLimited: true,
139
+ retryAfterMs: retrySec * 1000,
140
+ };
141
+ }
142
+
127
143
  if (response.status === 401 || response.status === 403) {
128
144
  return {
129
145
  fetchedAt: Date.now(),
@@ -1,11 +1,3 @@
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";
9
1
  import type {
10
2
  AgentPlugin,
11
3
  AnyPlugin,
@@ -14,7 +6,15 @@ import type {
14
6
  PluginType,
15
7
  ProviderPlugin,
16
8
  ThemePlugin,
17
- } from "./types/index.ts";
9
+ } from "@tokentop/plugin-sdk";
10
+ import type { PluginsConfig } from "@/config/schema.ts";
11
+ import {
12
+ discoverLocalPlugins,
13
+ loadLocalPlugin,
14
+ loadNpmPlugin,
15
+ resolvePluginPath,
16
+ } from "./loader.ts";
17
+ import { installAllNpmPlugins, resolveNpmPluginPath } from "./npm-installer.ts";
18
18
 
19
19
  export type PluginSource = "builtin" | "local" | "npm";
20
20
 
@@ -1,11 +1,11 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { PluginPermissionError } from "@tokentop/plugin-sdk";
2
3
  import {
3
4
  deepFreeze,
4
5
  getActivePluginGuard,
5
6
  installGlobalFetchGuard,
6
7
  runInPluginGuard,
7
8
  } from "./sandbox-guard.ts";
8
- import { PluginPermissionError } from "./types/base.ts";
9
9
 
10
10
  const originalFetch = globalThis.fetch;
11
11
  const fetchCalls: string[] = [];
@@ -11,9 +11,9 @@
11
11
  * 2. **Deep-freeze** — prevents plugins from mutating the PluginContext.
12
12
  */
13
13
 
14
+ import { PluginPermissionError, type PluginPermissions } from "@tokentop/plugin-sdk";
14
15
  import { AsyncLocalStorage } from "async_hooks";
15
16
  import { createPluginLogger } from "./sandbox.ts";
16
- import { PluginPermissionError, type PluginPermissions } from "./types/base.ts";
17
17
 
18
18
  // ---------------------------------------------------------------------------
19
19
  // Plugin execution context (AsyncLocalStorage)
@@ -3,7 +3,7 @@ import {
3
3
  type PluginLogger,
4
4
  PluginPermissionError,
5
5
  type PluginPermissions,
6
- } from "./types/index.ts";
6
+ } from "@tokentop/plugin-sdk";
7
7
 
8
8
  export function createSandboxedHttpClient(
9
9
  pluginId: string,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const catppuccinLatteTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const catppuccinMochaTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const claudeCodeTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const draculaTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const githubLightTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const gruvboxDarkTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const gruvboxLightTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const kanagawaTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const nordTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const oneDarkTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const opencodeTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const rosePineDawnTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const rosePineTheme: ThemePlugin = {
4
4
  apiVersion: 2,
@@ -1,4 +1,4 @@
1
- import type { ThemePlugin } from "../types/theme.ts";
1
+ import type { ThemePlugin } from "@tokentop/plugin-sdk";
2
2
 
3
3
  export const solarizedLightTheme: ThemePlugin = {
4
4
  apiVersion: 2,