@jx-grxf/patchpilot 0.4.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.env.example +17 -1
  2. package/README.md +113 -23
  3. package/SECURITY.md +7 -1
  4. package/dist/cli.js +103 -14
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent.d.ts +47 -1
  7. package/dist/core/agent.js +667 -76
  8. package/dist/core/agent.js.map +1 -1
  9. package/dist/core/cleanup.d.ts +3 -0
  10. package/dist/core/cleanup.js +29 -0
  11. package/dist/core/cleanup.js.map +1 -0
  12. package/dist/core/clipboard.d.ts +14 -0
  13. package/dist/core/clipboard.js +134 -0
  14. package/dist/core/clipboard.js.map +1 -0
  15. package/dist/core/codex.d.ts +8 -0
  16. package/dist/core/codex.js +28 -2
  17. package/dist/core/codex.js.map +1 -1
  18. package/dist/core/compaction.d.ts +23 -0
  19. package/dist/core/compaction.js +145 -0
  20. package/dist/core/compaction.js.map +1 -0
  21. package/dist/core/contextFormat.d.ts +21 -0
  22. package/dist/core/contextFormat.js +87 -0
  23. package/dist/core/contextFormat.js.map +1 -0
  24. package/dist/core/contextItem.d.ts +41 -0
  25. package/dist/core/contextItem.js +93 -0
  26. package/dist/core/contextItem.js.map +1 -0
  27. package/dist/core/contextStore.d.ts +48 -0
  28. package/dist/core/contextStore.js +306 -0
  29. package/dist/core/contextStore.js.map +1 -0
  30. package/dist/core/doctor.d.ts +4 -1
  31. package/dist/core/doctor.js +122 -3
  32. package/dist/core/doctor.js.map +1 -1
  33. package/dist/core/gemini.js +10 -4
  34. package/dist/core/gemini.js.map +1 -1
  35. package/dist/core/geminiWrapper.d.ts +92 -0
  36. package/dist/core/geminiWrapper.js +1258 -0
  37. package/dist/core/geminiWrapper.js.map +1 -0
  38. package/dist/core/http.js +70 -6
  39. package/dist/core/http.js.map +1 -1
  40. package/dist/core/json.d.ts +1 -1
  41. package/dist/core/json.js +81 -19
  42. package/dist/core/json.js.map +1 -1
  43. package/dist/core/memory.d.ts +16 -0
  44. package/dist/core/memory.js +108 -0
  45. package/dist/core/memory.js.map +1 -0
  46. package/dist/core/modelClient.js +7 -0
  47. package/dist/core/modelClient.js.map +1 -1
  48. package/dist/core/nvidia.d.ts +1 -1
  49. package/dist/core/nvidia.js +13 -4
  50. package/dist/core/nvidia.js.map +1 -1
  51. package/dist/core/ollama.js +13 -3
  52. package/dist/core/ollama.js.map +1 -1
  53. package/dist/core/openrouter.js +15 -6
  54. package/dist/core/openrouter.js.map +1 -1
  55. package/dist/core/projectInit.d.ts +6 -0
  56. package/dist/core/projectInit.js +44 -0
  57. package/dist/core/projectInit.js.map +1 -0
  58. package/dist/core/reasoning.js +6 -0
  59. package/dist/core/reasoning.js.map +1 -1
  60. package/dist/core/session.d.ts +1 -0
  61. package/dist/core/session.js +55 -3
  62. package/dist/core/session.js.map +1 -1
  63. package/dist/core/tokenAccounting.d.ts +4 -0
  64. package/dist/core/tokenAccounting.js +75 -13
  65. package/dist/core/tokenAccounting.js.map +1 -1
  66. package/dist/core/types.d.ts +65 -5
  67. package/dist/core/types.js +30 -1
  68. package/dist/core/types.js.map +1 -1
  69. package/dist/core/updateCheck.d.ts +19 -0
  70. package/dist/core/updateCheck.js +103 -0
  71. package/dist/core/updateCheck.js.map +1 -0
  72. package/dist/core/workspace.d.ts +37 -0
  73. package/dist/core/workspace.js +1535 -84
  74. package/dist/core/workspace.js.map +1 -1
  75. package/dist/tui/App.d.ts +1 -0
  76. package/dist/tui/App.js +1841 -140
  77. package/dist/tui/App.js.map +1 -1
  78. package/dist/tui/commands.js +141 -9
  79. package/dist/tui/commands.js.map +1 -1
  80. package/dist/tui/components/ApprovalPanel.js +16 -1
  81. package/dist/tui/components/ApprovalPanel.js.map +1 -1
  82. package/dist/tui/components/CommandSuggestions.js +33 -5
  83. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  84. package/dist/tui/components/Composer.d.ts +3 -0
  85. package/dist/tui/components/Composer.js +57 -5
  86. package/dist/tui/components/Composer.js.map +1 -1
  87. package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
  88. package/dist/tui/components/ExperimentalPanel.js +38 -0
  89. package/dist/tui/components/ExperimentalPanel.js.map +1 -0
  90. package/dist/tui/components/Header.js +3 -3
  91. package/dist/tui/components/Header.js.map +1 -1
  92. package/dist/tui/components/OnboardingPanel.d.ts +25 -1
  93. package/dist/tui/components/OnboardingPanel.js +87 -25
  94. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  95. package/dist/tui/components/Sidebar.js +17 -13
  96. package/dist/tui/components/Sidebar.js.map +1 -1
  97. package/dist/tui/components/StartupBanner.d.ts +4 -0
  98. package/dist/tui/components/StartupBanner.js +9 -0
  99. package/dist/tui/components/StartupBanner.js.map +1 -0
  100. package/dist/tui/components/Transcript.d.ts +7 -0
  101. package/dist/tui/components/Transcript.js +87 -17
  102. package/dist/tui/components/Transcript.js.map +1 -1
  103. package/dist/tui/contextCommands.d.ts +8 -0
  104. package/dist/tui/contextCommands.js +205 -0
  105. package/dist/tui/contextCommands.js.map +1 -0
  106. package/dist/tui/experimental/AnimatedText.d.ts +38 -0
  107. package/dist/tui/experimental/AnimatedText.js +55 -0
  108. package/dist/tui/experimental/AnimatedText.js.map +1 -0
  109. package/dist/tui/experimental/Banner.d.ts +10 -0
  110. package/dist/tui/experimental/Banner.js +33 -0
  111. package/dist/tui/experimental/Banner.js.map +1 -0
  112. package/dist/tui/experimental/CommandPalette.d.ts +11 -0
  113. package/dist/tui/experimental/CommandPalette.js +25 -0
  114. package/dist/tui/experimental/CommandPalette.js.map +1 -0
  115. package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
  116. package/dist/tui/experimental/ExperimentalShell.js +366 -0
  117. package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
  118. package/dist/tui/experimental/ThemePicker.d.ts +13 -0
  119. package/dist/tui/experimental/ThemePicker.js +12 -0
  120. package/dist/tui/experimental/ThemePicker.js.map +1 -0
  121. package/dist/tui/experimental/attachments.d.ts +35 -0
  122. package/dist/tui/experimental/attachments.js +244 -0
  123. package/dist/tui/experimental/attachments.js.map +1 -0
  124. package/dist/tui/experimental/composer.d.ts +24 -0
  125. package/dist/tui/experimental/composer.js +84 -0
  126. package/dist/tui/experimental/composer.js.map +1 -0
  127. package/dist/tui/experimental/geminiPricing.d.ts +16 -0
  128. package/dist/tui/experimental/geminiPricing.js +39 -0
  129. package/dist/tui/experimental/geminiPricing.js.map +1 -0
  130. package/dist/tui/experimental/layout.d.ts +46 -0
  131. package/dist/tui/experimental/layout.js +112 -0
  132. package/dist/tui/experimental/layout.js.map +1 -0
  133. package/dist/tui/experimental/theme.d.ts +35 -0
  134. package/dist/tui/experimental/theme.js +86 -0
  135. package/dist/tui/experimental/theme.js.map +1 -0
  136. package/dist/tui/experimental/transcriptRows.d.ts +20 -0
  137. package/dist/tui/experimental/transcriptRows.js +169 -0
  138. package/dist/tui/experimental/transcriptRows.js.map +1 -0
  139. package/dist/tui/experimental/ultraModes.d.ts +46 -0
  140. package/dist/tui/experimental/ultraModes.js +95 -0
  141. package/dist/tui/experimental/ultraModes.js.map +1 -0
  142. package/dist/tui/experimental/ultramaxx.d.ts +19 -0
  143. package/dist/tui/experimental/ultramaxx.js +43 -0
  144. package/dist/tui/experimental/ultramaxx.js.map +1 -0
  145. package/dist/tui/format.d.ts +4 -2
  146. package/dist/tui/format.js +21 -7
  147. package/dist/tui/format.js.map +1 -1
  148. package/dist/tui/hosts.js +7 -1
  149. package/dist/tui/hosts.js.map +1 -1
  150. package/dist/tui/layout.d.ts +26 -0
  151. package/dist/tui/layout.js +66 -0
  152. package/dist/tui/layout.js.map +1 -0
  153. package/dist/tui/modelSelection.d.ts +1 -1
  154. package/dist/tui/modelSelection.js +8 -6
  155. package/dist/tui/modelSelection.js.map +1 -1
  156. package/dist/tui/modes.d.ts +8 -1
  157. package/dist/tui/modes.js +20 -2
  158. package/dist/tui/modes.js.map +1 -1
  159. package/dist/tui/onboardingPreferences.d.ts +37 -0
  160. package/dist/tui/onboardingPreferences.js +118 -0
  161. package/dist/tui/onboardingPreferences.js.map +1 -0
  162. package/dist/tui/runStatus.d.ts +50 -0
  163. package/dist/tui/runStatus.js +164 -0
  164. package/dist/tui/runStatus.js.map +1 -0
  165. package/dist/tui/types.d.ts +8 -0
  166. package/dist/tui/types.js.map +1 -1
  167. package/docs/architecture.md +115 -0
  168. package/docs/gemini-wrapper.md +110 -0
  169. package/docs/product-context.md +43 -0
  170. package/docs/releases/v0.1.1-beta.md +18 -0
  171. package/docs/releases/v0.2.1.md +1 -1
  172. package/docs/releases/v0.3.1-beta.md +4 -0
  173. package/docs/releases/v0.4.0.md +1 -1
  174. package/docs/releases/v1.0.0.md +28 -0
  175. package/docs/releases/v1.0.1.md +25 -0
  176. package/docs/releases/v1.1.0.md +30 -0
  177. package/docs/releases/v1.2.0.md +28 -0
  178. package/docs/showcase/patchpilot-banner.png +0 -0
  179. package/docs/showcase/patchpilot-logo.png +0 -0
  180. package/package.json +8 -3
@@ -0,0 +1,1258 @@
1
+ import { spawn } from "node:child_process";
2
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getPatchPilotConfigDir } from "./env.js";
5
+ import { fetchWithTimeout } from "./http.js";
6
+ import { attachTokenCost, estimateTokens } from "./tokenAccounting.js";
7
+ export const defaultGeminiWrapperModel = "auto";
8
+ // Gemini 3 has no flash-lite tier — gemini_webapi 2.0.0 exposes only
9
+ // pro / flash / flash-thinking. Offering "flash-lite" advertised a model that
10
+ // does not exist and hard-failed every request, so it is no longer a shortcut.
11
+ export const geminiWrapperShortcutModels = ["auto", "flash", "pro"];
12
+ export const geminiWrapperLegacyModels = ["thinking"];
13
+ export const geminiWrapperCuratedModels = [...geminiWrapperShortcutModels, ...geminiWrapperLegacyModels];
14
+ export const geminiWebApiVersion = "2.0.0";
15
+ export const geminiWebApiInstallCommand = process.platform === "win32"
16
+ ? `PatchPilot managed install: python -m venv %USERPROFILE%\\.patchpilot\\gemini-wrapper-venv && %USERPROFILE%\\.patchpilot\\gemini-wrapper-venv\\Scripts\\python.exe -m pip install gemini_webapi==${geminiWebApiVersion} browser-cookie3`
17
+ : `PatchPilot managed install: python3 -m venv ~/.patchpilot/gemini-wrapper-venv && ~/.patchpilot/gemini-wrapper-venv/bin/python -m pip install gemini_webapi==${geminiWebApiVersion} browser-cookie3`;
18
+ const pythonBridgeReadyTtlMs = 5 * 60_000;
19
+ const geminiBrowserCookieImportTimeoutMs = 60_000;
20
+ const geminiBridgeOutputMaxBytes = 2 * 1024 * 1024;
21
+ const geminiWrapperBrowserCookieNames = new Set([
22
+ "__Secure-1PSID",
23
+ "__Secure-1PSIDTS",
24
+ "__Secure-1PSIDCC",
25
+ "__Secure-1PAPISID",
26
+ "__Secure-3PSID",
27
+ "__Secure-3PSIDTS",
28
+ "__Secure-3PSIDCC",
29
+ "__Secure-3PAPISID",
30
+ "__Secure-ENID",
31
+ "AEC",
32
+ "COMPASS",
33
+ "GOOGLE_ABUSE_EXEMPTION",
34
+ "NID",
35
+ "SID",
36
+ "HSID",
37
+ "SSID",
38
+ "APISID",
39
+ "SAPISID"
40
+ ]);
41
+ export class GeminiWrapperClient {
42
+ baseUrl;
43
+ apiKey;
44
+ runtimeOptions;
45
+ mode;
46
+ pythonCommand;
47
+ cookiesJson;
48
+ modelDescriptorCache = null;
49
+ pythonBridgeReadyUntil = 0;
50
+ pythonBridgeReadyPromise = null;
51
+ constructor(baseUrl = readGeminiWrapperBaseUrl(), apiKey = readGeminiWrapperApiKey(), runtimeOptions = readGeminiWrapperRuntimeOptions(), mode = readGeminiWrapperMode(), pythonCommand = readGeminiWrapperPythonCommand(), cookiesJson = readGeminiWrapperCookiesJson()) {
52
+ this.baseUrl = baseUrl.replace(/\/$/, "");
53
+ this.apiKey = apiKey;
54
+ this.runtimeOptions = runtimeOptions;
55
+ this.mode = mode;
56
+ this.pythonCommand = pythonCommand;
57
+ this.cookiesJson = cookiesJson;
58
+ }
59
+ async chat(options) {
60
+ if (this.usesPythonBridge()) {
61
+ return await this.chatWithPythonBridge(options);
62
+ }
63
+ this.assertConfigured();
64
+ const startedAt = Date.now();
65
+ const response = await this.fetchGeminiWrapper("/chat/completions", {
66
+ method: "POST",
67
+ headers: this.headers(),
68
+ body: JSON.stringify(cleanUndefined({
69
+ model: normalizeGeminiWrapperModel(options.model),
70
+ messages: options.messages,
71
+ max_tokens: this.runtimeOptions.maxTokens,
72
+ temperature: this.runtimeOptions.temperature,
73
+ response_format: options.formatJson ? { type: "json_object" } : undefined
74
+ })),
75
+ signal: options.signal
76
+ });
77
+ const durationMs = Date.now() - startedAt;
78
+ const { payload, text } = await readGeminiWrapperResponse(response);
79
+ if (!response.ok || payload.error) {
80
+ const reason = formatGeminiWrapperErrorReason(payload, text, response);
81
+ if (response.status === 401 || response.status === 403) {
82
+ throw new Error("Gemini-Wrapper authentication failed. Check PATCHPILOT_GEMINI_WRAPPER_API_KEY.");
83
+ }
84
+ if (response.status === 429) {
85
+ throw new Error(`Gemini-Wrapper rate limit hit for model "${options.model}".${reason}`);
86
+ }
87
+ throw new Error(`Gemini-Wrapper chat failed for model "${options.model}": HTTP ${response.status}.${reason}`);
88
+ }
89
+ const content = payload.choices?.[0]?.message?.content?.trim() ?? "";
90
+ if (!content) {
91
+ throw new Error("Gemini-Wrapper returned an empty response.");
92
+ }
93
+ return {
94
+ content,
95
+ telemetry: toTelemetry(payload, durationMs, options.model)
96
+ };
97
+ }
98
+ async listModels() {
99
+ return (await this.listModelDescriptors()).map((model) => model.id);
100
+ }
101
+ async listModelDescriptors() {
102
+ if (this.modelDescriptorCache && this.modelDescriptorCache.expiresAt > Date.now()) {
103
+ return this.modelDescriptorCache.descriptors;
104
+ }
105
+ if (this.usesPythonBridge()) {
106
+ await this.assertPythonBridgeReady();
107
+ const result = await this.runPythonBridge({
108
+ command: "models",
109
+ model: defaultGeminiWrapperModel
110
+ });
111
+ if (result.error) {
112
+ throw new Error(result.error);
113
+ }
114
+ const descriptors = normalizeGeminiWrapperModelDescriptors(result.modelDescriptors && result.modelDescriptors.length > 0 ? result.modelDescriptors : result.models ?? []);
115
+ const mergedDescriptors = mergeGeminiWrapperModelDescriptors(descriptors);
116
+ this.modelDescriptorCache = {
117
+ descriptors: mergedDescriptors,
118
+ expiresAt: Date.now() + 5 * 60_000
119
+ };
120
+ return mergedDescriptors;
121
+ }
122
+ this.assertConfigured();
123
+ const response = await this.fetchGeminiWrapper("/models", {
124
+ headers: this.headers()
125
+ });
126
+ const { payload, text } = await readGeminiWrapperResponse(response);
127
+ if (!response.ok || payload.error) {
128
+ const reason = formatGeminiWrapperErrorReason(payload, text, response);
129
+ throw new Error(`Gemini-Wrapper models failed with HTTP ${response.status}.${reason}`);
130
+ }
131
+ const descriptors = normalizeGeminiWrapperModelDescriptors(payload.data
132
+ ?.map((model) => model.id?.trim())
133
+ .filter((model) => Boolean(model))
134
+ .filter(isLikelyGeminiWrapperChatModel) ?? []);
135
+ const mergedDescriptors = mergeGeminiWrapperModelDescriptors(descriptors);
136
+ this.modelDescriptorCache = {
137
+ descriptors: mergedDescriptors,
138
+ expiresAt: Date.now() + 5 * 60_000
139
+ };
140
+ return mergedDescriptors;
141
+ }
142
+ supportsFileAnalysis() {
143
+ return this.usesPythonBridge();
144
+ }
145
+ async analyzeFile(options) {
146
+ if (!this.usesPythonBridge()) {
147
+ throw new Error("Gemini-Wrapper file analysis is only available through the managed Python Gemini-API bridge.");
148
+ }
149
+ await this.assertPythonBridgeReady();
150
+ const startedAt = Date.now();
151
+ const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
152
+ const result = await this.runPythonBridge({
153
+ command: "chat",
154
+ model: bridgeModel,
155
+ prompt: options.prompt,
156
+ files: [options.path]
157
+ }, options.signal, getGeminiWrapperBridgeTimeoutMs(bridgeModel || defaultGeminiWrapperModel, this.runtimeOptions.bridgeTimeoutMs));
158
+ const durationMs = Date.now() - startedAt;
159
+ const content = result.content?.trim() ?? "";
160
+ if (!content) {
161
+ throw new Error(result.error ? `Gemini-API bridge failed: ${result.error}` : "Gemini-API bridge returned an empty file analysis response.");
162
+ }
163
+ return {
164
+ content,
165
+ telemetry: toEstimatedTelemetry(`${options.prompt}\nFILE:${options.path}`, content, durationMs, result.model ?? options.model)
166
+ };
167
+ }
168
+ async checkBridgeAuth() {
169
+ await this.assertPythonBridgeReady();
170
+ const result = await this.runPythonBridge({
171
+ command: "authCheck",
172
+ model: defaultGeminiWrapperModel
173
+ });
174
+ if (result.error) {
175
+ throw new Error(result.error);
176
+ }
177
+ if (isUnauthenticatedGeminiWebStatus(result.accountStatus)) {
178
+ throw new Error("Gemini-API bridge cookies are expired or unauthenticated. Refresh Gemini-Wrapper cookies.");
179
+ }
180
+ }
181
+ async fetchGeminiWrapper(path, init) {
182
+ try {
183
+ return await fetchWithTimeout(`${this.baseUrl}${path}`, init, {
184
+ timeoutMs: init?.method === "POST" ? 90_000 : 8000,
185
+ retries: init?.method === "POST" ? 2 : 1,
186
+ label: `Gemini-Wrapper ${path}`
187
+ });
188
+ }
189
+ catch (error) {
190
+ const suffix = error instanceof Error ? ` ${error.message}` : "";
191
+ throw new Error(`Cannot reach Gemini-Wrapper API at ${this.baseUrl}.${suffix}`);
192
+ }
193
+ }
194
+ headers() {
195
+ return cleanUndefined({
196
+ "Content-Type": "application/json",
197
+ Authorization: this.apiKey ? `Bearer ${this.apiKey}` : undefined
198
+ });
199
+ }
200
+ assertConfigured() {
201
+ if (this.usesPythonBridge()) {
202
+ return;
203
+ }
204
+ if (!this.baseUrl) {
205
+ throw new Error(`Gemini-Wrapper requires either an explicit OpenAI-compatible wrapper URL or the installed Gemini-API Python wrapper. Set PATCHPILOT_GEMINI_WRAPPER_BASE_URL, or install the bridge with: ${geminiWebApiInstallCommand}`);
206
+ }
207
+ if (geminiWrapperRequiresApiKey(this.baseUrl) && !this.apiKey) {
208
+ throw new Error("Gemini-Wrapper remote URLs require an explicit API key. Set PATCHPILOT_GEMINI_WRAPPER_API_KEY or GEMINI_WRAPPER_API_KEY. PatchPilot does not collect browser cookies.");
209
+ }
210
+ }
211
+ usesPythonBridge() {
212
+ return this.mode === "python" || (this.mode === "auto" && !this.baseUrl);
213
+ }
214
+ async chatWithPythonBridge(options) {
215
+ await this.assertPythonBridgeReady();
216
+ const startedAt = Date.now();
217
+ const prompt = toBridgePrompt(options.messages, options.formatJson);
218
+ const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
219
+ const result = await this.runPythonBridge({
220
+ command: "chat",
221
+ model: bridgeModel,
222
+ prompt
223
+ }, options.signal, getGeminiWrapperBridgeTimeoutMs(bridgeModel || defaultGeminiWrapperModel, this.runtimeOptions.bridgeTimeoutMs));
224
+ const durationMs = Date.now() - startedAt;
225
+ const content = result.content?.trim() ?? "";
226
+ if (!content) {
227
+ throw new Error(result.error ? `Gemini-API bridge failed: ${result.error}` : "Gemini-API bridge returned an empty response.");
228
+ }
229
+ return {
230
+ content,
231
+ telemetry: toEstimatedTelemetry(prompt, content, durationMs, result.model ?? options.model)
232
+ };
233
+ }
234
+ async runPythonBridge(input, signal, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model, this.runtimeOptions.bridgeTimeoutMs)) {
235
+ const result = await runThrottledGeminiWebApiBridge(this.pythonCommand, {
236
+ ...input,
237
+ cookiesJson: this.cookiesJson || undefined,
238
+ secure1psid: readGeminiWrapperSecure1psid() || undefined,
239
+ secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
240
+ proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
241
+ }, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
242
+ if (!isUnauthenticatedGeminiWebError(result.error)) {
243
+ return result;
244
+ }
245
+ clearGeminiWrapperCookieCache();
246
+ return await runThrottledGeminiWebApiBridge(this.pythonCommand, {
247
+ ...input,
248
+ cookiesJson: this.cookiesJson || undefined,
249
+ secure1psid: readGeminiWrapperSecure1psid() || undefined,
250
+ secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
251
+ proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
252
+ }, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
253
+ }
254
+ async resolveGeminiWrapperBridgeModel(model) {
255
+ const normalizedModel = normalizeGeminiWrapperModel(model).trim();
256
+ if (normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default") {
257
+ return "";
258
+ }
259
+ if (isGeminiWrapperShortcutModel(normalizedModel)) {
260
+ const descriptors = await this.getCachedModelDescriptors().catch(() => []);
261
+ const descriptor = resolveGeminiWrapperShortcutDescriptor(normalizedModel, descriptors);
262
+ if (descriptor) {
263
+ return descriptor.id;
264
+ }
265
+ }
266
+ return normalizeGeminiWrapperBridgeModelFallback(normalizedModel);
267
+ }
268
+ async getCachedModelDescriptors() {
269
+ if (this.modelDescriptorCache && this.modelDescriptorCache.expiresAt > Date.now()) {
270
+ return this.modelDescriptorCache.descriptors;
271
+ }
272
+ return await this.listModelDescriptors();
273
+ }
274
+ async assertPythonBridgeReady() {
275
+ if (!this.cookiesJson && !readGeminiWrapperSecure1psid()) {
276
+ throw new Error("Gemini-API bridge needs explicit auth. Run `patchpilot gemini-wrapper import-cookies`, set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON to a JSON cookie file, or set GEMINI_SECURE_1PSID / GEMINI_SECURE_1PSIDTS.");
277
+ }
278
+ if (this.pythonBridgeReadyUntil > Date.now()) {
279
+ return;
280
+ }
281
+ if (!this.pythonBridgeReadyPromise) {
282
+ this.pythonBridgeReadyPromise = this.checkPythonBridgeReady().finally(() => {
283
+ this.pythonBridgeReadyPromise = null;
284
+ });
285
+ }
286
+ await this.pythonBridgeReadyPromise;
287
+ }
288
+ async checkPythonBridgeReady() {
289
+ const installed = await isGeminiWebApiInstalled(this.pythonCommand);
290
+ if (!installed) {
291
+ throw new Error(`Gemini-API Python wrapper is not installed for ${this.pythonCommand}. Run /doctor fix or patchpilot doctor --fix to install the pinned managed bridge. Manual fallback: ${geminiWebApiInstallCommand}`);
292
+ }
293
+ this.pythonBridgeReadyUntil = Date.now() + pythonBridgeReadyTtlMs;
294
+ }
295
+ }
296
+ export function readGeminiWrapperMode(env = process.env) {
297
+ const value = env.PATCHPILOT_GEMINI_WRAPPER_MODE?.trim().toLowerCase();
298
+ return value === "http" || value === "python" || value === "auto" ? value : "auto";
299
+ }
300
+ export function readGeminiWrapperBaseUrl(env = process.env) {
301
+ return env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL?.trim() || "";
302
+ }
303
+ export function readGeminiWrapperApiKey(env = process.env) {
304
+ return env.PATCHPILOT_GEMINI_WRAPPER_API_KEY?.trim() || env.GEMINI_WRAPPER_API_KEY?.trim() || "";
305
+ }
306
+ export function readGeminiWrapperCookiesJson(env = process.env) {
307
+ return env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON?.trim() || env.GEMINI_COOKIES_JSON?.trim() || "";
308
+ }
309
+ export function getDefaultGeminiWrapperCookiesPath(env = process.env) {
310
+ return path.join(getPatchPilotConfigDir(env), "gemini-cookies.json");
311
+ }
312
+ export function saveGeminiWrapperCookieFile(values, env = process.env) {
313
+ const secure1psid = values.secure1psid.trim();
314
+ const secure1psidts = values.secure1psidts?.trim() ?? "";
315
+ if (!secure1psid) {
316
+ throw new Error("__Secure-1PSID cannot be empty.");
317
+ }
318
+ const configDir = getPatchPilotConfigDir(env);
319
+ mkdirSync(configDir, {
320
+ recursive: true,
321
+ mode: 0o700
322
+ });
323
+ tryChmod(configDir, 0o700);
324
+ const cookiesPath = getDefaultGeminiWrapperCookiesPath(env);
325
+ writeFileSync(cookiesPath, `${JSON.stringify({
326
+ cookies: {
327
+ "__Secure-1PSID": secure1psid,
328
+ ...(secure1psidts ? { "__Secure-1PSIDTS": secure1psidts } : {})
329
+ }
330
+ }, null, 2)}\n`, {
331
+ encoding: "utf8",
332
+ mode: 0o600
333
+ });
334
+ tryChmod(cookiesPath, 0o600);
335
+ clearGeminiWrapperCookieCache(env);
336
+ return cookiesPath;
337
+ }
338
+ export function saveGeminiWrapperCookieJarFile(cookies, env = process.env) {
339
+ const sanitizedCookies = sanitizeGeminiWrapperBrowserCookies(cookies);
340
+ if (!sanitizedCookies.some((cookie) => cookie.name === "__Secure-1PSID")) {
341
+ throw new Error("Imported Gemini browser cookies did not include __Secure-1PSID.");
342
+ }
343
+ const configDir = getPatchPilotConfigDir(env);
344
+ mkdirSync(configDir, {
345
+ recursive: true,
346
+ mode: 0o700
347
+ });
348
+ tryChmod(configDir, 0o700);
349
+ const cookiesPath = getDefaultGeminiWrapperCookiesPath(env);
350
+ writeFileSync(cookiesPath, `${JSON.stringify({
351
+ cookies: sanitizedCookies
352
+ }, null, 2)}\n`, {
353
+ encoding: "utf8",
354
+ mode: 0o600
355
+ });
356
+ tryChmod(cookiesPath, 0o600);
357
+ clearGeminiWrapperCookieCache(env);
358
+ return cookiesPath;
359
+ }
360
+ export async function importGeminiWrapperBrowserCookies(options = {}) {
361
+ const env = options.env ?? process.env;
362
+ const pythonCommand = options.pythonCommand ?? readGeminiWrapperPythonCommand(env);
363
+ const isInstalled = await ensureGeminiWebApiInstalled(pythonCommand, env);
364
+ if (!isInstalled) {
365
+ throw new Error(`Gemini browser cookie import needs the managed bridge. Run /doctor fix or install manually: ${geminiWebApiInstallCommand}`);
366
+ }
367
+ if (!(await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
368
+ throw new Error("Gemini browser cookie import needs the optional browser-cookie3 dependency. Run `patchpilot doctor --provider gemini-wrapper --fix`, then retry.");
369
+ }
370
+ const output = await runGeminiBrowserCookieImportBridge(pythonCommand, options.timeoutMs ?? geminiBrowserCookieImportTimeoutMs);
371
+ if (output.error) {
372
+ throw new Error(output.error);
373
+ }
374
+ const cookies = sanitizeGeminiWrapperBrowserCookies(output.cookies ?? []);
375
+ const hasSecure1psid = cookies.some((cookie) => cookie.name === "__Secure-1PSID");
376
+ const hasSecure1psidts = cookies.some((cookie) => cookie.name === "__Secure-1PSIDTS");
377
+ if (!hasSecure1psid) {
378
+ throw new Error("No __Secure-1PSID cookie was found in supported browsers. Sign in to Gemini in a supported browser, then retry the explicit import.");
379
+ }
380
+ const cookiesPath = saveGeminiWrapperCookieJarFile(cookies, env);
381
+ return {
382
+ cookiesPath,
383
+ cookieCount: cookies.length,
384
+ source: output.source ?? "browser",
385
+ availableSources: output.availableSources ?? [],
386
+ hasSecure1psid,
387
+ hasSecure1psidts
388
+ };
389
+ }
390
+ export function readGeminiWrapperPythonCommand(env = process.env) {
391
+ return env.PATCHPILOT_GEMINI_WRAPPER_PYTHON?.trim() || getManagedGeminiWrapperPythonPath(env);
392
+ }
393
+ export function readGeminiWrapperBootstrapPythonCommand(env = process.env) {
394
+ // Windows ships the `python` launcher (or the `py` redirector); `python3` is
395
+ // a POSIX convention and is usually not on PATH there.
396
+ return env.PATCHPILOT_GEMINI_WRAPPER_BOOTSTRAP_PYTHON?.trim() || (process.platform === "win32" ? "python" : "python3");
397
+ }
398
+ export function getGeminiWrapperVenvDir(env = process.env) {
399
+ return path.join(getPatchPilotConfigDir(env), "gemini-wrapper-venv");
400
+ }
401
+ export function getGeminiWrapperCookieCacheDir(env = process.env) {
402
+ return path.join(getPatchPilotConfigDir(env), "gemini-webapi-cache");
403
+ }
404
+ export function clearGeminiWrapperCookieCache(env = process.env) {
405
+ const cacheDir = getGeminiWrapperCookieCacheDir(env);
406
+ rmSync(cacheDir, {
407
+ recursive: true,
408
+ force: true
409
+ });
410
+ mkdirSync(cacheDir, {
411
+ recursive: true,
412
+ mode: 0o700
413
+ });
414
+ tryChmod(cacheDir, 0o700);
415
+ }
416
+ export function getManagedGeminiWrapperPythonPath(env = process.env) {
417
+ return path.join(getGeminiWrapperVenvDir(env), process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
418
+ }
419
+ export function readGeminiWrapperSecure1psid(env = process.env) {
420
+ return env.GEMINI_SECURE_1PSID?.trim() || "";
421
+ }
422
+ export function readGeminiWrapperSecure1psidts(env = process.env) {
423
+ return env.GEMINI_SECURE_1PSIDTS?.trim() || "";
424
+ }
425
+ export function geminiWrapperRequiresApiKey(baseUrl) {
426
+ return !isLocalWrapperUrl(baseUrl);
427
+ }
428
+ export async function isGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand()) {
429
+ const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi"], 20_000);
430
+ return result.ok;
431
+ }
432
+ export async function isGeminiBrowserCookieImportInstalled(pythonCommand = readGeminiWrapperPythonCommand()) {
433
+ const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi, browser_cookie3"], 20_000);
434
+ return result.ok;
435
+ }
436
+ export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand(), env = process.env) {
437
+ if ((await isGeminiWebApiInstalled(pythonCommand)) && (await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
438
+ return true;
439
+ }
440
+ const managedPython = getManagedGeminiWrapperPythonPath(env);
441
+ if (path.resolve(pythonCommand) === path.resolve(managedPython)) {
442
+ const venvDir = getGeminiWrapperVenvDir(env);
443
+ mkdirSync(getPatchPilotConfigDir(env), {
444
+ recursive: true,
445
+ mode: 0o700
446
+ });
447
+ tryChmod(getPatchPilotConfigDir(env), 0o700);
448
+ if (!existsSync(managedPython)) {
449
+ const venvResult = await runQuietCommand(readGeminiWrapperBootstrapPythonCommand(env), ["-m", "venv", venvDir], 120_000);
450
+ if (!venvResult.ok) {
451
+ return false;
452
+ }
453
+ }
454
+ }
455
+ const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`, "browser-cookie3"], 180_000);
456
+ return installResult.ok && (await isGeminiBrowserCookieImportInstalled(pythonCommand));
457
+ }
458
+ function isLocalWrapperUrl(baseUrl) {
459
+ try {
460
+ const url = new URL(baseUrl);
461
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
462
+ }
463
+ catch {
464
+ return false;
465
+ }
466
+ }
467
+ function normalizeGeminiWrapperModel(model) {
468
+ const trimmedModel = model.trim();
469
+ return trimmedModel || defaultGeminiWrapperModel;
470
+ }
471
+ /**
472
+ * Map a curated shortcut to the model string the gemini_webapi bridge accepts
473
+ * when live discovery is unavailable. Every returned non-empty value must be a
474
+ * valid gemini_webapi `Model` model_name, or the bridge request hard-fails.
475
+ */
476
+ export function normalizeGeminiWrapperBridgeModelFallback(model) {
477
+ const normalizedModel = normalizeGeminiWrapperModel(model).trim();
478
+ if (normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default") {
479
+ return "";
480
+ }
481
+ // Legacy alias — a stray "flash-lite" maps to the closest valid tier rather
482
+ // than crashing the bridge with an unknown-model error.
483
+ if (normalizedModel === "flash-lite") {
484
+ return "gemini-3-flash";
485
+ }
486
+ if (normalizedModel === "flash") {
487
+ return "gemini-3-flash";
488
+ }
489
+ if (normalizedModel === "thinking") {
490
+ return "gemini-3-flash-thinking";
491
+ }
492
+ if (normalizedModel === "pro") {
493
+ return "gemini-3-pro";
494
+ }
495
+ return normalizedModel;
496
+ }
497
+ function normalizeGeminiWrapperModelDescriptors(models) {
498
+ return models
499
+ .map((model) => (typeof model === "string" ? descriptorFromModelId(model) : normalizeGeminiWrapperModelDescriptor(model)))
500
+ .filter((model) => Boolean(model?.id && isLikelyGeminiWrapperChatModel(formatModelDescriptorSearchText(model))));
501
+ }
502
+ function normalizeGeminiWrapperModelDescriptor(model) {
503
+ const id = String(model.id || model.modelName || model.displayName || "").trim();
504
+ if (!id) {
505
+ return null;
506
+ }
507
+ return cleanUndefined({
508
+ id,
509
+ modelName: model.modelName?.trim() || undefined,
510
+ displayName: model.displayName?.trim() || undefined,
511
+ description: model.description?.trim() || undefined,
512
+ isAvailable: model.isAvailable,
513
+ capacity: typeof model.capacity === "number" && Number.isFinite(model.capacity) ? model.capacity : undefined,
514
+ capacityField: typeof model.capacityField === "number" && Number.isFinite(model.capacityField) ? model.capacityField : undefined,
515
+ advancedOnly: model.advancedOnly,
516
+ legacy: model.legacy
517
+ });
518
+ }
519
+ function descriptorFromModelId(model) {
520
+ const id = model.trim();
521
+ return {
522
+ id,
523
+ modelName: id,
524
+ displayName: id
525
+ };
526
+ }
527
+ function mergeGeminiWrapperModelDescriptors(models) {
528
+ const descriptors = [
529
+ {
530
+ id: "auto",
531
+ displayName: "Auto",
532
+ description: "Let Gemini Web choose its current default model."
533
+ },
534
+ {
535
+ id: "flash",
536
+ displayName: "Flash",
537
+ description: "Gemini 3 Flash — fast tier, resolved from live Gemini Web discovery."
538
+ },
539
+ {
540
+ id: "pro",
541
+ displayName: "Pro",
542
+ description: "Shortcut resolved from live Gemini Web discovery."
543
+ },
544
+ {
545
+ id: "thinking",
546
+ modelName: "gemini-3-flash-thinking",
547
+ displayName: "Thinking legacy",
548
+ description: "Legacy gemini_webapi shortcut; Gemini Web now exposes Denkaufwand instead of a recommended thinking model.",
549
+ legacy: true
550
+ }
551
+ ];
552
+ for (const model of models) {
553
+ if (!hasGeminiWrapperDescriptor(descriptors, model)) {
554
+ descriptors.push(model);
555
+ }
556
+ }
557
+ return descriptors;
558
+ }
559
+ function hasGeminiWrapperDescriptor(descriptors, model) {
560
+ const keys = descriptorKeys(model);
561
+ return descriptors.some((descriptor) => descriptorKeys(descriptor).some((key) => keys.includes(key)));
562
+ }
563
+ function descriptorKeys(model) {
564
+ return [model.id, model.modelName, model.displayName]
565
+ .filter((value) => Boolean(value?.trim()))
566
+ .map((value) => value.trim().toLowerCase());
567
+ }
568
+ function resolveGeminiWrapperShortcutDescriptor(shortcut, descriptors) {
569
+ const dynamicDescriptors = descriptors.filter((descriptor) => !geminiWrapperCuratedModels.includes(descriptor.id));
570
+ const matches = (pattern) => dynamicDescriptors.filter((descriptor) => pattern.test(formatModelDescriptorSearchText(descriptor)));
571
+ if (shortcut === "flash") {
572
+ return matches(/\bflash\b/i).find((descriptor) => !/lite|thinking/i.test(formatModelDescriptorSearchText(descriptor))) ?? null;
573
+ }
574
+ if (shortcut === "pro") {
575
+ return matches(/\bpro\b/i)[0] ?? null;
576
+ }
577
+ if (shortcut === "thinking") {
578
+ return matches(/thinking/i)[0] ?? null;
579
+ }
580
+ return null;
581
+ }
582
+ function isGeminiWrapperShortcutModel(model) {
583
+ return geminiWrapperCuratedModels.includes(model);
584
+ }
585
+ function formatModelDescriptorSearchText(model) {
586
+ return [model.id, model.modelName, model.displayName, model.description].filter(Boolean).join(" ");
587
+ }
588
+ function isLikelyGeminiWrapperChatModel(model) {
589
+ const normalizedModel = model.toLowerCase();
590
+ return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|bidi|live)/.test(normalizedModel);
591
+ }
592
+ function isUnauthenticatedGeminiWebError(error) {
593
+ return Boolean(error?.includes("Gemini web cookies are expired or unauthenticated"));
594
+ }
595
+ function isUnauthenticatedGeminiWebStatus(status) {
596
+ return /unauth|expired|invalid/i.test(status ?? "");
597
+ }
598
+ function readGeminiWrapperRuntimeOptions(env = process.env) {
599
+ return {
600
+ maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 8192),
601
+ temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1),
602
+ bridgeMinIntervalMs: readNonNegativeInteger(env.PATCHPILOT_GEMINI_WRAPPER_MIN_INTERVAL_MS, 1500),
603
+ bridgeTimeoutMs: readPositiveInteger(env.PATCHPILOT_GEMINI_WRAPPER_TIMEOUT_MS, 180_000)
604
+ };
605
+ }
606
+ function toBridgePrompt(messages, formatJson) {
607
+ const body = messages
608
+ .map((message) => `${message.role.toUpperCase()}:\n${message.content}`)
609
+ .join("\n\n");
610
+ return formatJson ? `${body}\n\nReturn only valid JSON.` : body;
611
+ }
612
+ function toTelemetry(payload, durationMs, model) {
613
+ const promptTokens = payload.usage?.prompt_tokens ?? 0;
614
+ const responseTokens = payload.usage?.completion_tokens ?? 0;
615
+ return attachTokenCost({
616
+ promptTokens,
617
+ cachedPromptTokens: payload.usage?.prompt_tokens_details?.cached_tokens ?? 0,
618
+ cacheWriteTokens: payload.usage?.prompt_tokens_details?.cache_write_tokens ?? 0,
619
+ responseTokens,
620
+ totalTokens: payload.usage?.total_tokens ?? promptTokens + responseTokens,
621
+ evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
622
+ promptDurationMs: 0,
623
+ responseDurationMs: durationMs,
624
+ totalDurationMs: durationMs,
625
+ tokenSource: "provider"
626
+ }, "gemini-wrapper", model);
627
+ }
628
+ function toEstimatedTelemetry(prompt, content, durationMs, model) {
629
+ const promptTokens = estimateTokens(prompt);
630
+ const responseTokens = estimateTokens(content);
631
+ return attachTokenCost({
632
+ promptTokens,
633
+ cachedPromptTokens: 0,
634
+ cacheWriteTokens: 0,
635
+ responseTokens,
636
+ totalTokens: promptTokens + responseTokens,
637
+ evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
638
+ promptDurationMs: 0,
639
+ responseDurationMs: durationMs,
640
+ totalDurationMs: durationMs,
641
+ tokenSource: "estimated"
642
+ }, "gemini-wrapper", model);
643
+ }
644
+ let geminiBridgeQueue = Promise.resolve();
645
+ let lastGeminiBridgeStartedAt = 0;
646
+ function getGeminiWrapperBridgeTimeoutMs(model, configuredTimeoutMs = readGeminiWrapperRuntimeOptions().bridgeTimeoutMs) {
647
+ const timeoutMs = configuredTimeoutMs ?? 180_000;
648
+ return model.includes("pro") ? Math.max(timeoutMs, 240_000) : timeoutMs;
649
+ }
650
+ function runThrottledGeminiWebApiBridge(pythonCommand, input, signal, minIntervalMs = readGeminiWrapperRuntimeOptions().bridgeMinIntervalMs, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model)) {
651
+ const run = async () => {
652
+ const intervalMs = minIntervalMs ?? 0;
653
+ const waitMs = Math.max(0, intervalMs - (Date.now() - lastGeminiBridgeStartedAt));
654
+ if (waitMs > 0) {
655
+ await sleep(waitMs, signal);
656
+ }
657
+ lastGeminiBridgeStartedAt = Date.now();
658
+ return await runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal);
659
+ };
660
+ const result = geminiBridgeQueue.then(run, run);
661
+ geminiBridgeQueue = result.catch(() => undefined);
662
+ return result;
663
+ }
664
+ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
665
+ return new Promise((resolve, reject) => {
666
+ const cookieCacheDir = getGeminiWrapperCookieCacheDir();
667
+ mkdirSync(cookieCacheDir, {
668
+ recursive: true,
669
+ mode: 0o700
670
+ });
671
+ tryChmod(cookieCacheDir, 0o700);
672
+ const child = spawn(pythonCommand, ["-c", geminiWebApiBridgeScript], {
673
+ env: {
674
+ ...process.env,
675
+ GEMINI_COOKIE_PATH: cookieCacheDir
676
+ },
677
+ stdio: ["pipe", "pipe", "pipe"],
678
+ detached: process.platform !== "win32",
679
+ windowsHide: true
680
+ });
681
+ let settled = false;
682
+ let pendingKillError = null;
683
+ let killTimer = null;
684
+ const cleanup = () => {
685
+ clearTimeout(timeout);
686
+ if (killTimer) {
687
+ clearTimeout(killTimer);
688
+ }
689
+ signal?.removeEventListener("abort", abort);
690
+ };
691
+ const settleReject = (error) => {
692
+ if (settled) {
693
+ return;
694
+ }
695
+ settled = true;
696
+ cleanup();
697
+ reject(error);
698
+ };
699
+ const settleResolve = (output) => {
700
+ if (settled) {
701
+ return;
702
+ }
703
+ settled = true;
704
+ cleanup();
705
+ resolve(output);
706
+ };
707
+ const killChild = (signalName) => {
708
+ if (child.pid && process.platform !== "win32") {
709
+ try {
710
+ process.kill(-child.pid, signalName);
711
+ return;
712
+ }
713
+ catch {
714
+ // Fall through to killing the child directly.
715
+ }
716
+ }
717
+ child.kill(signalName);
718
+ };
719
+ const terminateChild = (error) => {
720
+ if (settled || pendingKillError) {
721
+ return;
722
+ }
723
+ pendingKillError = error;
724
+ killChild("SIGTERM");
725
+ killTimer = setTimeout(() => {
726
+ killChild("SIGKILL");
727
+ }, 1500);
728
+ };
729
+ const abort = () => {
730
+ terminateChild(new Error("Gemini-API bridge request aborted."));
731
+ };
732
+ signal?.addEventListener("abort", abort, {
733
+ once: true
734
+ });
735
+ const timeout = setTimeout(() => {
736
+ terminateChild(new Error(`Gemini-API bridge timed out after ${Math.round(timeoutMs / 1000)}s.`));
737
+ }, timeoutMs);
738
+ let stdout = "";
739
+ let stderr = "";
740
+ child.stdout.on("data", (chunk) => {
741
+ stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
742
+ });
743
+ child.stderr.on("data", (chunk) => {
744
+ stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
745
+ });
746
+ child.on("error", (error) => {
747
+ settleReject(error);
748
+ });
749
+ child.on("close", (exitCode) => {
750
+ if (pendingKillError) {
751
+ settleReject(pendingKillError);
752
+ return;
753
+ }
754
+ if (exitCode !== 0) {
755
+ settleReject(new Error(stderr.trim() || `Gemini-API bridge exited with ${exitCode}.`));
756
+ return;
757
+ }
758
+ try {
759
+ settleResolve(JSON.parse(stdout));
760
+ }
761
+ catch {
762
+ settleReject(new Error(`Gemini-API bridge returned invalid JSON.${stderr.trim() ? ` ${stderr.trim()}` : ""}`));
763
+ }
764
+ });
765
+ child.stdin.end(JSON.stringify({
766
+ ...input,
767
+ timeoutSeconds: Math.max(30, Math.min(300, Math.floor(timeoutMs / 1000)))
768
+ }));
769
+ });
770
+ }
771
+ function runGeminiBrowserCookieImportBridge(pythonCommand, timeoutMs) {
772
+ return new Promise((resolve, reject) => {
773
+ const child = spawn(pythonCommand, ["-c", geminiBrowserCookieImportScript], {
774
+ env: process.env,
775
+ stdio: ["ignore", "pipe", "pipe"],
776
+ detached: process.platform !== "win32",
777
+ windowsHide: true
778
+ });
779
+ let stdout = "";
780
+ let stderr = "";
781
+ let settled = false;
782
+ const timeout = setTimeout(() => {
783
+ if (settled) {
784
+ return;
785
+ }
786
+ settled = true;
787
+ killChildProcess(child, "SIGTERM");
788
+ reject(new Error("Gemini browser cookie import timed out."));
789
+ }, timeoutMs);
790
+ child.stdout.on("data", (chunk) => {
791
+ stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
792
+ });
793
+ child.stderr.on("data", (chunk) => {
794
+ stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
795
+ });
796
+ child.on("error", (error) => {
797
+ if (settled) {
798
+ return;
799
+ }
800
+ settled = true;
801
+ clearTimeout(timeout);
802
+ reject(error);
803
+ });
804
+ child.on("close", (code) => {
805
+ if (settled) {
806
+ return;
807
+ }
808
+ settled = true;
809
+ clearTimeout(timeout);
810
+ if (code !== 0) {
811
+ reject(new Error(`Gemini browser cookie import failed.${stderr.trim() ? ` ${redactCookieValues(stderr.trim())}` : ""}`));
812
+ return;
813
+ }
814
+ try {
815
+ resolve(JSON.parse(stdout.trim() || "{}"));
816
+ }
817
+ catch {
818
+ reject(new Error("Gemini browser cookie import did not return valid JSON."));
819
+ }
820
+ });
821
+ });
822
+ }
823
+ function killChildProcess(child, signalName) {
824
+ if (child.pid && process.platform !== "win32") {
825
+ try {
826
+ process.kill(-child.pid, signalName);
827
+ return;
828
+ }
829
+ catch {
830
+ // Fall through to killing the child directly.
831
+ }
832
+ }
833
+ child.kill(signalName);
834
+ }
835
+ function sanitizeGeminiWrapperBrowserCookies(cookies) {
836
+ const byName = new Map();
837
+ for (const cookie of cookies) {
838
+ if (!cookie || typeof cookie.name !== "string" || typeof cookie.value !== "string") {
839
+ continue;
840
+ }
841
+ const name = cookie.name.trim();
842
+ const value = cookie.value.trim();
843
+ if (!name || !value || !geminiWrapperBrowserCookieNames.has(name)) {
844
+ continue;
845
+ }
846
+ const domain = typeof cookie.domain === "string" ? cookie.domain.trim() : "";
847
+ const normalizedDomain = domain.replace(/^\./, "").toLowerCase();
848
+ if (normalizedDomain && normalizedDomain !== "google.com" && !normalizedDomain.endsWith(".google.com")) {
849
+ continue;
850
+ }
851
+ const previousCookie = byName.get(name);
852
+ const previousExpires = typeof previousCookie?.expires === "number" ? previousCookie.expires : 0;
853
+ const nextExpires = typeof cookie.expires === "number" ? cookie.expires : 0;
854
+ if (previousCookie && previousExpires > nextExpires) {
855
+ continue;
856
+ }
857
+ byName.set(name, {
858
+ name,
859
+ value,
860
+ ...(domain ? { domain } : {}),
861
+ ...(typeof cookie.path === "string" && cookie.path ? { path: cookie.path } : {}),
862
+ ...(typeof cookie.expires === "number" ? { expires: cookie.expires } : {}),
863
+ ...(typeof cookie.source === "string" && cookie.source ? { source: cookie.source } : {})
864
+ });
865
+ }
866
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
867
+ }
868
+ function redactCookieValues(value) {
869
+ return value
870
+ .replace(/(__Secure-[A-Za-z0-9_-]+)\s*=\s*([^\s,;]+)/g, "$1=<redacted>")
871
+ .replace(/(PSID[A-Z]*)\s*[:=]\s*([^\s,;]+)/gi, "$1=<redacted>");
872
+ }
873
+ function appendClipped(currentValue, chunk, maxLength) {
874
+ const nextValue = currentValue + chunk;
875
+ if (nextValue.length <= maxLength) {
876
+ return nextValue;
877
+ }
878
+ const clippedMarker = `\n...[clipped ${nextValue.length - maxLength} chars]...\n`;
879
+ return `${clippedMarker}${nextValue.slice(-maxLength + clippedMarker.length)}`;
880
+ }
881
+ const geminiBrowserCookieImportScript = String.raw `
882
+ import json
883
+
884
+ ALLOWED_COOKIE_NAMES = {
885
+ "__Secure-1PSID",
886
+ "__Secure-1PSIDTS",
887
+ "__Secure-1PSIDCC",
888
+ "__Secure-1PAPISID",
889
+ "__Secure-3PSID",
890
+ "__Secure-3PSIDTS",
891
+ "__Secure-3PSIDCC",
892
+ "__Secure-3PAPISID",
893
+ "__Secure-ENID",
894
+ "AEC",
895
+ "COMPASS",
896
+ "GOOGLE_ABUSE_EXEMPTION",
897
+ "NID",
898
+ "SID",
899
+ "HSID",
900
+ "SSID",
901
+ "APISID",
902
+ "SAPISID",
903
+ }
904
+
905
+ def normalize_domain(value):
906
+ return (value or "").lstrip(".").lower()
907
+
908
+ def is_google_domain(value):
909
+ domain = normalize_domain(value)
910
+ return not domain or domain == "google.com" or domain.endswith(".google.com")
911
+
912
+ try:
913
+ from gemini_webapi.utils.load_browser_cookies import load_browser_cookies
914
+
915
+ browser_cookies = load_browser_cookies(domain_name="google.com", verbose=False)
916
+ candidates = []
917
+ available_sources = []
918
+ for browser_name, cookies in browser_cookies.items():
919
+ filtered = []
920
+ for cookie in cookies or []:
921
+ name = cookie.get("name")
922
+ value = cookie.get("value")
923
+ if name not in ALLOWED_COOKIE_NAMES or not value or not is_google_domain(cookie.get("domain")):
924
+ continue
925
+ filtered.append({
926
+ "name": name,
927
+ "value": value,
928
+ "domain": cookie.get("domain") or ".google.com",
929
+ "path": cookie.get("path") or "/",
930
+ "expires": cookie.get("expires"),
931
+ "source": browser_name,
932
+ })
933
+ if filtered:
934
+ available_sources.append(browser_name)
935
+ has_psid = any(cookie["name"] == "__Secure-1PSID" for cookie in filtered)
936
+ has_psidts = any(cookie["name"] == "__Secure-1PSIDTS" for cookie in filtered)
937
+ candidates.append({
938
+ "browser": browser_name,
939
+ "cookies": filtered,
940
+ "score": (1 if has_psid else 0, 1 if has_psidts else 0, len(filtered), browser_name),
941
+ })
942
+
943
+ candidates.sort(key=lambda item: item["score"], reverse=True)
944
+ if not candidates or not any(cookie["name"] == "__Secure-1PSID" for cookie in candidates[0]["cookies"]):
945
+ print(json.dumps({
946
+ "error": "No Gemini browser cookies were found in supported browsers. Sign in to Gemini in Chrome, Brave, Edge, Firefox, Safari, or another supported browser, then retry the explicit import.",
947
+ "availableSources": available_sources,
948
+ }))
949
+ else:
950
+ selected = candidates[0]
951
+ print(json.dumps({
952
+ "cookies": selected["cookies"],
953
+ "source": selected["browser"],
954
+ "availableSources": available_sources,
955
+ }))
956
+ except Exception as exc:
957
+ print(json.dumps({
958
+ "error": f"Gemini browser cookie import failed: {type(exc).__name__}. Unlock the browser profile or macOS Keychain access, then retry."
959
+ }))
960
+ `;
961
+ const geminiWebApiBridgeScript = String.raw `
962
+ import asyncio
963
+ import json
964
+ import os
965
+ import sys
966
+
967
+ async def main():
968
+ from gemini_webapi import GeminiClient
969
+
970
+ payload = json.load(sys.stdin)
971
+ cookies = {}
972
+ cookies_path = payload.get("cookiesJson")
973
+ if cookies_path:
974
+ with open(cookies_path, "r", encoding="utf-8") as handle:
975
+ data = json.load(handle)
976
+ if isinstance(data, dict) and isinstance(data.get("cookies"), dict):
977
+ cookies.update(data["cookies"])
978
+ elif isinstance(data, dict) and isinstance(data.get("cookies"), list):
979
+ cookies.update({item.get("name"): item.get("value") for item in data["cookies"] if item.get("name") and item.get("value")})
980
+ elif isinstance(data, list):
981
+ cookies.update({item.get("name"): item.get("value") for item in data if item.get("name") and item.get("value")})
982
+ elif isinstance(data, dict):
983
+ cookies.update({key: value for key, value in data.items() if isinstance(value, str)})
984
+
985
+ psid = cookies.get("__Secure-1PSID") or payload.get("secure1psid") or os.getenv("GEMINI_SECURE_1PSID")
986
+ psidts = cookies.get("__Secure-1PSIDTS") or payload.get("secure1psidts") or os.getenv("GEMINI_SECURE_1PSIDTS") or ""
987
+ if not psid:
988
+ print(json.dumps({"error": "Missing __Secure-1PSID. Set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON or GEMINI_SECURE_1PSID."}))
989
+ return
990
+
991
+ extra = {key: value for key, value in cookies.items() if key not in {"__Secure-1PSID", "__Secure-1PSIDTS"}}
992
+ attempt_timeout = max(30, int(payload.get("timeoutSeconds") or 60))
993
+
994
+ def is_transient_network_error(message):
995
+ lower = message.lower()
996
+ return (
997
+ "curl: (28)" in lower
998
+ or "curl: (56)" in lower
999
+ or "connection timed out" in lower
1000
+ or "connection closed abruptly" in lower
1001
+ or "connection reset" in lower
1002
+ or "server returned nothing" in lower
1003
+ or "unexpected eof" in lower
1004
+ or "stream error" in lower
1005
+ or "http/2 stream" in lower
1006
+ or "operation timed out" in lower
1007
+ or "readtimeout" in lower
1008
+ or "timeouterror" in lower
1009
+ or "temporarily unavailable" in lower
1010
+ )
1011
+
1012
+ def account_status_name(client):
1013
+ status = getattr(client, "account_status", None)
1014
+ return getattr(status, "name", str(status or ""))
1015
+
1016
+ def expired_cookie_error():
1017
+ return "Gemini web cookies are expired or unauthenticated. Refresh ~/.patchpilot/gemini-cookies.json through Gemini-Wrapper onboarding."
1018
+
1019
+ def clear_cookie_cache():
1020
+ cache_dir = os.getenv("GEMINI_COOKIE_PATH")
1021
+ if not cache_dir:
1022
+ return
1023
+ try:
1024
+ for filename in os.listdir(cache_dir):
1025
+ if filename.startswith(".cached_cookies_") and filename.endswith(".json"):
1026
+ os.remove(os.path.join(cache_dir, filename))
1027
+ except OSError:
1028
+ pass
1029
+
1030
+ async def generate_once(psidts_value):
1031
+ client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts_value, cookies=extra or None, proxy=payload.get("proxy"))
1032
+ await client.init(timeout=attempt_timeout, auto_refresh=True, verbose=False)
1033
+ try:
1034
+ status_name = account_status_name(client)
1035
+ if "unauth" in status_name.lower() or "expired" in status_name.lower() or "invalid" in status_name.lower():
1036
+ return {"error": expired_cookie_error(), "accountStatus": status_name}
1037
+
1038
+ if payload.get("command") == "authCheck":
1039
+ return {"content": "ok", "accountStatus": status_name}
1040
+
1041
+ if payload.get("command") == "models":
1042
+ models = []
1043
+ model_descriptors = []
1044
+ for model in client.list_models() or []:
1045
+ if not getattr(model, "is_available", True):
1046
+ continue
1047
+ model_id = getattr(model, "model_id", None) or ""
1048
+ name = getattr(model, "model_name", None) or ""
1049
+ display_name = getattr(model, "display_name", None) or ""
1050
+ description = getattr(model, "description", None) or ""
1051
+ selection_id = model_id or name or display_name
1052
+ if selection_id:
1053
+ descriptor = {
1054
+ "id": selection_id,
1055
+ "modelName": name or None,
1056
+ "displayName": display_name or name or selection_id,
1057
+ "description": description or None,
1058
+ "isAvailable": bool(getattr(model, "is_available", True)),
1059
+ "advancedOnly": bool(getattr(model, "advanced_only", False)),
1060
+ }
1061
+ capacity = getattr(model, "capacity", None)
1062
+ capacity_field = getattr(model, "capacity_field", None)
1063
+ if isinstance(capacity, (int, float)):
1064
+ descriptor["capacity"] = capacity
1065
+ if isinstance(capacity_field, (int, float)):
1066
+ descriptor["capacityField"] = capacity_field
1067
+ model_descriptors.append({key: value for key, value in descriptor.items() if value is not None})
1068
+ name = name or display_name or model_id
1069
+ if name:
1070
+ models.append(name)
1071
+ return {"models": models, "modelDescriptors": model_descriptors, "accountStatus": status_name}
1072
+
1073
+ request_model = payload.get("model") or ""
1074
+ request_kwargs = {"temporary": True}
1075
+ if request_model:
1076
+ request_kwargs["model"] = request_model
1077
+ files = payload.get("files") or []
1078
+ if files:
1079
+ request_kwargs["files"] = files
1080
+ used_model = request_model or "auto"
1081
+ model_warning = None
1082
+ prompt_text = payload.get("prompt") or ""
1083
+ try:
1084
+ response = await client.generate_content(prompt_text, **request_kwargs)
1085
+ except ValueError as model_error:
1086
+ # An unknown model name must not fail the whole request — retry
1087
+ # on Gemini Web's default model and report it back.
1088
+ if "model" not in str(model_error).lower():
1089
+ raise
1090
+ request_kwargs.pop("model", None)
1091
+ used_model = "auto"
1092
+ model_warning = f"Requested model '{request_model}' is unavailable; used the Gemini Web default instead."
1093
+ response = await client.generate_content(prompt_text, **request_kwargs)
1094
+ text = getattr(response, "text", None) or str(response)
1095
+ result = {"content": text, "accountStatus": status_name, "model": used_model}
1096
+ if model_warning:
1097
+ result["warning"] = model_warning
1098
+ return result
1099
+ finally:
1100
+ await client.close()
1101
+
1102
+ async def generate_with_timestamp(psidts_value):
1103
+ last_error = None
1104
+ max_attempts = 2 if payload.get("files") else 3
1105
+ for attempt in range(max_attempts):
1106
+ try:
1107
+ return await asyncio.wait_for(generate_once(psidts_value), timeout=attempt_timeout)
1108
+ except asyncio.TimeoutError:
1109
+ if attempt >= max_attempts - 1:
1110
+ raise TimeoutError(f"Gemini-API bridge attempt timed out after {attempt_timeout}s.")
1111
+ await asyncio.sleep(1.5 * (attempt + 1))
1112
+ except Exception as exc:
1113
+ last_error = exc
1114
+ if attempt >= max_attempts - 1 or not is_transient_network_error(str(exc)):
1115
+ raise
1116
+ await asyncio.sleep(1.5 * (attempt + 1))
1117
+ raise last_error
1118
+
1119
+ try:
1120
+ result = await generate_with_timestamp(psidts)
1121
+ except Exception as exc:
1122
+ message = str(exc)
1123
+ if psidts and ("__Secure-1PSIDTS" in message or "SECURE_1PSIDTS" in message):
1124
+ clear_cookie_cache()
1125
+ result = await generate_with_timestamp("")
1126
+ else:
1127
+ raise
1128
+ else:
1129
+ if psidts and isinstance(result, dict) and result.get("error") == expired_cookie_error():
1130
+ clear_cookie_cache()
1131
+ retry_result = await generate_with_timestamp("")
1132
+ if not retry_result.get("error"):
1133
+ retry_result["warning"] = "Retried without stale __Secure-1PSIDTS."
1134
+ print(json.dumps(retry_result))
1135
+ return
1136
+ print(json.dumps(result))
1137
+ return
1138
+
1139
+ result["warning"] = "Retried without stale __Secure-1PSIDTS."
1140
+ print(json.dumps(result))
1141
+
1142
+ try:
1143
+ asyncio.run(main())
1144
+ except Exception as exc:
1145
+ message = str(exc)
1146
+ if "currently unavailable or the request structure is outdated" in message:
1147
+ message = f"{message} Try /model auto and refresh Gemini-Wrapper cookies if this persists."
1148
+ print(json.dumps({"error": message}))
1149
+ `;
1150
+ function cleanUndefined(value) {
1151
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
1152
+ }
1153
+ async function readGeminiWrapperResponse(response) {
1154
+ const text = await response.text().catch(() => "");
1155
+ if (!text.trim()) {
1156
+ return {
1157
+ payload: {},
1158
+ text: ""
1159
+ };
1160
+ }
1161
+ try {
1162
+ return {
1163
+ payload: JSON.parse(text),
1164
+ text
1165
+ };
1166
+ }
1167
+ catch {
1168
+ return {
1169
+ payload: {},
1170
+ text
1171
+ };
1172
+ }
1173
+ }
1174
+ function formatGeminiWrapperErrorReason(payload, text, response) {
1175
+ const retryAfter = response.headers.get("retry-after");
1176
+ const providerMessage = payload.error?.message?.trim() || text.replace(/\s+/g, " ").trim().slice(0, 300);
1177
+ const parts = [providerMessage, retryAfter ? `retry-after ${retryAfter}s` : ""].filter(Boolean);
1178
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
1179
+ }
1180
+ function readPositiveInteger(value, fallback) {
1181
+ const parsedValue = Number.parseInt(value ?? "", 10);
1182
+ return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
1183
+ }
1184
+ function readNonNegativeInteger(value, fallback) {
1185
+ const parsedValue = Number.parseInt(value ?? "", 10);
1186
+ return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
1187
+ }
1188
+ function readTemperature(value, fallback) {
1189
+ const parsedValue = Number.parseFloat(value ?? "");
1190
+ return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
1191
+ }
1192
+ function sleep(ms, signal) {
1193
+ if (ms <= 0) {
1194
+ return Promise.resolve();
1195
+ }
1196
+ return new Promise((resolve, reject) => {
1197
+ if (signal?.aborted) {
1198
+ reject(new Error("Gemini-API bridge request aborted."));
1199
+ return;
1200
+ }
1201
+ const timeout = setTimeout(() => {
1202
+ signal?.removeEventListener("abort", abort);
1203
+ resolve();
1204
+ }, ms);
1205
+ const abort = () => {
1206
+ clearTimeout(timeout);
1207
+ reject(new Error("Gemini-API bridge request aborted."));
1208
+ };
1209
+ signal?.addEventListener("abort", abort, {
1210
+ once: true
1211
+ });
1212
+ });
1213
+ }
1214
+ function tryChmod(filePath, mode) {
1215
+ try {
1216
+ chmodSync(filePath, mode);
1217
+ }
1218
+ catch {
1219
+ // Best-effort hardening for platforms that do not support POSIX permissions.
1220
+ }
1221
+ }
1222
+ function runQuietCommand(command, args, timeoutMs) {
1223
+ return new Promise((resolve) => {
1224
+ const child = spawn(command, args, {
1225
+ stdio: ["ignore", "pipe", "pipe"],
1226
+ windowsHide: true
1227
+ });
1228
+ const timeout = setTimeout(() => {
1229
+ child.kill();
1230
+ resolve({
1231
+ ok: false,
1232
+ output: `${command} ${args.join(" ")} timed out.`
1233
+ });
1234
+ }, timeoutMs);
1235
+ let output = "";
1236
+ child.stdout.on("data", (chunk) => {
1237
+ output += chunk.toString("utf8");
1238
+ });
1239
+ child.stderr.on("data", (chunk) => {
1240
+ output += chunk.toString("utf8");
1241
+ });
1242
+ child.on("error", (error) => {
1243
+ clearTimeout(timeout);
1244
+ resolve({
1245
+ ok: false,
1246
+ output: error.message
1247
+ });
1248
+ });
1249
+ child.on("close", (exitCode) => {
1250
+ clearTimeout(timeout);
1251
+ resolve({
1252
+ ok: exitCode === 0,
1253
+ output: output.trim()
1254
+ });
1255
+ });
1256
+ });
1257
+ }
1258
+ //# sourceMappingURL=geminiWrapper.js.map