@jx-grxf/patchpilot 0.4.0 → 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.
- package/.env.example +17 -1
- package/README.md +69 -14
- package/SECURITY.md +7 -1
- package/dist/cli.js +59 -13
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +3 -0
- package/dist/core/agent.js +56 -12
- package/dist/core/agent.js.map +1 -1
- package/dist/core/cleanup.d.ts +3 -0
- package/dist/core/cleanup.js +29 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/doctor.d.ts +4 -1
- package/dist/core/doctor.js +119 -1
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +51 -0
- package/dist/core/geminiWrapper.js +718 -0
- package/dist/core/geminiWrapper.js.map +1 -0
- package/dist/core/json.js +65 -1
- package/dist/core/json.js.map +1 -1
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.js +108 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/modelClient.js +7 -0
- package/dist/core/modelClient.js.map +1 -1
- package/dist/core/nvidia.js +1 -1
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/projectInit.d.ts +6 -0
- package/dist/core/projectInit.js +44 -0
- package/dist/core/projectInit.js.map +1 -0
- package/dist/core/reasoning.js +3 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +46 -0
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +9 -4
- package/dist/core/workspace.d.ts +8 -0
- package/dist/core/workspace.js +293 -21
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.js +536 -69
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +35 -6
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +8 -3
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.js +1 -1
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
- package/dist/tui/components/ExperimentalPanel.js +33 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -0
- package/dist/tui/components/Header.js +3 -3
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +13 -1
- package/dist/tui/components/OnboardingPanel.js +23 -9
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/Sidebar.js +17 -13
- package/dist/tui/components/Sidebar.js.map +1 -1
- package/dist/tui/components/Transcript.js +2 -2
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/format.js +7 -7
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/modes.d.ts +1 -1
- package/dist/tui/modes.js +8 -2
- package/dist/tui/modes.js.map +1 -1
- package/docs/gemini-wrapper.md +87 -0
- package/docs/releases/v0.1.1-beta.md +18 -0
- package/docs/releases/v0.2.1.md +1 -1
- package/docs/releases/v0.3.1-beta.md +4 -0
- package/docs/releases/v0.4.0.md +1 -1
- package/docs/releases/v1.0.0.md +28 -0
- package/docs/showcase/patchpilot-banner.png +0 -0
- package/docs/showcase/patchpilot-logo.png +0 -0
- 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
|