@jx-grxf/patchpilot 0.3.1-beta → 1.0.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 (81) hide show
  1. package/.env.example +17 -1
  2. package/README.md +80 -22
  3. package/SECURITY.md +10 -2
  4. package/dist/cli.js +59 -13
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent.d.ts +3 -0
  7. package/dist/core/agent.js +56 -12
  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/doctor.d.ts +4 -1
  13. package/dist/core/doctor.js +119 -1
  14. package/dist/core/doctor.js.map +1 -1
  15. package/dist/core/gemini.js +27 -14
  16. package/dist/core/gemini.js.map +1 -1
  17. package/dist/core/geminiWrapper.d.ts +51 -0
  18. package/dist/core/geminiWrapper.js +718 -0
  19. package/dist/core/geminiWrapper.js.map +1 -0
  20. package/dist/core/json.js +65 -1
  21. package/dist/core/json.js.map +1 -1
  22. package/dist/core/memory.d.ts +16 -0
  23. package/dist/core/memory.js +108 -0
  24. package/dist/core/memory.js.map +1 -0
  25. package/dist/core/modelClient.js +7 -0
  26. package/dist/core/modelClient.js.map +1 -1
  27. package/dist/core/nvidia.js +20 -2
  28. package/dist/core/nvidia.js.map +1 -1
  29. package/dist/core/openrouter.d.ts +2 -0
  30. package/dist/core/openrouter.js +51 -7
  31. package/dist/core/openrouter.js.map +1 -1
  32. package/dist/core/projectInit.d.ts +6 -0
  33. package/dist/core/projectInit.js +44 -0
  34. package/dist/core/projectInit.js.map +1 -0
  35. package/dist/core/reasoning.js +3 -0
  36. package/dist/core/reasoning.js.map +1 -1
  37. package/dist/core/session.d.ts +1 -0
  38. package/dist/core/session.js +46 -0
  39. package/dist/core/session.js.map +1 -1
  40. package/dist/core/types.d.ts +9 -4
  41. package/dist/core/workspace.d.ts +8 -0
  42. package/dist/core/workspace.js +314 -21
  43. package/dist/core/workspace.js.map +1 -1
  44. package/dist/tui/App.js +571 -81
  45. package/dist/tui/App.js.map +1 -1
  46. package/dist/tui/commands.js +35 -6
  47. package/dist/tui/commands.js.map +1 -1
  48. package/dist/tui/components/ApprovalPanel.d.ts +6 -0
  49. package/dist/tui/components/ApprovalPanel.js +16 -0
  50. package/dist/tui/components/ApprovalPanel.js.map +1 -0
  51. package/dist/tui/components/CommandSuggestions.js +8 -3
  52. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  53. package/dist/tui/components/Composer.d.ts +1 -0
  54. package/dist/tui/components/Composer.js +1 -1
  55. package/dist/tui/components/Composer.js.map +1 -1
  56. package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
  57. package/dist/tui/components/ExperimentalPanel.js +33 -0
  58. package/dist/tui/components/ExperimentalPanel.js.map +1 -0
  59. package/dist/tui/components/Header.js +3 -3
  60. package/dist/tui/components/Header.js.map +1 -1
  61. package/dist/tui/components/OnboardingPanel.d.ts +13 -1
  62. package/dist/tui/components/OnboardingPanel.js +23 -9
  63. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  64. package/dist/tui/components/Sidebar.js +32 -26
  65. package/dist/tui/components/Sidebar.js.map +1 -1
  66. package/dist/tui/components/Transcript.js +4 -3
  67. package/dist/tui/components/Transcript.js.map +1 -1
  68. package/dist/tui/format.js +7 -7
  69. package/dist/tui/format.js.map +1 -1
  70. package/dist/tui/modes.d.ts +1 -1
  71. package/dist/tui/modes.js +8 -2
  72. package/dist/tui/modes.js.map +1 -1
  73. package/docs/gemini-wrapper.md +87 -0
  74. package/docs/releases/v0.1.1-beta.md +18 -0
  75. package/docs/releases/v0.2.1.md +1 -1
  76. package/docs/releases/v0.3.1-beta.md +4 -0
  77. package/docs/releases/v0.4.0.md +27 -0
  78. package/docs/releases/v1.0.0.md +28 -0
  79. package/docs/showcase/patchpilot-banner.png +0 -0
  80. package/docs/showcase/patchpilot-logo.png +0 -0
  81. package/package.json +5 -2
@@ -0,0 +1,718 @@
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
+ export const geminiWebApiVersion = "2.0.0";
9
+ export const geminiWebApiInstallCommand = `PatchPilot managed install: python3 -m venv ~/.patchpilot/gemini-wrapper-venv && ~/.patchpilot/gemini-wrapper-venv/bin/python -m pip install gemini_webapi==${geminiWebApiVersion}`;
10
+ export class GeminiWrapperClient {
11
+ baseUrl;
12
+ apiKey;
13
+ runtimeOptions;
14
+ mode;
15
+ pythonCommand;
16
+ cookiesJson;
17
+ constructor(baseUrl = readGeminiWrapperBaseUrl(), apiKey = readGeminiWrapperApiKey(), runtimeOptions = readGeminiWrapperRuntimeOptions(), mode = readGeminiWrapperMode(), pythonCommand = readGeminiWrapperPythonCommand(), cookiesJson = readGeminiWrapperCookiesJson()) {
18
+ this.baseUrl = baseUrl.replace(/\/$/, "");
19
+ this.apiKey = apiKey;
20
+ this.runtimeOptions = runtimeOptions;
21
+ this.mode = mode;
22
+ this.pythonCommand = pythonCommand;
23
+ this.cookiesJson = cookiesJson;
24
+ }
25
+ async chat(options) {
26
+ if (this.usesPythonBridge()) {
27
+ return await this.chatWithPythonBridge(options);
28
+ }
29
+ this.assertConfigured();
30
+ const startedAt = Date.now();
31
+ const response = await this.fetchGeminiWrapper("/chat/completions", {
32
+ method: "POST",
33
+ headers: this.headers(),
34
+ body: JSON.stringify(cleanUndefined({
35
+ model: normalizeGeminiWrapperModel(options.model),
36
+ messages: options.messages,
37
+ max_tokens: this.runtimeOptions.maxTokens,
38
+ temperature: this.runtimeOptions.temperature,
39
+ response_format: options.formatJson ? { type: "json_object" } : undefined
40
+ })),
41
+ signal: options.signal
42
+ });
43
+ const durationMs = Date.now() - startedAt;
44
+ const payload = (await readJsonSafely(response));
45
+ if (!response.ok || payload.error) {
46
+ const reason = payload.error?.message ? ` ${payload.error.message}` : "";
47
+ if (response.status === 401 || response.status === 403) {
48
+ throw new Error("Gemini-Wrapper authentication failed. Check PATCHPILOT_GEMINI_WRAPPER_API_KEY.");
49
+ }
50
+ if (response.status === 429) {
51
+ throw new Error(`Gemini-Wrapper rate limit hit for model "${options.model}".${reason}`);
52
+ }
53
+ throw new Error(`Gemini-Wrapper chat failed for model "${options.model}": HTTP ${response.status}.${reason}`);
54
+ }
55
+ const content = payload.choices?.[0]?.message?.content?.trim() ?? "";
56
+ if (!content) {
57
+ throw new Error("Gemini-Wrapper returned an empty response.");
58
+ }
59
+ return {
60
+ content,
61
+ telemetry: toTelemetry(payload, durationMs, options.model)
62
+ };
63
+ }
64
+ async listModels() {
65
+ if (this.usesPythonBridge()) {
66
+ await this.assertPythonBridgeReady();
67
+ const result = await this.runPythonBridge({
68
+ command: "models",
69
+ model: defaultGeminiWrapperModel
70
+ });
71
+ if (result.error) {
72
+ throw new Error(result.error);
73
+ }
74
+ const models = [
75
+ ...new Set((result.models ?? [])
76
+ .map((model) => model.trim())
77
+ .filter((model) => model && isLikelyGeminiWrapperChatModel(model)))
78
+ ];
79
+ return [defaultGeminiWrapperModel, ...models.filter((model) => model !== defaultGeminiWrapperModel)];
80
+ }
81
+ this.assertConfigured();
82
+ const response = await this.fetchGeminiWrapper("/models", {
83
+ headers: this.headers()
84
+ });
85
+ const payload = (await readJsonSafely(response));
86
+ if (!response.ok || payload.error) {
87
+ const reason = payload.error?.message ? ` ${payload.error.message}` : "";
88
+ throw new Error(`Gemini-Wrapper models failed with HTTP ${response.status}.${reason}`);
89
+ }
90
+ const models = [
91
+ ...new Set(payload.data
92
+ ?.map((model) => model.id?.trim())
93
+ .filter((model) => Boolean(model))
94
+ .filter(isLikelyGeminiWrapperChatModel) ?? [])
95
+ ].sort();
96
+ return models.length > 0 ? models : [defaultGeminiWrapperModel];
97
+ }
98
+ async checkBridgeAuth() {
99
+ await this.assertPythonBridgeReady();
100
+ const result = await this.runPythonBridge({
101
+ command: "authCheck",
102
+ model: defaultGeminiWrapperModel
103
+ });
104
+ if (result.error) {
105
+ throw new Error(result.error);
106
+ }
107
+ }
108
+ async fetchGeminiWrapper(path, init) {
109
+ try {
110
+ return await fetchWithTimeout(`${this.baseUrl}${path}`, init, {
111
+ timeoutMs: init?.method === "POST" ? 90_000 : 8000,
112
+ retries: init?.method === "POST" ? 0 : 1,
113
+ label: `Gemini-Wrapper ${path}`
114
+ });
115
+ }
116
+ catch (error) {
117
+ const suffix = error instanceof Error ? ` ${error.message}` : "";
118
+ throw new Error(`Cannot reach Gemini-Wrapper API at ${this.baseUrl}.${suffix}`);
119
+ }
120
+ }
121
+ headers() {
122
+ return cleanUndefined({
123
+ "Content-Type": "application/json",
124
+ Authorization: this.apiKey ? `Bearer ${this.apiKey}` : undefined
125
+ });
126
+ }
127
+ assertConfigured() {
128
+ if (this.usesPythonBridge()) {
129
+ return;
130
+ }
131
+ if (!this.baseUrl) {
132
+ 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}`);
133
+ }
134
+ if (geminiWrapperRequiresApiKey(this.baseUrl) && !this.apiKey) {
135
+ 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.");
136
+ }
137
+ }
138
+ usesPythonBridge() {
139
+ return this.mode === "python" || (this.mode === "auto" && !this.baseUrl);
140
+ }
141
+ async chatWithPythonBridge(options) {
142
+ await this.assertPythonBridgeReady();
143
+ const startedAt = Date.now();
144
+ const prompt = toBridgePrompt(options.messages, options.formatJson);
145
+ const bridgeModel = normalizeGeminiWrapperBridgeModel(options.model);
146
+ const result = await this.runPythonBridge({
147
+ command: "chat",
148
+ model: bridgeModel,
149
+ prompt
150
+ }, options.signal, getGeminiWrapperBridgeTimeoutMs(bridgeModel || defaultGeminiWrapperModel, this.runtimeOptions.bridgeTimeoutMs));
151
+ const durationMs = Date.now() - startedAt;
152
+ const content = result.content?.trim() ?? "";
153
+ if (!content) {
154
+ throw new Error(result.error ? `Gemini-API bridge failed: ${result.error}` : "Gemini-API bridge returned an empty response.");
155
+ }
156
+ return {
157
+ content,
158
+ telemetry: toEstimatedTelemetry(prompt, content, durationMs, result.model ?? options.model)
159
+ };
160
+ }
161
+ async runPythonBridge(input, signal, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model, this.runtimeOptions.bridgeTimeoutMs)) {
162
+ const result = await runThrottledGeminiWebApiBridge(this.pythonCommand, {
163
+ ...input,
164
+ cookiesJson: this.cookiesJson || undefined,
165
+ secure1psid: readGeminiWrapperSecure1psid() || undefined,
166
+ secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
167
+ proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
168
+ }, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
169
+ if (!isUnauthenticatedGeminiWebError(result.error)) {
170
+ return result;
171
+ }
172
+ clearGeminiWrapperCookieCache();
173
+ return await runThrottledGeminiWebApiBridge(this.pythonCommand, {
174
+ ...input,
175
+ cookiesJson: this.cookiesJson || undefined,
176
+ secure1psid: readGeminiWrapperSecure1psid() || undefined,
177
+ secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
178
+ proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
179
+ }, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
180
+ }
181
+ async assertPythonBridgeReady() {
182
+ if (!this.cookiesJson && !readGeminiWrapperSecure1psid()) {
183
+ throw new Error("Gemini-API bridge needs explicit auth. Set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON to a JSON cookie file, or set GEMINI_SECURE_1PSID / GEMINI_SECURE_1PSIDTS. PatchPilot will not scan browser cookies.");
184
+ }
185
+ const installed = await isGeminiWebApiInstalled(this.pythonCommand);
186
+ if (!installed) {
187
+ 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
+ }
189
+ }
190
+ }
191
+ export function readGeminiWrapperMode(env = process.env) {
192
+ const value = env.PATCHPILOT_GEMINI_WRAPPER_MODE?.trim().toLowerCase();
193
+ return value === "http" || value === "python" || value === "auto" ? value : "auto";
194
+ }
195
+ export function readGeminiWrapperBaseUrl(env = process.env) {
196
+ return env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL?.trim() || "";
197
+ }
198
+ export function readGeminiWrapperApiKey(env = process.env) {
199
+ return env.PATCHPILOT_GEMINI_WRAPPER_API_KEY?.trim() || env.GEMINI_WRAPPER_API_KEY?.trim() || "";
200
+ }
201
+ export function readGeminiWrapperCookiesJson(env = process.env) {
202
+ return env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON?.trim() || env.GEMINI_COOKIES_JSON?.trim() || "";
203
+ }
204
+ export function getDefaultGeminiWrapperCookiesPath(env = process.env) {
205
+ return path.join(getPatchPilotConfigDir(env), "gemini-cookies.json");
206
+ }
207
+ export function saveGeminiWrapperCookieFile(values, env = process.env) {
208
+ const secure1psid = values.secure1psid.trim();
209
+ const secure1psidts = values.secure1psidts?.trim() ?? "";
210
+ if (!secure1psid) {
211
+ throw new Error("__Secure-1PSID cannot be empty.");
212
+ }
213
+ const configDir = getPatchPilotConfigDir(env);
214
+ mkdirSync(configDir, {
215
+ recursive: true,
216
+ mode: 0o700
217
+ });
218
+ tryChmod(configDir, 0o700);
219
+ const cookiesPath = getDefaultGeminiWrapperCookiesPath(env);
220
+ writeFileSync(cookiesPath, `${JSON.stringify({
221
+ cookies: {
222
+ "__Secure-1PSID": secure1psid,
223
+ ...(secure1psidts ? { "__Secure-1PSIDTS": secure1psidts } : {})
224
+ }
225
+ }, null, 2)}\n`, {
226
+ encoding: "utf8",
227
+ mode: 0o600
228
+ });
229
+ tryChmod(cookiesPath, 0o600);
230
+ clearGeminiWrapperCookieCache(env);
231
+ return cookiesPath;
232
+ }
233
+ export function readGeminiWrapperPythonCommand(env = process.env) {
234
+ return env.PATCHPILOT_GEMINI_WRAPPER_PYTHON?.trim() || getManagedGeminiWrapperPythonPath(env);
235
+ }
236
+ export function readGeminiWrapperBootstrapPythonCommand(env = process.env) {
237
+ return env.PATCHPILOT_GEMINI_WRAPPER_BOOTSTRAP_PYTHON?.trim() || "python3";
238
+ }
239
+ export function getGeminiWrapperVenvDir(env = process.env) {
240
+ return path.join(getPatchPilotConfigDir(env), "gemini-wrapper-venv");
241
+ }
242
+ export function getGeminiWrapperCookieCacheDir(env = process.env) {
243
+ return path.join(getPatchPilotConfigDir(env), "gemini-webapi-cache");
244
+ }
245
+ export function clearGeminiWrapperCookieCache(env = process.env) {
246
+ const cacheDir = getGeminiWrapperCookieCacheDir(env);
247
+ rmSync(cacheDir, {
248
+ recursive: true,
249
+ force: true
250
+ });
251
+ mkdirSync(cacheDir, {
252
+ recursive: true,
253
+ mode: 0o700
254
+ });
255
+ tryChmod(cacheDir, 0o700);
256
+ }
257
+ export function getManagedGeminiWrapperPythonPath(env = process.env) {
258
+ return path.join(getGeminiWrapperVenvDir(env), process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
259
+ }
260
+ export function readGeminiWrapperSecure1psid(env = process.env) {
261
+ return env.GEMINI_SECURE_1PSID?.trim() || "";
262
+ }
263
+ export function readGeminiWrapperSecure1psidts(env = process.env) {
264
+ return env.GEMINI_SECURE_1PSIDTS?.trim() || "";
265
+ }
266
+ export function geminiWrapperRequiresApiKey(baseUrl) {
267
+ return !isLocalWrapperUrl(baseUrl);
268
+ }
269
+ export async function isGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand()) {
270
+ const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi"], 20_000);
271
+ return result.ok;
272
+ }
273
+ export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand(), env = process.env) {
274
+ if (await isGeminiWebApiInstalled(pythonCommand)) {
275
+ return true;
276
+ }
277
+ const managedPython = getManagedGeminiWrapperPythonPath(env);
278
+ if (path.resolve(pythonCommand) === path.resolve(managedPython)) {
279
+ const venvDir = getGeminiWrapperVenvDir(env);
280
+ mkdirSync(getPatchPilotConfigDir(env), {
281
+ recursive: true,
282
+ mode: 0o700
283
+ });
284
+ tryChmod(getPatchPilotConfigDir(env), 0o700);
285
+ if (!existsSync(managedPython)) {
286
+ const venvResult = await runQuietCommand(readGeminiWrapperBootstrapPythonCommand(env), ["-m", "venv", venvDir], 120_000);
287
+ if (!venvResult.ok) {
288
+ return false;
289
+ }
290
+ }
291
+ }
292
+ const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`], 180_000);
293
+ return installResult.ok && (await isGeminiWebApiInstalled(pythonCommand));
294
+ }
295
+ function isLocalWrapperUrl(baseUrl) {
296
+ try {
297
+ const url = new URL(baseUrl);
298
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
299
+ }
300
+ catch {
301
+ return false;
302
+ }
303
+ }
304
+ function normalizeGeminiWrapperModel(model) {
305
+ const trimmedModel = model.trim();
306
+ return trimmedModel || defaultGeminiWrapperModel;
307
+ }
308
+ function normalizeGeminiWrapperBridgeModel(model) {
309
+ const normalizedModel = normalizeGeminiWrapperModel(model).trim();
310
+ return normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default" ? "" : normalizedModel;
311
+ }
312
+ function isLikelyGeminiWrapperChatModel(model) {
313
+ const normalizedModel = model.toLowerCase();
314
+ return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|vision|bidi|live)/.test(normalizedModel);
315
+ }
316
+ function isUnauthenticatedGeminiWebError(error) {
317
+ return Boolean(error?.includes("Gemini web cookies are expired or unauthenticated"));
318
+ }
319
+ function readGeminiWrapperRuntimeOptions(env = process.env) {
320
+ return {
321
+ maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 1024),
322
+ temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1),
323
+ bridgeMinIntervalMs: readNonNegativeInteger(env.PATCHPILOT_GEMINI_WRAPPER_MIN_INTERVAL_MS, 1500),
324
+ bridgeTimeoutMs: readPositiveInteger(env.PATCHPILOT_GEMINI_WRAPPER_TIMEOUT_MS, 180_000)
325
+ };
326
+ }
327
+ function toBridgePrompt(messages, formatJson) {
328
+ const body = messages
329
+ .map((message) => `${message.role.toUpperCase()}:\n${message.content}`)
330
+ .join("\n\n");
331
+ return formatJson ? `${body}\n\nReturn only valid JSON.` : body;
332
+ }
333
+ function toTelemetry(payload, durationMs, model) {
334
+ const promptTokens = payload.usage?.prompt_tokens ?? 0;
335
+ const responseTokens = payload.usage?.completion_tokens ?? 0;
336
+ return attachTokenCost({
337
+ promptTokens,
338
+ cachedPromptTokens: payload.usage?.prompt_tokens_details?.cached_tokens ?? 0,
339
+ cacheWriteTokens: payload.usage?.prompt_tokens_details?.cache_write_tokens ?? 0,
340
+ responseTokens,
341
+ totalTokens: payload.usage?.total_tokens ?? promptTokens + responseTokens,
342
+ evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
343
+ promptDurationMs: 0,
344
+ responseDurationMs: durationMs,
345
+ totalDurationMs: durationMs,
346
+ tokenSource: "provider"
347
+ }, "gemini-wrapper", model);
348
+ }
349
+ function toEstimatedTelemetry(prompt, content, durationMs, model) {
350
+ const promptTokens = estimateTokens(prompt);
351
+ const responseTokens = estimateTokens(content);
352
+ return attachTokenCost({
353
+ promptTokens,
354
+ cachedPromptTokens: 0,
355
+ cacheWriteTokens: 0,
356
+ responseTokens,
357
+ totalTokens: promptTokens + responseTokens,
358
+ evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
359
+ promptDurationMs: 0,
360
+ responseDurationMs: durationMs,
361
+ totalDurationMs: durationMs,
362
+ tokenSource: "estimated"
363
+ }, "gemini-wrapper", model);
364
+ }
365
+ let geminiBridgeQueue = Promise.resolve();
366
+ let lastGeminiBridgeStartedAt = 0;
367
+ function getGeminiWrapperBridgeTimeoutMs(model, configuredTimeoutMs = readGeminiWrapperRuntimeOptions().bridgeTimeoutMs) {
368
+ const timeoutMs = configuredTimeoutMs ?? 180_000;
369
+ return model.includes("pro") ? Math.max(timeoutMs, 240_000) : timeoutMs;
370
+ }
371
+ function runThrottledGeminiWebApiBridge(pythonCommand, input, signal, minIntervalMs = readGeminiWrapperRuntimeOptions().bridgeMinIntervalMs, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model)) {
372
+ const run = async () => {
373
+ const intervalMs = minIntervalMs ?? 0;
374
+ const waitMs = Math.max(0, intervalMs - (Date.now() - lastGeminiBridgeStartedAt));
375
+ if (waitMs > 0) {
376
+ await sleep(waitMs, signal);
377
+ }
378
+ lastGeminiBridgeStartedAt = Date.now();
379
+ return await runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal);
380
+ };
381
+ const result = geminiBridgeQueue.then(run, run);
382
+ geminiBridgeQueue = result.catch(() => undefined);
383
+ return result;
384
+ }
385
+ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
386
+ return new Promise((resolve, reject) => {
387
+ const cookieCacheDir = getGeminiWrapperCookieCacheDir();
388
+ mkdirSync(cookieCacheDir, {
389
+ recursive: true,
390
+ mode: 0o700
391
+ });
392
+ tryChmod(cookieCacheDir, 0o700);
393
+ const child = spawn(pythonCommand, ["-c", geminiWebApiBridgeScript], {
394
+ env: {
395
+ ...process.env,
396
+ GEMINI_COOKIE_PATH: cookieCacheDir
397
+ },
398
+ stdio: ["pipe", "pipe", "pipe"],
399
+ detached: process.platform !== "win32",
400
+ windowsHide: true
401
+ });
402
+ let settled = false;
403
+ let pendingKillError = null;
404
+ let killTimer = null;
405
+ const cleanup = () => {
406
+ clearTimeout(timeout);
407
+ if (killTimer) {
408
+ clearTimeout(killTimer);
409
+ }
410
+ signal?.removeEventListener("abort", abort);
411
+ };
412
+ const settleReject = (error) => {
413
+ if (settled) {
414
+ return;
415
+ }
416
+ settled = true;
417
+ cleanup();
418
+ reject(error);
419
+ };
420
+ const settleResolve = (output) => {
421
+ if (settled) {
422
+ return;
423
+ }
424
+ settled = true;
425
+ cleanup();
426
+ resolve(output);
427
+ };
428
+ const killChild = (signalName) => {
429
+ if (child.pid && process.platform !== "win32") {
430
+ try {
431
+ process.kill(-child.pid, signalName);
432
+ return;
433
+ }
434
+ catch {
435
+ // Fall through to killing the child directly.
436
+ }
437
+ }
438
+ child.kill(signalName);
439
+ };
440
+ const terminateChild = (error) => {
441
+ if (settled || pendingKillError) {
442
+ return;
443
+ }
444
+ pendingKillError = error;
445
+ killChild("SIGTERM");
446
+ killTimer = setTimeout(() => {
447
+ killChild("SIGKILL");
448
+ }, 1500);
449
+ };
450
+ const abort = () => {
451
+ terminateChild(new Error("Gemini-API bridge request aborted."));
452
+ };
453
+ signal?.addEventListener("abort", abort, {
454
+ once: true
455
+ });
456
+ const timeout = setTimeout(() => {
457
+ terminateChild(new Error(`Gemini-API bridge timed out after ${Math.round(timeoutMs / 1000)}s.`));
458
+ }, timeoutMs);
459
+ let stdout = "";
460
+ let stderr = "";
461
+ child.stdout.on("data", (chunk) => {
462
+ stdout += chunk.toString("utf8");
463
+ });
464
+ child.stderr.on("data", (chunk) => {
465
+ stderr += chunk.toString("utf8");
466
+ });
467
+ child.on("error", (error) => {
468
+ settleReject(error);
469
+ });
470
+ child.on("close", (exitCode) => {
471
+ if (pendingKillError) {
472
+ settleReject(pendingKillError);
473
+ return;
474
+ }
475
+ if (exitCode !== 0) {
476
+ settleReject(new Error(stderr.trim() || `Gemini-API bridge exited with ${exitCode}.`));
477
+ return;
478
+ }
479
+ try {
480
+ settleResolve(JSON.parse(stdout));
481
+ }
482
+ catch {
483
+ settleReject(new Error(`Gemini-API bridge returned invalid JSON.${stderr.trim() ? ` ${stderr.trim()}` : ""}`));
484
+ }
485
+ });
486
+ child.stdin.end(JSON.stringify({
487
+ ...input,
488
+ timeoutSeconds: Math.max(30, Math.min(300, Math.floor(timeoutMs / 1000)))
489
+ }));
490
+ });
491
+ }
492
+ const geminiWebApiBridgeScript = String.raw `
493
+ import asyncio
494
+ import json
495
+ import os
496
+ import sys
497
+
498
+ async def main():
499
+ from gemini_webapi import GeminiClient
500
+
501
+ payload = json.load(sys.stdin)
502
+ cookies = {}
503
+ cookies_path = payload.get("cookiesJson")
504
+ if cookies_path:
505
+ with open(cookies_path, "r", encoding="utf-8") as handle:
506
+ data = json.load(handle)
507
+ if isinstance(data, dict) and isinstance(data.get("cookies"), dict):
508
+ cookies.update(data["cookies"])
509
+ elif isinstance(data, dict) and isinstance(data.get("cookies"), list):
510
+ cookies.update({item.get("name"): item.get("value") for item in data["cookies"] if item.get("name") and item.get("value")})
511
+ elif isinstance(data, list):
512
+ cookies.update({item.get("name"): item.get("value") for item in data if item.get("name") and item.get("value")})
513
+ elif isinstance(data, dict):
514
+ cookies.update({key: value for key, value in data.items() if isinstance(value, str)})
515
+
516
+ psid = cookies.get("__Secure-1PSID") or payload.get("secure1psid") or os.getenv("GEMINI_SECURE_1PSID")
517
+ psidts = cookies.get("__Secure-1PSIDTS") or payload.get("secure1psidts") or os.getenv("GEMINI_SECURE_1PSIDTS") or ""
518
+ if not psid:
519
+ print(json.dumps({"error": "Missing __Secure-1PSID. Set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON or GEMINI_SECURE_1PSID."}))
520
+ return
521
+
522
+ extra = {key: value for key, value in cookies.items() if key not in {"__Secure-1PSID", "__Secure-1PSIDTS"}}
523
+ attempt_timeout = max(30, int(payload.get("timeoutSeconds") or 60))
524
+
525
+ def is_transient_network_error(message):
526
+ lower = message.lower()
527
+ return (
528
+ "curl: (28)" in lower
529
+ or "connection timed out" in lower
530
+ or "operation timed out" in lower
531
+ or "readtimeout" in lower
532
+ or "timeouterror" in lower
533
+ or "temporarily unavailable" in lower
534
+ )
535
+
536
+ def account_status_name(client):
537
+ status = getattr(client, "account_status", None)
538
+ return getattr(status, "name", str(status or ""))
539
+
540
+ def expired_cookie_error():
541
+ return "Gemini web cookies are expired or unauthenticated. Refresh ~/.patchpilot/gemini-cookies.json through Gemini-Wrapper onboarding."
542
+
543
+ def clear_cookie_cache():
544
+ cache_dir = os.getenv("GEMINI_COOKIE_PATH")
545
+ if not cache_dir:
546
+ return
547
+ try:
548
+ for filename in os.listdir(cache_dir):
549
+ if filename.startswith(".cached_cookies_") and filename.endswith(".json"):
550
+ os.remove(os.path.join(cache_dir, filename))
551
+ except OSError:
552
+ pass
553
+
554
+ async def generate_once(psidts_value):
555
+ client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts_value, cookies=extra or None, proxy=payload.get("proxy"))
556
+ await client.init(timeout=90, auto_refresh=False, verbose=False)
557
+ try:
558
+ status_name = account_status_name(client)
559
+ if payload.get("command") == "authCheck":
560
+ return {"content": "ok", "accountStatus": status_name}
561
+
562
+ if payload.get("command") == "models":
563
+ models = []
564
+ for model in client.list_models() or []:
565
+ if not getattr(model, "is_available", True):
566
+ continue
567
+ name = getattr(model, "model_name", None) or getattr(model, "display_name", None)
568
+ if name:
569
+ models.append(name)
570
+ return {"models": models, "accountStatus": status_name}
571
+
572
+ request_model = payload.get("model") or ""
573
+ request_kwargs = {"temporary": True}
574
+ if request_model:
575
+ request_kwargs["model"] = request_model
576
+ response = await client.generate_content(payload.get("prompt") or "", **request_kwargs)
577
+ text = getattr(response, "text", None) or str(response)
578
+ return {"content": text, "accountStatus": status_name, "model": request_model or "auto"}
579
+ finally:
580
+ await client.close()
581
+
582
+ async def generate_with_timestamp(psidts_value):
583
+ last_error = None
584
+ for attempt in range(3):
585
+ try:
586
+ return await asyncio.wait_for(generate_once(psidts_value), timeout=attempt_timeout)
587
+ except asyncio.TimeoutError:
588
+ if attempt >= 2:
589
+ raise TimeoutError(f"Gemini-API bridge attempt timed out after {attempt_timeout}s.")
590
+ await asyncio.sleep(1.5 * (attempt + 1))
591
+ except Exception as exc:
592
+ last_error = exc
593
+ if attempt >= 2 or not is_transient_network_error(str(exc)):
594
+ raise
595
+ await asyncio.sleep(1.5 * (attempt + 1))
596
+ raise last_error
597
+
598
+ try:
599
+ result = await generate_with_timestamp(psidts)
600
+ except Exception as exc:
601
+ message = str(exc)
602
+ if psidts and ("__Secure-1PSIDTS" in message or "SECURE_1PSIDTS" in message):
603
+ clear_cookie_cache()
604
+ result = await generate_with_timestamp("")
605
+ else:
606
+ raise
607
+ else:
608
+ if psidts and isinstance(result, dict) and result.get("error") == expired_cookie_error():
609
+ clear_cookie_cache()
610
+ retry_result = await generate_with_timestamp("")
611
+ if not retry_result.get("error"):
612
+ retry_result["warning"] = "Retried without stale __Secure-1PSIDTS."
613
+ print(json.dumps(retry_result))
614
+ return
615
+ print(json.dumps(result))
616
+ return
617
+
618
+ result["warning"] = "Retried without stale __Secure-1PSIDTS."
619
+ print(json.dumps(result))
620
+
621
+ try:
622
+ asyncio.run(main())
623
+ except Exception as exc:
624
+ message = str(exc)
625
+ if "currently unavailable or the request structure is outdated" in message:
626
+ message = f"{message} Try /model auto and refresh Gemini-Wrapper cookies if this persists."
627
+ print(json.dumps({"error": message}))
628
+ `;
629
+ function cleanUndefined(value) {
630
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
631
+ }
632
+ async function readJsonSafely(response) {
633
+ try {
634
+ return await response.json();
635
+ }
636
+ catch {
637
+ return {};
638
+ }
639
+ }
640
+ function readPositiveInteger(value, fallback) {
641
+ const parsedValue = Number.parseInt(value ?? "", 10);
642
+ return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
643
+ }
644
+ function readNonNegativeInteger(value, fallback) {
645
+ const parsedValue = Number.parseInt(value ?? "", 10);
646
+ return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
647
+ }
648
+ function readTemperature(value, fallback) {
649
+ const parsedValue = Number.parseFloat(value ?? "");
650
+ return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
651
+ }
652
+ function sleep(ms, signal) {
653
+ if (ms <= 0) {
654
+ return Promise.resolve();
655
+ }
656
+ return new Promise((resolve, reject) => {
657
+ if (signal?.aborted) {
658
+ reject(new Error("Gemini-API bridge request aborted."));
659
+ return;
660
+ }
661
+ const timeout = setTimeout(() => {
662
+ signal?.removeEventListener("abort", abort);
663
+ resolve();
664
+ }, ms);
665
+ const abort = () => {
666
+ clearTimeout(timeout);
667
+ reject(new Error("Gemini-API bridge request aborted."));
668
+ };
669
+ signal?.addEventListener("abort", abort, {
670
+ once: true
671
+ });
672
+ });
673
+ }
674
+ function tryChmod(filePath, mode) {
675
+ try {
676
+ chmodSync(filePath, mode);
677
+ }
678
+ catch {
679
+ // Best-effort hardening for platforms that do not support POSIX permissions.
680
+ }
681
+ }
682
+ function runQuietCommand(command, args, timeoutMs) {
683
+ return new Promise((resolve) => {
684
+ const child = spawn(command, args, {
685
+ stdio: ["ignore", "pipe", "pipe"],
686
+ windowsHide: true
687
+ });
688
+ const timeout = setTimeout(() => {
689
+ child.kill();
690
+ resolve({
691
+ ok: false,
692
+ output: `${command} ${args.join(" ")} timed out.`
693
+ });
694
+ }, timeoutMs);
695
+ let output = "";
696
+ child.stdout.on("data", (chunk) => {
697
+ output += chunk.toString("utf8");
698
+ });
699
+ child.stderr.on("data", (chunk) => {
700
+ output += chunk.toString("utf8");
701
+ });
702
+ child.on("error", (error) => {
703
+ clearTimeout(timeout);
704
+ resolve({
705
+ ok: false,
706
+ output: error.message
707
+ });
708
+ });
709
+ child.on("close", (exitCode) => {
710
+ clearTimeout(timeout);
711
+ resolve({
712
+ ok: exitCode === 0,
713
+ output: output.trim()
714
+ });
715
+ });
716
+ });
717
+ }
718
+ //# sourceMappingURL=geminiWrapper.js.map