Skip to content

Commit ba3a674

Browse files
feat(grok): add image command for grok.com image generation (#906)
* feat(grok): add image command for grok.com image generation Add `opencli grok image <prompt>` which submits a prompt via the existing grok.com browser session and returns the generated image URLs from the latest assistant bubble. Because assets.grok.com URLs are gated by Cloudflare and cannot be downloaded with a plain HTTP client, the --out flag triggers an in-page fetch(credentials: 'include') so the browser session's cookies and referer are attached, then writes the decoded blob to disk. Flags: - --new start a fresh chat before sending - --timeout max seconds to wait for the image (default 240) - --count minimum number of images to wait for before returning - --out directory to save downloaded images Ships with unit tests for the helpers (isOnGrok, normalizeBooleanFlag, dedupeBySrc, imagesSignature, extFromContentType, buildFilename). * fix(grok): harden image composer and bubble detection * fix(grok): harden image flow and docs --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 0e38fd8 commit ba3a674

7 files changed

Lines changed: 478 additions & 3 deletions

File tree

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ npm link
233233
| **sinafinance** | `news` | 🌐 公开 |
234234
| **barchart** | `quote` `options` `greeks` `flow` | 浏览器 |
235235
| **chaoxing** | `assignments` `exams` | 浏览器 |
236-
| **grok** | `ask` | 浏览器 |
236+
| **grok** | `ask` `image` | 浏览器 |
237237
| **hf** | `top` | 公开 |
238238
| **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` | 浏览器 |
239239
| **jimeng** | `generate` `history` | 浏览器 |

clis/grok/image.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { IPage } from '@jackwener/opencli/types';
3+
import { __test__ } from './image.js';
4+
5+
describe('grok image helpers', () => {
6+
describe('isOnGrok', () => {
7+
const fakePage = (url: string | Error): IPage =>
8+
({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) }) as unknown as IPage;
9+
10+
it('returns true for grok.com URLs', async () => {
11+
expect(await __test__.isOnGrok(fakePage('https://grok.com/'))).toBe(true);
12+
expect(await __test__.isOnGrok(fakePage('https://grok.com/chat/abc123'))).toBe(true);
13+
});
14+
15+
it('returns true for grok.com subdomains', async () => {
16+
expect(await __test__.isOnGrok(fakePage('https://assets.grok.com/foo'))).toBe(true);
17+
});
18+
19+
it('returns false for non-grok domains', async () => {
20+
expect(await __test__.isOnGrok(fakePage('https://fakegrok.com/'))).toBe(false);
21+
expect(await __test__.isOnGrok(fakePage('about:blank'))).toBe(false);
22+
});
23+
24+
it('returns false when evaluate throws (detached tab)', async () => {
25+
expect(await __test__.isOnGrok(fakePage(new Error('detached')))).toBe(false);
26+
});
27+
});
28+
29+
it('normalizes boolean flags', () => {
30+
expect(__test__.normalizeBooleanFlag(true)).toBe(true);
31+
expect(__test__.normalizeBooleanFlag('true')).toBe(true);
32+
expect(__test__.normalizeBooleanFlag('1')).toBe(true);
33+
expect(__test__.normalizeBooleanFlag('yes')).toBe(true);
34+
expect(__test__.normalizeBooleanFlag('on')).toBe(true);
35+
36+
expect(__test__.normalizeBooleanFlag(false)).toBe(false);
37+
expect(__test__.normalizeBooleanFlag('false')).toBe(false);
38+
expect(__test__.normalizeBooleanFlag(undefined)).toBe(false);
39+
});
40+
41+
it('dedupes images by src', () => {
42+
const deduped = __test__.dedupeBySrc([
43+
{ src: 'https://a.example/1.jpg', w: 500, h: 500 },
44+
{ src: 'https://a.example/1.jpg', w: 500, h: 500 },
45+
{ src: 'https://a.example/2.jpg', w: 500, h: 500 },
46+
{ src: '', w: 500, h: 500 },
47+
]);
48+
expect(deduped.map(i => i.src)).toEqual([
49+
'https://a.example/1.jpg',
50+
'https://a.example/2.jpg',
51+
]);
52+
});
53+
54+
it('builds a deterministic-ish signature order-independent by src', () => {
55+
const sigA = __test__.imagesSignature([
56+
{ src: 'https://a.example/1.jpg', w: 1, h: 1 },
57+
{ src: 'https://a.example/2.jpg', w: 1, h: 1 },
58+
]);
59+
const sigB = __test__.imagesSignature([
60+
{ src: 'https://a.example/2.jpg', w: 1, h: 1 },
61+
{ src: 'https://a.example/1.jpg', w: 1, h: 1 },
62+
]);
63+
expect(sigA).toBe(sigB);
64+
});
65+
66+
it('maps content-type to sensible image extensions', () => {
67+
expect(__test__.extFromContentType('image/png')).toBe('png');
68+
expect(__test__.extFromContentType('image/webp')).toBe('webp');
69+
expect(__test__.extFromContentType('image/gif')).toBe('gif');
70+
expect(__test__.extFromContentType('image/jpeg')).toBe('jpg');
71+
expect(__test__.extFromContentType(undefined)).toBe('jpg');
72+
expect(__test__.extFromContentType('')).toBe('jpg');
73+
});
74+
75+
it('builds filenames with a stable sha1 slice tied to the src', () => {
76+
const a1 = __test__.buildFilename('https://a.example/1.jpg', 'image/jpeg');
77+
const a2 = __test__.buildFilename('https://a.example/1.jpg', 'image/jpeg');
78+
const b1 = __test__.buildFilename('https://a.example/2.jpg', 'image/png');
79+
// Same URL → same 12-char hash slice (timestamps may differ).
80+
expect(a1.split('-')[2].split('.')[0]).toBe(a2.split('-')[2].split('.')[0]);
81+
expect(a1.split('-')[2].split('.')[0]).not.toBe(b1.split('-')[2].split('.')[0]);
82+
expect(a1.endsWith('.jpg')).toBe(true);
83+
expect(b1.endsWith('.png')).toBe(true);
84+
});
85+
86+
it('only accepts image bubbles that appeared after the baseline', () => {
87+
const candidate = __test__.pickLatestImageCandidate([
88+
[{ src: 'https://a.example/stale.jpg', w: 512, h: 512 }],
89+
[],
90+
[{ src: 'https://a.example/fresh.jpg', w: 1024, h: 1024 }],
91+
], 1);
92+
93+
expect(candidate).toEqual([
94+
{ src: 'https://a.example/fresh.jpg', w: 1024, h: 1024 },
95+
]);
96+
});
97+
98+
it('does not reuse stale images when no new image bubble appears after baseline', () => {
99+
const candidate = __test__.pickLatestImageCandidate([
100+
[{ src: 'https://a.example/stale.jpg', w: 512, h: 512 }],
101+
[],
102+
[],
103+
], 1);
104+
105+
expect(candidate).toEqual([]);
106+
});
107+
});

0 commit comments

Comments
 (0)