clawmoney 0.15.56 → 0.15.58
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.
|
@@ -222,11 +222,10 @@ export async function relaySetupCommand() {
|
|
|
222
222
|
})));
|
|
223
223
|
}
|
|
224
224
|
if (selectedClis.includes("gemini") && !hasGeminiFingerprint()) {
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
tasks.push(bootstrapGeminiFingerprint({ timeoutMs: 25_000 })
|
|
225
|
+
// Gemini's capture typically completes in 5-15s on a working
|
|
226
|
+
// network. 45s is generous headroom for token refresh
|
|
227
|
+
// round-trips through a slow HTTPS_PROXY.
|
|
228
|
+
tasks.push(bootstrapGeminiFingerprint({ timeoutMs: 45_000 })
|
|
230
229
|
.then((fp) => ({
|
|
231
230
|
cli: "gemini",
|
|
232
231
|
ok: true,
|
|
@@ -274,10 +273,9 @@ export async function relaySetupCommand() {
|
|
|
274
273
|
// fingerprint file.
|
|
275
274
|
const startLine = `${chalk.gray("◇")} Configuring providers`;
|
|
276
275
|
process.stdout.write(startLine);
|
|
277
|
-
const tickEvery = 500;
|
|
278
276
|
const ticker = setInterval(() => {
|
|
279
277
|
process.stdout.write(chalk.dim("."));
|
|
280
|
-
},
|
|
278
|
+
}, 1200);
|
|
281
279
|
const results = await runAllBootstraps();
|
|
282
280
|
clearInterval(ticker);
|
|
283
281
|
try {
|
|
@@ -276,22 +276,17 @@ export async function bootstrapClaudeFingerprint(opts = {}) {
|
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
278
|
const port = addr.port;
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
279
|
+
// Inherit HTTPS_PROXY so claude can reach sso.anthropic.com
|
|
280
|
+
// for OAuth refresh if its cached token is near expiry —
|
|
281
|
+
// same reason gemini-bootstrap keeps it. NO_PROXY=127.0.0.1
|
|
282
|
+
// keeps claude's call to our local listener from tunneling
|
|
283
|
+
// through the proxy.
|
|
283
284
|
const childEnv = {
|
|
284
285
|
...process.env,
|
|
285
286
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
286
287
|
NO_PROXY: "127.0.0.1,localhost",
|
|
287
288
|
no_proxy: "127.0.0.1,localhost",
|
|
288
289
|
};
|
|
289
|
-
delete childEnv.HTTPS_PROXY;
|
|
290
|
-
delete childEnv.https_proxy;
|
|
291
|
-
delete childEnv.HTTP_PROXY;
|
|
292
|
-
delete childEnv.http_proxy;
|
|
293
|
-
delete childEnv.ALL_PROXY;
|
|
294
|
-
delete childEnv.all_proxy;
|
|
295
290
|
// Launch `claude -p "hi"` — same command the manual capture
|
|
296
291
|
// script documents. `-p` is non-interactive print mode; in
|
|
297
292
|
// recent claude versions it skips the trust dialog for
|
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Programmatic Gemini fingerprint capture.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spawns the existing scripts/capture-gemini-request.mjs as a
|
|
5
|
+
* subprocess and runs `gemini -p hi` against it, rather than
|
|
6
|
+
* reimplementing the proxy in TypeScript. The TS port I tried
|
|
7
|
+
* first timed out at 25s even though the mjs script captures in
|
|
8
|
+
* ~4s on the same machine — the mjs path is proven and reused
|
|
9
|
+
* code, so keep the pattern consistent with codex-bootstrap.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2. Spawn `gemini -p "hi"` with CODE_ASSIST_ENDPOINT pointing at us.
|
|
11
|
-
* 3. When the first POST hits a /v1internal:generateContent (or
|
|
12
|
-
* similar) path, extract project_id / user_agent / cli_version /
|
|
13
|
-
* x_goog_api_client from the body + headers, persist to
|
|
14
|
-
* ~/.clawmoney/gemini-fingerprint.json, and forward the request
|
|
15
|
-
* to cloudcode-pa.googleapis.com so the gemini CLI still sees a
|
|
16
|
-
* valid response.
|
|
17
|
-
* 4. Clean up proxy server + gemini subprocess.
|
|
18
|
-
*
|
|
19
|
-
* Note: the :loadCodeAssist bootstrap request that Gemini CLI fires
|
|
20
|
-
* first carries only `{metadata}` without a project — we skip it and
|
|
21
|
-
* wait for a subsequent v1internal request that actually carries a
|
|
22
|
-
* project field. Mirrors the mjs script's extractFingerprint guard.
|
|
11
|
+
* Note: the mjs script hardcodes port 8789. A collision surfaces
|
|
12
|
+
* as a spawn error we forward to the caller.
|
|
23
13
|
*/
|
|
24
14
|
export interface GeminiFingerprint {
|
|
25
15
|
project_id: string;
|
|
@@ -1,124 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Programmatic Gemini fingerprint capture.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spawns the existing scripts/capture-gemini-request.mjs as a
|
|
5
|
+
* subprocess and runs `gemini -p hi` against it, rather than
|
|
6
|
+
* reimplementing the proxy in TypeScript. The TS port I tried
|
|
7
|
+
* first timed out at 25s even though the mjs script captures in
|
|
8
|
+
* ~4s on the same machine — the mjs path is proven and reused
|
|
9
|
+
* code, so keep the pattern consistent with codex-bootstrap.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2. Spawn `gemini -p "hi"` with CODE_ASSIST_ENDPOINT pointing at us.
|
|
11
|
-
* 3. When the first POST hits a /v1internal:generateContent (or
|
|
12
|
-
* similar) path, extract project_id / user_agent / cli_version /
|
|
13
|
-
* x_goog_api_client from the body + headers, persist to
|
|
14
|
-
* ~/.clawmoney/gemini-fingerprint.json, and forward the request
|
|
15
|
-
* to cloudcode-pa.googleapis.com so the gemini CLI still sees a
|
|
16
|
-
* valid response.
|
|
17
|
-
* 4. Clean up proxy server + gemini subprocess.
|
|
18
|
-
*
|
|
19
|
-
* Note: the :loadCodeAssist bootstrap request that Gemini CLI fires
|
|
20
|
-
* first carries only `{metadata}` without a project — we skip it and
|
|
21
|
-
* wait for a subsequent v1internal request that actually carries a
|
|
22
|
-
* project field. Mirrors the mjs script's extractFingerprint guard.
|
|
11
|
+
* Note: the mjs script hardcodes port 8789. A collision surfaces
|
|
12
|
+
* as a spawn error we forward to the caller.
|
|
23
13
|
*/
|
|
24
|
-
import { createServer } from "node:http";
|
|
25
|
-
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
|
-
import { homedir } from "node:os";
|
|
27
|
-
import { join } from "node:path";
|
|
28
14
|
import { spawn } from "node:child_process";
|
|
29
|
-
import {
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
30
20
|
const CONFIG_DIR = join(homedir(), ".clawmoney");
|
|
31
21
|
const FINGERPRINT_PATH = join(CONFIG_DIR, "gemini-fingerprint.json");
|
|
22
|
+
const CAPTURE_PORT = 8789;
|
|
23
|
+
const CAPTURE_SCRIPT = "capture-gemini-request.mjs";
|
|
32
24
|
export function hasGeminiFingerprint() {
|
|
33
25
|
return existsSync(FINGERPRINT_PATH);
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
continue;
|
|
47
|
-
if (HOP_BY_HOP.has(k.toLowerCase()))
|
|
48
|
-
continue;
|
|
49
|
-
out[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
// /v1beta and /v1alpha go to generativelanguage (AI Studio).
|
|
54
|
-
// Everything else (notably /v1internal for Code Assist) goes to
|
|
55
|
-
// cloudcode-pa. Matches the manual script's routing.
|
|
56
|
-
function resolveUpstreamURL(path) {
|
|
57
|
-
if (path.startsWith("/v1beta") ||
|
|
58
|
-
path.startsWith("/v1/beta") ||
|
|
59
|
-
path.startsWith("/v1alpha")) {
|
|
60
|
-
return `https://generativelanguage.googleapis.com${path}`;
|
|
61
|
-
}
|
|
62
|
-
return `https://cloudcode-pa.googleapis.com${path}`;
|
|
63
|
-
}
|
|
64
|
-
function extractFingerprint(body, headers) {
|
|
65
|
-
if (!body || typeof body !== "object")
|
|
66
|
-
return null;
|
|
67
|
-
const projectRaw = body.project;
|
|
68
|
-
const projectId = typeof projectRaw === "string" ? projectRaw.trim() : "";
|
|
69
|
-
// :loadCodeAssist carries {metadata} without a project — wait for
|
|
70
|
-
// the next request that does.
|
|
71
|
-
if (!projectId)
|
|
72
|
-
return null;
|
|
73
|
-
const uaRaw = headers["user-agent"];
|
|
74
|
-
const ua = (Array.isArray(uaRaw) ? uaRaw.join(", ") : (uaRaw ?? "")).trim();
|
|
75
|
-
const versionMatch = ua.match(/GeminiCLI\/(\d+\.\d+[.\d]*)/i);
|
|
76
|
-
const cliVersion = versionMatch ? versionMatch[1] : "unknown";
|
|
77
|
-
const xGoogRaw = headers["x-goog-api-client"];
|
|
78
|
-
const xGoog = (Array.isArray(xGoogRaw) ? xGoogRaw.join(", ") : (xGoogRaw ?? "")).trim();
|
|
79
|
-
return {
|
|
80
|
-
project_id: projectId,
|
|
81
|
-
cli_version: cliVersion,
|
|
82
|
-
user_agent: ua || `GeminiCLI/${cliVersion}`,
|
|
83
|
-
x_goog_api_client: xGoog || "gl-node/unknown",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function scrubCaptureFiles() {
|
|
87
|
-
try {
|
|
88
|
-
for (const f of readdirSync(CONFIG_DIR)) {
|
|
89
|
-
if (/^capture-gemini-\d+\.json$/.test(f)) {
|
|
90
|
-
unlinkSync(join(CONFIG_DIR, f));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// ignore — best-effort
|
|
27
|
+
function findCaptureScript() {
|
|
28
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
29
|
+
const thisDir = dirname(thisFile);
|
|
30
|
+
const candidates = [
|
|
31
|
+
join(thisDir, "..", "..", "..", "scripts", CAPTURE_SCRIPT),
|
|
32
|
+
join(thisDir, "..", "..", "scripts", CAPTURE_SCRIPT),
|
|
33
|
+
join(thisDir, "..", "scripts", CAPTURE_SCRIPT),
|
|
34
|
+
];
|
|
35
|
+
for (const c of candidates) {
|
|
36
|
+
if (existsSync(c))
|
|
37
|
+
return c;
|
|
96
38
|
}
|
|
39
|
+
return null;
|
|
97
40
|
}
|
|
98
41
|
export async function bootstrapGeminiFingerprint(opts = {}) {
|
|
99
42
|
const timeoutMs = opts.timeoutMs ?? 45_000;
|
|
100
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
101
43
|
if (hasGeminiFingerprint()) {
|
|
102
44
|
throw new Error("gemini-fingerprint.json already exists — delete it to re-bootstrap");
|
|
103
45
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// it's talking to 127.0.0.1 (us), and routing 127.0.0.1 through
|
|
108
|
-
// http_proxy tends to wedge.
|
|
109
|
-
const proxyUrl = process.env.HTTPS_PROXY ||
|
|
110
|
-
process.env.https_proxy ||
|
|
111
|
-
process.env.HTTP_PROXY ||
|
|
112
|
-
process.env.http_proxy;
|
|
113
|
-
let upstreamDispatcher;
|
|
114
|
-
if (proxyUrl && /^https?:\/\//.test(proxyUrl)) {
|
|
115
|
-
upstreamDispatcher = new ProxyAgent(proxyUrl);
|
|
46
|
+
const scriptPath = findCaptureScript();
|
|
47
|
+
if (!scriptPath) {
|
|
48
|
+
throw new Error("capture-gemini-request.mjs not found in the installed clawmoney package");
|
|
116
49
|
}
|
|
117
|
-
let
|
|
50
|
+
let proxyChild = null;
|
|
118
51
|
let geminiChild = null;
|
|
119
|
-
let
|
|
120
|
-
let
|
|
52
|
+
let pollInterval = null;
|
|
53
|
+
let done = false;
|
|
121
54
|
const cleanup = () => {
|
|
55
|
+
if (pollInterval) {
|
|
56
|
+
clearInterval(pollInterval);
|
|
57
|
+
pollInterval = null;
|
|
58
|
+
}
|
|
122
59
|
if (geminiChild && !geminiChild.killed) {
|
|
123
60
|
try {
|
|
124
61
|
geminiChild.kill("SIGTERM");
|
|
@@ -127,190 +64,141 @@ export async function bootstrapGeminiFingerprint(opts = {}) {
|
|
|
127
64
|
// ignore
|
|
128
65
|
}
|
|
129
66
|
}
|
|
130
|
-
if (
|
|
67
|
+
if (proxyChild && !proxyChild.killed) {
|
|
131
68
|
try {
|
|
132
|
-
|
|
69
|
+
// SIGINT lets the mjs script scrub its capture-gemini-*.json
|
|
70
|
+
// stragglers (they contain OAuth bearer tokens).
|
|
71
|
+
proxyChild.kill("SIGINT");
|
|
133
72
|
}
|
|
134
73
|
catch {
|
|
135
74
|
// ignore
|
|
136
75
|
}
|
|
137
|
-
server = null;
|
|
138
76
|
}
|
|
139
77
|
};
|
|
140
78
|
return new Promise((resolve, reject) => {
|
|
141
79
|
const timer = setTimeout(() => {
|
|
142
|
-
if (
|
|
80
|
+
if (done)
|
|
143
81
|
return;
|
|
144
|
-
|
|
82
|
+
done = true;
|
|
145
83
|
cleanup();
|
|
146
84
|
reject(new Error(`gemini fingerprint capture timed out after ${timeoutMs}ms`));
|
|
147
85
|
}, timeoutMs);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(req.url.includes("generateContent") || req.url.includes("v1internal"));
|
|
164
|
-
if (!capturedFp &&
|
|
165
|
-
isGenerate &&
|
|
166
|
-
parsedBody &&
|
|
167
|
-
typeof parsedBody === "object") {
|
|
168
|
-
const fp = extractFingerprint(parsedBody, req.headers);
|
|
169
|
-
if (fp) {
|
|
170
|
-
capturedFp = fp;
|
|
171
|
-
try {
|
|
172
|
-
writeFileSync(FINGERPRINT_PATH, JSON.stringify(fp, null, 2), "utf-8");
|
|
173
|
-
scrubCaptureFiles();
|
|
174
|
-
}
|
|
175
|
-
catch (writeErr) {
|
|
176
|
-
if (!resolved) {
|
|
177
|
-
resolved = true;
|
|
178
|
-
clearTimeout(timer);
|
|
179
|
-
cleanup();
|
|
180
|
-
reject(new Error(`failed to write gemini fingerprint: ${writeErr.message}`));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Forward to real Google upstream.
|
|
187
|
-
const upstreamURL = resolveUpstreamURL(req.url || "/");
|
|
188
|
-
const targetHost = new URL(upstreamURL).host;
|
|
189
|
-
try {
|
|
190
|
-
const upstreamHeaders = cloneHeaders(req.headers);
|
|
191
|
-
upstreamHeaders["host"] = targetHost;
|
|
192
|
-
const upstreamResp = await undiciFetch(upstreamURL, {
|
|
193
|
-
method: req.method,
|
|
194
|
-
headers: upstreamHeaders,
|
|
195
|
-
body: req.method === "GET" || req.method === "HEAD"
|
|
196
|
-
? undefined
|
|
197
|
-
: bodyBuf,
|
|
198
|
-
dispatcher: upstreamDispatcher,
|
|
199
|
-
});
|
|
200
|
-
const respHeaders = {};
|
|
201
|
-
upstreamResp.headers.forEach((v, k) => {
|
|
202
|
-
const lower = k.toLowerCase();
|
|
203
|
-
if (lower === "content-encoding" ||
|
|
204
|
-
lower === "content-length" ||
|
|
205
|
-
lower === "transfer-encoding")
|
|
206
|
-
return;
|
|
207
|
-
respHeaders[k] = v;
|
|
208
|
-
});
|
|
209
|
-
res.writeHead(upstreamResp.status, respHeaders);
|
|
210
|
-
if (upstreamResp.body) {
|
|
211
|
-
const reader = upstreamResp.body.getReader();
|
|
212
|
-
while (true) {
|
|
213
|
-
const { done: rDone, value } = await reader.read();
|
|
214
|
-
if (rDone)
|
|
215
|
-
break;
|
|
216
|
-
res.write(Buffer.from(value));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
res.end();
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
try {
|
|
223
|
-
res.writeHead(502);
|
|
224
|
-
res.end();
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// ignore
|
|
228
|
-
}
|
|
229
|
-
if (!resolved && !capturedFp) {
|
|
230
|
-
resolved = true;
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
cleanup();
|
|
233
|
-
reject(new Error(`upstream google request failed: ${err.message}`));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (capturedFp && !resolved) {
|
|
238
|
-
resolved = true;
|
|
239
|
-
clearTimeout(timer);
|
|
240
|
-
cleanup();
|
|
241
|
-
resolve(capturedFp);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
86
|
+
// 1. Spawn the capture proxy (mjs script). It needs HTTPS_PROXY
|
|
87
|
+
// to reach cloudcode-pa.googleapis.com from a GFW egress.
|
|
88
|
+
proxyChild = spawn("node", [scriptPath], {
|
|
89
|
+
env: { ...process.env },
|
|
90
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
91
|
+
});
|
|
92
|
+
let proxyStderr = "";
|
|
93
|
+
proxyChild.stderr?.on("data", (c) => {
|
|
94
|
+
proxyStderr += c.toString();
|
|
95
|
+
if (proxyStderr.length > 4_000) {
|
|
96
|
+
proxyStderr = proxyStderr.slice(-4_000);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
proxyChild.stdout?.on("data", () => {
|
|
100
|
+
// drain — the mjs prints a banner we ignore
|
|
244
101
|
});
|
|
245
|
-
|
|
246
|
-
if (
|
|
102
|
+
proxyChild.on("error", (err) => {
|
|
103
|
+
if (done)
|
|
247
104
|
return;
|
|
248
|
-
|
|
105
|
+
done = true;
|
|
249
106
|
clearTimeout(timer);
|
|
250
107
|
cleanup();
|
|
251
|
-
reject(new Error(`
|
|
108
|
+
reject(new Error(`failed to spawn capture proxy: ${err.message}`));
|
|
252
109
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
resolved = true;
|
|
110
|
+
proxyChild.on("exit", (code) => {
|
|
111
|
+
if (done)
|
|
112
|
+
return;
|
|
113
|
+
if (!hasGeminiFingerprint()) {
|
|
114
|
+
done = true;
|
|
259
115
|
clearTimeout(timer);
|
|
260
116
|
cleanup();
|
|
261
|
-
|
|
262
|
-
|
|
117
|
+
const tail = proxyStderr.trim().slice(-400);
|
|
118
|
+
const detail = tail ? ` stderr: ${tail}` : "";
|
|
119
|
+
reject(new Error(`capture proxy exited (code ${code ?? "unknown"}) before fingerprint.${detail}`));
|
|
263
120
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
121
|
+
});
|
|
122
|
+
// Give the mjs proxy a moment to bind port 8789, then spawn
|
|
123
|
+
// gemini. 1.5s is enough on every machine I've tested.
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (done)
|
|
126
|
+
return;
|
|
127
|
+
// Poll the fingerprint file — the mjs script writes it as
|
|
128
|
+
// soon as the first v1internal request with a project field
|
|
129
|
+
// comes through.
|
|
130
|
+
pollInterval = setInterval(() => {
|
|
131
|
+
if (done) {
|
|
132
|
+
if (pollInterval) {
|
|
133
|
+
clearInterval(pollInterval);
|
|
134
|
+
pollInterval = null;
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (hasGeminiFingerprint()) {
|
|
139
|
+
done = true;
|
|
140
|
+
if (pollInterval) {
|
|
141
|
+
clearInterval(pollInterval);
|
|
142
|
+
pollInterval = null;
|
|
143
|
+
}
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
cleanup();
|
|
146
|
+
try {
|
|
147
|
+
const raw = JSON.parse(readFileSync(FINGERPRINT_PATH, "utf-8"));
|
|
148
|
+
resolve(raw);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
reject(new Error(`fingerprint file written but unreadable: ${err.message}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, 500);
|
|
155
|
+
// DO inherit HTTPS_PROXY — gemini CLI needs it to reach
|
|
156
|
+
// oauth2.googleapis.com for token refresh (see gemini-api.ts
|
|
157
|
+
// line 184). NO_PROXY=127.0.0.1 makes gemini bypass the proxy
|
|
158
|
+
// for our local capture listener, so HTTPS_PROXY + NO_PROXY
|
|
159
|
+
// together give gemini proxy access to Google AND direct
|
|
160
|
+
// access to our listener.
|
|
267
161
|
const childEnv = {
|
|
268
162
|
...process.env,
|
|
269
|
-
CODE_ASSIST_ENDPOINT: `http://127.0.0.1:${
|
|
163
|
+
CODE_ASSIST_ENDPOINT: `http://127.0.0.1:${CAPTURE_PORT}`,
|
|
270
164
|
NO_PROXY: "127.0.0.1,localhost",
|
|
271
165
|
no_proxy: "127.0.0.1,localhost",
|
|
272
166
|
};
|
|
273
|
-
delete childEnv.HTTPS_PROXY;
|
|
274
|
-
delete childEnv.https_proxy;
|
|
275
|
-
delete childEnv.HTTP_PROXY;
|
|
276
|
-
delete childEnv.http_proxy;
|
|
277
|
-
delete childEnv.ALL_PROXY;
|
|
278
|
-
delete childEnv.all_proxy;
|
|
279
167
|
geminiChild = spawn("gemini", ["-p", "hi"], {
|
|
280
168
|
env: childEnv,
|
|
281
169
|
stdio: ["ignore", "pipe", "pipe"],
|
|
282
170
|
shell: process.platform === "win32",
|
|
283
171
|
});
|
|
284
|
-
let
|
|
285
|
-
geminiChild.stderr?.on("data", (
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
172
|
+
let geminiStderr = "";
|
|
173
|
+
geminiChild.stderr?.on("data", (c) => {
|
|
174
|
+
geminiStderr += c.toString();
|
|
175
|
+
if (geminiStderr.length > 4_000) {
|
|
176
|
+
geminiStderr = geminiStderr.slice(-4_000);
|
|
289
177
|
}
|
|
290
178
|
});
|
|
291
179
|
geminiChild.stdout?.on("data", () => {
|
|
292
180
|
// drain
|
|
293
181
|
});
|
|
294
182
|
geminiChild.on("error", (err) => {
|
|
295
|
-
if (
|
|
183
|
+
if (done)
|
|
296
184
|
return;
|
|
297
|
-
|
|
185
|
+
done = true;
|
|
298
186
|
clearTimeout(timer);
|
|
299
187
|
cleanup();
|
|
300
188
|
reject(new Error(`failed to spawn gemini: ${err.message} (is the gemini CLI installed and in PATH?)`));
|
|
301
189
|
});
|
|
302
190
|
geminiChild.on("exit", (code) => {
|
|
303
191
|
setTimeout(() => {
|
|
304
|
-
if (
|
|
192
|
+
if (done || hasGeminiFingerprint())
|
|
305
193
|
return;
|
|
306
|
-
|
|
194
|
+
done = true;
|
|
307
195
|
clearTimeout(timer);
|
|
308
196
|
cleanup();
|
|
309
|
-
const tail =
|
|
197
|
+
const tail = geminiStderr.trim().slice(-400);
|
|
310
198
|
const detail = tail ? ` stderr: ${tail}` : "";
|
|
311
|
-
reject(new Error(`gemini -p hi exited with code ${code ?? "unknown"} before
|
|
312
|
-
},
|
|
199
|
+
reject(new Error(`gemini -p hi exited with code ${code ?? "unknown"} before the capture proxy saw a v1internal request with a project field.${detail}`));
|
|
200
|
+
}, 800);
|
|
313
201
|
});
|
|
314
|
-
});
|
|
202
|
+
}, 1500);
|
|
315
203
|
});
|
|
316
204
|
}
|