@tokentop/ttop 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,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
|
-
|
|
7
|
+
ProviderFetchContext,
|
|
8
|
+
ProviderPlugin,
|
|
9
|
+
ProviderUsageData,
|
|
10
|
+
RefreshedCredentials,
|
|
11
|
+
UsageLimit,
|
|
12
|
+
} from "../types/provider.ts";
|
|
13
13
|
|
|
14
|
-
const GOOGLE_ENDPOINT =
|
|
14
|
+
const GOOGLE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
15
15
|
|
|
16
16
|
const GEMINI_CLI_HEADERS = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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:
|
|
65
|
-
type:
|
|
66
|
-
name:
|
|
67
|
-
version:
|
|
69
|
+
id: "gemini",
|
|
70
|
+
type: "provider",
|
|
71
|
+
name: "Gemini",
|
|
72
|
+
version: "1.0.0",
|
|
68
73
|
|
|
69
74
|
meta: {
|
|
70
|
-
description:
|
|
71
|
-
homepage:
|
|
72
|
-
brandColor:
|
|
73
|
-
providerAliases: [
|
|
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: [
|
|
84
|
+
allowedDomains: ["googleapis.com", "oauth2.googleapis.com"],
|
|
80
85
|
},
|
|
81
86
|
env: {
|
|
82
87
|
read: true,
|
|
83
|
-
vars: [
|
|
88
|
+
vars: ["GOOGLE_CLOUD_PROJECT", "GCP_PROJECT", "GCLOUD_PROJECT"],
|
|
84
89
|
},
|
|
85
90
|
filesystem: {
|
|
86
91
|
read: true,
|
|
87
|
-
paths: [
|
|
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:
|
|
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:
|
|
117
|
+
return { ok: true, credentials: { oauth, source: "external" } };
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
return {
|
|
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(
|
|
134
|
+
throw new Error("No refresh token available");
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
const response = await fetch(
|
|
129
|
-
method:
|
|
130
|
-
headers: {
|
|
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:
|
|
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:
|
|
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 =
|
|
165
|
-
|
|
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(
|
|
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(
|
|
183
|
+
log.debug("Token refreshed successfully");
|
|
173
184
|
} catch (err) {
|
|
174
185
|
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
-
log.error(
|
|
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:
|
|
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:
|
|
205
|
+
method: "POST",
|
|
195
206
|
headers: {
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
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:
|
|
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 ===
|
|
244
|
+
if (typeof bucket.remainingFraction === "number") {
|
|
234
245
|
const usedPercent = Math.round((1 - bucket.remainingFraction) * 100);
|
|
235
|
-
const label = `${bucket.modelId ??
|
|
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:
|
|
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:
|
|
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(
|
|
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[
|
|
311
|
-
log: ProviderFetchContext[
|
|
321
|
+
http: ProviderFetchContext["http"],
|
|
322
|
+
log: ProviderFetchContext["logger"],
|
|
312
323
|
): Promise<string> {
|
|
313
|
-
const configuredProjectId =
|
|
314
|
-
|
|
315
|
-
|
|
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:
|
|
333
|
+
method: "POST",
|
|
324
334
|
headers: {
|
|
325
|
-
|
|
326
|
-
|
|
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:
|
|
332
|
-
platform:
|
|
333
|
-
pluginType:
|
|
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(
|
|
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(
|
|
352
|
-
|
|
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[
|
|
359
|
-
log: ProviderFetchContext[
|
|
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:
|
|
376
|
+
method: "POST",
|
|
363
377
|
headers: {
|
|
364
|
-
|
|
365
|
-
|
|
378
|
+
Authorization: `Bearer ${accessToken}`,
|
|
379
|
+
"Content-Type": "application/json",
|
|
366
380
|
...GEMINI_CLI_HEADERS,
|
|
367
381
|
},
|
|
368
382
|
body: JSON.stringify({
|
|
369
|
-
tierId:
|
|
383
|
+
tierId: "FREE",
|
|
370
384
|
metadata: {
|
|
371
|
-
ideType:
|
|
372
|
-
platform:
|
|
373
|
-
pluginType:
|
|
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(
|
|
381
|
-
throw new Error(
|
|
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 {
|
|
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(
|
|
409
|
+
throw new Error("Onboarding incomplete. Set GOOGLE_CLOUD_PROJECT environment variable.");
|
|
391
410
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
CredentialResult,
|
|
3
|
+
Credentials,
|
|
4
|
+
PluginContext,
|
|
5
|
+
ProviderAuth,
|
|
3
6
|
ProviderFetchContext,
|
|
7
|
+
ProviderPlugin,
|
|
4
8
|
ProviderUsageData,
|
|
5
9
|
UsageLimit,
|
|
6
|
-
|
|
7
|
-
PluginContext,
|
|
8
|
-
CredentialResult,
|
|
9
|
-
Credentials,
|
|
10
|
-
} from '../types/provider.ts';
|
|
10
|
+
} from "../types/provider.ts";
|
|
11
11
|
|
|
12
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:
|
|
33
|
-
type:
|
|
34
|
-
name:
|
|
35
|
-
version:
|
|
32
|
+
id: "github-copilot",
|
|
33
|
+
type: "provider",
|
|
34
|
+
name: "GitHub Copilot",
|
|
35
|
+
version: "1.0.0",
|
|
36
36
|
|
|
37
37
|
meta: {
|
|
38
|
-
description:
|
|
39
|
-
homepage:
|
|
40
|
-
brandColor:
|
|
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: [
|
|
46
|
+
allowedDomains: ["api.github.com"],
|
|
47
47
|
},
|
|
48
48
|
env: {
|
|
49
49
|
read: true,
|
|
50
|
-
vars: [
|
|
50
|
+
vars: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
51
51
|
},
|
|
52
52
|
filesystem: {
|
|
53
53
|
read: true,
|
|
54
|
-
paths: [
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
80
|
+
const githubToken = ctx.authSources.env.get("GITHUB_TOKEN");
|
|
81
81
|
if (githubToken) {
|
|
82
|
-
return { ok: true, credentials: { apiKey: githubToken, source:
|
|
82
|
+
return { ok: true, credentials: { apiKey: githubToken, source: "env" } };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const ghToken = ctx.authSources.env.get(
|
|
85
|
+
const ghToken = ctx.authSources.env.get("GH_TOKEN");
|
|
86
86
|
if (ghToken) {
|
|
87
|
-
return { ok: true, credentials: { apiKey: ghToken, source:
|
|
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
|
-
|
|
93
|
-
}>(
|
|
92
|
+
"github.com"?: { oauth_token?: string; user?: string };
|
|
93
|
+
}>("~/.config/github-copilot/hosts.json");
|
|
94
94
|
if (hostsData) {
|
|
95
|
-
const githubEntry = hostsData[
|
|
95
|
+
const githubEntry = hostsData["github.com"];
|
|
96
96
|
if (githubEntry?.oauth_token) {
|
|
97
|
-
return { ok: true, credentials: { apiKey: githubEntry.oauth_token, source:
|
|
97
|
+
return { ok: true, credentials: { apiKey: githubEntry.oauth_token, source: "external" } };
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
return {
|
|
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:
|
|
120
|
+
error: "Not configured: missing GitHub token",
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
const headers: Record<string, string> = {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
128
|
-
method:
|
|
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:
|
|
139
|
+
planType: "None",
|
|
136
140
|
fetchedAt: Date.now(),
|
|
137
|
-
error:
|
|
141
|
+
error: "Copilot not enabled for this account",
|
|
138
142
|
};
|
|
139
143
|
}
|
|
140
|
-
log.warn(
|
|
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 ??
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
?
|
|
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(
|
|
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 :
|
|
194
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
191
195
|
};
|
|
192
196
|
}
|
|
193
197
|
},
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
export { anthropicPlugin } from
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
|
|
7
|
-
export { openaiApiPlugin } from
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
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";
|