@algochad/archcoder 2.0.2

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 (157) hide show
  1. package/README.md +113 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +5108 -0
  5. package/bin/cli.test.js +56 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +67 -0
  12. package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
  13. package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
  14. package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
  15. package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
  16. package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
  17. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  18. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  19. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  20. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  21. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  22. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  23. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  24. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  25. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  26. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  27. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  28. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  29. package/dist/assets/index-CtCEGYrr.css +1 -0
  30. package/dist/assets/index-o_d2wtWC.js +48 -0
  31. package/dist/assets/main-5QGBtzdq.css +1 -0
  32. package/dist/assets/main-B6oiMU86.js +8033 -0
  33. package/dist/assets/vendor--DbVqbJpV.css +1 -0
  34. package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
  35. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  36. package/dist/assets/worker-bqd4RMrj.js +155 -0
  37. package/dist/favicon-16.png +0 -0
  38. package/dist/favicon-32.png +0 -0
  39. package/dist/favicon.png +0 -0
  40. package/dist/favicon.svg +67 -0
  41. package/dist/index.html +533 -0
  42. package/dist/logo-dark-192x192.png +0 -0
  43. package/dist/logo-dark-512x512.svg +16 -0
  44. package/dist/logo-light-192x192.png +0 -0
  45. package/dist/logo-light-512x512.svg +16 -0
  46. package/dist/pwa-192.png +0 -0
  47. package/dist/pwa-512.png +0 -0
  48. package/dist/pwa-maskable-192.png +0 -0
  49. package/dist/pwa-maskable-512.png +0 -0
  50. package/dist/site.webmanifest +22 -0
  51. package/dist/sw.js +1 -0
  52. package/package.json +107 -0
  53. package/public/apple-touch-icon-120x120.png +0 -0
  54. package/public/apple-touch-icon-152x152.png +0 -0
  55. package/public/apple-touch-icon-167x167.png +0 -0
  56. package/public/apple-touch-icon-180x180.png +0 -0
  57. package/public/apple-touch-icon.png +0 -0
  58. package/public/apple-touch-icon.svg +67 -0
  59. package/public/favicon-16.png +0 -0
  60. package/public/favicon-32.png +0 -0
  61. package/public/favicon.png +0 -0
  62. package/public/favicon.svg +67 -0
  63. package/public/logo-dark-192x192.png +0 -0
  64. package/public/logo-dark-512x512.svg +16 -0
  65. package/public/logo-light-192x192.png +0 -0
  66. package/public/logo-light-512x512.svg +16 -0
  67. package/public/pwa-192.png +0 -0
  68. package/public/pwa-512.png +0 -0
  69. package/public/pwa-maskable-192.png +0 -0
  70. package/public/pwa-maskable-512.png +0 -0
  71. package/public/site.webmanifest +22 -0
  72. package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
  73. package/server/index.d.ts +37 -0
  74. package/server/index.js +14694 -0
  75. package/server/lib/cloudflare-tunnel.js +650 -0
  76. package/server/lib/git/DOCUMENTATION.md +146 -0
  77. package/server/lib/git/credentials.js +74 -0
  78. package/server/lib/git/identity-storage.js +110 -0
  79. package/server/lib/git/index.js +6 -0
  80. package/server/lib/git/service.js +3117 -0
  81. package/server/lib/github/DOCUMENTATION.md +170 -0
  82. package/server/lib/github/auth.js +307 -0
  83. package/server/lib/github/device-flow.js +50 -0
  84. package/server/lib/github/index.js +24 -0
  85. package/server/lib/github/octokit.js +10 -0
  86. package/server/lib/github/pr-status.js +478 -0
  87. package/server/lib/github/repo/index.js +55 -0
  88. package/server/lib/installer/desktop.js +289 -0
  89. package/server/lib/installer/download.js +208 -0
  90. package/server/lib/installer/index.js +45 -0
  91. package/server/lib/installer/platform.js +100 -0
  92. package/server/lib/notifications/DOCUMENTATION.md +61 -0
  93. package/server/lib/notifications/index.js +1 -0
  94. package/server/lib/notifications/message.js +49 -0
  95. package/server/lib/notifications/message.test.js +59 -0
  96. package/server/lib/opencode/DOCUMENTATION.md +59 -0
  97. package/server/lib/opencode/agents.js +634 -0
  98. package/server/lib/opencode/auth.js +81 -0
  99. package/server/lib/opencode/commands.js +339 -0
  100. package/server/lib/opencode/index.js +66 -0
  101. package/server/lib/opencode/mcp.js +206 -0
  102. package/server/lib/opencode/providers.js +96 -0
  103. package/server/lib/opencode/shared.js +527 -0
  104. package/server/lib/opencode/skills.js +480 -0
  105. package/server/lib/opencode/tunnel-auth.js +591 -0
  106. package/server/lib/opencode/ui-auth.js +510 -0
  107. package/server/lib/package-manager.js +505 -0
  108. package/server/lib/quota/DOCUMENTATION.md +55 -0
  109. package/server/lib/quota/index.js +24 -0
  110. package/server/lib/quota/providers/claude.js +107 -0
  111. package/server/lib/quota/providers/codex.js +113 -0
  112. package/server/lib/quota/providers/copilot.js +165 -0
  113. package/server/lib/quota/providers/google/api.js +92 -0
  114. package/server/lib/quota/providers/google/auth.js +108 -0
  115. package/server/lib/quota/providers/google/index.js +124 -0
  116. package/server/lib/quota/providers/google/transforms.js +109 -0
  117. package/server/lib/quota/providers/index.js +152 -0
  118. package/server/lib/quota/providers/interface.js +55 -0
  119. package/server/lib/quota/providers/kimi.js +108 -0
  120. package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
  121. package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
  122. package/server/lib/quota/providers/minimax-shared.js +136 -0
  123. package/server/lib/quota/providers/nanogpt.js +124 -0
  124. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  125. package/server/lib/quota/providers/openai.js +91 -0
  126. package/server/lib/quota/providers/openrouter.js +92 -0
  127. package/server/lib/quota/providers/zai.js +91 -0
  128. package/server/lib/quota/utils/auth.js +46 -0
  129. package/server/lib/quota/utils/formatters.js +76 -0
  130. package/server/lib/quota/utils/index.js +10 -0
  131. package/server/lib/quota/utils/transformers.js +55 -0
  132. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  133. package/server/lib/skills-catalog/cache.js +32 -0
  134. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  135. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  136. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  137. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  138. package/server/lib/skills-catalog/curated-sources.js +21 -0
  139. package/server/lib/skills-catalog/git.js +77 -0
  140. package/server/lib/skills-catalog/index.js +42 -0
  141. package/server/lib/skills-catalog/install.js +294 -0
  142. package/server/lib/skills-catalog/scan.js +221 -0
  143. package/server/lib/skills-catalog/source.js +85 -0
  144. package/server/lib/terminal/DOCUMENTATION.md +114 -0
  145. package/server/lib/terminal/index.js +12 -0
  146. package/server/lib/terminal/input-ws-protocol.js +66 -0
  147. package/server/lib/terminal/input-ws-protocol.test.js +138 -0
  148. package/server/lib/tts/DOCUMENTATION.md +134 -0
  149. package/server/lib/tts/index.js +16 -0
  150. package/server/lib/tts/service.js +162 -0
  151. package/server/lib/tts/summarization.js +171 -0
  152. package/server/lib/tunnels/index.js +166 -0
  153. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  154. package/server/lib/tunnels/registry.js +51 -0
  155. package/server/lib/tunnels/types.js +219 -0
  156. package/server/lib/utils/lru.js +107 -0
  157. package/server/lib/utils/sse.js +121 -0
@@ -0,0 +1,113 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import {
3
+ getAuthEntry,
4
+ normalizeAuthEntry,
5
+ buildResult,
6
+ toUsageWindow,
7
+ toNumber,
8
+ toTimestamp,
9
+ formatMoney
10
+ } from '../utils/index.js';
11
+
12
+ export const providerId = 'codex';
13
+ export const providerName = 'Codex';
14
+ export const aliases = ['openai', 'codex', 'chatgpt'];
15
+
16
+ export const isConfigured = () => {
17
+ const auth = readAuthFile();
18
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
19
+ return Boolean(entry?.access || entry?.token);
20
+ };
21
+
22
+ export const fetchQuota = async () => {
23
+ const auth = readAuthFile();
24
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
25
+ const accessToken = entry?.access ?? entry?.token;
26
+ const accountId = entry?.accountId;
27
+
28
+ if (!accessToken) {
29
+ return buildResult({
30
+ providerId,
31
+ providerName,
32
+ ok: false,
33
+ configured: false,
34
+ error: 'Not configured'
35
+ });
36
+ }
37
+
38
+ try {
39
+ const headers = {
40
+ Authorization: `Bearer ${accessToken}`,
41
+ 'Content-Type': 'application/json',
42
+ ...(accountId ? { 'ChatGPT-Account-Id': accountId } : {})
43
+ };
44
+ const response = await fetch('https://chatgpt.com/backend-api/wham/usage', {
45
+ method: 'GET',
46
+ headers
47
+ });
48
+
49
+ if (!response.ok) {
50
+ return buildResult({
51
+ providerId,
52
+ providerName,
53
+ ok: false,
54
+ configured: true,
55
+ error: response.status === 401
56
+ ? 'Session expired \u2014 please re-authenticate with OpenAI'
57
+ : `API error: ${response.status}`
58
+ });
59
+ }
60
+
61
+ const payload = await response.json();
62
+ const primary = payload?.rate_limit?.primary_window ?? null;
63
+ const secondary = payload?.rate_limit?.secondary_window ?? null;
64
+ const credits = payload?.credits ?? null;
65
+
66
+ const windows = {};
67
+ if (primary) {
68
+ windows['5h'] = toUsageWindow({
69
+ usedPercent: toNumber(primary.used_percent),
70
+ windowSeconds: toNumber(primary.limit_window_seconds),
71
+ resetAt: toTimestamp(primary.reset_at)
72
+ });
73
+ }
74
+ if (secondary) {
75
+ windows['weekly'] = toUsageWindow({
76
+ usedPercent: toNumber(secondary.used_percent),
77
+ windowSeconds: toNumber(secondary.limit_window_seconds),
78
+ resetAt: toTimestamp(secondary.reset_at)
79
+ });
80
+ }
81
+ if (credits) {
82
+ const balance = toNumber(credits.balance);
83
+ const unlimited = Boolean(credits.unlimited);
84
+ const label = unlimited
85
+ ? 'Unlimited'
86
+ : balance !== null
87
+ ? `$${formatMoney(balance)} remaining`
88
+ : null;
89
+ windows.credits = toUsageWindow({
90
+ usedPercent: null,
91
+ windowSeconds: null,
92
+ resetAt: null,
93
+ valueLabel: label
94
+ });
95
+ }
96
+
97
+ return buildResult({
98
+ providerId,
99
+ providerName,
100
+ ok: true,
101
+ configured: true,
102
+ usage: { windows }
103
+ });
104
+ } catch (error) {
105
+ return buildResult({
106
+ providerId,
107
+ providerName,
108
+ ok: false,
109
+ configured: true,
110
+ error: error instanceof Error ? error.message : 'Request failed'
111
+ });
112
+ }
113
+ };
@@ -0,0 +1,165 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import {
3
+ getAuthEntry,
4
+ normalizeAuthEntry,
5
+ buildResult,
6
+ toUsageWindow,
7
+ toNumber,
8
+ toTimestamp
9
+ } from '../utils/index.js';
10
+
11
+ const buildCopilotWindows = (payload) => {
12
+ const quota = payload?.quota_snapshots ?? {};
13
+ const resetAt = toTimestamp(payload?.quota_reset_date);
14
+ const windows = {};
15
+
16
+ const addWindow = (label, snapshot) => {
17
+ if (!snapshot) return;
18
+ const entitlement = toNumber(snapshot.entitlement);
19
+ const remaining = toNumber(snapshot.remaining);
20
+ const usedPercent = entitlement && remaining !== null
21
+ ? Math.max(0, Math.min(100, 100 - (remaining / entitlement) * 100))
22
+ : null;
23
+ const valueLabel = entitlement !== null && remaining !== null
24
+ ? `${remaining.toFixed(0)} / ${entitlement.toFixed(0)} left`
25
+ : null;
26
+ windows[label] = toUsageWindow({
27
+ usedPercent,
28
+ windowSeconds: null,
29
+ resetAt,
30
+ valueLabel
31
+ });
32
+ };
33
+
34
+ addWindow('chat', quota.chat);
35
+ addWindow('completions', quota.completions);
36
+ addWindow('premium', quota.premium_interactions);
37
+
38
+ return windows;
39
+ };
40
+
41
+ export const providerId = 'github-copilot';
42
+ export const providerName = 'GitHub Copilot';
43
+ export const aliases = ['github-copilot', 'copilot'];
44
+
45
+ export const isConfigured = () => {
46
+ const auth = readAuthFile();
47
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
48
+ return Boolean(entry?.access || entry?.token);
49
+ };
50
+
51
+ export const fetchQuota = async () => {
52
+ const auth = readAuthFile();
53
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
54
+ const accessToken = entry?.access ?? entry?.token;
55
+
56
+ if (!accessToken) {
57
+ return buildResult({
58
+ providerId,
59
+ providerName,
60
+ ok: false,
61
+ configured: false,
62
+ error: 'Not configured'
63
+ });
64
+ }
65
+
66
+ try {
67
+ const response = await fetch('https://api.github.com/copilot_internal/user', {
68
+ method: 'GET',
69
+ headers: {
70
+ Authorization: `token ${accessToken}`,
71
+ Accept: 'application/json',
72
+ 'Editor-Version': 'vscode/1.96.2',
73
+ 'X-Github-Api-Version': '2025-04-01'
74
+ }
75
+ });
76
+
77
+ if (!response.ok) {
78
+ return buildResult({
79
+ providerId,
80
+ providerName,
81
+ ok: false,
82
+ configured: true,
83
+ error: `API error: ${response.status}`
84
+ });
85
+ }
86
+
87
+ const payload = await response.json();
88
+ return buildResult({
89
+ providerId,
90
+ providerName,
91
+ ok: true,
92
+ configured: true,
93
+ usage: { windows: buildCopilotWindows(payload) }
94
+ });
95
+ } catch (error) {
96
+ return buildResult({
97
+ providerId,
98
+ providerName,
99
+ ok: false,
100
+ configured: true,
101
+ error: error instanceof Error ? error.message : 'Request failed'
102
+ });
103
+ }
104
+ };
105
+
106
+ export const providerIdAddon = 'github-copilot-addon';
107
+ export const providerNameAddon = 'GitHub Copilot Add-on';
108
+
109
+ export const fetchQuotaAddon = async () => {
110
+ const auth = readAuthFile();
111
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
112
+ const accessToken = entry?.access ?? entry?.token;
113
+
114
+ if (!accessToken) {
115
+ return buildResult({
116
+ providerId: providerIdAddon,
117
+ providerName: providerNameAddon,
118
+ ok: false,
119
+ configured: false,
120
+ error: 'Not configured'
121
+ });
122
+ }
123
+
124
+ try {
125
+ const response = await fetch('https://api.github.com/copilot_internal/user', {
126
+ method: 'GET',
127
+ headers: {
128
+ Authorization: `token ${accessToken}`,
129
+ Accept: 'application/json',
130
+ 'Editor-Version': 'vscode/1.96.2',
131
+ 'X-Github-Api-Version': '2025-04-01'
132
+ }
133
+ });
134
+
135
+ if (!response.ok) {
136
+ return buildResult({
137
+ providerId: providerIdAddon,
138
+ providerName: providerNameAddon,
139
+ ok: false,
140
+ configured: true,
141
+ error: `API error: ${response.status}`
142
+ });
143
+ }
144
+
145
+ const payload = await response.json();
146
+ const windows = buildCopilotWindows(payload);
147
+ const premium = windows.premium ? { premium: windows.premium } : windows;
148
+
149
+ return buildResult({
150
+ providerId: providerIdAddon,
151
+ providerName: providerNameAddon,
152
+ ok: true,
153
+ configured: true,
154
+ usage: { windows: premium }
155
+ });
156
+ } catch (error) {
157
+ return buildResult({
158
+ providerId: providerIdAddon,
159
+ providerName: providerNameAddon,
160
+ ok: false,
161
+ configured: true,
162
+ error: error instanceof Error ? error.message : 'Request failed'
163
+ });
164
+ }
165
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Google Provider - API
3
+ *
4
+ * API calls for Google quota providers.
5
+ * @module quota/providers/google/api
6
+ */
7
+
8
+ const GOOGLE_PRIMARY_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
9
+
10
+ const GOOGLE_ENDPOINTS = [
11
+ 'https://daily-cloudcode-pa.sandbox.googleapis.com',
12
+ 'https://autopush-cloudcode-pa.sandbox.googleapis.com',
13
+ GOOGLE_PRIMARY_ENDPOINT
14
+ ];
15
+
16
+ const GOOGLE_HEADERS = {
17
+ 'User-Agent': 'antigravity/1.11.5 windows/amd64',
18
+ 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
19
+ 'Client-Metadata':
20
+ '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
21
+ };
22
+
23
+ export const refreshGoogleAccessToken = async (refreshToken, clientId, clientSecret) => {
24
+ const response = await fetch('https://oauth2.googleapis.com/token', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
27
+ body: new URLSearchParams({
28
+ client_id: clientId,
29
+ client_secret: clientSecret,
30
+ refresh_token: refreshToken,
31
+ grant_type: 'refresh_token'
32
+ })
33
+ });
34
+
35
+ if (!response.ok) {
36
+ return null;
37
+ }
38
+
39
+ const data = await response.json();
40
+ return typeof data?.access_token === 'string' ? data.access_token : null;
41
+ };
42
+
43
+ export const fetchGoogleQuotaBuckets = async (accessToken, projectId) => {
44
+ const body = projectId ? { project: projectId } : {};
45
+
46
+ try {
47
+ const response = await fetch(`${GOOGLE_PRIMARY_ENDPOINT}/v1internal:retrieveUserQuota`, {
48
+ method: 'POST',
49
+ headers: {
50
+ Authorization: `Bearer ${accessToken}`,
51
+ 'Content-Type': 'application/json'
52
+ },
53
+ body: JSON.stringify(body),
54
+ signal: AbortSignal.timeout(15000)
55
+ });
56
+
57
+ if (!response.ok) {
58
+ return null;
59
+ }
60
+
61
+ return await response.json();
62
+ } catch {
63
+ return null;
64
+ }
65
+ };
66
+
67
+ export const fetchGoogleModels = async (accessToken, projectId) => {
68
+ const body = projectId ? { project: projectId } : {};
69
+
70
+ for (const endpoint of GOOGLE_ENDPOINTS) {
71
+ try {
72
+ const response = await fetch(`${endpoint}/v1internal:fetchAvailableModels`, {
73
+ method: 'POST',
74
+ headers: {
75
+ Authorization: `Bearer ${accessToken}`,
76
+ 'Content-Type': 'application/json',
77
+ ...GOOGLE_HEADERS
78
+ },
79
+ body: JSON.stringify(body),
80
+ signal: AbortSignal.timeout(15000)
81
+ });
82
+
83
+ if (response.ok) {
84
+ return await response.json();
85
+ }
86
+ } catch {
87
+ continue;
88
+ }
89
+ }
90
+
91
+ return null;
92
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Google Provider - Auth
3
+ *
4
+ * Authentication resolution logic for Google quota providers.
5
+ * @module quota/providers/google/auth
6
+ */
7
+
8
+ import {
9
+ ANTIGRAVITY_ACCOUNTS_PATHS,
10
+ readJsonFile,
11
+ getAuthEntry,
12
+ normalizeAuthEntry,
13
+ asObject,
14
+ asNonEmptyString,
15
+ toTimestamp
16
+ } from '../../utils/index.js';
17
+ import { readAuthFile } from '../../../opencode/auth.js';
18
+ import { parseGoogleRefreshToken } from './transforms.js';
19
+
20
+ const ANTIGRAVITY_GOOGLE_CLIENT_ID =
21
+ '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
22
+ const ANTIGRAVITY_GOOGLE_CLIENT_SECRET = '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';
23
+ const GEMINI_GOOGLE_CLIENT_ID =
24
+ '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
25
+ const GEMINI_GOOGLE_CLIENT_SECRET = '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';
26
+ export const DEFAULT_PROJECT_ID = 'rising-fact-p41fc';
27
+
28
+ export const resolveGoogleOAuthClient = (sourceId) => {
29
+ if (sourceId === 'gemini') {
30
+ return {
31
+ clientId: GEMINI_GOOGLE_CLIENT_ID,
32
+ clientSecret: GEMINI_GOOGLE_CLIENT_SECRET
33
+ };
34
+ }
35
+
36
+ return {
37
+ clientId: ANTIGRAVITY_GOOGLE_CLIENT_ID,
38
+ clientSecret: ANTIGRAVITY_GOOGLE_CLIENT_SECRET
39
+ };
40
+ };
41
+
42
+ export const resolveGeminiCliAuth = (auth) => {
43
+ const entry = normalizeAuthEntry(getAuthEntry(auth, ['google', 'google.oauth']));
44
+ const entryObject = asObject(entry);
45
+ if (!entryObject) {
46
+ return null;
47
+ }
48
+
49
+ const oauthObject = asObject(entryObject.oauth) ?? entryObject;
50
+ const accessToken = asNonEmptyString(oauthObject.access) ?? asNonEmptyString(oauthObject.token);
51
+ const refreshParts = parseGoogleRefreshToken(oauthObject.refresh);
52
+
53
+ if (!accessToken && !refreshParts.refreshToken) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ sourceId: 'gemini',
59
+ sourceLabel: 'Gemini',
60
+ accessToken,
61
+ refreshToken: refreshParts.refreshToken,
62
+ projectId: refreshParts.projectId ?? refreshParts.managedProjectId,
63
+ expires: toTimestamp(oauthObject.expires)
64
+ };
65
+ };
66
+
67
+ export const resolveAntigravityAuth = () => {
68
+ for (const filePath of ANTIGRAVITY_ACCOUNTS_PATHS) {
69
+ const data = readJsonFile(filePath);
70
+ const accounts = data?.accounts;
71
+ if (Array.isArray(accounts) && accounts.length > 0) {
72
+ const index = typeof data.activeIndex === 'number' ? data.activeIndex : 0;
73
+ const account = accounts[index] ?? accounts[0];
74
+ if (account?.refreshToken) {
75
+ const refreshParts = parseGoogleRefreshToken(account.refreshToken);
76
+ return {
77
+ sourceId: 'antigravity',
78
+ sourceLabel: 'Antigravity',
79
+ refreshToken: refreshParts.refreshToken,
80
+ projectId: asNonEmptyString(account.projectId)
81
+ ?? asNonEmptyString(account.managedProjectId)
82
+ ?? refreshParts.projectId
83
+ ?? refreshParts.managedProjectId,
84
+ email: account.email
85
+ };
86
+ }
87
+ }
88
+ }
89
+
90
+ return null;
91
+ };
92
+
93
+ export const resolveGoogleAuthSources = () => {
94
+ const auth = readAuthFile();
95
+ const sources = [];
96
+
97
+ const geminiAuth = resolveGeminiCliAuth(auth);
98
+ if (geminiAuth) {
99
+ sources.push(geminiAuth);
100
+ }
101
+
102
+ const antigravityAuth = resolveAntigravityAuth();
103
+ if (antigravityAuth) {
104
+ sources.push(antigravityAuth);
105
+ }
106
+
107
+ return sources;
108
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Google Provider
3
+ *
4
+ * Google quota provider implementation.
5
+ * @module quota/providers/google
6
+ */
7
+
8
+ export {
9
+ resolveGoogleOAuthClient,
10
+ resolveGeminiCliAuth,
11
+ resolveAntigravityAuth,
12
+ resolveGoogleAuthSources,
13
+ DEFAULT_PROJECT_ID
14
+ } from './auth.js';
15
+
16
+ export {
17
+ resolveGoogleWindow,
18
+ transformQuotaBucket,
19
+ transformModelData
20
+ } from './transforms.js';
21
+
22
+ export {
23
+ refreshGoogleAccessToken,
24
+ fetchGoogleQuotaBuckets,
25
+ fetchGoogleModels
26
+ } from './api.js';
27
+
28
+ import { buildResult } from '../../utils/index.js';
29
+ import {
30
+ resolveGoogleAuthSources,
31
+ resolveGoogleOAuthClient,
32
+ DEFAULT_PROJECT_ID
33
+ } from './auth.js';
34
+ import { transformQuotaBucket, transformModelData } from './transforms.js';
35
+ import {
36
+ refreshGoogleAccessToken,
37
+ fetchGoogleQuotaBuckets,
38
+ fetchGoogleModels
39
+ } from './api.js';
40
+
41
+ export const fetchGoogleQuota = async () => {
42
+ const authSources = resolveGoogleAuthSources();
43
+ if (!authSources.length) {
44
+ return buildResult({
45
+ providerId: 'google',
46
+ providerName: 'Google',
47
+ ok: false,
48
+ configured: false,
49
+ error: 'Not configured'
50
+ });
51
+ }
52
+
53
+ const models = {};
54
+ const sourceErrors = [];
55
+
56
+ for (const source of authSources) {
57
+ const now = Date.now();
58
+ let accessToken = source.accessToken;
59
+
60
+ if (!accessToken || (typeof source.expires === 'number' && source.expires <= now)) {
61
+ if (!source.refreshToken) {
62
+ sourceErrors.push(`${source.sourceLabel}: Missing refresh token`);
63
+ continue;
64
+ }
65
+ const { clientId, clientSecret } = resolveGoogleOAuthClient(source.sourceId);
66
+ accessToken = await refreshGoogleAccessToken(source.refreshToken, clientId, clientSecret);
67
+ }
68
+
69
+ if (!accessToken) {
70
+ sourceErrors.push(`${source.sourceLabel}: Failed to refresh OAuth token`);
71
+ continue;
72
+ }
73
+
74
+ const projectId = source.projectId ?? DEFAULT_PROJECT_ID;
75
+ let mergedAnyModel = false;
76
+
77
+ if (source.sourceId === 'gemini') {
78
+ const quotaPayload = await fetchGoogleQuotaBuckets(accessToken, projectId);
79
+ const buckets = Array.isArray(quotaPayload?.buckets) ? quotaPayload.buckets : [];
80
+
81
+ for (const bucket of buckets) {
82
+ const transformed = transformQuotaBucket(bucket, source.sourceId);
83
+ if (transformed) {
84
+ Object.assign(models, transformed);
85
+ mergedAnyModel = true;
86
+ }
87
+ }
88
+ }
89
+
90
+ const payload = await fetchGoogleModels(accessToken, projectId);
91
+ if (payload) {
92
+ for (const [modelName, modelData] of Object.entries(payload.models ?? {})) {
93
+ const transformed = transformModelData(modelName, modelData, source.sourceId);
94
+ Object.assign(models, transformed);
95
+ mergedAnyModel = true;
96
+ }
97
+ }
98
+
99
+ if (!mergedAnyModel) {
100
+ sourceErrors.push(`${source.sourceLabel}: Failed to fetch models`);
101
+ }
102
+ }
103
+
104
+ if (!Object.keys(models).length) {
105
+ return buildResult({
106
+ providerId: 'google',
107
+ providerName: 'Google',
108
+ ok: false,
109
+ configured: true,
110
+ error: sourceErrors[0] ?? 'Failed to fetch models'
111
+ });
112
+ }
113
+
114
+ return buildResult({
115
+ providerId: 'google',
116
+ providerName: 'Google',
117
+ ok: true,
118
+ configured: true,
119
+ usage: {
120
+ windows: {},
121
+ models: Object.keys(models).length ? models : undefined
122
+ }
123
+ });
124
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Google Provider - Transforms
3
+ *
4
+ * Data transformation functions for Google quota responses.
5
+ * @module quota/providers/google/transforms
6
+ */
7
+
8
+ import {
9
+ asNonEmptyString,
10
+ toNumber,
11
+ toTimestamp,
12
+ toUsageWindow
13
+ } from '../../utils/index.js';
14
+
15
+ const GOOGLE_FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
16
+ const GOOGLE_DAILY_WINDOW_SECONDS = 24 * 60 * 60;
17
+
18
+ export const parseGoogleRefreshToken = (rawRefreshToken) => {
19
+ const refreshToken = asNonEmptyString(rawRefreshToken);
20
+ if (!refreshToken) {
21
+ return { refreshToken: null, projectId: null, managedProjectId: null };
22
+ }
23
+
24
+ const [rawToken = '', rawProject = '', rawManagedProject = ''] = refreshToken.split('|');
25
+ return {
26
+ refreshToken: asNonEmptyString(rawToken),
27
+ projectId: asNonEmptyString(rawProject),
28
+ managedProjectId: asNonEmptyString(rawManagedProject)
29
+ };
30
+ };
31
+
32
+ export const resolveGoogleWindow = (sourceId, resetAt) => {
33
+ if (sourceId === 'gemini') {
34
+ return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
35
+ }
36
+
37
+ if (sourceId === 'antigravity') {
38
+ const remainingSeconds = typeof resetAt === 'number'
39
+ ? Math.max(0, Math.round((resetAt - Date.now()) / 1000))
40
+ : null;
41
+
42
+ if (remainingSeconds !== null && remainingSeconds > 10 * 60 * 60) {
43
+ return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
44
+ }
45
+
46
+ return { label: '5h', seconds: GOOGLE_FIVE_HOUR_WINDOW_SECONDS };
47
+ }
48
+
49
+ return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
50
+ };
51
+
52
+ export const transformQuotaBucket = (bucket, sourceId) => {
53
+ const modelId = asNonEmptyString(bucket?.modelId);
54
+ if (!modelId) {
55
+ return null;
56
+ }
57
+
58
+ const scopedName = modelId.startsWith(`${sourceId}/`)
59
+ ? modelId
60
+ : `${sourceId}/${modelId}`;
61
+
62
+ const remainingFraction = toNumber(bucket?.remainingFraction);
63
+ const remainingPercent = remainingFraction !== null
64
+ ? Math.round(remainingFraction * 100)
65
+ : null;
66
+ const usedPercent = remainingPercent !== null ? Math.max(0, 100 - remainingPercent) : null;
67
+ const resetAt = toTimestamp(bucket?.resetTime);
68
+ const window = resolveGoogleWindow(sourceId, resetAt);
69
+
70
+ return {
71
+ [scopedName]: {
72
+ windows: {
73
+ [window.label]: toUsageWindow({
74
+ usedPercent,
75
+ windowSeconds: window.seconds,
76
+ resetAt
77
+ })
78
+ }
79
+ }
80
+ };
81
+ };
82
+
83
+ export const transformModelData = (modelName, modelData, sourceId) => {
84
+ const scopedName = modelName.startsWith(`${sourceId}/`)
85
+ ? modelName
86
+ : `${sourceId}/${modelName}`;
87
+
88
+ const remainingFraction = modelData?.quotaInfo?.remainingFraction;
89
+ const remainingPercent = typeof remainingFraction === 'number'
90
+ ? Math.round(remainingFraction * 100)
91
+ : null;
92
+ const usedPercent = remainingPercent !== null ? Math.max(0, 100 - remainingPercent) : null;
93
+ const resetAt = modelData?.quotaInfo?.resetTime
94
+ ? new Date(modelData.quotaInfo.resetTime).getTime()
95
+ : null;
96
+ const window = resolveGoogleWindow(sourceId, resetAt);
97
+
98
+ return {
99
+ [scopedName]: {
100
+ windows: {
101
+ [window.label]: toUsageWindow({
102
+ usedPercent,
103
+ windowSeconds: window.seconds,
104
+ resetAt
105
+ })
106
+ }
107
+ }
108
+ };
109
+ };