@hiai-gg/hiai-opencode 0.1.5 → 0.1.7

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 (180) hide show
  1. package/.env.example +21 -8
  2. package/AGENTS.md +60 -6
  3. package/ARCHITECTURE.md +6 -3
  4. package/LICENSE.md +0 -1
  5. package/README.md +113 -33
  6. package/assets/cli/hiai-opencode.mjs +668 -7
  7. package/assets/mcp/mempalace.mjs +159 -25
  8. package/config/hiai-opencode.schema.json +29 -3
  9. package/dist/agents/agent-skills.d.ts +7 -0
  10. package/dist/agents/bob/default.d.ts +1 -0
  11. package/dist/agents/bob/gemini.d.ts +1 -0
  12. package/dist/agents/bob/gpt-pro.d.ts +1 -0
  13. package/dist/agents/brainstormer.d.ts +7 -0
  14. package/dist/agents/coder/gpt-codex.d.ts +1 -1
  15. package/dist/agents/coder/gpt-pro.d.ts +1 -0
  16. package/dist/agents/coder/gpt.d.ts +2 -1
  17. package/dist/agents/designer.d.ts +7 -0
  18. package/dist/agents/dynamic-agent-core-sections.d.ts +4 -1
  19. package/dist/agents/dynamic-agent-prompt-builder.d.ts +1 -1
  20. package/dist/agents/strategist/gemini.d.ts +1 -0
  21. package/dist/agents/strategist/gpt.d.ts +1 -0
  22. package/dist/agents/types.d.ts +3 -1
  23. package/dist/config/index.d.ts +0 -1
  24. package/dist/config/platform-schema.d.ts +34 -6
  25. package/dist/config/schema/commands.d.ts +1 -0
  26. package/dist/config/schema/hooks.d.ts +0 -2
  27. package/dist/config/schema/index.d.ts +0 -2
  28. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -9
  29. package/dist/config/types.d.ts +4 -4
  30. package/dist/create-hooks.d.ts +0 -2
  31. package/dist/features/builtin-commands/templates/doctor.d.ts +1 -0
  32. package/dist/features/builtin-commands/types.d.ts +1 -1
  33. package/dist/features/builtin-skills/skills/hiai-opencode-setup.d.ts +2 -0
  34. package/dist/features/builtin-skills/skills/index.d.ts +2 -0
  35. package/dist/features/builtin-skills/skills/website-copywriting.d.ts +2 -0
  36. package/dist/hooks/agent-usage-reminder/constants.d.ts +1 -1
  37. package/dist/hooks/index.d.ts +0 -2
  38. package/dist/hooks/keyword-detector/ultrawork/default.d.ts +1 -1
  39. package/dist/hooks/keyword-detector/ultrawork/gemini.d.ts +1 -1
  40. package/dist/hooks/keyword-detector/ultrawork/gpt.d.ts +1 -1
  41. package/dist/hooks/keyword-detector/ultrawork/planner.d.ts +1 -1
  42. package/dist/index.js +7719 -153698
  43. package/dist/mcp/index.d.ts +0 -1
  44. package/dist/mcp/registry.d.ts +1 -1
  45. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -2
  46. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -3
  47. package/dist/plugin/startup-diagnostics.d.ts +1 -0
  48. package/dist/shared/logger.d.ts +2 -0
  49. package/dist/shared/mcp-static-export.d.ts +22 -0
  50. package/dist/shared/mode-routing.d.ts +6 -0
  51. package/dist/tools/ast-grep/constants.d.ts +1 -1
  52. package/dist/tools/ast-grep/environment-check.d.ts +1 -5
  53. package/dist/tools/ast-grep/language-support.d.ts +0 -1
  54. package/dist/tools/ast-grep/types.d.ts +1 -2
  55. package/dist/tools/delegate-task/git-categories.d.ts +2 -0
  56. package/dist/tools/delegate-task/sub-agent.d.ts +2 -0
  57. package/dist/tools/skill-mcp/constants.d.ts +1 -1
  58. package/hiai-opencode.json +50 -19
  59. package/package.json +10 -5
  60. package/src/agents/agent-skills.ts +70 -0
  61. package/src/agents/bob/default.ts +7 -1
  62. package/src/agents/bob/gemini.ts +1 -0
  63. package/src/agents/bob/gpt-pro.ts +3 -1
  64. package/src/agents/bob.ts +3 -0
  65. package/src/agents/brainstormer.ts +72 -0
  66. package/src/agents/builtin-agents.ts +59 -3
  67. package/src/agents/coder/gpt-codex.ts +5 -3
  68. package/src/agents/coder/gpt-pro.ts +4 -2
  69. package/src/agents/coder/gpt.ts +3 -1
  70. package/src/agents/critic/agent.ts +1 -0
  71. package/src/agents/designer.ts +70 -0
  72. package/src/agents/dynamic-agent-category-skills-guide.ts +6 -0
  73. package/src/agents/dynamic-agent-core-sections.ts +36 -0
  74. package/src/agents/dynamic-agent-prompt-builder.ts +1 -0
  75. package/src/agents/guard/default.ts +1 -0
  76. package/src/agents/guard/gemini.ts +1 -0
  77. package/src/agents/guard/gpt.ts +1 -0
  78. package/src/agents/platform-manager.ts +17 -1
  79. package/src/agents/prompt-library/platform.ts +34 -0
  80. package/src/agents/researcher.ts +1 -0
  81. package/src/agents/strategist/gemini.ts +1 -0
  82. package/src/agents/strategist/gpt.ts +1 -0
  83. package/src/agents/types.ts +4 -1
  84. package/src/agents/ui.ts +1 -0
  85. package/src/config/defaults.ts +45 -13
  86. package/src/config/index.ts +0 -1
  87. package/src/config/model-slots-and-export.test.ts +73 -0
  88. package/src/config/platform-schema.ts +3 -3
  89. package/src/config/schema/commands.ts +1 -0
  90. package/src/config/schema/hooks.ts +0 -2
  91. package/src/config/schema/index.ts +0 -2
  92. package/src/config/schema/oh-my-opencode-config.ts +0 -5
  93. package/src/config/types.ts +4 -4
  94. package/src/features/builtin-commands/commands.ts +7 -0
  95. package/src/features/builtin-commands/templates/doctor.ts +43 -0
  96. package/src/features/builtin-commands/types.ts +1 -1
  97. package/src/features/builtin-skills/skills/hiai-opencode-setup.ts +69 -0
  98. package/src/features/builtin-skills/skills/index.ts +2 -0
  99. package/src/features/builtin-skills/skills/website-copywriting.ts +41 -0
  100. package/src/features/builtin-skills/skills.test.ts +8 -0
  101. package/src/features/builtin-skills/skills.ts +12 -1
  102. package/src/features/skill-mcp-manager/AGENTS.md +1 -1
  103. package/src/hooks/agent-usage-reminder/constants.ts +4 -4
  104. package/src/hooks/index.ts +0 -2
  105. package/src/hooks/keyword-detector/ultrawork/default.ts +18 -18
  106. package/src/hooks/keyword-detector/ultrawork/gemini.ts +21 -21
  107. package/src/hooks/keyword-detector/ultrawork/gpt.ts +6 -8
  108. package/src/hooks/keyword-detector/ultrawork/planner.ts +5 -5
  109. package/src/index.ts +8 -78
  110. package/src/internals/plugins/subtask2/commands/manifest.ts +2 -6
  111. package/src/internals/plugins/subtask2/hooks/command-hooks.ts +2 -2
  112. package/src/internals/plugins/subtask2/hooks/message-hooks.ts +1 -1
  113. package/src/internals/plugins/subtask2/parsing/parallel.ts +13 -10
  114. package/src/mcp/index.ts +0 -1
  115. package/src/mcp/registry.ts +27 -0
  116. package/src/plugin/chat-message.ts +0 -2
  117. package/src/plugin/hooks/create-session-hooks.ts +0 -17
  118. package/src/plugin/startup-diagnostics.ts +27 -0
  119. package/src/plugin-handlers/agent-config-handler.ts +3 -2
  120. package/src/plugin-handlers/mcp-config-handler.test.ts +63 -0
  121. package/src/plugin-handlers/mcp-config-handler.ts +29 -14
  122. package/src/plugin-handlers/strategist-agent-config-builder.ts +1 -1
  123. package/src/shared/agent-display-names.test.ts +9 -0
  124. package/src/shared/agent-display-names.ts +5 -0
  125. package/src/shared/log-legacy-plugin-startup-warning.ts +6 -8
  126. package/src/shared/logger.ts +8 -0
  127. package/src/shared/mcp-static-export.ts +119 -0
  128. package/src/shared/migration/agent-names.ts +8 -0
  129. package/src/shared/migration/hook-names.ts +1 -1
  130. package/src/shared/mode-routing.test.ts +88 -0
  131. package/src/shared/mode-routing.ts +30 -0
  132. package/src/shared/startup-diagnostics.ts +6 -7
  133. package/src/tools/ast-grep/constants.ts +1 -1
  134. package/src/tools/ast-grep/environment-check.ts +2 -32
  135. package/src/tools/ast-grep/language-support.ts +0 -3
  136. package/src/tools/ast-grep/types.ts +1 -2
  137. package/src/tools/call-omo-agent/tools.ts +11 -4
  138. package/src/tools/delegate-task/anthropic-categories.ts +3 -3
  139. package/src/tools/delegate-task/builtin-categories.ts +2 -0
  140. package/src/tools/delegate-task/categories.test.ts +87 -0
  141. package/src/tools/delegate-task/category-resolver.ts +8 -9
  142. package/src/tools/delegate-task/git-categories.ts +30 -0
  143. package/src/tools/delegate-task/model-string-parser.test.ts +90 -0
  144. package/src/tools/delegate-task/openai-categories.ts +26 -22
  145. package/src/tools/delegate-task/sub-agent.ts +10 -0
  146. package/src/tools/delegate-task/subagent-discovery.test.ts +123 -0
  147. package/src/tools/delegate-task/subagent-resolver.ts +18 -1
  148. package/src/tools/skill-mcp/constants.ts +1 -1
  149. package/src/tools/skill-mcp/tools.test.ts +44 -0
  150. package/dist/ast-grep-napi.win32-x64-msvc-67c0y8nc.node +0 -0
  151. package/dist/config/loader.test.d.ts +0 -1
  152. package/dist/config/models.d.ts +0 -13
  153. package/dist/config/schema/websearch.d.ts +0 -13
  154. package/dist/hooks/no-bob-gpt/hook.d.ts +0 -16
  155. package/dist/hooks/no-bob-gpt/index.d.ts +0 -1
  156. package/dist/hooks/no-coder-non-gpt/hook.d.ts +0 -20
  157. package/dist/hooks/no-coder-non-gpt/index.d.ts +0 -1
  158. package/dist/internals/plugins/websearch-cited/google.d.ts +0 -38
  159. package/dist/internals/plugins/websearch-cited/index.d.ts +0 -17
  160. package/dist/internals/plugins/websearch-cited/openai.d.ts +0 -9
  161. package/dist/internals/plugins/websearch-cited/openrouter.d.ts +0 -2
  162. package/dist/internals/plugins/websearch-cited/types.d.ts +0 -5
  163. package/dist/mcp/grep-app.d.ts +0 -6
  164. package/dist/mcp/omo-mcp-index.d.ts +0 -10
  165. package/dist/mcp/websearch.d.ts +0 -11
  166. package/src/config/schema/websearch.ts +0 -15
  167. package/src/hooks/no-bob-gpt/hook.ts +0 -56
  168. package/src/hooks/no-bob-gpt/index.ts +0 -1
  169. package/src/hooks/no-coder-non-gpt/hook.ts +0 -67
  170. package/src/hooks/no-coder-non-gpt/index.ts +0 -1
  171. package/src/internals/plugins/websearch-cited/LICENSE +0 -214
  172. package/src/internals/plugins/websearch-cited/codex_prompt.txt +0 -79
  173. package/src/internals/plugins/websearch-cited/google.ts +0 -749
  174. package/src/internals/plugins/websearch-cited/index.ts +0 -306
  175. package/src/internals/plugins/websearch-cited/openai.ts +0 -407
  176. package/src/internals/plugins/websearch-cited/openrouter.ts +0 -190
  177. package/src/internals/plugins/websearch-cited/types.ts +0 -7
  178. package/src/mcp/grep-app.ts +0 -6
  179. package/src/mcp/omo-mcp-index.ts +0 -30
  180. package/src/mcp/websearch.ts +0 -44
@@ -1,749 +0,0 @@
1
- import type { Auth as ProviderAuth } from "@opencode-ai/sdk";
2
- import type { GetAuth, WebsearchClient } from "./types.ts";
3
-
4
- type GeminiChunkWeb = {
5
- title?: string;
6
- uri?: string;
7
- };
8
-
9
- type GeminiChunk = {
10
- web?: GeminiChunkWeb;
11
- };
12
-
13
- type GeminiSupportSegment = {
14
- startIndex?: number;
15
- endIndex?: number;
16
- };
17
-
18
- type GeminiSupport = {
19
- segment?: GeminiSupportSegment;
20
- groundingChunkIndices?: number[];
21
- };
22
-
23
- type GeminiMetadata = {
24
- groundingChunks?: GeminiChunk[];
25
- groundingSupports?: GeminiSupport[];
26
- };
27
-
28
- type GeminiTextPart = {
29
- text?: string;
30
- thought?: unknown;
31
- };
32
-
33
- type GeminiContent = {
34
- role?: string;
35
- parts?: GeminiTextPart[];
36
- };
37
-
38
- type GeminiCandidate = {
39
- content?: GeminiContent;
40
- groundingMetadata?: GeminiMetadata;
41
- };
42
-
43
- type GeminiGenerateContentResponse = {
44
- candidates?: GeminiCandidate[];
45
- };
46
-
47
- type CitationInsertion = {
48
- index: number;
49
- marker: string;
50
- };
51
-
52
- type GeminiWebSearchOptions = {
53
- apiKey: string;
54
- model: string;
55
- query: string;
56
- abortSignal: AbortSignal;
57
- };
58
-
59
- type GeminiClientConfig = {
60
- mode: "api";
61
- apiKey: string;
62
- model: string;
63
- };
64
-
65
- type OAuthAuthDetails = {
66
- type: "oauth";
67
- access?: string;
68
- refresh?: string;
69
- expires?: unknown;
70
- };
71
-
72
- type RefreshParts = {
73
- refreshToken: string;
74
- projectId?: string;
75
- managedProjectId?: string;
76
- };
77
-
78
- type RefreshedToken = {
79
- accessToken: string;
80
- expiresAt: number;
81
- };
82
-
83
- interface WebSearchClient {
84
- search(query: string, abortSignal: AbortSignal): Promise<string>;
85
- }
86
-
87
- const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
88
- const ANTIGRAVITY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.sandbox.googleapis.com";
89
- const ANTIGRAVITY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
90
- const ANTIGRAVITY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
91
- const GEMINI_CODE_ASSIST_GENERATE_PATH = "/v1internal:generateContent";
92
- const GEMINI_CODE_ASSIST_LOAD_PATH = "/v1internal:loadCodeAssist";
93
- const CODE_ASSIST_GENERATE_ENDPOINTS = [
94
- ANTIGRAVITY_ENDPOINT_DAILY,
95
- ANTIGRAVITY_ENDPOINT_AUTOPUSH,
96
- ANTIGRAVITY_ENDPOINT_PROD,
97
- ] as const;
98
- const CODE_ASSIST_LOAD_ENDPOINTS = [
99
- ANTIGRAVITY_ENDPOINT_PROD,
100
- ANTIGRAVITY_ENDPOINT_DAILY,
101
- ANTIGRAVITY_ENDPOINT_AUTOPUSH,
102
- ] as const;
103
- const ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc";
104
- const OAUTH_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
105
-
106
- const ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
107
- const ANTIGRAVITY_CLIENT_SECRET = "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";
108
-
109
- const REFRESH_BUFFER_MS = 60_000;
110
-
111
- const CODE_ASSIST_HEADERS = {
112
- "User-Agent": "antigravity/1.11.5 windows/amd64",
113
- "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
114
- "Client-Metadata": '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
115
- } as const;
116
-
117
- const tokenCache = new Map<string, { accessToken: string; expiresAt: number }>();
118
- const projectCache = new Map<string, string>();
119
-
120
- function buildGeminiUrl(model: string): string {
121
- const encoded = encodeURIComponent(model);
122
- return `${GEMINI_API_BASE}/models/${encoded}:generateContent`;
123
- }
124
-
125
- async function runGeminiWebSearch(options: GeminiWebSearchOptions): Promise<GeminiGenerateContentResponse> {
126
- const response = await fetch(buildGeminiUrl(options.model), {
127
- method: "POST",
128
- headers: {
129
- "Content-Type": "application/json",
130
- "x-goog-api-key": options.apiKey,
131
- "User-Agent": CODE_ASSIST_HEADERS["User-Agent"],
132
- "X-Goog-Api-Client": CODE_ASSIST_HEADERS["X-Goog-Api-Client"],
133
- "Client-Metadata": CODE_ASSIST_HEADERS["Client-Metadata"],
134
- },
135
- body: JSON.stringify({
136
- contents: [
137
- {
138
- role: "user",
139
- parts: [{ text: options.query }],
140
- },
141
- ],
142
- tools: [{ googleSearch: {} }],
143
- }),
144
- signal: options.abortSignal,
145
- });
146
-
147
- if (!response.ok) {
148
- const message = await readErrorMessage(response);
149
- throw new Error(message ?? `Request failed with status ${response.status}`);
150
- }
151
-
152
- return (await response.json()) as GeminiGenerateContentResponse;
153
- }
154
-
155
- export function formatWebSearchResponse(response: GeminiGenerateContentResponse, query: string): string {
156
- const responseText = extractResponseText(response);
157
-
158
- if (!responseText || !responseText.trim()) {
159
- return `No search results or information found for query: "${query}"`;
160
- }
161
-
162
- const metadata = extractGroundingMetadata(response);
163
- const sources = metadata?.groundingChunks;
164
- const hasSources = Boolean(sources && sources.length > 0);
165
-
166
- let modifiedText = responseText;
167
-
168
- if (hasSources && metadata) {
169
- const insertions = buildCitationInsertions(metadata);
170
- if (insertions.length > 0) {
171
- modifiedText = insertMarkersByUtf8Index(modifiedText, insertions);
172
- }
173
- }
174
-
175
- if (hasSources && sources) {
176
- const sourceLines = sources.map((source, index) => {
177
- const title = source.web?.title || "Untitled";
178
- const uri = source.web?.uri || "No URI";
179
- return `[${index + 1}] ${title} (${uri})`;
180
- });
181
- modifiedText += `\n\nSources:\n${sourceLines.join("\n")}`;
182
- }
183
-
184
- return modifiedText;
185
- }
186
-
187
- function extractResponseText(response: GeminiGenerateContentResponse): string | undefined {
188
- const parts = response.candidates?.[0]?.content?.parts;
189
- if (!parts || parts.length === 0) {
190
- return undefined;
191
- }
192
-
193
- let combined = "";
194
- for (const part of parts) {
195
- if (part.thought) {
196
- continue;
197
- }
198
- if (typeof part.text === "string") {
199
- combined += part.text;
200
- }
201
- }
202
-
203
- return combined || undefined;
204
- }
205
-
206
- function extractGroundingMetadata(response: GeminiGenerateContentResponse): GeminiMetadata | undefined {
207
- return response.candidates?.[0]?.groundingMetadata;
208
- }
209
-
210
- function buildCitationInsertions(metadata?: GeminiMetadata): CitationInsertion[] {
211
- const supports = metadata?.groundingSupports;
212
- if (!supports || supports.length === 0) {
213
- return [];
214
- }
215
-
216
- const insertions: CitationInsertion[] = [];
217
-
218
- for (const support of supports) {
219
- const segment = support.segment;
220
- const indices = support.groundingChunkIndices;
221
- if (!segment || segment.endIndex == null || !indices || indices.length === 0) {
222
- continue;
223
- }
224
-
225
- const uniqueSorted = Array.from(new Set(indices)).sort((a, b) => a - b);
226
- const marker = uniqueSorted.map((idx) => `[${idx + 1}]`).join("");
227
-
228
- insertions.push({
229
- index: segment.endIndex,
230
- marker,
231
- });
232
- }
233
-
234
- insertions.sort((a, b) => b.index - a.index);
235
- return insertions;
236
- }
237
-
238
- function insertMarkersByUtf8Index(text: string, insertions: CitationInsertion[]): string {
239
- if (insertions.length === 0) {
240
- return text;
241
- }
242
-
243
- const encoder = new TextEncoder();
244
- const responseBytes = encoder.encode(text);
245
- const parts: Uint8Array[] = [];
246
- let lastIndex = responseBytes.length;
247
-
248
- for (const insertion of insertions) {
249
- const position = Math.min(insertion.index, lastIndex);
250
- parts.unshift(responseBytes.subarray(position, lastIndex));
251
- parts.unshift(encoder.encode(insertion.marker));
252
- lastIndex = position;
253
- }
254
-
255
- parts.unshift(responseBytes.subarray(0, lastIndex));
256
-
257
- const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
258
- const finalBytes = new Uint8Array(totalLength);
259
- let offset = 0;
260
- for (const part of parts) {
261
- finalBytes.set(part, offset);
262
- offset += part.length;
263
- }
264
-
265
- return new TextDecoder().decode(finalBytes);
266
- }
267
-
268
- class GeminiApiKeyClient implements WebSearchClient {
269
- private readonly apiKey: string;
270
- private readonly model: string;
271
-
272
- constructor(apiKey: string, model: string) {
273
- const normalizedKey = apiKey.trim();
274
- const normalizedModel = model.trim();
275
- if (!normalizedKey || !normalizedModel) {
276
- throw new Error("Invalid Google API configuration");
277
- }
278
- this.apiKey = normalizedKey;
279
- this.model = normalizedModel;
280
- }
281
-
282
- async search(query: string, abortSignal: AbortSignal): Promise<string> {
283
- const normalizedQuery = query.trim();
284
- const response = await runGeminiWebSearch({
285
- apiKey: this.apiKey,
286
- model: this.model,
287
- query: normalizedQuery,
288
- abortSignal,
289
- });
290
- return formatWebSearchResponse(response, normalizedQuery);
291
- }
292
- }
293
-
294
- function parseRefresh(refresh: string): RefreshParts {
295
- const normalized = refresh.trim();
296
- if (!normalized) {
297
- return { refreshToken: "" };
298
- }
299
- const [token, project, managed] = normalized.split("|");
300
- const refreshToken = token?.trim() ?? "";
301
- const projectId = project?.trim() ?? "";
302
- const managedProjectId = managed?.trim() ?? "";
303
- return {
304
- refreshToken,
305
- projectId: projectId || undefined,
306
- managedProjectId: managedProjectId || undefined,
307
- };
308
- }
309
-
310
- function getCachedAccess(refreshToken: string): { accessToken: string; expiresAt: number } | undefined {
311
- const cached = tokenCache.get(refreshToken);
312
- if (!cached) {
313
- return undefined;
314
- }
315
- if (cached.expiresAt <= Date.now() + REFRESH_BUFFER_MS) {
316
- tokenCache.delete(refreshToken);
317
- return undefined;
318
- }
319
- return cached;
320
- }
321
-
322
- function cacheToken(refreshToken: string, accessToken: string, expiresAt?: number): void {
323
- if (!refreshToken || !accessToken) {
324
- return;
325
- }
326
- if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) {
327
- tokenCache.set(refreshToken, { accessToken, expiresAt });
328
- }
329
- }
330
-
331
- async function requestToken(refreshToken: string): Promise<RefreshedToken> {
332
- const requestTime = Date.now();
333
- const response = await fetch(OAUTH_TOKEN_ENDPOINT, {
334
- method: "POST",
335
- headers: {
336
- "Content-Type": "application/x-www-form-urlencoded",
337
- },
338
- body: new URLSearchParams({
339
- grant_type: "refresh_token",
340
- refresh_token: refreshToken,
341
- client_id: ANTIGRAVITY_CLIENT_ID,
342
- client_secret: ANTIGRAVITY_CLIENT_SECRET,
343
- }),
344
- });
345
-
346
- if (!response.ok) {
347
- const message = await readErrorMessage(response);
348
- throw new Error(message ?? `Request failed with status ${response.status}`);
349
- }
350
-
351
- const payload = (await response.json()) as {
352
- access_token?: string;
353
- expires_in?: number;
354
- };
355
- if (!payload.access_token) {
356
- throw new Error("Token refresh response missing access_token");
357
- }
358
- const expiresIn =
359
- typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) ? payload.expires_in : 3600;
360
- const expiresAt = expiresIn > 0 ? requestTime + expiresIn * 1000 : requestTime;
361
-
362
- return {
363
- accessToken: payload.access_token,
364
- expiresAt,
365
- };
366
- }
367
-
368
- async function refreshAccessToken(refreshToken: string): Promise<RefreshedToken> {
369
- const result = await requestToken(refreshToken);
370
- cacheToken(refreshToken, result.accessToken, result.expiresAt);
371
- return result;
372
- }
373
-
374
- type LoadCodeAssistPayload = {
375
- cloudaicompanionProject?: string | { id?: string };
376
- };
377
-
378
- function buildMetadata(projectId?: string): Record<string, string> {
379
- const metadata: Record<string, string> = {
380
- ideType: "IDE_UNSPECIFIED",
381
- platform: "PLATFORM_UNSPECIFIED",
382
- pluginType: "GEMINI",
383
- };
384
- if (projectId) {
385
- metadata.duetProject = projectId;
386
- }
387
- return metadata;
388
- }
389
-
390
- async function loadManagedProject(
391
- accessToken: string,
392
- projectId: string | undefined,
393
- abortSignal: AbortSignal
394
- ): Promise<LoadCodeAssistPayload | null> {
395
- const loadHeaders: Record<string, string> = {
396
- "Content-Type": "application/json",
397
- Authorization: `Bearer ${accessToken}`,
398
- "User-Agent": "google-api-nodejs-client/9.15.1",
399
- "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
400
- "Client-Metadata": CODE_ASSIST_HEADERS["Client-Metadata"],
401
- };
402
-
403
- const requestBody = {
404
- metadata: buildMetadata(projectId),
405
- };
406
-
407
- const loadEndpoints = Array.from(new Set<string>([...CODE_ASSIST_LOAD_ENDPOINTS, ...CODE_ASSIST_GENERATE_ENDPOINTS]));
408
-
409
- for (const baseEndpoint of loadEndpoints) {
410
- try {
411
- const response = await fetch(`${baseEndpoint}${GEMINI_CODE_ASSIST_LOAD_PATH}`, {
412
- method: "POST",
413
- headers: loadHeaders,
414
- body: JSON.stringify(requestBody),
415
- signal: abortSignal,
416
- });
417
-
418
- if (!response.ok) {
419
- continue;
420
- }
421
-
422
- return (await response.json()) as LoadCodeAssistPayload;
423
- } catch {}
424
- }
425
-
426
- return null;
427
- }
428
-
429
- function extractManagedProjectId(payload?: LoadCodeAssistPayload | null): string | undefined {
430
- if (!payload) {
431
- return undefined;
432
- }
433
- const project = payload.cloudaicompanionProject;
434
- if (typeof project === "string" && project.trim() !== "") {
435
- return project;
436
- }
437
- if (project && typeof project === "object" && project.id) {
438
- const id = project.id;
439
- if (typeof id === "string" && id.trim() !== "") {
440
- return id;
441
- }
442
- }
443
- return undefined;
444
- }
445
-
446
- async function resolveProjectId(
447
- accessToken: string,
448
- refreshToken: string,
449
- refreshParts: RefreshParts,
450
- abortSignal: AbortSignal
451
- ): Promise<string> {
452
- if (refreshParts.managedProjectId) {
453
- return refreshParts.managedProjectId;
454
- }
455
-
456
- const cached = projectCache.get(refreshToken);
457
- if (cached) {
458
- return cached;
459
- }
460
-
461
- const fallbackProjectId = ANTIGRAVITY_DEFAULT_PROJECT_ID;
462
- const desiredProjectId = refreshParts.projectId ?? fallbackProjectId;
463
- const loadPayload = await loadManagedProject(accessToken, desiredProjectId, abortSignal);
464
- const resolvedManagedProjectId = extractManagedProjectId(loadPayload);
465
-
466
- if (resolvedManagedProjectId) {
467
- projectCache.set(refreshToken, resolvedManagedProjectId);
468
- return resolvedManagedProjectId;
469
- }
470
-
471
- if (refreshParts.projectId) {
472
- return refreshParts.projectId;
473
- }
474
-
475
- return fallbackProjectId;
476
- }
477
-
478
- function parseExpires(expires: unknown): number | undefined {
479
- if (typeof expires === "number" && Number.isFinite(expires)) {
480
- return expires;
481
- }
482
- return undefined;
483
- }
484
-
485
- function accessTokenExpired(accessToken: string, expiresAt?: number): boolean {
486
- if (!accessToken || typeof expiresAt !== "number") {
487
- return true;
488
- }
489
- return expiresAt <= Date.now() + REFRESH_BUFFER_MS;
490
- }
491
-
492
- async function requestGenerateContent(
493
- accessToken: string,
494
- projectId: string,
495
- model: string,
496
- query: string,
497
- abortSignal: AbortSignal
498
- ): Promise<{ ok: true; body: GeminiGenerateContentResponse } | { ok: false; status: number; message?: string }> {
499
- const requestPayload: Record<string, unknown> = {
500
- contents: [
501
- {
502
- role: "user",
503
- parts: [{ text: query }],
504
- },
505
- ],
506
- tools: [{ googleSearch: {} }],
507
- };
508
-
509
- const body = JSON.stringify({
510
- project: projectId,
511
- model,
512
- request: requestPayload,
513
- requestType: "agent",
514
- userAgent: "antigravity",
515
- requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
516
- });
517
-
518
- const headers: Record<string, string> = {
519
- "Content-Type": "application/json",
520
- Authorization: `Bearer ${accessToken}`,
521
- "User-Agent": CODE_ASSIST_HEADERS["User-Agent"],
522
- "X-Goog-Api-Client": CODE_ASSIST_HEADERS["X-Goog-Api-Client"],
523
- "Client-Metadata": CODE_ASSIST_HEADERS["Client-Metadata"],
524
- };
525
-
526
- let lastError: { status: number; message?: string } | undefined;
527
-
528
- for (const baseUrl of CODE_ASSIST_GENERATE_ENDPOINTS) {
529
- const response = await fetch(`${baseUrl}${GEMINI_CODE_ASSIST_GENERATE_PATH}`, {
530
- method: "POST",
531
- headers,
532
- body,
533
- signal: abortSignal,
534
- });
535
-
536
- if (!response.ok) {
537
- const message = await readErrorMessage(response);
538
- if (response.status === 401 || response.status === 403) {
539
- return { ok: false, status: response.status, message };
540
- }
541
- lastError = { status: response.status, message };
542
- continue;
543
- }
544
-
545
- const text = await response.text();
546
- if (!text) {
547
- throw new Error("Empty response from Google Code Assist");
548
- }
549
-
550
- let parsed: unknown;
551
- try {
552
- parsed = JSON.parse(text) as unknown;
553
- } catch {
554
- throw new Error("Invalid JSON response from Google Code Assist");
555
- }
556
-
557
- const effectiveResponse = extractGenerateContentResponse(parsed);
558
- if (!effectiveResponse) {
559
- throw new Error("Google Code Assist response did not include a valid response payload");
560
- }
561
-
562
- return { ok: true, body: effectiveResponse };
563
- }
564
-
565
- if (lastError) {
566
- return { ok: false, status: lastError.status, message: lastError.message };
567
- }
568
-
569
- return {
570
- ok: false,
571
- status: 502,
572
- message: "Request failed for all Google Code Assist endpoints.",
573
- };
574
- }
575
-
576
- function createGeminiOAuthWebSearchClient(authDetails: OAuthAuthDetails, model: string): WebSearchClient {
577
- const refreshParts = parseRefresh(authDetails.refresh ?? "");
578
- const refreshToken = refreshParts.refreshToken;
579
- if (!refreshToken) {
580
- throw new Error("Missing Google OAuth refresh token");
581
- }
582
-
583
- const initialAccess = authDetails.access?.trim() ?? "";
584
- const initialExpires = parseExpires(authDetails.expires);
585
-
586
- return {
587
- async search(query: string, abortSignal: AbortSignal): Promise<string> {
588
- const normalizedQuery = query.trim();
589
-
590
- const cached = getCachedAccess(refreshToken);
591
- let accessToken = cached?.accessToken ?? initialAccess;
592
- let expiresAt = cached?.expiresAt ?? initialExpires;
593
- let refreshedThisRequest = false;
594
-
595
- if (accessTokenExpired(accessToken, expiresAt)) {
596
- const refreshed = await refreshAccessToken(refreshToken);
597
- accessToken = refreshed.accessToken;
598
- expiresAt = refreshed.expiresAt;
599
- refreshedThisRequest = true;
600
- }
601
-
602
- if (!accessToken) {
603
- throw new Error("Missing Google OAuth access token");
604
- }
605
-
606
- if (typeof expiresAt === "number") {
607
- cacheToken(refreshToken, accessToken, expiresAt);
608
- }
609
-
610
- const effectiveProjectId = await resolveProjectId(accessToken, refreshToken, refreshParts, abortSignal);
611
-
612
- const firstAttempt = await requestGenerateContent(
613
- accessToken,
614
- effectiveProjectId,
615
- model,
616
- normalizedQuery,
617
- abortSignal
618
- );
619
-
620
- if (firstAttempt.ok) {
621
- return formatWebSearchResponse(firstAttempt.body, normalizedQuery);
622
- }
623
-
624
- const shouldRetry = (firstAttempt.status === 401 || firstAttempt.status === 403) && !refreshedThisRequest;
625
-
626
- if (!shouldRetry) {
627
- throw new Error(firstAttempt.message ?? `Request failed with status ${firstAttempt.status}`);
628
- }
629
-
630
- tokenCache.delete(refreshToken);
631
- const refreshed = await refreshAccessToken(refreshToken);
632
- accessToken = refreshed.accessToken;
633
- expiresAt = refreshed.expiresAt;
634
- refreshedThisRequest = true;
635
- cacheToken(refreshToken, accessToken, expiresAt);
636
-
637
- const retry = await requestGenerateContent(accessToken, effectiveProjectId, model, normalizedQuery, abortSignal);
638
-
639
- if (retry.ok) {
640
- return formatWebSearchResponse(retry.body, normalizedQuery);
641
- }
642
-
643
- throw new Error(retry.message ?? `Request failed with status ${retry.status}`);
644
- },
645
- };
646
- }
647
-
648
- function extractGenerateContentResponse(payload: unknown): GeminiGenerateContentResponse | undefined {
649
- const candidateObject = (() => {
650
- if (Array.isArray(payload)) {
651
- for (const item of payload) {
652
- if (item && typeof item === "object") {
653
- return item as Record<string, unknown>;
654
- }
655
- }
656
- return undefined;
657
- }
658
- if (payload && typeof payload === "object") {
659
- return payload as Record<string, unknown>;
660
- }
661
- return undefined;
662
- })();
663
-
664
- if (!candidateObject) {
665
- return undefined;
666
- }
667
-
668
- const withResponse = candidateObject as {
669
- response?: unknown;
670
- candidates?: unknown;
671
- };
672
-
673
- if (withResponse.response && typeof withResponse.response === "object") {
674
- return withResponse.response as GeminiGenerateContentResponse;
675
- }
676
-
677
- if (withResponse.candidates) {
678
- return candidateObject as unknown as GeminiGenerateContentResponse;
679
- }
680
-
681
- return undefined;
682
- }
683
-
684
- async function readErrorMessage(response: Response): Promise<string | undefined> {
685
- try {
686
- const text = await response.text();
687
- const trimmed = text.trim();
688
- return trimmed === "" ? undefined : trimmed;
689
- } catch {
690
- return undefined;
691
- }
692
- }
693
-
694
- function createGeminiWebSearchClient(config: GeminiClientConfig): WebSearchClient {
695
- return new GeminiApiKeyClient(config.apiKey, config.model);
696
- }
697
-
698
- function createWebSearchClientForGoogle(authDetails: ProviderAuth, model: string): WebSearchClient {
699
- if (authDetails.type === "api") {
700
- const apiKey = extractApiKey(authDetails);
701
- if (!apiKey) {
702
- throw new Error("Missing Google API key");
703
- }
704
- return createGeminiWebSearchClient({
705
- mode: "api",
706
- apiKey,
707
- model,
708
- });
709
- }
710
-
711
- if (authDetails.type === "oauth") {
712
- const oauthAuth = authDetails as OAuthAuthDetails;
713
- return createGeminiOAuthWebSearchClient(oauthAuth, model);
714
- }
715
-
716
- throw new Error("Unsupported auth type for Google web search");
717
- }
718
-
719
- function extractApiKey(authDetails?: ProviderAuth | null): string | undefined {
720
- if (!authDetails || authDetails.type !== "api") {
721
- return undefined;
722
- }
723
- const normalized = authDetails.key.trim();
724
- return normalized === "" ? undefined : normalized;
725
- }
726
-
727
- export function createGoogleWebsearchClient(model: string): WebsearchClient {
728
- const normalizedModel = model.trim();
729
- if (!normalizedModel) {
730
- throw new Error("Invalid Google web search model");
731
- }
732
-
733
- return {
734
- async search(query, abortSignal, getAuth: GetAuth) {
735
- const normalizedQuery = query.trim();
736
- if (!normalizedQuery) {
737
- throw new Error("Query must not be empty");
738
- }
739
-
740
- const auth = await getAuth();
741
- if (!auth) {
742
- throw new Error('Missing auth for provider "google"');
743
- }
744
-
745
- const client = createWebSearchClientForGoogle(auth, normalizedModel);
746
- return client.search(normalizedQuery, abortSignal);
747
- },
748
- };
749
- }