clawmoney 0.15.53 → 0.15.55

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.
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import * as readline from "node:readline";
5
6
  import { intro, outro, multiselect, select, spinner, isCancel, cancel, log, } from "@clack/prompts";
6
7
  import chalk from "chalk";
7
8
  import { apiPost } from "../utils/api.js";
@@ -9,6 +10,8 @@ import { loadConfig, requireConfig } from "../utils/config.js";
9
10
  import { setupCommand } from "./setup.js";
10
11
  import { API_PRICES, PLATFORM_FEE } from "../relay/pricing.js";
11
12
  import { hasClaudeFingerprint, bootstrapClaudeFingerprint, } from "../relay/upstream/claude-bootstrap.js";
13
+ import { hasGeminiFingerprint, bootstrapGeminiFingerprint, } from "../relay/upstream/gemini-bootstrap.js";
14
+ import { hasCodexFingerprint, bootstrapCodexFingerprint, } from "../relay/upstream/codex-bootstrap.js";
12
15
  // ── Per-cli_type model catalogs ──
13
16
  //
14
17
  // `RECOMMENDED_MODELS` is what gets registered when the user picks "all
@@ -204,48 +207,95 @@ export async function relaySetupCommand() {
204
207
  process.exit(0);
205
208
  }
206
209
  const selectedClis = familyChoice;
207
- // ── Step 2b: bootstrap per-cli fingerprints if missing ──
210
+ // Per-cli inline fingerprint bootstrap helper. Called from Step 3
211
+ // right after the "cli: N models" line so the capture flow is
212
+ // visually grouped with its owning cli family.
208
213
  //
209
- // Claude / Codex / Gemini daemons each need a fingerprint file
214
+ // The daemon needs a fingerprint file per cli_type
210
215
  // (~/.clawmoney/<cli>-fingerprint.json) to mimic the real CLI's
211
- // device_id + account_uuid when relaying requests. Without it,
212
- // every relay request fails at execution time and the buyer sees
213
- // 502s.
216
+ // device identity on every upstream request. Without it, the
217
+ // daemon fails at execution time and buyers see 502s. We
218
+ // previously required a manual two-terminal capture dance; now
219
+ // it's inlined into the wizard.
214
220
  //
215
- // Previously users had to run a two-terminal capture dance
216
- // manually. Now we do it inline: start a local proxy, invoke
217
- // `claude -p hi` (etc) against it, intercept the first
218
- // /v1/messages request, extract the fingerprint, forward the
219
- // request upstream so the CLI call still succeeds.
221
+ // claude uses a plain HTTP proxy (in-process TS), same for gemini.
222
+ // codex uses the existing mjs capture script via subprocess
223
+ // because Codex CLI 0.118+ talks WebSocket.
220
224
  //
221
- // Only claude is wired up for now; codex/gemini will follow the
222
- // same pattern.
223
- if (selectedClis.includes("claude") && !hasClaudeFingerprint()) {
224
- // Append-only progress: print the ◇ line once, then append a
225
- // single "." every 600ms until the bootstrap finishes. This
226
- // avoids the `\r`-based spinner that stacks frames in Jack's
227
- // terminal (see earlier iteration history), while still giving
228
- // visible "working…" feedback.
229
- process.stdout.write(`${chalk.gray("◇")} Capturing Claude fingerprint ${chalk.dim("(runs `claude -p hi` once, ~5-15s)")}`);
230
- const ticker = setInterval(() => {
231
- process.stdout.write(chalk.dim("."));
232
- }, 600);
225
+ // antigravity is handled separately via `clawmoney antigravity
226
+ // login` and doesn't need a fingerprint file.
227
+ const runCliBootstrap = async (cli) => {
228
+ let startLabel;
229
+ let run;
230
+ let check;
231
+ if (cli === "claude") {
232
+ if (hasClaudeFingerprint())
233
+ return;
234
+ startLabel = "Capturing Claude fingerprint (runs `claude -p hi` once, ~5-15s)";
235
+ check = hasClaudeFingerprint;
236
+ run = async () => {
237
+ const fp = await bootstrapClaudeFingerprint({ timeoutMs: 45_000 });
238
+ return `device=${fp.device_id.slice(0, 8)}… cc_version=${fp.cc_version || "?"}`;
239
+ };
240
+ }
241
+ else if (cli === "gemini") {
242
+ if (hasGeminiFingerprint())
243
+ return;
244
+ startLabel = "Capturing Gemini fingerprint (runs `gemini -p hi` once, ~10-20s)";
245
+ check = hasGeminiFingerprint;
246
+ run = async () => {
247
+ const fp = await bootstrapGeminiFingerprint({ timeoutMs: 60_000 });
248
+ return `project=${fp.project_id} cli_version=${fp.cli_version}`;
249
+ };
250
+ }
251
+ else if (cli === "codex") {
252
+ if (hasCodexFingerprint())
253
+ return;
254
+ startLabel = "Capturing Codex fingerprint (runs `codex -p hi` once, ~15-30s)";
255
+ check = hasCodexFingerprint;
256
+ run = async () => {
257
+ await bootstrapCodexFingerprint({ timeoutMs: 60_000 });
258
+ return "from chatgpt.com WS handshake";
259
+ };
260
+ }
261
+ else {
262
+ // antigravity or unknown — skip, no fingerprint needed.
263
+ return;
264
+ }
265
+ // In-place line replacement: print start line without a newline,
266
+ // then clear it on completion and print the result over the top.
267
+ // Uses readline.cursorTo/clearLine for terminal portability.
268
+ const startLine = `${chalk.gray("◇")} ${chalk.bold(startLabel)}`;
269
+ process.stdout.write(startLine);
270
+ const clearStartLine = () => {
271
+ try {
272
+ readline.cursorTo(process.stdout, 0);
273
+ readline.clearLine(process.stdout, 0);
274
+ }
275
+ catch {
276
+ process.stdout.write("\n");
277
+ }
278
+ };
233
279
  try {
234
- const fp = await bootstrapClaudeFingerprint({ timeoutMs: 45_000 });
235
- clearInterval(ticker);
236
- process.stdout.write("\n");
237
- log.success(`Claude fingerprint captured ` +
238
- chalk.dim(`(device=${fp.device_id.slice(0, 8)}… cc_version=${fp.cc_version || "?"})`));
280
+ const summary = await run();
281
+ clearStartLine();
282
+ process.stdout.write(`${chalk.green("")} ${chalk.bold(cli)} fingerprint captured ` +
283
+ chalk.dim(`(${summary})`) +
284
+ "\n");
239
285
  }
240
286
  catch (err) {
241
- clearInterval(ticker);
242
- process.stdout.write("\n");
243
- log.warn(`Claude fingerprint capture failed: ${err.message}`);
244
- log.message(chalk.dim("Claude providers will be registered but the daemon won't be able " +
245
- "to serve them until you bootstrap the fingerprint. " +
246
- "Make sure `claude` is installed and logged in, then re-run setup."));
287
+ clearStartLine();
288
+ process.stdout.write(`${chalk.yellow("⚠")} ${chalk.bold(cli)} fingerprint capture failed: ${err.message}\n`);
289
+ log.message(chalk.dim(`${cli} providers will be registered but the daemon won't be able ` +
290
+ "to serve them until the fingerprint is bootstrapped. " +
291
+ `Make sure \`${cli}\` is installed and logged in, then re-run setup.`));
247
292
  }
248
- }
293
+ // Defensive: if the bootstrap somehow resolved without writing
294
+ // the file, warn so the user isn't surprised later.
295
+ if (!check()) {
296
+ log.message(chalk.dim(`(${cli} fingerprint file still missing — daemon preflight will fail for this cli)`));
297
+ }
298
+ };
249
299
  const registrations = [];
250
300
  for (const cli of selectedClis) {
251
301
  const allModels = modelsForCli(cli);
@@ -255,6 +305,10 @@ export async function relaySetupCommand() {
255
305
  continue;
256
306
  }
257
307
  log.success(`${chalk.bold(cli)}: ${recommended.length} models ${chalk.dim("— " + recommended.join(", "))}`);
308
+ // Bootstrap the fingerprint for this cli right after its model
309
+ // line so the ◆ "fingerprint captured" message sits visually
310
+ // under its owning family (claude / codex / gemini / antigravity).
311
+ await runCliBootstrap(cli);
258
312
  for (const model of recommended) {
259
313
  const p = API_PRICES[model];
260
314
  registrations.push({
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Programmatic Codex fingerprint capture.
3
+ *
4
+ * Codex CLI 0.118+ talks to chatgpt.com over a WebSocket Upgrade
5
+ * (GET /v1/responses Upgrade: websocket) rather than plain HTTPS,
6
+ * which makes the in-process TS proxy approach used for Claude and
7
+ * Gemini much harder — we'd need to port the full handshake + frame
8
+ * decoder. So instead we reuse the existing
9
+ * `scripts/capture-codex-request.mjs` script which already handles
10
+ * all of that correctly: spawn it as a subprocess, run `codex -p hi`
11
+ * against it, and wait for `~/.clawmoney/codex-fingerprint.json` to
12
+ * appear. On success we SIGINT the capture proxy so it can scrub the
13
+ * transient capture files the way the manual flow does.
14
+ *
15
+ * Note: the mjs script hardcodes port 8788. If something else on the
16
+ * machine is already using that port, the spawn fails and we surface
17
+ * the error.
18
+ */
19
+ export declare function hasCodexFingerprint(): boolean;
20
+ export declare function bootstrapCodexFingerprint(opts?: {
21
+ timeoutMs?: number;
22
+ }): Promise<void>;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Programmatic Codex fingerprint capture.
3
+ *
4
+ * Codex CLI 0.118+ talks to chatgpt.com over a WebSocket Upgrade
5
+ * (GET /v1/responses Upgrade: websocket) rather than plain HTTPS,
6
+ * which makes the in-process TS proxy approach used for Claude and
7
+ * Gemini much harder — we'd need to port the full handshake + frame
8
+ * decoder. So instead we reuse the existing
9
+ * `scripts/capture-codex-request.mjs` script which already handles
10
+ * all of that correctly: spawn it as a subprocess, run `codex -p hi`
11
+ * against it, and wait for `~/.clawmoney/codex-fingerprint.json` to
12
+ * appear. On success we SIGINT the capture proxy so it can scrub the
13
+ * transient capture files the way the manual flow does.
14
+ *
15
+ * Note: the mjs script hardcodes port 8788. If something else on the
16
+ * machine is already using that port, the spawn fails and we surface
17
+ * the error.
18
+ */
19
+ import { spawn } from "node:child_process";
20
+ import { existsSync } from "node:fs";
21
+ import { homedir } from "node:os";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ const CONFIG_DIR = join(homedir(), ".clawmoney");
25
+ const FINGERPRINT_PATH = join(CONFIG_DIR, "codex-fingerprint.json");
26
+ const CAPTURE_PORT = 8788;
27
+ const CAPTURE_SCRIPT = "capture-codex-request.mjs";
28
+ export function hasCodexFingerprint() {
29
+ return existsSync(FINGERPRINT_PATH);
30
+ }
31
+ // Locate the mjs capture script relative to the installed dist.
32
+ // After TS compilation this file ends up at
33
+ // <pkg>/dist/relay/upstream/codex-bootstrap.js
34
+ // and the scripts live at <pkg>/scripts/capture-codex-request.mjs
35
+ // so the relative walk is ../../../scripts.
36
+ function findCaptureScript() {
37
+ const thisFile = fileURLToPath(import.meta.url);
38
+ const thisDir = dirname(thisFile);
39
+ const candidates = [
40
+ join(thisDir, "..", "..", "..", "scripts", CAPTURE_SCRIPT),
41
+ join(thisDir, "..", "..", "scripts", CAPTURE_SCRIPT),
42
+ join(thisDir, "..", "scripts", CAPTURE_SCRIPT),
43
+ ];
44
+ for (const c of candidates) {
45
+ if (existsSync(c))
46
+ return c;
47
+ }
48
+ return null;
49
+ }
50
+ export async function bootstrapCodexFingerprint(opts = {}) {
51
+ const timeoutMs = opts.timeoutMs ?? 60_000;
52
+ if (hasCodexFingerprint()) {
53
+ throw new Error("codex-fingerprint.json already exists — delete it to re-bootstrap");
54
+ }
55
+ const scriptPath = findCaptureScript();
56
+ if (!scriptPath) {
57
+ throw new Error(`capture-codex-request.mjs not found in the installed clawmoney package`);
58
+ }
59
+ let proxyChild = null;
60
+ let codexChild = null;
61
+ let pollInterval = null;
62
+ let done = false;
63
+ const cleanup = () => {
64
+ if (pollInterval) {
65
+ clearInterval(pollInterval);
66
+ pollInterval = null;
67
+ }
68
+ if (codexChild && !codexChild.killed) {
69
+ try {
70
+ codexChild.kill("SIGTERM");
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ }
76
+ if (proxyChild && !proxyChild.killed) {
77
+ try {
78
+ // SIGINT triggers the mjs script's capture-file scrub
79
+ // cleanup (scrubs OAuth bearer tokens).
80
+ proxyChild.kill("SIGINT");
81
+ }
82
+ catch {
83
+ // ignore
84
+ }
85
+ }
86
+ };
87
+ return new Promise((resolve, reject) => {
88
+ const timer = setTimeout(() => {
89
+ if (done)
90
+ return;
91
+ done = true;
92
+ cleanup();
93
+ reject(new Error(`codex fingerprint capture timed out after ${timeoutMs}ms`));
94
+ }, timeoutMs);
95
+ // 1. Spawn the capture proxy (mjs script). It inherits the
96
+ // current process's env, including HTTPS_PROXY which it needs
97
+ // to reach chatgpt.com.
98
+ proxyChild = spawn("node", [scriptPath], {
99
+ env: { ...process.env },
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+ let proxyStderr = "";
103
+ proxyChild.stderr?.on("data", (c) => {
104
+ proxyStderr += c.toString();
105
+ if (proxyStderr.length > 4_000) {
106
+ proxyStderr = proxyStderr.slice(-4_000);
107
+ }
108
+ });
109
+ proxyChild.stdout?.on("data", () => {
110
+ // drain — the mjs script prints a banner we don't care about
111
+ });
112
+ proxyChild.on("error", (err) => {
113
+ if (done)
114
+ return;
115
+ done = true;
116
+ clearTimeout(timer);
117
+ cleanup();
118
+ reject(new Error(`failed to spawn capture proxy: ${err.message}`));
119
+ });
120
+ proxyChild.on("exit", (code) => {
121
+ // If the proxy crashed before the fingerprint was captured,
122
+ // reject. If we killed it deliberately after capture, 'done'
123
+ // is already set.
124
+ if (done)
125
+ return;
126
+ if (!hasCodexFingerprint()) {
127
+ done = true;
128
+ clearTimeout(timer);
129
+ cleanup();
130
+ const tail = proxyStderr.trim().slice(-400);
131
+ const detail = tail ? ` stderr: ${tail}` : "";
132
+ reject(new Error(`capture proxy exited (code ${code ?? "unknown"}) before fingerprint.${detail}`));
133
+ }
134
+ });
135
+ // Wait a moment for the proxy to bind port 8788, then spawn
136
+ // codex. 1.5s is enough on every machine I've tested.
137
+ setTimeout(() => {
138
+ if (done)
139
+ return;
140
+ // Poll the fingerprint file — the mjs script writes it as
141
+ // soon as the first upgrade handshake decodes successfully.
142
+ pollInterval = setInterval(() => {
143
+ if (done) {
144
+ if (pollInterval) {
145
+ clearInterval(pollInterval);
146
+ pollInterval = null;
147
+ }
148
+ return;
149
+ }
150
+ if (hasCodexFingerprint()) {
151
+ done = true;
152
+ if (pollInterval) {
153
+ clearInterval(pollInterval);
154
+ pollInterval = null;
155
+ }
156
+ clearTimeout(timer);
157
+ cleanup();
158
+ resolve();
159
+ }
160
+ }, 500);
161
+ // Codex wants the proxy-local endpoint; strip HTTPS_PROXY so
162
+ // it talks to 127.0.0.1 directly (going through a proxy to
163
+ // loopback tends to wedge).
164
+ const childEnv = {
165
+ ...process.env,
166
+ OPENAI_BASE_URL: `http://127.0.0.1:${CAPTURE_PORT}/v1`,
167
+ NO_PROXY: "127.0.0.1,localhost",
168
+ no_proxy: "127.0.0.1,localhost",
169
+ };
170
+ delete childEnv.HTTPS_PROXY;
171
+ delete childEnv.https_proxy;
172
+ delete childEnv.HTTP_PROXY;
173
+ delete childEnv.http_proxy;
174
+ delete childEnv.ALL_PROXY;
175
+ delete childEnv.all_proxy;
176
+ codexChild = spawn("codex", ["-p", "hi"], {
177
+ env: childEnv,
178
+ stdio: ["ignore", "pipe", "pipe"],
179
+ shell: process.platform === "win32",
180
+ });
181
+ let codexStderr = "";
182
+ codexChild.stderr?.on("data", (c) => {
183
+ codexStderr += c.toString();
184
+ if (codexStderr.length > 4_000) {
185
+ codexStderr = codexStderr.slice(-4_000);
186
+ }
187
+ });
188
+ codexChild.stdout?.on("data", () => {
189
+ // drain
190
+ });
191
+ codexChild.on("error", (err) => {
192
+ if (done)
193
+ return;
194
+ done = true;
195
+ clearTimeout(timer);
196
+ cleanup();
197
+ reject(new Error(`failed to spawn codex: ${err.message} (is the codex CLI installed and in PATH?)`));
198
+ });
199
+ codexChild.on("exit", (code) => {
200
+ // Give the proxy a moment to finish writing the fingerprint
201
+ // after the WS upgrade completes. If no file after that,
202
+ // fail with the stderr tail for diagnostics.
203
+ setTimeout(() => {
204
+ if (done || hasCodexFingerprint())
205
+ return;
206
+ done = true;
207
+ clearTimeout(timer);
208
+ cleanup();
209
+ const tail = codexStderr.trim().slice(-400);
210
+ const detail = tail ? ` stderr: ${tail}` : "";
211
+ reject(new Error(`codex -p hi exited with code ${code ?? "unknown"} before the capture proxy saw a /v1/responses upgrade.${detail}`));
212
+ }, 800);
213
+ });
214
+ }, 1500);
215
+ });
216
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Programmatic Gemini fingerprint capture.
3
+ *
4
+ * Mirrors scripts/capture-gemini-request.mjs but runs inline so the
5
+ * setup wizard can bootstrap ~/.clawmoney/gemini-fingerprint.json
6
+ * without the two-terminal dance.
7
+ *
8
+ * Flow:
9
+ * 1. Listen on a random localhost port.
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.
23
+ */
24
+ export interface GeminiFingerprint {
25
+ project_id: string;
26
+ cli_version: string;
27
+ user_agent: string;
28
+ x_goog_api_client: string;
29
+ }
30
+ export declare function hasGeminiFingerprint(): boolean;
31
+ export declare function bootstrapGeminiFingerprint(opts?: {
32
+ timeoutMs?: number;
33
+ }): Promise<GeminiFingerprint>;
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Programmatic Gemini fingerprint capture.
3
+ *
4
+ * Mirrors scripts/capture-gemini-request.mjs but runs inline so the
5
+ * setup wizard can bootstrap ~/.clawmoney/gemini-fingerprint.json
6
+ * without the two-terminal dance.
7
+ *
8
+ * Flow:
9
+ * 1. Listen on a random localhost port.
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.
23
+ */
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
+ import { spawn } from "node:child_process";
29
+ import { fetch as undiciFetch, ProxyAgent } from "undici";
30
+ const CONFIG_DIR = join(homedir(), ".clawmoney");
31
+ const FINGERPRINT_PATH = join(CONFIG_DIR, "gemini-fingerprint.json");
32
+ export function hasGeminiFingerprint() {
33
+ return existsSync(FINGERPRINT_PATH);
34
+ }
35
+ const HOP_BY_HOP = new Set([
36
+ "host",
37
+ "connection",
38
+ "content-length",
39
+ "transfer-encoding",
40
+ "accept-encoding",
41
+ ]);
42
+ function cloneHeaders(src) {
43
+ const out = {};
44
+ for (const [k, v] of Object.entries(src)) {
45
+ if (v == null)
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
96
+ }
97
+ }
98
+ export async function bootstrapGeminiFingerprint(opts = {}) {
99
+ const timeoutMs = opts.timeoutMs ?? 45_000;
100
+ mkdirSync(CONFIG_DIR, { recursive: true });
101
+ if (hasGeminiFingerprint()) {
102
+ throw new Error("gemini-fingerprint.json already exists — delete it to re-bootstrap");
103
+ }
104
+ // Gemini talks to Google — Google is reachable only through a
105
+ // proxy from GFW-side networks, so we DO honor HTTPS_PROXY for the
106
+ // upstream forward. The child subprocess gets no proxy env because
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);
116
+ }
117
+ let server = null;
118
+ let geminiChild = null;
119
+ let resolved = false;
120
+ let capturedFp = null;
121
+ const cleanup = () => {
122
+ if (geminiChild && !geminiChild.killed) {
123
+ try {
124
+ geminiChild.kill("SIGTERM");
125
+ }
126
+ catch {
127
+ // ignore
128
+ }
129
+ }
130
+ if (server) {
131
+ try {
132
+ server.close();
133
+ }
134
+ catch {
135
+ // ignore
136
+ }
137
+ server = null;
138
+ }
139
+ };
140
+ return new Promise((resolve, reject) => {
141
+ const timer = setTimeout(() => {
142
+ if (resolved)
143
+ return;
144
+ resolved = true;
145
+ cleanup();
146
+ reject(new Error(`gemini fingerprint capture timed out after ${timeoutMs}ms`));
147
+ }, timeoutMs);
148
+ server = createServer((req, res) => {
149
+ const chunks = [];
150
+ req.on("data", (c) => chunks.push(c));
151
+ req.on("end", async () => {
152
+ const bodyBuf = Buffer.concat(chunks);
153
+ const bodyText = bodyBuf.toString("utf-8");
154
+ let parsedBody;
155
+ try {
156
+ parsedBody = JSON.parse(bodyText);
157
+ }
158
+ catch {
159
+ parsedBody = bodyText;
160
+ }
161
+ const isGenerate = req.method === "POST" &&
162
+ typeof req.url === "string" &&
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
+ });
244
+ });
245
+ server.on("error", (err) => {
246
+ if (resolved)
247
+ return;
248
+ resolved = true;
249
+ clearTimeout(timer);
250
+ cleanup();
251
+ reject(new Error(`gemini bootstrap proxy error: ${err.message}`));
252
+ });
253
+ server.listen(0, "127.0.0.1", () => {
254
+ const addr = server.address();
255
+ if (!addr || typeof addr === "string") {
256
+ if (resolved)
257
+ return;
258
+ resolved = true;
259
+ clearTimeout(timer);
260
+ cleanup();
261
+ reject(new Error("failed to bind gemini capture proxy"));
262
+ return;
263
+ }
264
+ const port = addr.port;
265
+ // Strip upstream proxy env vars from the child so it hits our
266
+ // local 127.0.0.1 listener directly. NO_PROXY is belt-and-braces.
267
+ const childEnv = {
268
+ ...process.env,
269
+ CODE_ASSIST_ENDPOINT: `http://127.0.0.1:${port}`,
270
+ NO_PROXY: "127.0.0.1,localhost",
271
+ no_proxy: "127.0.0.1,localhost",
272
+ };
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
+ geminiChild = spawn("gemini", ["-p", "hi"], {
280
+ env: childEnv,
281
+ stdio: ["ignore", "pipe", "pipe"],
282
+ shell: process.platform === "win32",
283
+ });
284
+ let stderrBuf = "";
285
+ geminiChild.stderr?.on("data", (chunk) => {
286
+ stderrBuf += chunk.toString();
287
+ if (stderrBuf.length > 4_000) {
288
+ stderrBuf = stderrBuf.slice(-4_000);
289
+ }
290
+ });
291
+ geminiChild.stdout?.on("data", () => {
292
+ // drain
293
+ });
294
+ geminiChild.on("error", (err) => {
295
+ if (resolved)
296
+ return;
297
+ resolved = true;
298
+ clearTimeout(timer);
299
+ cleanup();
300
+ reject(new Error(`failed to spawn gemini: ${err.message} (is the gemini CLI installed and in PATH?)`));
301
+ });
302
+ geminiChild.on("exit", (code) => {
303
+ setTimeout(() => {
304
+ if (capturedFp || resolved)
305
+ return;
306
+ resolved = true;
307
+ clearTimeout(timer);
308
+ cleanup();
309
+ const tail = stderrBuf.trim().slice(-400);
310
+ const detail = tail ? ` stderr: ${tail}` : "";
311
+ reject(new Error(`gemini -p hi exited with code ${code ?? "unknown"} before sending a v1internal request.${detail}`));
312
+ }, 500);
313
+ });
314
+ });
315
+ });
316
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.53",
3
+ "version": "0.15.55",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {