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
- // Shorter timeout on gemini recent CLI versions are flaky
226
- // under our subprocess-intercept approach. 25s is enough for
227
- // a working capture; beyond that we fall through to the
228
- // manual instruction path cleanly.
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
- }, tickEvery);
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
- // Build child env: inherit parent's env but strip HTTPS_PROXY
280
- // entries so claude doesn't try to tunnel its call to
281
- // http://127.0.0.1:<port> through the upstream proxy. Set
282
- // NO_PROXY=localhost as belt-and-braces.
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
- * 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.
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
- * 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.
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
- * 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.
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
- * 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.
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 { fetch as undiciFetch, ProxyAgent } from "undici";
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
- 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
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
- // 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);
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 server = null;
50
+ let proxyChild = null;
118
51
  let geminiChild = null;
119
- let resolved = false;
120
- let capturedFp = null;
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 (server) {
67
+ if (proxyChild && !proxyChild.killed) {
131
68
  try {
132
- server.close();
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 (resolved)
80
+ if (done)
143
81
  return;
144
- resolved = true;
82
+ done = true;
145
83
  cleanup();
146
84
  reject(new Error(`gemini fingerprint capture timed out after ${timeoutMs}ms`));
147
85
  }, 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
- });
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
- server.on("error", (err) => {
246
- if (resolved)
102
+ proxyChild.on("error", (err) => {
103
+ if (done)
247
104
  return;
248
- resolved = true;
105
+ done = true;
249
106
  clearTimeout(timer);
250
107
  cleanup();
251
- reject(new Error(`gemini bootstrap proxy error: ${err.message}`));
108
+ reject(new Error(`failed to spawn capture proxy: ${err.message}`));
252
109
  });
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;
110
+ proxyChild.on("exit", (code) => {
111
+ if (done)
112
+ return;
113
+ if (!hasGeminiFingerprint()) {
114
+ done = true;
259
115
  clearTimeout(timer);
260
116
  cleanup();
261
- reject(new Error("failed to bind gemini capture proxy"));
262
- return;
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
- 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.
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:${port}`,
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 stderrBuf = "";
285
- geminiChild.stderr?.on("data", (chunk) => {
286
- stderrBuf += chunk.toString();
287
- if (stderrBuf.length > 4_000) {
288
- stderrBuf = stderrBuf.slice(-4_000);
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 (resolved)
183
+ if (done)
296
184
  return;
297
- resolved = true;
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 (capturedFp || resolved)
192
+ if (done || hasGeminiFingerprint())
305
193
  return;
306
- resolved = true;
194
+ done = true;
307
195
  clearTimeout(timer);
308
196
  cleanup();
309
- const tail = stderrBuf.trim().slice(-400);
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 sending a v1internal request.${detail}`));
312
- }, 500);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.56",
3
+ "version": "0.15.58",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {