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.
Files changed (3) hide show
  1. package/README.md +570 -183
  2. package/package.json +1 -1
  3. package/types/image.js +116 -29
package/README.md CHANGED
@@ -1,25 +1,59 @@
1
- # Gemini-Reverse
1
+ ![Banner](https://napkinsdev.s3.us-east-1.amazonaws.com/next-s3-uploads/2a5843b5-3f5e-4ccd-bd61-f1ba6d6ae267/fb866bbfc5b3.png)
2
2
 
3
- An unofficial Node.js client for [gemini.google.com](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).
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
- - Send messages and receive responses from Gemini
10
- - Streaming support with text deltas
11
- - Multi-turn chat sessions with conversation history
12
- - File and image upload support
13
- - Video and media (audio) support
14
- - Gem (system prompt) management create, update, delete, fetch
15
- - Deep research support create plan, start, poll, and get results
16
- - Dynamic model discovery from account RPC
17
- - Account status detection
18
- - Auto cookie refresh to keep sessions alive
19
- - TypeScript type declarations included
20
- - Proxy support
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
- 1. Open gemini.google.com and log in
36
- 2. DevTools Application Cookies https://gemini.google.com
37
- 3. Copy __Secure-1PSID (and optionally __Secure-1PSIDTS)
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
- ## Quick Start
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 { GeminiClient } = require('gemini-reverse');
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
- const client = new GeminiClient({ secure_1psid: 'YOUR_SECURE_1PSID' });
119
+ ### Conversations Across Multiple Turns
48
120
 
49
- await client.init();
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 client.close();
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
- ## Usage
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
- ### Initialize Client
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
- await client.init({
66
- timeout: 300000,
67
- autoClose: false,
68
- closeDelay: 300000,
69
- autoRefresh: true,
70
- refreshInterval: 540000,
71
- verbose: false,
72
- watchdogTimeout: 30000,
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
- ### Generate Content
167
+ You can also read the history directly from the session:
77
168
 
78
169
  ```js
79
- const response = await client.generateContent({ prompt: 'What is the capital of France?' });
80
- console.log(response.text);
170
+ const history = await chat.readHistory(10); // fetch last 10 turns
81
171
  ```
82
172
 
83
- ### Streaming
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
- for await (const chunk of client.generateContentStream({ prompt: 'Tell me a long story.' })) {
87
- process.stdout.write(chunk.text_delta);
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
- ### Chat Session
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({ model: 'gemini-3-flash' });
191
+ const chat = client.startChat();
192
+ await chat.sendMessage({ prompt: 'This is a temporary conversation.' });
95
193
 
96
- const res1 = await chat.sendMessage({ prompt: 'My name is Alice.' });
97
- const res2 = await chat.sendMessage({ prompt: 'What is my name?' });
98
- console.log(res2.text);
194
+ await client.deleteChat(chat.cid);
195
+ console.log(`Chat deleted: ${chat.cid}`);
99
196
  ```
100
197
 
101
- ### Streaming in Chat
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
- for await (const chunk of chat.sendMessageStream({ prompt: 'Explain quantum computing.' })) {
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
- ### Temporary Chat
229
+ Streaming also works inside a chat session:
111
230
 
112
231
  ```js
113
- const response = await chat.sendMessage({ prompt: 'This will not appear in history.', temporary: true });
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
- ### Send with Files
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 response = await chat.sendMessage({
120
- prompt: 'Describe this image.',
121
- files: ['./photo.jpg'],
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
- ### Multiple Candidates
265
+ **Built-in model constants:**
126
266
 
127
- ```js
128
- const response = await chat.sendMessage({ prompt: 'Give me a poem.' });
129
- response.candidates.forEach((c, i) => console.log(`[${i}] ${c.text}`));
130
- chat.chooseCandidate(1);
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 m of models) {
139
- console.log(m.model_id, m.model_name, m.display_name);
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
- const chat = client.startChat({ model: 'gemini-3-flash' });
296
+ ### Apply System Prompt with Gemini Gems
144
297
 
145
- const { Model } = require('gemini-reverse');
146
- const chat2 = client.startChat({ model: Model.BASIC_FLASH });
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
- const chat3 = client.startChat({
149
- model: { model_name: 'my-model', model_header: { 'x-goog-ext-525001261-jspb': '...' } },
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
- **Built-in model constants:**
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
- | Constant | model_name |
156
- |---|---|
157
- | `Model.UNSPECIFIED` | `unspecified` (default) |
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
- for (const img of response.images) {
172
- console.log(img.url, img.title, img.alt);
173
- await img.save({ path: './downloads', verbose: true });
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
- ### Videos & Media
363
+ #### Delete a Custom Gem
178
364
 
179
365
  ```js
180
- for (const video of response.videos) {
181
- await video.save({ savePath: './downloads', verbose: true });
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
- for (const media of response.media) {
184
- await media.save({ savePath: './downloads', downloadType: 'both', verbose: true });
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
- ### Chat History
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 history = await client.readChat('c_YOUR_CHAT_ID');
192
- if (history) {
193
- for (const turn of history.turns) {
194
- console.log(turn.role, turn.text);
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
- const chats = client.listChats();
199
- if (chats) {
200
- for (const info of chats) {
201
- console.log(info.cid, info.title, info.is_pinned);
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
- const sessionHistory = await chat.readHistory(10);
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
- ### Delete Chat
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.deleteChat('c_YOUR_CHAT_ID');
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
- ### Deep Research
477
+ ### Check and Switch to Other Reply Candidates
215
478
 
216
- ```js
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
- const startOutput = await client.startDeepResearch(plan);
481
+ ```js
482
+ const chat = client.startChat();
483
+ const response = await chat.sendMessage({ prompt: 'Recommend a science fiction book.' });
221
484
 
222
- const result = await client.waitForDeepResearch(plan, 10000, 600000, (status) => {
223
- console.log(status.state, status.notes);
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
- const result2 = await client.deepResearch('Explain quantum computing', 10000, 600000);
228
- console.log(result2.text);
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
- ### Account Status
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 { AccountStatus } = require('gemini-reverse');
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
- if (client.accountStatus === AccountStatus.UNAUTHENTICATED) {
237
- console.error('Cookies expired.');
238
- } else if (client.accountStatus === AccountStatus.LOCATION_REJECTED) {
239
- console.error('Gemini not available in your region.');
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
- ### Gems
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 gems = await client.fetchGems();
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
- const newGem = await client.createGem({
252
- name: 'My Assistant',
253
- prompt: 'You are a helpful assistant that speaks formally.',
254
- });
557
+ await client.init();
255
558
 
256
- await client.updateGem({ gem: newGem, name: 'My Assistant v2', prompt: 'Speak casually.' });
257
- await client.deleteGem(newGem);
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 { AuthError, APIError, GeminiError, TimeoutError, UsageLimitExceeded, ModelInvalid, TemporarilyBlocked } = require('gemini-reverse');
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) console.error('Cookie expired or invalid.');
271
- else if (e instanceof UsageLimitExceeded) console.error('Usage limit reached.');
272
- else if (e instanceof TemporarilyBlocked) console.error('IP temporarily blocked.');
273
- else if (e instanceof TimeoutError) console.error('Request timed out.');
274
- else if (e instanceof ModelInvalid) console.error('Invalid model.');
275
- else if (e instanceof APIError) console.error('API error:', e.message);
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
- ## Disclaimer
709
+ [Gemini-API (Python)](https://github.com/HanaokaYuzu/Gemini-API) by [@HanaokaYuzu](https://github.com/HanaokaYuzu)
323
710
 
324
- Unofficial package, not affiliated with Google. Use at your own risk.
711
+ [acheong08/Bard](https://github.com/acheong08/Bard)
325
712
 
326
713
  ---
327
714
 
328
- ## License
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",
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, 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 };