@jx-grxf/patchpilot 1.0.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.
- package/README.md +51 -16
- package/dist/cli.js +46 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +44 -1
- package/dist/core/agent.js +617 -70
- package/dist/core/agent.js.map +1 -1
- package/dist/core/clipboard.d.ts +14 -0
- package/dist/core/clipboard.js +134 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/codex.d.ts +8 -0
- package/dist/core/codex.js +28 -2
- package/dist/core/codex.js.map +1 -1
- package/dist/core/compaction.d.ts +23 -0
- package/dist/core/compaction.js +145 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/contextFormat.d.ts +21 -0
- package/dist/core/contextFormat.js +87 -0
- package/dist/core/contextFormat.js.map +1 -0
- package/dist/core/contextItem.d.ts +41 -0
- package/dist/core/contextItem.js +93 -0
- package/dist/core/contextItem.js.map +1 -0
- package/dist/core/contextStore.d.ts +48 -0
- package/dist/core/contextStore.js +306 -0
- package/dist/core/contextStore.js.map +1 -0
- package/dist/core/doctor.js +9 -8
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/gemini.js +10 -4
- package/dist/core/gemini.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +43 -2
- package/dist/core/geminiWrapper.js +582 -42
- package/dist/core/geminiWrapper.js.map +1 -1
- package/dist/core/http.js +70 -6
- package/dist/core/http.js.map +1 -1
- package/dist/core/json.d.ts +1 -1
- package/dist/core/json.js +18 -20
- package/dist/core/json.js.map +1 -1
- package/dist/core/nvidia.d.ts +1 -1
- package/dist/core/nvidia.js +13 -4
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/ollama.js +13 -3
- package/dist/core/ollama.js.map +1 -1
- package/dist/core/openrouter.js +15 -6
- package/dist/core/openrouter.js.map +1 -1
- package/dist/core/reasoning.js +3 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.js +9 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/tokenAccounting.d.ts +4 -0
- package/dist/core/tokenAccounting.js +75 -13
- package/dist/core/tokenAccounting.js.map +1 -1
- package/dist/core/types.d.ts +58 -3
- package/dist/core/types.js +30 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/updateCheck.d.ts +19 -0
- package/dist/core/updateCheck.js +103 -0
- package/dist/core/updateCheck.js.map +1 -0
- package/dist/core/workspace.d.ts +29 -0
- package/dist/core/workspace.js +1271 -92
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1346 -112
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +109 -6
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/ApprovalPanel.js +16 -1
- package/dist/tui/components/ApprovalPanel.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +26 -3
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.d.ts +3 -0
- package/dist/tui/components/Composer.js +57 -5
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +1 -1
- package/dist/tui/components/ExperimentalPanel.js +5 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +12 -0
- package/dist/tui/components/OnboardingPanel.js +69 -21
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/StartupBanner.d.ts +4 -0
- package/dist/tui/components/StartupBanner.js +9 -0
- package/dist/tui/components/StartupBanner.js.map +1 -0
- package/dist/tui/components/Transcript.d.ts +7 -0
- package/dist/tui/components/Transcript.js +86 -16
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/contextCommands.d.ts +8 -0
- package/dist/tui/contextCommands.js +205 -0
- package/dist/tui/contextCommands.js.map +1 -0
- package/dist/tui/experimental/AnimatedText.d.ts +38 -0
- package/dist/tui/experimental/AnimatedText.js +55 -0
- package/dist/tui/experimental/AnimatedText.js.map +1 -0
- package/dist/tui/experimental/Banner.d.ts +10 -0
- package/dist/tui/experimental/Banner.js +33 -0
- package/dist/tui/experimental/Banner.js.map +1 -0
- package/dist/tui/experimental/CommandPalette.d.ts +11 -0
- package/dist/tui/experimental/CommandPalette.js +25 -0
- package/dist/tui/experimental/CommandPalette.js.map +1 -0
- package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
- package/dist/tui/experimental/ExperimentalShell.js +366 -0
- package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
- package/dist/tui/experimental/ThemePicker.d.ts +13 -0
- package/dist/tui/experimental/ThemePicker.js +12 -0
- package/dist/tui/experimental/ThemePicker.js.map +1 -0
- package/dist/tui/experimental/attachments.d.ts +35 -0
- package/dist/tui/experimental/attachments.js +244 -0
- package/dist/tui/experimental/attachments.js.map +1 -0
- package/dist/tui/experimental/composer.d.ts +24 -0
- package/dist/tui/experimental/composer.js +84 -0
- package/dist/tui/experimental/composer.js.map +1 -0
- package/dist/tui/experimental/geminiPricing.d.ts +16 -0
- package/dist/tui/experimental/geminiPricing.js +39 -0
- package/dist/tui/experimental/geminiPricing.js.map +1 -0
- package/dist/tui/experimental/layout.d.ts +46 -0
- package/dist/tui/experimental/layout.js +112 -0
- package/dist/tui/experimental/layout.js.map +1 -0
- package/dist/tui/experimental/theme.d.ts +35 -0
- package/dist/tui/experimental/theme.js +86 -0
- package/dist/tui/experimental/theme.js.map +1 -0
- package/dist/tui/experimental/transcriptRows.d.ts +20 -0
- package/dist/tui/experimental/transcriptRows.js +169 -0
- package/dist/tui/experimental/transcriptRows.js.map +1 -0
- package/dist/tui/experimental/ultraModes.d.ts +46 -0
- package/dist/tui/experimental/ultraModes.js +95 -0
- package/dist/tui/experimental/ultraModes.js.map +1 -0
- package/dist/tui/experimental/ultramaxx.d.ts +19 -0
- package/dist/tui/experimental/ultramaxx.js +43 -0
- package/dist/tui/experimental/ultramaxx.js.map +1 -0
- package/dist/tui/format.d.ts +4 -2
- package/dist/tui/format.js +14 -0
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/hosts.js +7 -1
- package/dist/tui/hosts.js.map +1 -1
- package/dist/tui/layout.d.ts +26 -0
- package/dist/tui/layout.js +66 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/modelSelection.d.ts +1 -1
- package/dist/tui/modelSelection.js +8 -6
- package/dist/tui/modelSelection.js.map +1 -1
- package/dist/tui/modes.d.ts +7 -0
- package/dist/tui/modes.js +12 -0
- package/dist/tui/modes.js.map +1 -1
- package/dist/tui/onboardingPreferences.d.ts +37 -0
- package/dist/tui/onboardingPreferences.js +118 -0
- package/dist/tui/onboardingPreferences.js.map +1 -0
- package/dist/tui/runStatus.d.ts +50 -0
- package/dist/tui/runStatus.js +164 -0
- package/dist/tui/runStatus.js.map +1 -0
- package/dist/tui/types.d.ts +8 -0
- package/dist/tui/types.js.map +1 -1
- package/docs/architecture.md +115 -0
- package/docs/gemini-wrapper.md +23 -0
- package/docs/product-context.md +43 -0
- package/docs/releases/v1.0.1.md +25 -0
- package/docs/releases/v1.1.0.md +30 -0
- package/docs/releases/v1.2.0.md +28 -0
- package/package.json +4 -2
|
@@ -5,8 +5,39 @@ import { getPatchPilotConfigDir } from "./env.js";
|
|
|
5
5
|
import { fetchWithTimeout } from "./http.js";
|
|
6
6
|
import { attachTokenCost, estimateTokens } from "./tokenAccounting.js";
|
|
7
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];
|
|
8
14
|
export const geminiWebApiVersion = "2.0.0";
|
|
9
|
-
export const geminiWebApiInstallCommand =
|
|
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
|
+
]);
|
|
10
41
|
export class GeminiWrapperClient {
|
|
11
42
|
baseUrl;
|
|
12
43
|
apiKey;
|
|
@@ -14,6 +45,9 @@ export class GeminiWrapperClient {
|
|
|
14
45
|
mode;
|
|
15
46
|
pythonCommand;
|
|
16
47
|
cookiesJson;
|
|
48
|
+
modelDescriptorCache = null;
|
|
49
|
+
pythonBridgeReadyUntil = 0;
|
|
50
|
+
pythonBridgeReadyPromise = null;
|
|
17
51
|
constructor(baseUrl = readGeminiWrapperBaseUrl(), apiKey = readGeminiWrapperApiKey(), runtimeOptions = readGeminiWrapperRuntimeOptions(), mode = readGeminiWrapperMode(), pythonCommand = readGeminiWrapperPythonCommand(), cookiesJson = readGeminiWrapperCookiesJson()) {
|
|
18
52
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
19
53
|
this.apiKey = apiKey;
|
|
@@ -41,9 +75,9 @@ export class GeminiWrapperClient {
|
|
|
41
75
|
signal: options.signal
|
|
42
76
|
});
|
|
43
77
|
const durationMs = Date.now() - startedAt;
|
|
44
|
-
const payload =
|
|
78
|
+
const { payload, text } = await readGeminiWrapperResponse(response);
|
|
45
79
|
if (!response.ok || payload.error) {
|
|
46
|
-
const reason = payload
|
|
80
|
+
const reason = formatGeminiWrapperErrorReason(payload, text, response);
|
|
47
81
|
if (response.status === 401 || response.status === 403) {
|
|
48
82
|
throw new Error("Gemini-Wrapper authentication failed. Check PATCHPILOT_GEMINI_WRAPPER_API_KEY.");
|
|
49
83
|
}
|
|
@@ -62,6 +96,12 @@ export class GeminiWrapperClient {
|
|
|
62
96
|
};
|
|
63
97
|
}
|
|
64
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
|
+
}
|
|
65
105
|
if (this.usesPythonBridge()) {
|
|
66
106
|
await this.assertPythonBridgeReady();
|
|
67
107
|
const result = await this.runPythonBridge({
|
|
@@ -71,29 +111,59 @@ export class GeminiWrapperClient {
|
|
|
71
111
|
if (result.error) {
|
|
72
112
|
throw new Error(result.error);
|
|
73
113
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
80
121
|
}
|
|
81
122
|
this.assertConfigured();
|
|
82
123
|
const response = await this.fetchGeminiWrapper("/models", {
|
|
83
124
|
headers: this.headers()
|
|
84
125
|
});
|
|
85
|
-
const payload =
|
|
126
|
+
const { payload, text } = await readGeminiWrapperResponse(response);
|
|
86
127
|
if (!response.ok || payload.error) {
|
|
87
|
-
const reason = payload
|
|
128
|
+
const reason = formatGeminiWrapperErrorReason(payload, text, response);
|
|
88
129
|
throw new Error(`Gemini-Wrapper models failed with HTTP ${response.status}.${reason}`);
|
|
89
130
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
};
|
|
97
167
|
}
|
|
98
168
|
async checkBridgeAuth() {
|
|
99
169
|
await this.assertPythonBridgeReady();
|
|
@@ -104,12 +174,15 @@ export class GeminiWrapperClient {
|
|
|
104
174
|
if (result.error) {
|
|
105
175
|
throw new Error(result.error);
|
|
106
176
|
}
|
|
177
|
+
if (isUnauthenticatedGeminiWebStatus(result.accountStatus)) {
|
|
178
|
+
throw new Error("Gemini-API bridge cookies are expired or unauthenticated. Refresh Gemini-Wrapper cookies.");
|
|
179
|
+
}
|
|
107
180
|
}
|
|
108
181
|
async fetchGeminiWrapper(path, init) {
|
|
109
182
|
try {
|
|
110
183
|
return await fetchWithTimeout(`${this.baseUrl}${path}`, init, {
|
|
111
184
|
timeoutMs: init?.method === "POST" ? 90_000 : 8000,
|
|
112
|
-
retries: init?.method === "POST" ?
|
|
185
|
+
retries: init?.method === "POST" ? 2 : 1,
|
|
113
186
|
label: `Gemini-Wrapper ${path}`
|
|
114
187
|
});
|
|
115
188
|
}
|
|
@@ -142,7 +215,7 @@ export class GeminiWrapperClient {
|
|
|
142
215
|
await this.assertPythonBridgeReady();
|
|
143
216
|
const startedAt = Date.now();
|
|
144
217
|
const prompt = toBridgePrompt(options.messages, options.formatJson);
|
|
145
|
-
const bridgeModel =
|
|
218
|
+
const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
|
|
146
219
|
const result = await this.runPythonBridge({
|
|
147
220
|
command: "chat",
|
|
148
221
|
model: bridgeModel,
|
|
@@ -178,14 +251,46 @@ export class GeminiWrapperClient {
|
|
|
178
251
|
proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
|
|
179
252
|
}, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
|
|
180
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
|
+
}
|
|
181
274
|
async assertPythonBridgeReady() {
|
|
182
275
|
if (!this.cookiesJson && !readGeminiWrapperSecure1psid()) {
|
|
183
|
-
throw new Error("Gemini-API bridge needs explicit auth.
|
|
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.");
|
|
184
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() {
|
|
185
289
|
const installed = await isGeminiWebApiInstalled(this.pythonCommand);
|
|
186
290
|
if (!installed) {
|
|
187
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}`);
|
|
188
292
|
}
|
|
293
|
+
this.pythonBridgeReadyUntil = Date.now() + pythonBridgeReadyTtlMs;
|
|
189
294
|
}
|
|
190
295
|
}
|
|
191
296
|
export function readGeminiWrapperMode(env = process.env) {
|
|
@@ -230,11 +335,65 @@ export function saveGeminiWrapperCookieFile(values, env = process.env) {
|
|
|
230
335
|
clearGeminiWrapperCookieCache(env);
|
|
231
336
|
return cookiesPath;
|
|
232
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
|
+
}
|
|
233
390
|
export function readGeminiWrapperPythonCommand(env = process.env) {
|
|
234
391
|
return env.PATCHPILOT_GEMINI_WRAPPER_PYTHON?.trim() || getManagedGeminiWrapperPythonPath(env);
|
|
235
392
|
}
|
|
236
393
|
export function readGeminiWrapperBootstrapPythonCommand(env = process.env) {
|
|
237
|
-
|
|
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");
|
|
238
397
|
}
|
|
239
398
|
export function getGeminiWrapperVenvDir(env = process.env) {
|
|
240
399
|
return path.join(getPatchPilotConfigDir(env), "gemini-wrapper-venv");
|
|
@@ -270,8 +429,12 @@ export async function isGeminiWebApiInstalled(pythonCommand = readGeminiWrapperP
|
|
|
270
429
|
const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi"], 20_000);
|
|
271
430
|
return result.ok;
|
|
272
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
|
+
}
|
|
273
436
|
export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand(), env = process.env) {
|
|
274
|
-
if (await isGeminiWebApiInstalled(pythonCommand)) {
|
|
437
|
+
if ((await isGeminiWebApiInstalled(pythonCommand)) && (await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
|
|
275
438
|
return true;
|
|
276
439
|
}
|
|
277
440
|
const managedPython = getManagedGeminiWrapperPythonPath(env);
|
|
@@ -289,8 +452,8 @@ export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrap
|
|
|
289
452
|
}
|
|
290
453
|
}
|
|
291
454
|
}
|
|
292
|
-
const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}
|
|
293
|
-
return installResult.ok && (await
|
|
455
|
+
const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`, "browser-cookie3"], 180_000);
|
|
456
|
+
return installResult.ok && (await isGeminiBrowserCookieImportInstalled(pythonCommand));
|
|
294
457
|
}
|
|
295
458
|
function isLocalWrapperUrl(baseUrl) {
|
|
296
459
|
try {
|
|
@@ -305,20 +468,136 @@ function normalizeGeminiWrapperModel(model) {
|
|
|
305
468
|
const trimmedModel = model.trim();
|
|
306
469
|
return trimmedModel || defaultGeminiWrapperModel;
|
|
307
470
|
}
|
|
308
|
-
|
|
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) {
|
|
309
477
|
const normalizedModel = normalizeGeminiWrapperModel(model).trim();
|
|
310
|
-
|
|
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(" ");
|
|
311
587
|
}
|
|
312
588
|
function isLikelyGeminiWrapperChatModel(model) {
|
|
313
589
|
const normalizedModel = model.toLowerCase();
|
|
314
|
-
return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|
|
|
590
|
+
return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|bidi|live)/.test(normalizedModel);
|
|
315
591
|
}
|
|
316
592
|
function isUnauthenticatedGeminiWebError(error) {
|
|
317
593
|
return Boolean(error?.includes("Gemini web cookies are expired or unauthenticated"));
|
|
318
594
|
}
|
|
595
|
+
function isUnauthenticatedGeminiWebStatus(status) {
|
|
596
|
+
return /unauth|expired|invalid/i.test(status ?? "");
|
|
597
|
+
}
|
|
319
598
|
function readGeminiWrapperRuntimeOptions(env = process.env) {
|
|
320
599
|
return {
|
|
321
|
-
maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT,
|
|
600
|
+
maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 8192),
|
|
322
601
|
temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1),
|
|
323
602
|
bridgeMinIntervalMs: readNonNegativeInteger(env.PATCHPILOT_GEMINI_WRAPPER_MIN_INTERVAL_MS, 1500),
|
|
324
603
|
bridgeTimeoutMs: readPositiveInteger(env.PATCHPILOT_GEMINI_WRAPPER_TIMEOUT_MS, 180_000)
|
|
@@ -459,10 +738,10 @@ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
|
|
|
459
738
|
let stdout = "";
|
|
460
739
|
let stderr = "";
|
|
461
740
|
child.stdout.on("data", (chunk) => {
|
|
462
|
-
stdout
|
|
741
|
+
stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
|
|
463
742
|
});
|
|
464
743
|
child.stderr.on("data", (chunk) => {
|
|
465
|
-
stderr
|
|
744
|
+
stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
|
|
466
745
|
});
|
|
467
746
|
child.on("error", (error) => {
|
|
468
747
|
settleReject(error);
|
|
@@ -489,6 +768,196 @@ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
|
|
|
489
768
|
}));
|
|
490
769
|
});
|
|
491
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
|
+
`;
|
|
492
961
|
const geminiWebApiBridgeScript = String.raw `
|
|
493
962
|
import asyncio
|
|
494
963
|
import json
|
|
@@ -526,7 +995,14 @@ async def main():
|
|
|
526
995
|
lower = message.lower()
|
|
527
996
|
return (
|
|
528
997
|
"curl: (28)" in lower
|
|
998
|
+
or "curl: (56)" in lower
|
|
529
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
|
|
530
1006
|
or "operation timed out" in lower
|
|
531
1007
|
or "readtimeout" in lower
|
|
532
1008
|
or "timeouterror" in lower
|
|
@@ -553,44 +1029,89 @@ async def main():
|
|
|
553
1029
|
|
|
554
1030
|
async def generate_once(psidts_value):
|
|
555
1031
|
client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts_value, cookies=extra or None, proxy=payload.get("proxy"))
|
|
556
|
-
await client.init(timeout=
|
|
1032
|
+
await client.init(timeout=attempt_timeout, auto_refresh=True, verbose=False)
|
|
557
1033
|
try:
|
|
558
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
|
+
|
|
559
1038
|
if payload.get("command") == "authCheck":
|
|
560
1039
|
return {"content": "ok", "accountStatus": status_name}
|
|
561
1040
|
|
|
562
1041
|
if payload.get("command") == "models":
|
|
563
1042
|
models = []
|
|
1043
|
+
model_descriptors = []
|
|
564
1044
|
for model in client.list_models() or []:
|
|
565
1045
|
if not getattr(model, "is_available", True):
|
|
566
1046
|
continue
|
|
567
|
-
|
|
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
|
|
568
1069
|
if name:
|
|
569
1070
|
models.append(name)
|
|
570
|
-
return {"models": models, "accountStatus": status_name}
|
|
1071
|
+
return {"models": models, "modelDescriptors": model_descriptors, "accountStatus": status_name}
|
|
571
1072
|
|
|
572
1073
|
request_model = payload.get("model") or ""
|
|
573
1074
|
request_kwargs = {"temporary": True}
|
|
574
1075
|
if request_model:
|
|
575
1076
|
request_kwargs["model"] = request_model
|
|
576
|
-
|
|
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)
|
|
577
1094
|
text = getattr(response, "text", None) or str(response)
|
|
578
|
-
|
|
1095
|
+
result = {"content": text, "accountStatus": status_name, "model": used_model}
|
|
1096
|
+
if model_warning:
|
|
1097
|
+
result["warning"] = model_warning
|
|
1098
|
+
return result
|
|
579
1099
|
finally:
|
|
580
1100
|
await client.close()
|
|
581
1101
|
|
|
582
1102
|
async def generate_with_timestamp(psidts_value):
|
|
583
1103
|
last_error = None
|
|
584
|
-
|
|
1104
|
+
max_attempts = 2 if payload.get("files") else 3
|
|
1105
|
+
for attempt in range(max_attempts):
|
|
585
1106
|
try:
|
|
586
1107
|
return await asyncio.wait_for(generate_once(psidts_value), timeout=attempt_timeout)
|
|
587
1108
|
except asyncio.TimeoutError:
|
|
588
|
-
if attempt >=
|
|
1109
|
+
if attempt >= max_attempts - 1:
|
|
589
1110
|
raise TimeoutError(f"Gemini-API bridge attempt timed out after {attempt_timeout}s.")
|
|
590
1111
|
await asyncio.sleep(1.5 * (attempt + 1))
|
|
591
1112
|
except Exception as exc:
|
|
592
1113
|
last_error = exc
|
|
593
|
-
if attempt >=
|
|
1114
|
+
if attempt >= max_attempts - 1 or not is_transient_network_error(str(exc)):
|
|
594
1115
|
raise
|
|
595
1116
|
await asyncio.sleep(1.5 * (attempt + 1))
|
|
596
1117
|
raise last_error
|
|
@@ -629,14 +1150,33 @@ except Exception as exc:
|
|
|
629
1150
|
function cleanUndefined(value) {
|
|
630
1151
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
631
1152
|
}
|
|
632
|
-
async function
|
|
1153
|
+
async function readGeminiWrapperResponse(response) {
|
|
1154
|
+
const text = await response.text().catch(() => "");
|
|
1155
|
+
if (!text.trim()) {
|
|
1156
|
+
return {
|
|
1157
|
+
payload: {},
|
|
1158
|
+
text: ""
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
633
1161
|
try {
|
|
634
|
-
return
|
|
1162
|
+
return {
|
|
1163
|
+
payload: JSON.parse(text),
|
|
1164
|
+
text
|
|
1165
|
+
};
|
|
635
1166
|
}
|
|
636
1167
|
catch {
|
|
637
|
-
return {
|
|
1168
|
+
return {
|
|
1169
|
+
payload: {},
|
|
1170
|
+
text
|
|
1171
|
+
};
|
|
638
1172
|
}
|
|
639
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
|
+
}
|
|
640
1180
|
function readPositiveInteger(value, fallback) {
|
|
641
1181
|
const parsedValue = Number.parseInt(value ?? "", 10);
|
|
642
1182
|
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
|