gemini-reverse 1.0.3 → 1.0.5
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/README.md +570 -183
- package/package.json +1 -1
- package/types/image.js +116 -29
package/README.md
CHANGED
|
@@ -1,25 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Gemini-Reverse
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An unofficial Node.js client for [Google Gemini](https://gemini.google.com), inspired by [Gemini-API](https://github.com/HanaokaYuzu/Gemini-API) — a Python reverse engineering project by [@HanaokaYuzu](https://github.com/HanaokaYuzu).
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
- **Persistent Cookies** — Automatically refreshes cookies in the background. Optimized for always-on services.
|
|
10
|
+
- **Image Generation** — Natively supports generating and editing images with natural language.
|
|
11
|
+
- **Video & Audio Generation** — Supports generating videos and audio/music content natively.
|
|
12
|
+
- **Deep Research** — Full deep research workflow with plan creation, status polling, and result retrieval.
|
|
13
|
+
- **System Prompt** — Supports customizing the model's system prompt with [Gemini Gems](https://gemini.google.com/gems/view).
|
|
14
|
+
- **Extension Support** — Supports generating content with Gemini extensions such as YouTube and Gmail.
|
|
15
|
+
- **Classified Outputs** — Categorizes text, thoughts, images, videos, and audio in the response.
|
|
16
|
+
- **Streaming Mode** — Supports stream generation, yielding partial outputs as they are generated.
|
|
17
|
+
- **Dynamic Model Discovery** — Automatically discovers available models from your account at initialization.
|
|
18
|
+
- **TypeScript Support** — Full TypeScript type declarations included out of the box.
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Table of Contents](#table-of-contents)
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Authentication](#authentication)
|
|
26
|
+
- [Usage](#usage)
|
|
27
|
+
- [Initialization](#initialization)
|
|
28
|
+
- [Generate Content](#generate-content)
|
|
29
|
+
- [Generate Content with Files](#generate-content-with-files)
|
|
30
|
+
- [Conversations Across Multiple Turns](#conversations-across-multiple-turns)
|
|
31
|
+
- [Continue Previous Conversations](#continue-previous-conversations)
|
|
32
|
+
- [Read Conversation History](#read-conversation-history)
|
|
33
|
+
- [List Recent Chats](#list-recent-chats)
|
|
34
|
+
- [Delete a Conversation](#delete-a-conversation)
|
|
35
|
+
- [Temporary Mode](#temporary-mode)
|
|
36
|
+
- [Streaming Mode](#streaming-mode)
|
|
37
|
+
- [Select Language Model](#select-language-model)
|
|
38
|
+
- [List Available Models](#list-available-models)
|
|
39
|
+
- [Apply System Prompt with Gemini Gems](#apply-system-prompt-with-gemini-gems)
|
|
40
|
+
- [Manage Custom Gems](#manage-custom-gems)
|
|
41
|
+
- [Create a Custom Gem](#create-a-custom-gem)
|
|
42
|
+
- [Update an Existing Gem](#update-an-existing-gem)
|
|
43
|
+
- [Delete a Custom Gem](#delete-a-custom-gem)
|
|
44
|
+
- [Retrieve Model's Thought Process](#retrieve-models-thought-process)
|
|
45
|
+
- [Retrieve Images in Response](#retrieve-images-in-response)
|
|
46
|
+
- [Generate and Edit Images](#generate-and-edit-images)
|
|
47
|
+
- [Retrieve Videos and Audio](#retrieve-videos-and-audio)
|
|
48
|
+
- [Generate Content with Gemini Extensions](#generate-content-with-gemini-extensions)
|
|
49
|
+
- [Check and Switch to Other Reply Candidates](#check-and-switch-to-other-reply-candidates)
|
|
50
|
+
- [Deep Research](#deep-research)
|
|
51
|
+
- [Account Status](#account-status)
|
|
52
|
+
- [Error Handling](#error-handling)
|
|
53
|
+
- [Cookie Persistence](#cookie-persistence)
|
|
54
|
+
- [TypeScript](#typescript)
|
|
55
|
+
- [Project Structure](#project-structure)
|
|
56
|
+
- [References](#references)
|
|
23
57
|
|
|
24
58
|
## Installation
|
|
25
59
|
|
|
@@ -27,304 +61,657 @@ An unofficial Node.js client for [gemini.google.com](https://gemini.google.com),
|
|
|
27
61
|
npm install gemini-reverse
|
|
28
62
|
```
|
|
29
63
|
|
|
30
|
-
---
|
|
31
|
-
|
|
32
64
|
## Authentication
|
|
33
65
|
|
|
66
|
+
- Go to [gemini.google.com](https://gemini.google.com) and log in with your Google account
|
|
67
|
+
- Press F12 to open DevTools, go to the `Application` tab → `Cookies` → `https://gemini.google.com`
|
|
68
|
+
- Copy the value of `__Secure-1PSID` (and optionally `__Secure-1PSIDTS`)
|
|
69
|
+
|
|
70
|
+
> `__Secure-1PSIDTS` is optional — the client will attempt to refresh and cache it automatically after the first successful initialization.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Initialization
|
|
75
|
+
|
|
76
|
+
Import the package and initialize a client with your cookies. After a successful initialization, the client will automatically refresh `__Secure-1PSIDTS` in the background as long as the process is alive.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const { GeminiClient } = require('gemini-reverse');
|
|
80
|
+
|
|
81
|
+
const client = new GeminiClient({
|
|
82
|
+
secure_1psid: 'YOUR_SECURE_1PSID',
|
|
83
|
+
secure_1psidts: 'YOUR_SECURE_1PSIDTS', // optional
|
|
84
|
+
proxy: null, // optional, e.g. 'http://host:port'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await client.init({
|
|
88
|
+
timeout: 300000, // request timeout in ms, default 300000
|
|
89
|
+
autoClose: false, // auto-close client after inactivity
|
|
90
|
+
closeDelay: 300000, // inactivity delay before closing in ms
|
|
91
|
+
autoRefresh: true, // auto-refresh cookies in the background
|
|
92
|
+
refreshInterval: 540000 // cookie refresh interval in ms
|
|
93
|
+
});
|
|
34
94
|
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
95
|
+
|
|
96
|
+
> `autoClose` and `closeDelay` are optional arguments for automatically closing the client after a period of inactivity. In an always-on service like a chatbot, it is recommended to set `autoClose` to `true` with a reasonable `closeDelay` value for better resource management.
|
|
97
|
+
|
|
98
|
+
### Generate Content
|
|
99
|
+
|
|
100
|
+
Ask a single-turn question by calling `generateContent`, which returns a `ModelOutput` object containing the generated text, images, thoughts, and conversation metadata.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const response = await client.generateContent({ prompt: 'Hello World!' });
|
|
104
|
+
console.log(response.text);
|
|
38
105
|
```
|
|
39
106
|
|
|
40
|
-
|
|
107
|
+
### Generate Content with Files
|
|
41
108
|
|
|
42
|
-
|
|
109
|
+
Gemini supports file input, including images and documents. Pass an array of file paths or `Buffer` objects alongside your text prompt.
|
|
43
110
|
|
|
44
111
|
```js
|
|
45
|
-
const
|
|
112
|
+
const response = await client.generateContent({
|
|
113
|
+
prompt: 'Describe the contents of these files.',
|
|
114
|
+
files: ['./document.pdf', './photo.png'],
|
|
115
|
+
});
|
|
116
|
+
console.log(response.text);
|
|
117
|
+
```
|
|
46
118
|
|
|
47
|
-
|
|
119
|
+
### Conversations Across Multiple Turns
|
|
48
120
|
|
|
49
|
-
|
|
121
|
+
Use `startChat` to create a `ChatSession` object and send messages through it. The conversation history is handled automatically and updated after each turn.
|
|
50
122
|
|
|
123
|
+
```js
|
|
51
124
|
const chat = client.startChat();
|
|
52
|
-
const response = await chat.sendMessage({ prompt: 'Hello, Gemini!' });
|
|
53
|
-
console.log(response.text);
|
|
54
125
|
|
|
55
|
-
await
|
|
126
|
+
const res1 = await chat.sendMessage({ prompt: 'My name is Alice.' });
|
|
127
|
+
console.log(res1.text);
|
|
128
|
+
|
|
129
|
+
const res2 = await chat.sendMessage({ prompt: 'What is my name?' });
|
|
130
|
+
console.log(res2.text); // remembers context
|
|
56
131
|
```
|
|
57
132
|
|
|
58
|
-
|
|
133
|
+
### Continue Previous Conversations
|
|
59
134
|
|
|
60
|
-
|
|
135
|
+
Pass a previous `ChatSession`'s metadata to `startChat` to resume a conversation. You can persist the metadata to a file or database to restore it across process restarts.
|
|
61
136
|
|
|
62
|
-
|
|
137
|
+
```js
|
|
138
|
+
const chat = client.startChat();
|
|
139
|
+
await chat.sendMessage({ prompt: 'Fine weather today.' });
|
|
140
|
+
|
|
141
|
+
// Save the session metadata
|
|
142
|
+
const savedMetadata = chat.metadata;
|
|
143
|
+
const savedCid = chat.cid;
|
|
144
|
+
|
|
145
|
+
// Resume in a new session
|
|
146
|
+
const previousChat = client.startChat({ metadata: savedMetadata });
|
|
147
|
+
const response = await previousChat.sendMessage({ prompt: 'What was my previous message?' });
|
|
148
|
+
console.log(response.text);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Read Conversation History
|
|
152
|
+
|
|
153
|
+
Fetch the full conversation history of a chat by calling `readChat` with the chat ID. It returns a `ChatHistory` object containing a list of `ChatTurn` objects ordered from newest to oldest.
|
|
63
154
|
|
|
64
155
|
```js
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
156
|
+
const chat = client.startChat();
|
|
157
|
+
await chat.sendMessage({ prompt: 'What is the capital of France?' });
|
|
158
|
+
|
|
159
|
+
const history = await client.readChat(chat.cid);
|
|
160
|
+
if (history) {
|
|
161
|
+
for (const turn of history.turns) {
|
|
162
|
+
console.log(`[${turn.role.toUpperCase()}] ${turn.text}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
74
165
|
```
|
|
75
166
|
|
|
76
|
-
|
|
167
|
+
You can also read the history directly from the session:
|
|
77
168
|
|
|
78
169
|
```js
|
|
79
|
-
const
|
|
80
|
-
console.log(response.text);
|
|
170
|
+
const history = await chat.readHistory(10); // fetch last 10 turns
|
|
81
171
|
```
|
|
82
172
|
|
|
83
|
-
###
|
|
173
|
+
### List Recent Chats
|
|
174
|
+
|
|
175
|
+
Use `listChats` to get a list of recent chat sessions cached at initialization.
|
|
84
176
|
|
|
85
177
|
```js
|
|
86
|
-
|
|
87
|
-
|
|
178
|
+
const chats = client.listChats();
|
|
179
|
+
if (chats) {
|
|
180
|
+
for (const info of chats) {
|
|
181
|
+
console.log(`${info.cid}: ${info.title} (pinned: ${info.is_pinned})`);
|
|
182
|
+
}
|
|
88
183
|
}
|
|
89
184
|
```
|
|
90
185
|
|
|
91
|
-
###
|
|
186
|
+
### Delete a Conversation
|
|
187
|
+
|
|
188
|
+
Delete a specific chat from Gemini history on the server by calling `deleteChat` with the chat ID.
|
|
92
189
|
|
|
93
190
|
```js
|
|
94
|
-
const chat = client.startChat(
|
|
191
|
+
const chat = client.startChat();
|
|
192
|
+
await chat.sendMessage({ prompt: 'This is a temporary conversation.' });
|
|
95
193
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(res2.text);
|
|
194
|
+
await client.deleteChat(chat.cid);
|
|
195
|
+
console.log(`Chat deleted: ${chat.cid}`);
|
|
99
196
|
```
|
|
100
197
|
|
|
101
|
-
###
|
|
198
|
+
### Temporary Mode
|
|
199
|
+
|
|
200
|
+
Pass `temporary: true` to `generateContent` or `sendMessage` to prevent the conversation from being saved to Gemini history.
|
|
102
201
|
|
|
103
202
|
```js
|
|
203
|
+
const response = await client.generateContent({
|
|
204
|
+
prompt: 'Hello World!',
|
|
205
|
+
temporary: true,
|
|
206
|
+
});
|
|
207
|
+
console.log(response.text);
|
|
208
|
+
|
|
209
|
+
// Also works in chat sessions
|
|
104
210
|
const chat = client.startChat();
|
|
105
|
-
|
|
211
|
+
await chat.sendMessage({ prompt: 'Fine weather today.', temporary: false });
|
|
212
|
+
const res2 = await chat.sendMessage({ prompt: "What's my last message?", temporary: true });
|
|
213
|
+
console.log(res2.text);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Streaming Mode
|
|
217
|
+
|
|
218
|
+
For longer responses, use streaming mode to receive partial outputs as they are generated. The `text_delta` attribute contains only the **new characters** received since the last yield, making it easy to display incremental updates.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
for await (const chunk of client.generateContentStream({
|
|
222
|
+
prompt: "What's the difference between 'await' and 'async for'?",
|
|
223
|
+
})) {
|
|
106
224
|
process.stdout.write(chunk.text_delta);
|
|
107
225
|
}
|
|
226
|
+
console.log();
|
|
108
227
|
```
|
|
109
228
|
|
|
110
|
-
|
|
229
|
+
Streaming also works inside a chat session:
|
|
111
230
|
|
|
112
231
|
```js
|
|
113
|
-
const
|
|
232
|
+
const chat = client.startChat();
|
|
233
|
+
for await (const chunk of chat.sendMessageStream({ prompt: 'Tell me a long story.' })) {
|
|
234
|
+
process.stdout.write(chunk.text_delta);
|
|
235
|
+
}
|
|
114
236
|
```
|
|
115
237
|
|
|
116
|
-
###
|
|
238
|
+
### Select Language Model
|
|
239
|
+
|
|
240
|
+
Specify which language model to use by passing a `model` argument. Available models are discovered dynamically at init time based on your account tier.
|
|
117
241
|
|
|
118
242
|
```js
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
243
|
+
const { Model } = require('gemini-reverse');
|
|
244
|
+
|
|
245
|
+
// Using a built-in constant
|
|
246
|
+
const response1 = await client.generateContent({
|
|
247
|
+
prompt: 'What is your model version?',
|
|
248
|
+
model: Model.BASIC_FLASH,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Using a model name string
|
|
252
|
+
const chat = client.startChat({ model: 'gemini-3-pro' });
|
|
253
|
+
|
|
254
|
+
// Using a custom model header dict
|
|
255
|
+
const chat2 = client.startChat({
|
|
256
|
+
model: {
|
|
257
|
+
model_name: 'custom',
|
|
258
|
+
model_header: {
|
|
259
|
+
'x-goog-ext-525001261-jspb': '[1,null,null,null,"MODEL_ID",null,null,0,[4],null,null,2]',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
122
262
|
});
|
|
123
263
|
```
|
|
124
264
|
|
|
125
|
-
|
|
265
|
+
**Built-in model constants:**
|
|
126
266
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
267
|
+
| Constant | `model_name` | Notes |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `Model.UNSPECIFIED` | `unspecified` | Default, lets Gemini choose |
|
|
270
|
+
| `Model.BASIC_PRO` | `gemini-3-pro` | Free tier |
|
|
271
|
+
| `Model.BASIC_FLASH` | `gemini-3-flash` | Free tier, fastest |
|
|
272
|
+
| `Model.BASIC_THINKING` | `gemini-3-flash-thinking` | Free tier, thinking model |
|
|
273
|
+
| `Model.PLUS_PRO` | `gemini-3-pro-plus` | Plus tier |
|
|
274
|
+
| `Model.PLUS_FLASH` | `gemini-3-flash-plus` | Plus tier |
|
|
275
|
+
| `Model.PLUS_THINKING` | `gemini-3-flash-thinking-plus` | Plus tier |
|
|
276
|
+
| `Model.ADVANCED_PRO` | `gemini-3-pro-advanced` | Advanced tier |
|
|
277
|
+
| `Model.ADVANCED_FLASH` | `gemini-3-flash-advanced` | Advanced tier |
|
|
278
|
+
| `Model.ADVANCED_THINKING` | `gemini-3-flash-thinking-advanced` | Advanced tier |
|
|
132
279
|
|
|
133
|
-
### Models
|
|
280
|
+
### List Available Models
|
|
281
|
+
|
|
282
|
+
The client dynamically discovers which models your account can access during initialization. Use `listModels` to inspect them.
|
|
134
283
|
|
|
135
284
|
```js
|
|
285
|
+
await client.init();
|
|
286
|
+
|
|
136
287
|
const models = client.listModels();
|
|
137
288
|
if (models) {
|
|
138
|
-
for (const
|
|
139
|
-
console.log(
|
|
289
|
+
for (const model of models) {
|
|
290
|
+
console.log(`${model.model_id} → ${model.model_name || model.display_name}`);
|
|
291
|
+
console.log(` capacity: ${model.capacity}, advanced_only: ${model.advanced_only}`);
|
|
140
292
|
}
|
|
141
293
|
}
|
|
294
|
+
```
|
|
142
295
|
|
|
143
|
-
|
|
296
|
+
### Apply System Prompt with Gemini Gems
|
|
144
297
|
|
|
145
|
-
|
|
146
|
-
|
|
298
|
+
System prompts can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). Pass the `gem` argument to `generateContent` or `startChat` — it can be a `Gem` object or a gem ID string.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// Fetch all gems for the account
|
|
302
|
+
await client.fetchGems();
|
|
303
|
+
const gems = client.gems;
|
|
147
304
|
|
|
148
|
-
|
|
149
|
-
|
|
305
|
+
// Get a specific gem
|
|
306
|
+
const codingPartner = gems.get({ name: 'Coding partner' });
|
|
307
|
+
|
|
308
|
+
const response = await client.generateContent({
|
|
309
|
+
prompt: "What's your system prompt?",
|
|
310
|
+
gem: codingPartner,
|
|
150
311
|
});
|
|
312
|
+
console.log(response.text);
|
|
313
|
+
|
|
314
|
+
// Use a gem in a multi-turn chat
|
|
315
|
+
const chat = client.startChat({ gem: codingPartner });
|
|
316
|
+
const res2 = await chat.sendMessage({ prompt: 'Help me write a binary search.' });
|
|
317
|
+
console.log(res2.text);
|
|
151
318
|
```
|
|
152
319
|
|
|
153
|
-
|
|
320
|
+
> There are some system predefined gems that are hidden by default. Use `fetchGems({ includeHidden: true })` to include them.
|
|
321
|
+
|
|
322
|
+
### Manage Custom Gems
|
|
154
323
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
| `Model.BASIC_PRO` | `gemini-3-pro` |
|
|
159
|
-
| `Model.BASIC_FLASH` | `gemini-3-flash` |
|
|
160
|
-
| `Model.BASIC_THINKING` | `gemini-3-flash-thinking` |
|
|
161
|
-
| `Model.PLUS_PRO` | `gemini-3-pro-plus` |
|
|
162
|
-
| `Model.PLUS_FLASH` | `gemini-3-flash-plus` |
|
|
163
|
-
| `Model.PLUS_THINKING` | `gemini-3-flash-thinking-plus` |
|
|
164
|
-
| `Model.ADVANCED_PRO` | `gemini-3-pro-advanced` |
|
|
165
|
-
| `Model.ADVANCED_FLASH` | `gemini-3-flash-advanced` |
|
|
166
|
-
| `Model.ADVANCED_THINKING` | `gemini-3-flash-thinking-advanced` |
|
|
167
|
-
|
|
168
|
-
### Images
|
|
324
|
+
You can create, update, and delete custom gems programmatically. Predefined system gems cannot be modified.
|
|
325
|
+
|
|
326
|
+
#### Create a Custom Gem
|
|
169
327
|
|
|
170
328
|
```js
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
329
|
+
const newGem = await client.createGem({
|
|
330
|
+
name: 'Python Tutor',
|
|
331
|
+
prompt: 'You are a helpful Python programming tutor. Always provide runnable code examples.',
|
|
332
|
+
description: 'A specialized gem for Python programming',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
console.log(`Created: ${newGem.id}`);
|
|
336
|
+
|
|
337
|
+
// Use the new gem immediately
|
|
338
|
+
const response = await client.generateContent({
|
|
339
|
+
prompt: 'Explain list comprehensions.',
|
|
340
|
+
gem: newGem,
|
|
341
|
+
});
|
|
342
|
+
console.log(response.text);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### Update an Existing Gem
|
|
346
|
+
|
|
347
|
+
> When updating a gem, all parameters (`name`, `prompt`, `description`) must be provided even if only one is changing.
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
await client.fetchGems();
|
|
351
|
+
const pythonTutor = client.gems.get({ name: 'Python Tutor' });
|
|
352
|
+
|
|
353
|
+
const updatedGem = await client.updateGem({
|
|
354
|
+
gem: pythonTutor,
|
|
355
|
+
name: 'Advanced Python Tutor',
|
|
356
|
+
prompt: 'You are an expert Python programming tutor. Focus on performance and best practices.',
|
|
357
|
+
description: 'An advanced Python programming assistant',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
console.log(`Updated: ${updatedGem.id}`);
|
|
175
361
|
```
|
|
176
362
|
|
|
177
|
-
|
|
363
|
+
#### Delete a Custom Gem
|
|
178
364
|
|
|
179
365
|
```js
|
|
180
|
-
|
|
181
|
-
|
|
366
|
+
await client.fetchGems();
|
|
367
|
+
const gemToDelete = client.gems.get({ name: 'Advanced Python Tutor' });
|
|
368
|
+
|
|
369
|
+
await client.deleteGem(gemToDelete); // can also pass a gem ID string
|
|
370
|
+
console.log(`Deleted: ${gemToDelete.name}`);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Retrieve Model's Thought Process
|
|
374
|
+
|
|
375
|
+
When using thinking-capable models, the model's internal reasoning is exposed via `response.thoughts`.
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
const response = await client.generateContent({
|
|
379
|
+
prompt: 'What is 17 × 23?',
|
|
380
|
+
model: Model.BASIC_THINKING,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (response.thoughts) {
|
|
384
|
+
console.log('Thoughts:', response.thoughts);
|
|
182
385
|
}
|
|
183
|
-
|
|
184
|
-
|
|
386
|
+
console.log('Answer:', response.text);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Retrieve Images in Response
|
|
390
|
+
|
|
391
|
+
Images in the response are stored as a list of `Image` objects accessible via `response.images`. Each image has a `url`, `title`, and `alt` description.
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
const response = await client.generateContent({ prompt: 'Send me some pictures of cats.' });
|
|
395
|
+
|
|
396
|
+
for (const image of response.images) {
|
|
397
|
+
console.log(`${image.title}: ${image.url}`);
|
|
398
|
+
console.log(`Alt: ${image.alt}`);
|
|
185
399
|
}
|
|
186
400
|
```
|
|
187
401
|
|
|
188
|
-
###
|
|
402
|
+
### Generate and Edit Images
|
|
403
|
+
|
|
404
|
+
Ask Gemini to generate or edit images using natural language. Generated images are returned as `GeneratedImage` objects and can be saved to disk.
|
|
405
|
+
|
|
406
|
+
> Google has limitations on image generation availability that vary by region and account type. Users under 18 cannot use this feature.
|
|
189
407
|
|
|
190
408
|
```js
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
409
|
+
const response = await client.generateContent({
|
|
410
|
+
prompt: 'Generate a photo-realistic image of a cat in a space suit.',
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
for (let i = 0; i < response.images.length; i++) {
|
|
414
|
+
const image = response.images[i];
|
|
415
|
+
const savedPath = await image.save({
|
|
416
|
+
path: './temp',
|
|
417
|
+
filename: `cat_space_${i}.png`,
|
|
418
|
+
verbose: true,
|
|
419
|
+
});
|
|
420
|
+
console.log(`Saved to: ${savedPath}`);
|
|
196
421
|
}
|
|
422
|
+
```
|
|
197
423
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
424
|
+
> When asking Gemini to "send" images, it returns web images (`WebImage`). When asking to "generate" images, it returns AI-generated images (`GeneratedImage`). Both are automatically categorized in `response.images`.
|
|
425
|
+
|
|
426
|
+
### Retrieve Videos and Audio
|
|
427
|
+
|
|
428
|
+
Gemini can generate short videos and audio/music. These are returned as `GeneratedVideo` and `GeneratedMedia` objects in `response.videos` and `response.media` respectively.
|
|
429
|
+
|
|
430
|
+
> You may need an active Gemini subscription to access video and audio generation.
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
// Generate a video
|
|
434
|
+
const videoResponse = await client.generateContent({
|
|
435
|
+
prompt: 'Generate a short video of waves on a beach.',
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
for (const video of videoResponse.videos) {
|
|
439
|
+
const result = await video.save({ savePath: './temp', verbose: true });
|
|
440
|
+
console.log('Video:', result.video);
|
|
441
|
+
console.log('Thumbnail:', result.video_thumbnail);
|
|
203
442
|
}
|
|
204
443
|
|
|
205
|
-
|
|
444
|
+
// Generate audio/music
|
|
445
|
+
const mediaResponse = await client.generateContent({
|
|
446
|
+
prompt: 'Compose a short calming piano melody.',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
for (const media of mediaResponse.media) {
|
|
450
|
+
// downloadType: 'audio' | 'video' | 'both' (default)
|
|
451
|
+
const result = await media.save({ savePath: './temp', downloadType: 'both', verbose: true });
|
|
452
|
+
console.log('MP3:', result.audio);
|
|
453
|
+
console.log('MP4:', result.video);
|
|
454
|
+
}
|
|
206
455
|
```
|
|
207
456
|
|
|
208
|
-
|
|
457
|
+
> `GeneratedMedia.save()` accepts a `downloadType` parameter: `"audio"`, `"video"`, or `"both"` (default). The save method polls automatically if content is still generating (HTTP 206).
|
|
458
|
+
|
|
459
|
+
### Generate Content with Gemini Extensions
|
|
460
|
+
|
|
461
|
+
To use Gemini extensions (Gmail, YouTube, etc.), you must first activate them on the [Gemini website](https://gemini.google.com/extensions). Reference them in prompts with the `@` prefix or in natural language.
|
|
462
|
+
|
|
463
|
+
> You must have Gemini Apps Activity enabled in your account to use extensions.
|
|
209
464
|
|
|
210
465
|
```js
|
|
211
|
-
await client.
|
|
466
|
+
const gmailResponse = await client.generateContent({
|
|
467
|
+
prompt: "@Gmail What's the latest message in my mailbox?",
|
|
468
|
+
});
|
|
469
|
+
console.log(gmailResponse.text);
|
|
470
|
+
|
|
471
|
+
const youtubeResponse = await client.generateContent({
|
|
472
|
+
prompt: "@YouTube What's the latest video from Fireship?",
|
|
473
|
+
});
|
|
474
|
+
console.log(youtubeResponse.text);
|
|
212
475
|
```
|
|
213
476
|
|
|
214
|
-
###
|
|
477
|
+
### Check and Switch to Other Reply Candidates
|
|
215
478
|
|
|
216
|
-
|
|
217
|
-
const plan = await client.createDeepResearchPlan('History of artificial intelligence');
|
|
218
|
-
console.log(plan.title, plan.steps);
|
|
479
|
+
A Gemini response sometimes contains multiple reply candidates with different generated content. You can inspect all candidates and choose one to continue the conversation flow.
|
|
219
480
|
|
|
220
|
-
|
|
481
|
+
```js
|
|
482
|
+
const chat = client.startChat();
|
|
483
|
+
const response = await chat.sendMessage({ prompt: 'Recommend a science fiction book.' });
|
|
221
484
|
|
|
222
|
-
|
|
223
|
-
|
|
485
|
+
// List all candidates
|
|
486
|
+
response.candidates.forEach((candidate, i) => {
|
|
487
|
+
console.log(`[${i}] ${candidate.text.slice(0, 80)}...`);
|
|
224
488
|
});
|
|
225
|
-
console.log(result.text);
|
|
226
489
|
|
|
227
|
-
|
|
228
|
-
|
|
490
|
+
if (response.candidates.length > 1) {
|
|
491
|
+
// Choose the second candidate to continue from
|
|
492
|
+
chat.chooseCandidate(1);
|
|
493
|
+
|
|
494
|
+
const followup = await chat.sendMessage({ prompt: 'Tell me more about it.' });
|
|
495
|
+
console.log(followup.text);
|
|
496
|
+
} else {
|
|
497
|
+
console.log('Only one candidate available.');
|
|
498
|
+
}
|
|
229
499
|
```
|
|
230
500
|
|
|
231
|
-
###
|
|
501
|
+
### Deep Research
|
|
502
|
+
|
|
503
|
+
Gemini's deep research feature is an autonomous agent that browses the web, analyzes sources, and produces a comprehensive report.
|
|
504
|
+
|
|
505
|
+
> You may need an active Gemini subscription to access deep research.
|
|
506
|
+
|
|
507
|
+
**Quick one-call method:**
|
|
232
508
|
|
|
233
509
|
```js
|
|
234
|
-
const
|
|
510
|
+
const result = await client.deepResearch(
|
|
511
|
+
'Compare the top 3 cloud providers and their AI offerings',
|
|
512
|
+
10000, // poll interval in ms
|
|
513
|
+
600000, // timeout in ms
|
|
514
|
+
(status) => console.log(`Status: ${status.state} — ${status.notes.slice(0, 1).join(', ')}`),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
console.log(`Done: ${result.done}`);
|
|
518
|
+
console.log(result.text);
|
|
519
|
+
```
|
|
235
520
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
521
|
+
**Step-by-step workflow** for more control:
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
// Step 1: Create a research plan
|
|
525
|
+
const plan = await client.createDeepResearchPlan(
|
|
526
|
+
'What are the latest advancements in quantum computing?'
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
console.log(`Title: ${plan.title}`);
|
|
530
|
+
console.log(`ETA: ${plan.eta_text}`);
|
|
531
|
+
for (const step of plan.steps) {
|
|
532
|
+
console.log(` - ${step}`);
|
|
240
533
|
}
|
|
534
|
+
|
|
535
|
+
// Step 2: Start the research
|
|
536
|
+
const startOutput = await client.startDeepResearch(plan);
|
|
537
|
+
console.log('Research started:', startOutput.text.slice(0, 100));
|
|
538
|
+
|
|
539
|
+
// Step 3: Poll for completion
|
|
540
|
+
const result = await client.waitForDeepResearch(
|
|
541
|
+
plan,
|
|
542
|
+
10000, // poll interval in ms
|
|
543
|
+
600000, // timeout in ms
|
|
544
|
+
(status) => console.log(`[${status.state}] ${status.notes[0] || ''}`),
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
console.log(result.text);
|
|
241
548
|
```
|
|
242
549
|
|
|
243
|
-
###
|
|
550
|
+
### Account Status
|
|
551
|
+
|
|
552
|
+
The client detects your account's capability tier at initialization and exposes it via `client.accountStatus`.
|
|
244
553
|
|
|
245
554
|
```js
|
|
246
|
-
const
|
|
247
|
-
const gem = gems.get({ name: 'Coding partner' });
|
|
248
|
-
const myGems = gems.filter({ predefined: false });
|
|
249
|
-
const chat = client.startChat({ gem });
|
|
555
|
+
const { AccountStatus } = require('gemini-reverse');
|
|
250
556
|
|
|
251
|
-
|
|
252
|
-
name: 'My Assistant',
|
|
253
|
-
prompt: 'You are a helpful assistant that speaks formally.',
|
|
254
|
-
});
|
|
557
|
+
await client.init();
|
|
255
558
|
|
|
256
|
-
|
|
257
|
-
|
|
559
|
+
if (client.accountStatus === AccountStatus.AVAILABLE) {
|
|
560
|
+
console.log('Account is fully authorized.');
|
|
561
|
+
} else if (client.accountStatus === AccountStatus.UNAUTHENTICATED) {
|
|
562
|
+
console.error('Cookies are expired or invalid.');
|
|
563
|
+
} else if (client.accountStatus === AccountStatus.LOCATION_REJECTED) {
|
|
564
|
+
console.error('Gemini is not available in your region.');
|
|
565
|
+
} else {
|
|
566
|
+
console.warn(`Account status: ${client.accountStatus.name} — ${client.accountStatus.description}`);
|
|
567
|
+
}
|
|
258
568
|
```
|
|
259
569
|
|
|
260
|
-
|
|
570
|
+
**All account status values:**
|
|
571
|
+
|
|
572
|
+
| Constant | Code | Description |
|
|
573
|
+
|---|---|---|
|
|
574
|
+
| `AccountStatus.AVAILABLE` | 1000 | Account is authorized and has normal access |
|
|
575
|
+
| `AccountStatus.ACCESS_TEMPORARILY_UNAVAILABLE` | 1014 | Access restricted, possibly regional/temporary |
|
|
576
|
+
| `AccountStatus.UNAUTHENTICATED` | 1016 | Cookies have expired or are invalid |
|
|
577
|
+
| `AccountStatus.ACCOUNT_REJECTED` | 1021 | Account access rejected |
|
|
578
|
+
| `AccountStatus.ACCOUNT_UNTRUSTED` | 1033 | Did not pass safety/trust checks |
|
|
579
|
+
| `AccountStatus.TOS_PENDING` | 1040 | Must accept latest Terms of Service |
|
|
580
|
+
| `AccountStatus.TOS_OUT_OF_DATE` | 1042 | Terms of Service are out of date |
|
|
581
|
+
| `AccountStatus.ACCOUNT_REJECTED_BY_GUARDIAN` | 1054 | Blocked by parent or guardian |
|
|
582
|
+
| `AccountStatus.GUARDIAN_APPROVAL_REQUIRED` | 1057 | Requires parental approval |
|
|
583
|
+
| `AccountStatus.LOCATION_REJECTED` | 1060 | Not available in your country/region |
|
|
261
584
|
|
|
262
585
|
## Error Handling
|
|
263
586
|
|
|
264
587
|
```js
|
|
265
|
-
const {
|
|
588
|
+
const {
|
|
589
|
+
AuthError,
|
|
590
|
+
APIError,
|
|
591
|
+
GeminiError,
|
|
592
|
+
TimeoutError,
|
|
593
|
+
UsageLimitExceeded,
|
|
594
|
+
ModelInvalid,
|
|
595
|
+
TemporarilyBlocked,
|
|
596
|
+
} = require('gemini-reverse');
|
|
266
597
|
|
|
267
598
|
try {
|
|
268
599
|
const response = await chat.sendMessage({ prompt: 'Hello!' });
|
|
600
|
+
console.log(response.text);
|
|
269
601
|
} catch (e) {
|
|
270
|
-
if (e instanceof AuthError)
|
|
271
|
-
|
|
272
|
-
else if (e instanceof
|
|
273
|
-
|
|
274
|
-
else if (e instanceof
|
|
275
|
-
|
|
602
|
+
if (e instanceof AuthError) {
|
|
603
|
+
console.error('Cookie expired or invalid. Please refresh your cookies.');
|
|
604
|
+
} else if (e instanceof UsageLimitExceeded) {
|
|
605
|
+
console.error('Usage limit reached. Try again later or switch to a different model.');
|
|
606
|
+
} else if (e instanceof TemporarilyBlocked) {
|
|
607
|
+
console.error('IP temporarily blocked by Google. Try using a proxy or wait a while.');
|
|
608
|
+
} else if (e instanceof TimeoutError) {
|
|
609
|
+
console.error('Request timed out. Try increasing the timeout value in init().');
|
|
610
|
+
} else if (e instanceof ModelInvalid) {
|
|
611
|
+
console.error('Invalid or unavailable model. Try a different model.');
|
|
612
|
+
} else if (e instanceof APIError) {
|
|
613
|
+
console.error('API error:', e.message);
|
|
614
|
+
} else {
|
|
615
|
+
throw e;
|
|
616
|
+
}
|
|
276
617
|
}
|
|
277
618
|
```
|
|
278
619
|
|
|
279
|
-
|
|
620
|
+
## Cookie Persistence
|
|
621
|
+
|
|
622
|
+
If your application runs in a containerized environment (e.g. Docker), you can persist the auto-refreshed cookie cache to a volume by setting the `GEMINI_COOKIE_PATH` environment variable to a writable path.
|
|
623
|
+
|
|
624
|
+
```yaml
|
|
625
|
+
# docker-compose.yml
|
|
626
|
+
services:
|
|
627
|
+
app:
|
|
628
|
+
environment:
|
|
629
|
+
GEMINI_COOKIE_PATH: /tmp/gemini_cache
|
|
630
|
+
volumes:
|
|
631
|
+
- ./gemini_cookies:/tmp/gemini_cache
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
By default, the cache is stored in `utils/temp/` relative to the package directory.
|
|
635
|
+
|
|
636
|
+
## TypeScript
|
|
637
|
+
|
|
638
|
+
This package includes full TypeScript declarations.
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
import {
|
|
642
|
+
GeminiClient,
|
|
643
|
+
ChatSession,
|
|
644
|
+
ModelOutput,
|
|
645
|
+
ChatHistory,
|
|
646
|
+
ChatTurn,
|
|
647
|
+
ChatInfo,
|
|
648
|
+
Gem,
|
|
649
|
+
AvailableModel,
|
|
650
|
+
Model,
|
|
651
|
+
AccountStatus,
|
|
652
|
+
DeepResearchPlan,
|
|
653
|
+
DeepResearchResult,
|
|
654
|
+
} from 'gemini-reverse';
|
|
655
|
+
|
|
656
|
+
const client = new GeminiClient({ secure_1psid: '...' });
|
|
657
|
+
await client.init();
|
|
658
|
+
|
|
659
|
+
const chat: ChatSession = client.startChat({ model: 'gemini-3-flash' });
|
|
660
|
+
const response: ModelOutput = await chat.sendMessage({ prompt: 'Hello!' });
|
|
661
|
+
|
|
662
|
+
console.log(response.text);
|
|
663
|
+
|
|
664
|
+
const history: ChatHistory | null = await chat.readHistory();
|
|
665
|
+
if (history) {
|
|
666
|
+
history.turns.forEach((turn: ChatTurn) => console.log(turn.role, turn.text));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const models: AvailableModel[] | null = client.listModels();
|
|
670
|
+
```
|
|
280
671
|
|
|
281
672
|
## Project Structure
|
|
282
673
|
|
|
283
674
|
```
|
|
284
675
|
gemini-reverse/
|
|
285
|
-
├── index.js
|
|
286
|
-
├── index.d.ts
|
|
287
|
-
├── client.js
|
|
288
|
-
├── constants.js
|
|
289
|
-
├── exceptions.js
|
|
676
|
+
├── index.js # entry point & exports
|
|
677
|
+
├── index.d.ts # TypeScript declarations
|
|
678
|
+
├── client.js # GeminiClient + ChatSession
|
|
679
|
+
├── constants.js # Endpoint, GRPC, Headers, Model, AccountStatus, ErrorCode
|
|
680
|
+
├── exceptions.js # custom error classes
|
|
290
681
|
├── types/
|
|
291
|
-
│ ├── availablemodel.js
|
|
292
|
-
│ ├── candidate.js
|
|
293
|
-
│ ├── chathistory.js
|
|
294
|
-
│ ├── chatinfo.js
|
|
295
|
-
│ ├── gem.js
|
|
296
|
-
│ ├── grpc.js
|
|
297
|
-
│ ├── image.js
|
|
298
|
-
│ ├── modeloutput.js
|
|
299
|
-
│ ├── research.js
|
|
300
|
-
│ ├── researchresult.js
|
|
301
|
-
│ └── video.js
|
|
682
|
+
│ ├── availablemodel.js # AvailableModel (dynamic model from API)
|
|
683
|
+
│ ├── candidate.js # Candidate
|
|
684
|
+
│ ├── chathistory.js # ChatTurn, ChatHistory
|
|
685
|
+
│ ├── chatinfo.js # ChatInfo
|
|
686
|
+
│ ├── gem.js # Gem, GemJar
|
|
687
|
+
│ ├── grpc.js # RPCData
|
|
688
|
+
│ ├── image.js # Image, WebImage, GeneratedImage
|
|
689
|
+
│ ├── modeloutput.js # ModelOutput
|
|
690
|
+
│ ├── research.js # DeepResearchPlan, DeepResearchStatus
|
|
691
|
+
│ ├── researchresult.js # DeepResearchResult
|
|
692
|
+
│ └── video.js # Video, GeneratedVideo, GeneratedMedia
|
|
302
693
|
├── utils/
|
|
303
|
-
│ ├── accessToken.js
|
|
304
|
-
│ ├── parsing.js
|
|
305
|
-
│ ├── research.js
|
|
306
|
-
│ ├── rotate.js
|
|
307
|
-
│ └── upload.js
|
|
694
|
+
│ ├── accessToken.js # cookie handling & init request
|
|
695
|
+
│ ├── parsing.js # response parsing utilities
|
|
696
|
+
│ ├── research.js # deep research payload extractors
|
|
697
|
+
│ ├── rotate.js # cookie rotation (kept from original)
|
|
698
|
+
│ └── upload.js # file upload helpers
|
|
308
699
|
└── components/
|
|
309
|
-
├── chatMixin.js
|
|
310
|
-
├── gemMixin.js
|
|
311
|
-
└── researchMixin.js
|
|
700
|
+
├── chatMixin.js # chat history methods
|
|
701
|
+
├── gemMixin.js # gem management methods
|
|
702
|
+
└── researchMixin.js # deep research methods
|
|
312
703
|
```
|
|
313
704
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
## Credits
|
|
317
|
-
|
|
318
|
-
Inspired by [Gemini-API](https://github.com/HanaokaYuzu/Gemini-API) by [@HanaokaYuzu](https://github.com/HanaokaYuzu).
|
|
705
|
+
## References
|
|
319
706
|
|
|
320
|
-
|
|
707
|
+
[Google AI Studio](https://ai.google.dev/tutorials/ai-studio_quickstart)
|
|
321
708
|
|
|
322
|
-
|
|
709
|
+
[Gemini-API (Python)](https://github.com/HanaokaYuzu/Gemini-API) by [@HanaokaYuzu](https://github.com/HanaokaYuzu)
|
|
323
710
|
|
|
324
|
-
|
|
711
|
+
[acheong08/Bard](https://github.com/acheong08/Bard)
|
|
325
712
|
|
|
326
713
|
---
|
|
327
714
|
|
|
328
|
-
|
|
715
|
+
**Disclaimer:** This is an unofficial package and is not affiliated with or endorsed by Google. Cookie-based authentication may break if Google changes its internal API. Use at your own risk.
|
|
329
716
|
|
|
330
|
-
MIT
|
|
717
|
+
**License:** MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-reverse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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",
|
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,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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(
|
|
43
|
+
const res = await axios.get(imgUrl, {
|
|
33
44
|
responseType: 'arraybuffer',
|
|
34
|
-
headers:
|
|
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 (
|
|
54
|
+
if (verbose) console.debug(`HTTP Request: GET ${imgUrl} [${res.status}]`);
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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.
|
|
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({
|
|
60
|
-
super(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
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
|
|
67
|
-
if (fullSize)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 };
|