/ clis / chatgpt / image.js
image.js
 1  import * as os from 'node:os';
 2  import * as path from 'node:path';
 3  import { cli, Strategy } from '@jackwener/opencli/registry';
 4  import { saveBase64ToFile } from '@jackwener/opencli/utils';
 5  import { getChatGPTVisibleImageUrls, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets } from './utils.js';
 6  
 7  const CHATGPT_DOMAIN = 'chatgpt.com';
 8  
 9  function extFromMime(mime) {
10      if (mime.includes('png')) return '.png';
11      if (mime.includes('webp')) return '.webp';
12      if (mime.includes('gif')) return '.gif';
13      return '.jpg';
14  }
15  
16  function normalizeBooleanFlag(value) {
17      if (typeof value === 'boolean') return value;
18      const normalized = String(value ?? '').trim().toLowerCase();
19      return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
20  }
21  
22  function displayPath(filePath) {
23      const home = os.homedir();
24      return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
25  }
26  
27  async function currentChatGPTLink(page) {
28      const url = await page.evaluate('window.location.href').catch(() => '');
29      return typeof url === 'string' && url ? url : 'https://chatgpt.com';
30  }
31  
32  export const imageCommand = cli({
33      site: 'chatgpt',
34      name: 'image',
35      description: 'Generate images with ChatGPT web and save them locally',
36      domain: CHATGPT_DOMAIN,
37      strategy: Strategy.COOKIE,
38      browser: true,
39      navigateBefore: false,
40      defaultFormat: 'plain',
41      timeoutSeconds: 240,
42      args: [
43          { name: 'prompt', positional: true, required: true, help: 'Image prompt to send to ChatGPT' },
44          { name: 'op', default: '~/Pictures/chatgpt', help: 'Output directory' },
45          { name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show ChatGPT link' },
46      ],
47      columns: ['status', 'file', 'link'],
48      func: async (page, kwargs) => {
49          const prompt = kwargs.prompt;
50          const outputDir = kwargs.op || path.join(os.homedir(), 'Pictures', 'chatgpt');
51          const skipDownloadRaw = kwargs.sd;
52          const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
53          const timeout = 120;
54  
55          // Navigate to chatgpt.com/new with full reload to clear React sidebar state
56          await page.goto(`https://${CHATGPT_DOMAIN}/new`, { settleMs: 2000 });
57  
58          const beforeUrls = await getChatGPTVisibleImageUrls(page);
59  
60          // Send the image generation prompt - must be explicit
61          const sent = await sendChatGPTMessage(page, `Generate an image of: ${prompt}`);
62          if (!sent) {
63              return [{ status: '⚠️ send-failed', file: '📁 -', link: `🔗 ${await currentChatGPTLink(page)}` }];
64          }
65  
66          // Wait for response and images
67          const urls = await waitForChatGPTImages(page, beforeUrls, timeout);
68          const link = await currentChatGPTLink(page);
69  
70          if (!urls.length) {
71              return [{ status: '⚠️ no-images', file: '📁 -', link: `🔗 ${link}` }];
72          }
73  
74          if (skipDownload) {
75              return [{ status: '🎨 generated', file: '📁 -', link: `🔗 ${link}` }];
76          }
77  
78          // Export and save images
79          const assets = await getChatGPTImageAssets(page, urls);
80          if (!assets.length) {
81              return [{ status: '⚠️ export-failed', file: '📁 -', link: `🔗 ${link}` }];
82          }
83  
84          const stamp = Date.now();
85          const results = [];
86          for (let index = 0; index < assets.length; index += 1) {
87              const asset = assets[index];
88              const base64 = asset.dataUrl.replace(/^data:[^;]+;base64,/, '');
89              const suffix = assets.length > 1 ? `_${index + 1}` : '';
90              const ext = extFromMime(asset.mimeType);
91              const filePath = path.join(outputDir, `chatgpt_${stamp}${suffix}${ext}`);
92              await saveBase64ToFile(base64, filePath);
93              results.push({ status: '✅ saved', file: `📁 ${displayPath(filePath)}`, link: `🔗 ${link}` });
94          }
95          return results;
96      },
97  });