@tokentop/ttop 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,48 +1,48 @@
1
1
  import type {
2
- ProviderPlugin,
3
- ProviderFetchContext,
4
- ProviderUsageData,
5
- ProviderAuth,
6
- PluginContext,
7
2
  CredentialResult,
8
3
  Credentials,
9
- } from '../types/provider.ts';
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: 'perplexity',
28
- type: 'provider',
29
- name: 'Perplexity',
30
- version: '1.0.0',
27
+ id: "perplexity",
28
+ type: "provider",
29
+ name: "Perplexity",
30
+ version: "1.0.0",
31
31
 
32
32
  meta: {
33
- description: 'Perplexity AI API usage (credit-based)',
34
- homepage: 'https://www.perplexity.ai',
35
- brandColor: '#20b2aa',
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: ['api.perplexity.ai'],
41
+ allowedDomains: ["api.perplexity.ai"],
42
42
  },
43
43
  env: {
44
44
  read: true,
45
- vars: ['PERPLEXITY_API_KEY'],
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('perplexity');
59
+ const entry = await ctx.authSources.opencode.getProviderEntry("perplexity");
60
60
  if (entry) {
61
- if (entry.type === 'api' && entry.key) {
62
- return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
61
+ if (entry.type === "api" && entry.key) {
62
+ return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
63
63
  }
64
- if (entry.type === 'wellknown' && (entry.token || entry.key)) {
65
- return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
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('PERPLEXITY_API_KEY');
73
+ const apiKey = ctx.authSources.env.get("PERPLEXITY_API_KEY");
71
74
  if (apiKey) {
72
- return { ok: true, credentials: { apiKey, source: 'env' } };
75
+ return { ok: true, credentials: { apiKey, source: "env" } };
73
76
  }
74
77
 
75
- return { ok: false, reason: 'missing', message: 'No Perplexity API key found. Set PERPLEXITY_API_KEY or configure in OpenCode.' };
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: 'API key required. Set PERPLEXITY_API_KEY environment variable.',
96
+ error: "API key required. Set PERPLEXITY_API_KEY environment variable.",
90
97
  };
91
98
  }
92
99
 
93
- log.info('Checking Perplexity API', { keyPrefix: credentials.apiKey.slice(0, 8) });
100
+ log.info("Checking Perplexity API", { keyPrefix: credentials.apiKey.slice(0, 8) });
94
101
 
95
102
  try {
96
- const response = await http.fetch('https://api.perplexity.ai/chat/completions', {
97
- method: 'POST',
103
+ const response = await http.fetch("https://api.perplexity.ai/chat/completions", {
104
+ method: "POST",
98
105
  headers: {
99
- 'Authorization': `Bearer ${credentials.apiKey}`,
100
- 'Content-Type': 'application/json',
106
+ Authorization: `Bearer ${credentials.apiKey}`,
107
+ "Content-Type": "application/json",
101
108
  },
102
109
  body: JSON.stringify({
103
- model: 'sonar',
104
- messages: [{ role: 'user', content: 'ping' }],
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: 'Invalid API key',
119
+ error: "Invalid API key",
113
120
  };
114
121
  }
115
122
 
116
123
  if (response.status === 402) {
117
124
  return {
118
- planType: 'API',
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: '$0.00',
131
+ balance: "$0.00",
125
132
  },
126
- error: 'Insufficient credits',
133
+ error: "Insufficient credits",
127
134
  };
128
135
  }
129
136
 
130
137
  if (response.status === 429) {
131
138
  return {
132
- planType: 'API',
139
+ planType: "API",
133
140
  allowed: true,
134
141
  fetchedAt: Date.now(),
135
- error: 'Rate limited',
142
+ error: "Rate limited",
136
143
  };
137
144
  }
138
145
 
139
146
  if (response.ok) {
140
- const data = await response.json().catch(() => null) as { usage?: { prompt_tokens?: number; completion_tokens?: number } } | null;
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: 'API',
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: 'API',
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('Perplexity fetch error', { error: msg });
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
- ProviderPlugin,
2
+ CredentialResult,
3
+ Credentials,
4
+ PluginContext,
5
+ ProviderAuth,
3
6
  ProviderFetchContext,
7
+ ProviderPlugin,
4
8
  ProviderUsageData,
5
9
  UsageLimit,
6
- ProviderAuth,
7
- PluginContext,
8
- CredentialResult,
9
- Credentials,
10
- } from '../types/provider.ts';
10
+ } from "../types/provider.ts";
11
11
 
12
- const ZAI_USAGE_URL = 'https://api.z.ai/api/monitor/usage/quota/limit';
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: 'zai-coding-plan',
32
- type: 'provider',
33
- name: 'Z.ai Coding Plan',
34
- version: '1.0.0',
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: 'Z.ai coding plan usage and quota tracking',
38
- homepage: 'https://z.ai',
39
- brandColor: '#10b981',
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: ['api.z.ai'],
45
+ allowedDomains: ["api.z.ai"],
46
46
  },
47
47
  filesystem: {
48
48
  read: true,
49
- paths: ['~/.local/share/opencode'],
49
+ paths: ["~/.local/share/opencode"],
50
50
  },
51
51
  env: {
52
52
  read: true,
53
- vars: ['ZAI_API_KEY'],
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('zai-coding-plan');
67
+ const entry = await ctx.authSources.opencode.getProviderEntry("zai-coding-plan");
68
68
  if (entry) {
69
- if (entry.type === 'api' && entry.key) {
70
- return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
69
+ if (entry.type === "api" && entry.key) {
70
+ return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
71
71
  }
72
- if (entry.type === 'wellknown' && (entry.token || entry.key)) {
73
- return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
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 === 'oauth' && entry.access) {
76
- return { ok: true, credentials: { apiKey: entry.access, source: 'opencode' } };
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('ZAI_API_KEY');
84
+ const apiKey = ctx.authSources.env.get("ZAI_API_KEY");
82
85
  if (apiKey) {
83
- return { ok: true, credentials: { apiKey, source: 'env' } };
86
+ return { ok: true, credentials: { apiKey, source: "env" } };
84
87
  }
85
88
 
86
- return { ok: false, reason: 'missing', message: 'No Z.ai API key found. Configure Z.ai in OpenCode.' };
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: 'API key required. Configure Z.ai in OpenCode.',
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: 'GET',
115
+ method: "GET",
109
116
  headers: {
110
- 'Authorization': `Bearer ${token}`,
111
- 'Accept': 'application/json',
117
+ Authorization: `Bearer ${token}`,
118
+ Accept: "application/json",
112
119
  },
113
120
  });
114
121
 
115
122
  if (!response.ok) {
116
- log.warn('Failed to fetch Z.ai usage', { status: response.status });
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: 'Authorization failed. Check your Z.ai API key.',
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: 'Z.ai',
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: 'Z.ai',
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(l =>
170
- (l.usedPercent !== null && l.usedPercent >= 100) ||
171
- (typeof l.remaining === 'number' && l.remaining <= 0)
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: 'Z.ai Coding Plan',
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('Failed to fetch Z.ai usage', { error: err });
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 : 'Unknown error',
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 === 'number') {
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 = typeof limit.currentValue === 'number'
203
- ? limit.currentValue
204
- : (total !== undefined && typeof limit.remaining === 'number')
205
- ? Math.max(0, total - limit.remaining)
206
- : undefined;
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 'Quota';
225
+ if (!type) return "Quota";
217
226
  switch (type) {
218
- case 'TIME_LIMIT':
219
- return 'Time';
220
- case 'TOKENS_LIMIT':
221
- return 'Tokens';
227
+ case "TIME_LIMIT":
228
+ return "Time";
229
+ case "TOKENS_LIMIT":
230
+ return "Tokens";
222
231
  default:
223
- return type.replace(/_/g, ' ').toLowerCase();
232
+ return type.replace(/_/g, " ").toLowerCase();
224
233
  }
225
234
  }
@@ -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
- PluginType,
12
+ NotificationPlugin,
4
13
  PluginByType,
14
+ PluginType,
5
15
  ProviderPlugin,
6
- AgentPlugin,
7
16
  ThemePlugin,
8
- NotificationPlugin,
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 = 'builtin' | 'local' | 'npm';
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 = 'builtin'): void {
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 === 'builtin' && source !== 'builtin') {
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 'provider':
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 'agent':
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 'theme':
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 'notification':
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('./providers/index.ts'),
119
- import('./agents/index.ts'),
120
- import('./themes/index.ts'),
121
- import('./notifications/index.ts'),
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, 'builtin');
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, 'builtin');
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, 'builtin');
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, 'builtin');
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, 'local');
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, 'npm');
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 ['provider', 'agent', 'theme', 'notification'] as PluginType[]) {
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('provider')} providers, ` +
217
- `${this.count('agent')} agents, ${this.count('theme')} themes, ` +
218
- `${this.count('notification')} notifications`
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(obj: unknown): obj is { id: string; type: string; name: string; version: string } {
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 === 'object' &&
227
- 'id' in obj &&
228
- 'type' in obj &&
229
- 'name' in obj &&
230
- 'version' in obj
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 === 'provider';
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 === 'agent';
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 === 'theme';
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 === 'notification';
251
+ return hasPluginShape(obj) && obj.type === "notification";
248
252
  }
249
253
 
250
254
  export const pluginRegistry = new PluginRegistryImpl();