gemini-reverse 1.0.4 → 1.0.6

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/client.js CHANGED
@@ -341,13 +341,25 @@ class GeminiClient {
341
341
  temporary = false,
342
342
  deep_research = false,
343
343
  } = {}) {
344
+ if (!this._running) {
345
+ await this.init({
346
+ timeout: this.timeout,
347
+ autoClose: this.autoClose,
348
+ closeDelay: this.closeDelay,
349
+ autoRefresh: this.autoRefresh,
350
+ refreshInterval: this.refreshInterval,
351
+ verbose: this.verbose,
352
+ watchdogTimeout: this.watchdogTimeout,
353
+ });
354
+ if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContent. Client initialization failed.`);
355
+ }
344
356
  if (this.autoClose) this._resetCloseTask();
345
357
 
346
358
  let fileData = null;
347
359
  if (files && files.length) {
348
360
  await this._sendBardActivity();
349
361
  const uploaded = await Promise.all(
350
- files.map(f => uploadFile(f, this.proxy, this.pushId)),
362
+ files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
351
363
  );
352
364
  fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
353
365
  }
@@ -375,13 +387,25 @@ class GeminiClient {
375
387
  temporary = false,
376
388
  deep_research = false,
377
389
  } = {}) {
390
+ if (!this._running) {
391
+ await this.init({
392
+ timeout: this.timeout,
393
+ autoClose: this.autoClose,
394
+ closeDelay: this.closeDelay,
395
+ autoRefresh: this.autoRefresh,
396
+ refreshInterval: this.refreshInterval,
397
+ verbose: this.verbose,
398
+ watchdogTimeout: this.watchdogTimeout,
399
+ });
400
+ if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContentStream. Client initialization failed.`);
401
+ }
378
402
  if (this.autoClose) this._resetCloseTask();
379
403
 
380
404
  let fileData = null;
381
405
  if (files && files.length) {
382
406
  await this._sendBardActivity();
383
407
  const uploaded = await Promise.all(
384
- files.map(f => uploadFile(f, this.proxy, this.pushId)),
408
+ files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
385
409
  );
386
410
  fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
387
411
  }
@@ -536,7 +560,7 @@ class GeminiClient {
536
560
  case ErrorCode.TEMPORARY_ERROR_1013:
537
561
  throw new APIError('Temporary error (1013). Retrying...');
538
562
  default:
539
- throw new APIError(`Unknown API error code: ${ec}.`);
563
+ throw new APIError(`Failed to generate contents. Unknown API error code: ${ec}. This might be a content policy rejection or a temporary Google service issue.`);
540
564
  }
541
565
  }
542
566
 
@@ -16,7 +16,8 @@ class GemMixin {
16
16
  return this._gems;
17
17
  }
18
18
 
19
- async fetchGems({ includeHidden = false, language = 'en' } = {}) {
19
+ async fetchGems({ includeHidden = false } = {}) {
20
+ const language = this.language || 'en';
20
21
  const response = await this._batchExecute([
21
22
  new RPCData({ rpcid: GRPC.LIST_GEMS, payload: includeHidden ? `[4,['${language}'],0]` : `[3,['${language}'],0]`, identifier: 'system' }),
22
23
  new RPCData({ rpcid: GRPC.LIST_GEMS, payload: `[2,['${language}'],0]`, identifier: 'custom' }),
package/index.d.ts CHANGED
@@ -430,7 +430,6 @@ export interface StartChatOptions {
430
430
 
431
431
  export interface FetchGemsOptions {
432
432
  includeHidden?: boolean;
433
- language?: string;
434
433
  }
435
434
 
436
435
  export interface CreateGemOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-reverse",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Unofficial Node.js client for gemini.google.com — inspired by Gemini-API (Python). Supports streaming, chat sessions, gems, file uploads, and TypeScript.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -5,12 +5,26 @@ const { WebImage, GeneratedImage } = require('./image');
5
5
  function decodeHtml(str) {
6
6
  if (!str) return str;
7
7
  return str
8
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10)))
9
+ .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
8
10
  .replace(/&/g, '&')
9
11
  .replace(/&lt;/g, '<')
10
12
  .replace(/&gt;/g, '>')
11
13
  .replace(/&quot;/g, '"')
12
- .replace(/&#39;/g, "'")
13
- .replace(/&apos;/g, "'");
14
+ .replace(/&apos;/g, "'")
15
+ .replace(/&nbsp;/g, ' ')
16
+ .replace(/&copy;/g, '©')
17
+ .replace(/&reg;/g, '®')
18
+ .replace(/&trade;/g, '™')
19
+ .replace(/&mdash;/g, '—')
20
+ .replace(/&ndash;/g, '–')
21
+ .replace(/&hellip;/g, '…')
22
+ .replace(/&laquo;/g, '«')
23
+ .replace(/&raquo;/g, '»')
24
+ .replace(/&ldquo;/g, '“')
25
+ .replace(/&rdquo;/g, '”')
26
+ .replace(/&lsquo;/g, '‘')
27
+ .replace(/&rsquo;/g, '’');
14
28
  }
15
29
 
16
30
  class Candidate {
package/types/image.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const axios = require('axios');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const crypto = require('crypto');
6
7
 
7
8
  class Image {
8
9
  constructor({ url, title = '[Image]', alt = '', proxy = null } = {}) {
@@ -12,43 +13,76 @@ class Image {
12
13
  this.proxy = proxy;
13
14
  }
14
15
 
16
+ _getUrlForHash() {
17
+ return this.url;
18
+ }
19
+
15
20
  toString() {
16
21
  const short = this.url.length <= 20 ? this.url : this.url.slice(0, 8) + '...' + this.url.slice(-12);
17
22
  return `Image(title='${this.title}', alt='${this.alt}', url='${short}')`;
18
23
  }
19
24
 
20
- async save({ path: savePath = 'temp', filename = null, cookies = null, verbose = false, skipInvalidFilename = false } = {}) {
21
- let fname = filename || this.url.split('/').pop().split('?')[0];
22
- const match = fname.match(/^(.*\.\w+)/);
23
- if (match) {
24
- fname = match[1];
25
- } else {
26
- if (verbose) console.warn(`Invalid filename: ${fname}`);
27
- if (skipInvalidFilename) return null;
25
+ async save({ path: savePath = 'temp', filename = null, verbose = false } = {}) {
26
+ if (!filename || !path.extname(filename)) {
27
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
28
+ const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
29
+ const baseName = filename ? path.parse(filename).name : 'image';
30
+ filename = `${timestamp}_${urlHash}_${baseName}`;
28
31
  }
29
32
 
30
- const proxyConfig = this.proxy ? (() => { try { const u = new URL(this.proxy); return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) }; } catch { return undefined; } })() : undefined;
33
+ fs.mkdirSync(savePath, { recursive: true });
34
+ return await this._performSave(savePath, filename, verbose);
35
+ }
36
+
37
+ async _performSave(savePath, filename, verbose) {
38
+ const imgUrl = this.url;
39
+ const proxyConfig = this._parseProxy(this.proxy);
40
+ const clientRef = this.client_ref || null;
41
+ const cookies = clientRef ? clientRef.cookies : null;
31
42
 
32
- const res = await axios.get(this.url, {
43
+ const res = await axios.get(imgUrl, {
33
44
  responseType: 'arraybuffer',
34
- headers: cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {},
45
+ headers: {
46
+ 'Origin': 'https://gemini.google.com',
47
+ 'Referer': 'https://gemini.google.com/',
48
+ ...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
49
+ },
35
50
  maxRedirects: 5,
36
51
  ...(proxyConfig ? { proxy: proxyConfig } : {}),
37
52
  });
38
53
 
39
- if (res.status !== 200) throw new Error(`Error downloading image: ${res.status}`);
54
+ if (verbose) console.debug(`HTTP Request: GET ${imgUrl} [${res.status}]`);
40
55
 
41
- const contentType = res.headers['content-type'] || '';
42
- if (contentType && !contentType.includes('image')) {
43
- console.warn(`Content type of ${fname} is not image, but ${contentType}.`);
56
+ if (res.status !== 200) {
57
+ throw new Error(`Error downloading image: ${res.status}`);
44
58
  }
45
59
 
46
- fs.mkdirSync(savePath, { recursive: true });
47
- const dest = path.join(savePath, fname);
60
+ const contentType = (res.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
61
+ let ext = path.extname(filename);
62
+ if (!ext) {
63
+ if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = '.jpg';
64
+ else if (contentType.includes('png')) ext = '.png';
65
+ else if (contentType.includes('webp')) ext = '.webp';
66
+ else if (contentType.includes('gif')) ext = '.gif';
67
+ else ext = '.png';
68
+ filename = filename + ext;
69
+ }
70
+
71
+ const dest = path.join(savePath, filename);
48
72
  fs.writeFileSync(dest, Buffer.from(res.data));
49
- if (verbose) console.log(`Image saved as ${path.resolve(dest)}`);
73
+ if (verbose) console.info(`Image saved as ${path.resolve(dest)}`);
50
74
  return path.resolve(dest);
51
75
  }
76
+
77
+ _parseProxy(proxyStr) {
78
+ if (!proxyStr) return undefined;
79
+ try {
80
+ const u = new URL(proxyStr);
81
+ return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) };
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
52
86
  }
53
87
 
54
88
  class WebImage extends Image {
@@ -56,19 +90,72 @@ class WebImage extends Image {
56
90
  }
57
91
 
58
92
  class GeneratedImage extends Image {
59
- constructor({ cookies, ...opts } = {}) {
60
- super(opts);
61
- if (!cookies || Object.keys(cookies).length === 0)
62
- throw new Error('GeneratedImage requires cookies from GeminiClient.');
63
- this.cookies = cookies;
93
+ constructor({ url, title = '[Image]', alt = '', proxy = null, client_ref = null, cid = '', rid = '', rcid = '', image_id = '' } = {}) {
94
+ super({ url, title, alt, proxy });
95
+ this.client_ref = client_ref;
96
+ this.cid = cid;
97
+ this.rid = rid;
98
+ this.rcid = rcid;
99
+ this.image_id = image_id;
64
100
  }
65
101
 
66
- async save({ path: savePath = 'temp', filename = null, cookies = null, verbose = false, skipInvalidFilename = false, fullSize = true } = {}) {
67
- if (fullSize) this.url += '=s2048';
68
- const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
69
- const fname = filename || `${ts}_${this.url.slice(-10)}.png`;
70
- return super.save({ path: savePath, filename: fname, cookies: cookies || this.cookies, verbose, skipInvalidFilename });
102
+ async _performSave(savePath, filename, verbose, fullSize = true) {
103
+ if (fullSize) {
104
+ if (this.client_ref && this.cid && this.rid && this.rcid && this.image_id) {
105
+ try {
106
+ const originalUrl = await this.client_ref._getFullSizeImage(
107
+ this.cid, this.rid, this.rcid, this.image_id,
108
+ );
109
+ if (originalUrl) {
110
+ const proxyConfig = this._parseProxy(this.proxy);
111
+ const cookies = this.client_ref ? this.client_ref.cookies : null;
112
+ const headers = {
113
+ 'Origin': 'https://gemini.google.com',
114
+ 'Referer': 'https://gemini.google.com/',
115
+ ...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
116
+ };
117
+
118
+ const r1 = await axios.get(`${originalUrl}=d-I?alr=yes`, {
119
+ headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
120
+ });
121
+ const r2 = await axios.get(r1.data.trim(), {
122
+ headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
123
+ });
124
+ this.url = r2.data.trim();
125
+ return await super._performSave(savePath, filename, verbose);
126
+ }
127
+ } catch (e) {
128
+ if (verbose) console.debug(`Failed to fetch full size image via RPC: ${e.message}, falling back.`);
129
+ }
130
+ }
131
+
132
+ if (this.url.includes('=s1024-rj')) {
133
+ this.url = this.url.replace('=s1024-rj', '=s2048-rj');
134
+ } else if (!this.url.includes('=s2048-rj')) {
135
+ this.url += '=s2048-rj';
136
+ }
137
+ } else {
138
+ if (this.url.includes('=s2048-rj')) {
139
+ this.url = this.url.replace('=s2048-rj', '=s1024-rj');
140
+ } else if (!this.url.includes('=s1024-rj')) {
141
+ this.url += '=s1024-rj';
142
+ }
143
+ }
144
+
145
+ return await super._performSave(savePath, filename, verbose);
146
+ }
147
+
148
+ async save({ path: savePath = 'temp', filename = null, verbose = false, fullSize = true } = {}) {
149
+ if (!filename || !path.extname(filename)) {
150
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
151
+ const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
152
+ const baseName = filename ? path.parse(filename).name : 'image';
153
+ filename = `${timestamp}_${urlHash}_${baseName}`;
154
+ }
155
+
156
+ fs.mkdirSync(savePath, { recursive: true });
157
+ return await this._performSave(savePath, filename, verbose, fullSize);
71
158
  }
72
159
  }
73
160
 
74
- module.exports = { Image, WebImage, GeneratedImage };
161
+ module.exports = { Image, WebImage, GeneratedImage };
package/types/video.js CHANGED
@@ -45,6 +45,10 @@ class Video {
45
45
  const proxyConfig = this.proxy ? this._parseProxy(this.proxy) : undefined;
46
46
  const res = await axios.get(url, {
47
47
  responseType: 'arraybuffer',
48
+ headers: {
49
+ 'Origin': 'https://gemini.google.com',
50
+ 'Referer': 'https://gemini.google.com/',
51
+ },
48
52
  ...(proxyConfig ? { proxy: proxyConfig } : {}),
49
53
  validateStatus: null,
50
54
  });
package/utils/upload.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const axios = require('axios');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const mime = require('mime-types');
6
7
  const FormData = require('form-data');
7
8
  const { Endpoint, Headers } = require('../constants');
8
9
  const { parseProxy } = require('./accessToken');
@@ -21,26 +22,36 @@ function parseFileName(file) {
21
22
  return generateRandomName();
22
23
  }
23
24
 
24
- async function uploadFile(file, proxy = null, filename = null) {
25
+ async function uploadFile(file, proxy = null, pushId = '', cookies = {}) {
25
26
  let content, fname;
26
27
 
27
28
  if (typeof file === 'string') {
28
29
  const fp = path.resolve(file);
29
30
  if (!fs.existsSync(fp)) throw new Error(`${fp} is not a valid file.`);
30
- fname = filename || path.basename(fp);
31
+ fname = path.basename(fp);
31
32
  content = fs.readFileSync(fp);
32
33
  } else if (Buffer.isBuffer(file)) {
33
34
  content = file;
34
- fname = filename || generateRandomName();
35
+ fname = generateRandomName();
35
36
  } else {
36
37
  throw new Error(`Unsupported file type: ${typeof file}`);
37
38
  }
38
39
 
40
+ const contentType = mime.lookup(fname) || 'application/octet-stream';
41
+
39
42
  const form = new FormData();
40
- form.append('file', content, { filename: fname });
43
+ form.append('file', content, { filename: fname, contentType });
44
+
45
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
41
46
 
42
47
  const res = await axios.post(Endpoint.UPLOAD, form, {
43
- headers: { ...Headers.UPLOAD, ...form.getHeaders() },
48
+ headers: {
49
+ ...Headers.REFERER,
50
+ ...Headers.UPLOAD,
51
+ ...form.getHeaders(),
52
+ 'Push-ID': pushId,
53
+ ...(cookieStr ? { 'Cookie': cookieStr } : {}),
54
+ },
44
55
  maxRedirects: 5,
45
56
  ...(proxy ? { proxy: parseProxy(proxy) } : {}),
46
57
  });
@@ -48,4 +59,4 @@ async function uploadFile(file, proxy = null, filename = null) {
48
59
  return res.data;
49
60
  }
50
61
 
51
- module.exports = { uploadFile, parseFileName, generateRandomName };
62
+ module.exports = { uploadFile, parseFileName, generateRandomName };