@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,30 +1,35 @@
1
1
  import type {
2
- ProviderPlugin,
3
- ProviderFetchContext,
4
- ProviderUsageData,
5
- UsageLimit,
6
- Credentials,
7
2
  CredentialResult,
3
+ Credentials,
8
4
  OAuthCredentials,
9
- RefreshedCredentials,
10
5
  PluginContext,
11
6
  ProviderAuth,
12
- } from '../types/provider.ts';
7
+ ProviderFetchContext,
8
+ ProviderPlugin,
9
+ ProviderUsageData,
10
+ RefreshedCredentials,
11
+ UsageLimit,
12
+ } from "../types/provider.ts";
13
13
 
14
- const GOOGLE_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
14
+ const GOOGLE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
15
15
 
16
16
  const GEMINI_CLI_HEADERS = {
17
- 'User-Agent': 'google-api-nodejs-client/9.15.1',
18
- 'X-Goog-Api-Client': 'gl-node/22.17.0',
19
- 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI',
17
+ "User-Agent": "google-api-nodejs-client/9.15.1",
18
+ "X-Goog-Api-Client": "gl-node/22.17.0",
19
+ "Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
20
20
  };
21
21
 
22
22
  // Base64 encoded to avoid triggering GitHub secret scanning.
23
23
  // These are publicly-known OAuth client IDs from the open-source Gemini CLI.
24
- const GEMINI_CLI_CLIENT_ID = process.env.GEMINI_CLI_CLIENT_ID
25
- ?? Buffer.from('NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t', 'base64').toString();
26
- const GEMINI_CLI_CLIENT_SECRET = process.env.GEMINI_CLI_CLIENT_SECRET
27
- ?? Buffer.from('R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw=', 'base64').toString();
24
+ const GEMINI_CLI_CLIENT_ID =
25
+ process.env.GEMINI_CLI_CLIENT_ID ??
26
+ Buffer.from(
27
+ "NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t",
28
+ "base64",
29
+ ).toString();
30
+ const GEMINI_CLI_CLIENT_SECRET =
31
+ process.env.GEMINI_CLI_CLIENT_SECRET ??
32
+ Buffer.from("R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw=", "base64").toString();
28
33
  const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
29
34
 
30
35
  interface GeminiCliCredentials {
@@ -61,30 +66,30 @@ interface LoadCodeAssistResponse {
61
66
 
62
67
  export const geminiPlugin: ProviderPlugin = {
63
68
  apiVersion: 2,
64
- id: 'gemini',
65
- type: 'provider',
66
- name: 'Gemini',
67
- version: '1.0.0',
69
+ id: "gemini",
70
+ type: "provider",
71
+ name: "Gemini",
72
+ version: "1.0.0",
68
73
 
69
74
  meta: {
70
- description: 'Gemini CLI usage tracking',
71
- homepage: 'https://cloud.google.com/gemini',
72
- brandColor: '#4285f4',
73
- providerAliases: ['google'],
75
+ description: "Gemini CLI usage tracking",
76
+ homepage: "https://cloud.google.com/gemini",
77
+ brandColor: "#4285f4",
78
+ providerAliases: ["google"],
74
79
  },
75
80
 
76
81
  permissions: {
77
82
  network: {
78
83
  enabled: true,
79
- allowedDomains: ['googleapis.com', 'oauth2.googleapis.com'],
84
+ allowedDomains: ["googleapis.com", "oauth2.googleapis.com"],
80
85
  },
81
86
  env: {
82
87
  read: true,
83
- vars: ['GOOGLE_CLOUD_PROJECT', 'GCP_PROJECT', 'GCLOUD_PROJECT'],
88
+ vars: ["GOOGLE_CLOUD_PROJECT", "GCP_PROJECT", "GCLOUD_PROJECT"],
84
89
  },
85
90
  filesystem: {
86
91
  read: true,
87
- paths: ['~/.gemini'],
92
+ paths: ["~/.gemini"],
88
93
  },
89
94
  },
90
95
 
@@ -96,7 +101,7 @@ export const geminiPlugin: ProviderPlugin = {
96
101
  },
97
102
 
98
103
  pricing: {
99
- modelsDevProviderId: 'google',
104
+ modelsDevProviderId: "google",
100
105
  },
101
106
 
102
107
  auth: {
@@ -105,14 +110,18 @@ export const geminiPlugin: ProviderPlugin = {
105
110
  const geminiData = await ctx.authSources.files.readJson<GeminiCliCredentials>(geminiPath);
106
111
  if (geminiData?.access_token || geminiData?.refresh_token) {
107
112
  const oauth: OAuthCredentials = {
108
- accessToken: geminiData.access_token ?? '',
113
+ accessToken: geminiData.access_token ?? "",
109
114
  ...(geminiData.refresh_token !== undefined && { refreshToken: geminiData.refresh_token }),
110
115
  ...(geminiData.expiry_date !== undefined && { expiresAt: geminiData.expiry_date }),
111
116
  };
112
- return { ok: true, credentials: { oauth, source: 'external' } };
117
+ return { ok: true, credentials: { oauth, source: "external" } };
113
118
  }
114
119
 
115
- return { ok: false, reason: 'missing', message: 'No Gemini CLI credentials found. Run `gemini` to authenticate.' };
120
+ return {
121
+ ok: false,
122
+ reason: "missing",
123
+ message: "No Gemini CLI credentials found. Run `gemini` to authenticate.",
124
+ };
116
125
  },
117
126
 
118
127
  isConfigured(credentials: Credentials): boolean {
@@ -122,14 +131,14 @@ export const geminiPlugin: ProviderPlugin = {
122
131
 
123
132
  async refreshToken(auth: OAuthCredentials): Promise<RefreshedCredentials> {
124
133
  if (!auth.refreshToken) {
125
- throw new Error('No refresh token available');
134
+ throw new Error("No refresh token available");
126
135
  }
127
136
 
128
- const response = await fetch('https://oauth2.googleapis.com/token', {
129
- method: 'POST',
130
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
137
+ const response = await fetch("https://oauth2.googleapis.com/token", {
138
+ method: "POST",
139
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
131
140
  body: new URLSearchParams({
132
- grant_type: 'refresh_token',
141
+ grant_type: "refresh_token",
133
142
  refresh_token: auth.refreshToken,
134
143
  client_id: GEMINI_CLI_CLIENT_ID,
135
144
  client_secret: GEMINI_CLI_CLIENT_SECRET,
@@ -137,7 +146,7 @@ export const geminiPlugin: ProviderPlugin = {
137
146
  });
138
147
 
139
148
  if (!response.ok) {
140
- const errorText = await response.text().catch(() => '');
149
+ const errorText = await response.text().catch(() => "");
141
150
  throw new Error(`Token refresh failed: ${response.status} ${errorText.slice(0, 100)}`);
142
151
  }
143
152
 
@@ -156,23 +165,25 @@ export const geminiPlugin: ProviderPlugin = {
156
165
  if (!credentials.oauth?.accessToken && !credentials.oauth?.refreshToken) {
157
166
  return {
158
167
  fetchedAt: Date.now(),
159
- error: 'OAuth token required. Login via Gemini CLI with Google account.',
168
+ error: "OAuth token required. Login via Gemini CLI with Google account.",
160
169
  };
161
170
  }
162
171
 
163
172
  let accessToken = credentials.oauth.accessToken;
164
- const needsRefresh = !accessToken ||
165
- (credentials.oauth.expiresAt && credentials.oauth.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS);
173
+ const needsRefresh =
174
+ !accessToken ||
175
+ (credentials.oauth.expiresAt &&
176
+ credentials.oauth.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS);
166
177
 
167
178
  if (needsRefresh && credentials.oauth.refreshToken) {
168
179
  try {
169
- log.debug('Access token expired or missing, refreshing...');
180
+ log.debug("Access token expired or missing, refreshing...");
170
181
  const refreshed = await this.refreshToken!(credentials.oauth);
171
182
  accessToken = refreshed.accessToken;
172
- log.debug('Token refreshed successfully');
183
+ log.debug("Token refreshed successfully");
173
184
  } catch (err) {
174
185
  const msg = err instanceof Error ? err.message : String(err);
175
- log.error('Token refresh failed', { error: msg });
186
+ log.error("Token refresh failed", { error: msg });
176
187
  return {
177
188
  fetchedAt: Date.now(),
178
189
  error: `Token refresh failed: ${msg}`,
@@ -183,29 +194,29 @@ export const geminiPlugin: ProviderPlugin = {
183
194
  if (!accessToken) {
184
195
  return {
185
196
  fetchedAt: Date.now(),
186
- error: 'No valid access token and refresh failed.',
197
+ error: "No valid access token and refresh failed.",
187
198
  };
188
199
  }
189
200
 
190
201
  try {
191
202
  const projectId = await ensureProjectContext(accessToken, http, log);
192
-
203
+
193
204
  const response = await http.fetch(`${GOOGLE_ENDPOINT}/v1internal:retrieveUserQuota`, {
194
- method: 'POST',
205
+ method: "POST",
195
206
  headers: {
196
- 'Authorization': `Bearer ${accessToken}`,
197
- 'Content-Type': 'application/json',
207
+ Authorization: `Bearer ${accessToken}`,
208
+ "Content-Type": "application/json",
198
209
  ...GEMINI_CLI_HEADERS,
199
210
  },
200
211
  body: JSON.stringify({ project: projectId }),
201
212
  });
202
213
 
203
214
  if (!response.ok) {
204
- const errorText = await response.text().catch(() => '');
215
+ const errorText = await response.text().catch(() => "");
205
216
  if (response.status === 401) {
206
217
  return {
207
218
  fetchedAt: Date.now(),
208
- error: 'OAuth token expired or invalid. Re-authenticate via Gemini CLI.',
219
+ error: "OAuth token expired or invalid. Re-authenticate via Gemini CLI.",
209
220
  };
210
221
  }
211
222
  return {
@@ -219,7 +230,7 @@ export const geminiPlugin: ProviderPlugin = {
219
230
 
220
231
  if (buckets.length === 0) {
221
232
  return {
222
- planType: 'Gemini Code Assist',
233
+ planType: "Gemini Code Assist",
223
234
  allowed: true,
224
235
  fetchedAt: Date.now(),
225
236
  };
@@ -230,10 +241,10 @@ export const geminiPlugin: ProviderPlugin = {
230
241
  let resetTime: string | undefined;
231
242
 
232
243
  for (const bucket of buckets) {
233
- if (typeof bucket.remainingFraction === 'number') {
244
+ if (typeof bucket.remainingFraction === "number") {
234
245
  const usedPercent = Math.round((1 - bucket.remainingFraction) * 100);
235
- const label = `${bucket.modelId ?? 'Unknown'} ${bucket.tokenType ?? ''}`.trim();
236
-
246
+ const label = `${bucket.modelId ?? "Unknown"} ${bucket.tokenType ?? ""}`.trim();
247
+
237
248
  const limit: UsageLimit = {
238
249
  usedPercent,
239
250
  label,
@@ -269,7 +280,7 @@ export const geminiPlugin: ProviderPlugin = {
269
280
  }
270
281
 
271
282
  const result: ProviderUsageData = {
272
- planType: 'Gemini Code Assist',
283
+ planType: "Gemini Code Assist",
273
284
  allowed: !limitReached,
274
285
  limitReached,
275
286
  fetchedAt: Date.now(),
@@ -280,7 +291,7 @@ export const geminiPlugin: ProviderPlugin = {
280
291
  if (primary) {
281
292
  const primaryLimit: UsageLimit = {
282
293
  usedPercent,
283
- label: 'Daily',
294
+ label: "Daily",
284
295
  };
285
296
  if (resetsAt !== undefined) {
286
297
  primaryLimit.resetsAt = resetsAt;
@@ -296,7 +307,7 @@ export const geminiPlugin: ProviderPlugin = {
296
307
  return result;
297
308
  } catch (err) {
298
309
  const msg = err instanceof Error ? err.message : String(err);
299
- log.error('Failed to fetch Google usage', { error: msg });
310
+ log.error("Failed to fetch Google usage", { error: msg });
300
311
  return {
301
312
  fetchedAt: Date.now(),
302
313
  error: msg,
@@ -307,85 +318,93 @@ export const geminiPlugin: ProviderPlugin = {
307
318
 
308
319
  async function ensureProjectContext(
309
320
  accessToken: string,
310
- http: ProviderFetchContext['http'],
311
- log: ProviderFetchContext['logger']
321
+ http: ProviderFetchContext["http"],
322
+ log: ProviderFetchContext["logger"],
312
323
  ): Promise<string> {
313
- const configuredProjectId = process.env.GOOGLE_CLOUD_PROJECT ??
314
- process.env.GCP_PROJECT ??
315
- process.env.GCLOUD_PROJECT;
316
-
324
+ const configuredProjectId =
325
+ process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT;
326
+
317
327
  if (configuredProjectId?.trim()) {
318
328
  return configuredProjectId.trim();
319
329
  }
320
330
 
321
331
  try {
322
332
  const response = await http.fetch(`${GOOGLE_ENDPOINT}/v1internal:loadCodeAssist`, {
323
- method: 'POST',
333
+ method: "POST",
324
334
  headers: {
325
- 'Authorization': `Bearer ${accessToken}`,
326
- 'Content-Type': 'application/json',
335
+ Authorization: `Bearer ${accessToken}`,
336
+ "Content-Type": "application/json",
327
337
  ...GEMINI_CLI_HEADERS,
328
338
  },
329
339
  body: JSON.stringify({
330
340
  metadata: {
331
- ideType: 'IDE_UNSPECIFIED',
332
- platform: 'PLATFORM_UNSPECIFIED',
333
- pluginType: 'GEMINI',
341
+ ideType: "IDE_UNSPECIFIED",
342
+ platform: "PLATFORM_UNSPECIFIED",
343
+ pluginType: "GEMINI",
334
344
  },
335
345
  }),
336
346
  });
337
347
 
338
348
  if (!response.ok) {
339
- log.debug('loadCodeAssist failed, using default project discovery');
349
+ log.debug("loadCodeAssist failed, using default project discovery");
340
350
  return await onboardToFreeProject(accessToken, http, log);
341
351
  }
342
352
 
343
353
  const data = (await response.json()) as LoadCodeAssistResponse;
344
-
354
+
345
355
  if (data.cloudaicompanionProject) {
346
356
  return data.cloudaicompanionProject;
347
357
  }
348
358
 
349
359
  return await onboardToFreeProject(accessToken, http, log);
350
360
  } catch (err) {
351
- log.debug('Project discovery failed', { error: err instanceof Error ? err.message : String(err) });
352
- throw new Error('Failed to discover Google Cloud project. Set GOOGLE_CLOUD_PROJECT environment variable.');
361
+ log.debug("Project discovery failed", {
362
+ error: err instanceof Error ? err.message : String(err),
363
+ });
364
+ throw new Error(
365
+ "Failed to discover Google Cloud project. Set GOOGLE_CLOUD_PROJECT environment variable.",
366
+ );
353
367
  }
354
368
  }
355
369
 
356
370
  async function onboardToFreeProject(
357
371
  accessToken: string,
358
- http: ProviderFetchContext['http'],
359
- log: ProviderFetchContext['logger']
372
+ http: ProviderFetchContext["http"],
373
+ log: ProviderFetchContext["logger"],
360
374
  ): Promise<string> {
361
375
  const response = await http.fetch(`${GOOGLE_ENDPOINT}/v1internal:onboardUser`, {
362
- method: 'POST',
376
+ method: "POST",
363
377
  headers: {
364
- 'Authorization': `Bearer ${accessToken}`,
365
- 'Content-Type': 'application/json',
378
+ Authorization: `Bearer ${accessToken}`,
379
+ "Content-Type": "application/json",
366
380
  ...GEMINI_CLI_HEADERS,
367
381
  },
368
382
  body: JSON.stringify({
369
- tierId: 'FREE',
383
+ tierId: "FREE",
370
384
  metadata: {
371
- ideType: 'IDE_UNSPECIFIED',
372
- platform: 'PLATFORM_UNSPECIFIED',
373
- pluginType: 'GEMINI',
385
+ ideType: "IDE_UNSPECIFIED",
386
+ platform: "PLATFORM_UNSPECIFIED",
387
+ pluginType: "GEMINI",
374
388
  },
375
389
  }),
376
390
  });
377
391
 
378
392
  if (!response.ok) {
379
- const errorText = await response.text().catch(() => '');
380
- log.error('onboardUser failed', { status: response.status, error: errorText.slice(0, 100) });
381
- throw new Error('Failed to onboard to free tier. Set GOOGLE_CLOUD_PROJECT environment variable.');
393
+ const errorText = await response.text().catch(() => "");
394
+ log.error("onboardUser failed", { status: response.status, error: errorText.slice(0, 100) });
395
+ throw new Error(
396
+ "Failed to onboard to free tier. Set GOOGLE_CLOUD_PROJECT environment variable.",
397
+ );
382
398
  }
383
399
 
384
- const data = (await response.json()) as { response?: { cloudaicompanionProject?: { id?: string } }; done?: boolean };
385
-
400
+ const data = (await response.json()) as {
401
+ response?: { cloudaicompanionProject?: { id?: string } };
402
+ done?: boolean;
403
+ };
404
+
386
405
  if (data.done && data.response?.cloudaicompanionProject?.id) {
387
406
  return data.response.cloudaicompanionProject.id;
388
407
  }
389
408
 
390
- throw new Error('Onboarding incomplete. Set GOOGLE_CLOUD_PROJECT environment variable.');
409
+ throw new Error("Onboarding incomplete. Set GOOGLE_CLOUD_PROJECT environment variable.");
391
410
  }
@@ -1,13 +1,13 @@
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
12
  interface CopilotUserResponse {
13
13
  access_type_sku?: string;
@@ -29,29 +29,29 @@ interface CopilotQuotaSnapshot {
29
29
 
30
30
  export const githubCopilotPlugin: ProviderPlugin = {
31
31
  apiVersion: 2,
32
- id: 'github-copilot',
33
- type: 'provider',
34
- name: 'GitHub Copilot',
35
- version: '1.0.0',
32
+ id: "github-copilot",
33
+ type: "provider",
34
+ name: "GitHub Copilot",
35
+ version: "1.0.0",
36
36
 
37
37
  meta: {
38
- description: 'GitHub Copilot usage tracking including premium requests',
39
- homepage: 'https://github.com/features/copilot',
40
- brandColor: '#6e40c9',
38
+ description: "GitHub Copilot usage tracking including premium requests",
39
+ homepage: "https://github.com/features/copilot",
40
+ brandColor: "#6e40c9",
41
41
  },
42
42
 
43
43
  permissions: {
44
44
  network: {
45
45
  enabled: true,
46
- allowedDomains: ['api.github.com'],
46
+ allowedDomains: ["api.github.com"],
47
47
  },
48
48
  env: {
49
49
  read: true,
50
- vars: ['GITHUB_TOKEN', 'GH_TOKEN'],
50
+ vars: ["GITHUB_TOKEN", "GH_TOKEN"],
51
51
  },
52
52
  filesystem: {
53
53
  read: true,
54
- paths: ['~/.config/github-copilot'],
54
+ paths: ["~/.config/github-copilot"],
55
55
  },
56
56
  },
57
57
 
@@ -63,42 +63,46 @@ export const githubCopilotPlugin: ProviderPlugin = {
63
63
  },
64
64
 
65
65
  pricing: {
66
- modelsDevProviderId: 'openai',
66
+ modelsDevProviderId: "openai",
67
67
  },
68
68
 
69
69
  auth: {
70
70
  async discover(ctx: PluginContext): Promise<CredentialResult> {
71
- const entry = await ctx.authSources.opencode.getProviderEntry('github-copilot');
71
+ const entry = await ctx.authSources.opencode.getProviderEntry("github-copilot");
72
72
  if (entry) {
73
73
  const token = entry.token || entry.key || entry.access;
74
74
  if (token) {
75
- return { ok: true, credentials: { apiKey: token, source: 'opencode' } };
75
+ return { ok: true, credentials: { apiKey: token, source: "opencode" } };
76
76
  }
77
77
  }
78
78
 
79
79
  // 2. Try env vars
80
- const githubToken = ctx.authSources.env.get('GITHUB_TOKEN');
80
+ const githubToken = ctx.authSources.env.get("GITHUB_TOKEN");
81
81
  if (githubToken) {
82
- return { ok: true, credentials: { apiKey: githubToken, source: 'env' } };
82
+ return { ok: true, credentials: { apiKey: githubToken, source: "env" } };
83
83
  }
84
84
 
85
- const ghToken = ctx.authSources.env.get('GH_TOKEN');
85
+ const ghToken = ctx.authSources.env.get("GH_TOKEN");
86
86
  if (ghToken) {
87
- return { ok: true, credentials: { apiKey: ghToken, source: 'env' } };
87
+ return { ok: true, credentials: { apiKey: ghToken, source: "env" } };
88
88
  }
89
89
 
90
90
  // 3. Try external file: ~/.config/github-copilot/hosts.json
91
91
  const hostsData = await ctx.authSources.files.readJson<{
92
- 'github.com'?: { oauth_token?: string; user?: string };
93
- }>('~/.config/github-copilot/hosts.json');
92
+ "github.com"?: { oauth_token?: string; user?: string };
93
+ }>("~/.config/github-copilot/hosts.json");
94
94
  if (hostsData) {
95
- const githubEntry = hostsData['github.com'];
95
+ const githubEntry = hostsData["github.com"];
96
96
  if (githubEntry?.oauth_token) {
97
- return { ok: true, credentials: { apiKey: githubEntry.oauth_token, source: 'external' } };
97
+ return { ok: true, credentials: { apiKey: githubEntry.oauth_token, source: "external" } };
98
98
  }
99
99
  }
100
100
 
101
- return { ok: false, reason: 'missing', message: 'No GitHub token found. Set GITHUB_TOKEN or install GitHub Copilot CLI.' };
101
+ return {
102
+ ok: false,
103
+ reason: "missing",
104
+ message: "No GitHub token found. Set GITHUB_TOKEN or install GitHub Copilot CLI.",
105
+ };
102
106
  },
103
107
 
104
108
  isConfigured(credentials: Credentials): boolean {
@@ -113,31 +117,31 @@ export const githubCopilotPlugin: ProviderPlugin = {
113
117
  if (!token) {
114
118
  return {
115
119
  fetchedAt: Date.now(),
116
- error: 'Not configured: missing GitHub token',
120
+ error: "Not configured: missing GitHub token",
117
121
  };
118
122
  }
119
123
 
120
124
  const headers: Record<string, string> = {
121
- 'Accept': 'application/vnd.github+json',
122
- 'Authorization': `token ${token}`,
123
- 'X-GitHub-Api-Version': '2022-11-28',
125
+ Accept: "application/vnd.github+json",
126
+ Authorization: `token ${token}`,
127
+ "X-GitHub-Api-Version": "2022-11-28",
124
128
  };
125
129
 
126
130
  try {
127
- const response = await http.fetch('https://api.github.com/copilot_internal/user', {
128
- method: 'GET',
131
+ const response = await http.fetch("https://api.github.com/copilot_internal/user", {
132
+ method: "GET",
129
133
  headers,
130
134
  });
131
135
 
132
136
  if (!response.ok) {
133
137
  if (response.status === 404) {
134
138
  return {
135
- planType: 'None',
139
+ planType: "None",
136
140
  fetchedAt: Date.now(),
137
- error: 'Copilot not enabled for this account',
141
+ error: "Copilot not enabled for this account",
138
142
  };
139
143
  }
140
- log.warn('Failed to fetch Copilot usage', { status: response.status });
144
+ log.warn("Failed to fetch Copilot usage", { status: response.status });
141
145
  return {
142
146
  fetchedAt: Date.now(),
143
147
  error: `API error: ${response.status} ${response.statusText}`,
@@ -146,7 +150,7 @@ export const githubCopilotPlugin: ProviderPlugin = {
146
150
 
147
151
  const data = (await response.json()) as CopilotUserResponse;
148
152
 
149
- const planType = data.copilot_plan ?? data.access_type_sku ?? 'Copilot';
153
+ const planType = data.copilot_plan ?? data.access_type_sku ?? "Copilot";
150
154
  const result: ProviderUsageData = {
151
155
  planType,
152
156
  fetchedAt: Date.now(),
@@ -158,13 +162,13 @@ export const githubCopilotPlugin: ProviderPlugin = {
158
162
  const usedPercent = premiumSnapshot.unlimited
159
163
  ? 0
160
164
  : premiumSnapshot.percent_remaining !== undefined
161
- ? Math.max(0, 100 - premiumSnapshot.percent_remaining)
162
- : premiumSnapshot.entitlement > 0
163
- ? (used / premiumSnapshot.entitlement) * 100
164
- : 0;
165
+ ? Math.max(0, 100 - premiumSnapshot.percent_remaining)
166
+ : premiumSnapshot.entitlement > 0
167
+ ? (used / premiumSnapshot.entitlement) * 100
168
+ : 0;
165
169
 
166
170
  const label = premiumSnapshot.unlimited
167
- ? 'Premium requests (unlimited)'
171
+ ? "Premium requests (unlimited)"
168
172
  : `Premium requests (${Math.max(0, used)}/${premiumSnapshot.entitlement})`;
169
173
 
170
174
  const primary: UsageLimit = {
@@ -184,10 +188,10 @@ export const githubCopilotPlugin: ProviderPlugin = {
184
188
 
185
189
  return result;
186
190
  } catch (err) {
187
- log.error('Failed to fetch Copilot usage', { error: err });
191
+ log.error("Failed to fetch Copilot usage", { error: err });
188
192
  return {
189
193
  fetchedAt: Date.now(),
190
- error: err instanceof Error ? err.message : 'Unknown error',
194
+ error: err instanceof Error ? err.message : "Unknown error",
191
195
  };
192
196
  }
193
197
  },
@@ -1,12 +1,10 @@
1
- export { anthropicPlugin } from './anthropic.ts';
2
- export { codexPlugin } from './codex.ts';
3
- export { antigravityPlugin } from './antigravity.ts';
4
- export { githubCopilotPlugin } from './github-copilot.ts';
5
- export { geminiPlugin } from './gemini.ts';
6
-
7
- export { openaiApiPlugin } from './openai-api.ts';
8
- export { perplexityPlugin } from './perplexity.ts';
9
- export { opencodeZenPlugin } from './opencode-zen.ts';
10
-
11
- export { zaiCodingPlanPlugin } from './zai.ts';
12
- export { minimaxPlugin, minimaxCodingPlanPlugin } from './minimax.ts';
1
+ export { anthropicPlugin } from "./anthropic.ts";
2
+ export { antigravityPlugin } from "./antigravity.ts";
3
+ export { codexPlugin } from "./codex.ts";
4
+ export { geminiPlugin } from "./gemini.ts";
5
+ export { githubCopilotPlugin } from "./github-copilot.ts";
6
+ export { minimaxCodingPlanPlugin, minimaxPlugin } from "./minimax.ts";
7
+ export { openaiApiPlugin } from "./openai-api.ts";
8
+ export { opencodeZenPlugin } from "./opencode-zen.ts";
9
+ export { perplexityPlugin } from "./perplexity.ts";
10
+ export { zaiCodingPlanPlugin } from "./zai.ts";