@jackwener/opencli 1.7.15 → 1.7.17
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 +15 -13
- package/README.zh-CN.md +15 -12
- package/cli-manifest.json +165 -209
- package/clis/chatgpt/ask.js +3 -2
- package/clis/chatgpt/commands.test.js +2 -2
- package/clis/chatgpt/detail.js +7 -2
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/image.js +38 -4
- package/clis/chatgpt/image.test.js +68 -1
- package/clis/chatgpt/new.js +1 -1
- package/clis/chatgpt/read.js +3 -2
- package/clis/chatgpt/send.js +3 -2
- package/clis/chatgpt/status.js +1 -1
- package/clis/chatgpt/utils.js +259 -25
- package/clis/chatgpt/utils.test.js +166 -2
- package/clis/claude/ask.js +23 -8
- package/clis/claude/detail.js +10 -3
- package/clis/claude/history.js +1 -1
- package/clis/claude/new.js +9 -3
- package/clis/claude/read.js +3 -2
- package/clis/claude/send.js +9 -4
- package/clis/claude/status.js +1 -1
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +22 -9
- package/clis/deepseek/detail.js +10 -2
- package/clis/deepseek/history.js +1 -1
- package/clis/deepseek/new.js +14 -3
- package/clis/deepseek/read.js +3 -2
- package/clis/deepseek/send.js +1 -1
- package/clis/deepseek/status.js +1 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/doubao/ask.js +1 -1
- package/clis/doubao/detail.js +1 -1
- package/clis/doubao/history.js +1 -1
- package/clis/doubao/meeting-summary.js +1 -1
- package/clis/doubao/meeting-transcript.js +1 -1
- package/clis/doubao/new.js +1 -1
- package/clis/doubao/read.js +1 -1
- package/clis/doubao/send.js +1 -1
- package/clis/doubao/status.js +1 -1
- package/clis/gemini/ask.js +1 -1
- package/clis/gemini/deep-research-result.js +1 -1
- package/clis/gemini/deep-research.js +1 -1
- package/clis/gemini/image.js +1 -1
- package/clis/gemini/new.js +1 -1
- package/clis/grok/ask.js +1 -1
- package/clis/grok/detail.js +1 -1
- package/clis/grok/history.js +1 -1
- package/clis/grok/image.js +1 -1
- package/clis/grok/new.js +1 -1
- package/clis/grok/read.js +1 -1
- package/clis/grok/send.js +1 -1
- package/clis/grok/status.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +2 -2
- package/clis/notebooklm/open.test.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- package/clis/qwen/ask.js +1 -1
- package/clis/qwen/detail.js +1 -1
- package/clis/qwen/history.js +1 -1
- package/clis/qwen/image.js +1 -1
- package/clis/qwen/new.js +1 -1
- package/clis/qwen/read.js +1 -1
- package/clis/qwen/send.js +1 -1
- package/clis/qwen/status.js +1 -1
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +1 -0
- package/clis/reddit/subreddit.js +1 -0
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +1 -0
- package/clis/reddit/user-posts.js +1 -0
- package/clis/reddit/user.js +1 -0
- package/clis/twitter/article.js +7 -4
- package/clis/twitter/bookmark-folder.js +3 -5
- package/clis/twitter/bookmark-folder.test.js +5 -2
- package/clis/twitter/bookmark-folders.js +3 -5
- package/clis/twitter/bookmark-folders.test.js +3 -1
- package/clis/twitter/bookmarks.js +3 -5
- package/clis/twitter/download.js +1 -0
- package/clis/twitter/followers.js +1 -0
- package/clis/twitter/following.js +3 -6
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/likes.js +3 -5
- package/clis/twitter/list-add.js +4 -3
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +4 -3
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +3 -5
- package/clis/twitter/lists.js +3 -5
- package/clis/twitter/notifications.js +1 -0
- package/clis/twitter/profile.js +7 -4
- package/clis/twitter/search.js +1 -0
- package/clis/twitter/thread.js +5 -7
- package/clis/twitter/timeline.js +5 -7
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +3 -6
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -1
- package/clis/yuanbao/ask.js +1 -1
- package/clis/yuanbao/detail.js +1 -1
- package/clis/yuanbao/history.js +1 -1
- package/clis/yuanbao/new.js +1 -1
- package/clis/yuanbao/read.js +1 -1
- package/clis/yuanbao/send.js +1 -1
- package/clis/yuanbao/status.js +1 -1
- package/dist/src/browser/bridge.d.ts +4 -1
- package/dist/src/browser/bridge.js +3 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/daemon-client.d.ts +9 -16
- package/dist/src/browser/daemon-client.js +8 -9
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/network-cache.d.ts +5 -5
- package/dist/src/browser/network-cache.js +8 -8
- package/dist/src/browser/network-cache.test.js +4 -4
- package/dist/src/browser/page.d.ts +9 -7
- package/dist/src/browser/page.js +27 -16
- package/dist/src/browser/page.test.js +60 -30
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli.js +91 -125
- package/dist/src/cli.test.js +293 -180
- package/dist/src/commanderAdapter.js +9 -0
- package/dist/src/discovery.js +1 -1
- package/dist/src/doctor.d.ts +0 -4
- package/dist/src/doctor.js +8 -72
- package/dist/src/doctor.test.js +26 -97
- package/dist/src/execution.d.ts +3 -0
- package/dist/src/execution.js +47 -23
- package/dist/src/execution.test.js +68 -45
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +36 -1
- package/dist/src/main.js +0 -29
- package/dist/src/manifest-types.d.ts +2 -4
- package/dist/src/observation/artifact.js +1 -1
- package/dist/src/observation/artifact.test.js +3 -3
- package/dist/src/observation/events.d.ts +1 -1
- package/dist/src/observation/manager.js +1 -1
- package/dist/src/observation/manager.test.js +3 -3
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +3 -12
- package/dist/src/registry.js +6 -10
- package/dist/src/runtime.d.ts +10 -2
- package/dist/src/runtime.js +4 -1
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/types.d.ts +0 -15
- package/package.json +1 -1
package/clis/chatgpt/ask.js
CHANGED
|
@@ -22,7 +22,7 @@ export const askCommand = cli({
|
|
|
22
22
|
domain: CHATGPT_DOMAIN,
|
|
23
23
|
strategy: Strategy.COOKIE,
|
|
24
24
|
browser: true,
|
|
25
|
-
|
|
25
|
+
siteSession: 'persistent',
|
|
26
26
|
navigateBefore: false,
|
|
27
27
|
args: [
|
|
28
28
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
@@ -43,7 +43,8 @@ export const askCommand = cli({
|
|
|
43
43
|
} else {
|
|
44
44
|
await ensureOnChatGPT(page);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
// startNewChat / ensureOnChatGPT now wait for the composer selector
|
|
47
|
+
// after navigating, so the previous standalone 2 s settle is redundant.
|
|
47
48
|
await ensureChatGPTComposer(page, 'ChatGPT ask requires a logged-in ChatGPT session with a visible composer.');
|
|
48
49
|
|
|
49
50
|
const baseline = await getBubbleCount(page);
|
|
@@ -10,7 +10,7 @@ import './status.js';
|
|
|
10
10
|
import './image.js';
|
|
11
11
|
|
|
12
12
|
describe('chatgpt browser command registration', () => {
|
|
13
|
-
it('registers the baseline web chat commands with site
|
|
13
|
+
it('registers the baseline web chat commands with persistent site sessions', () => {
|
|
14
14
|
const expectedAccess = {
|
|
15
15
|
ask: 'write',
|
|
16
16
|
send: 'write',
|
|
@@ -29,7 +29,7 @@ describe('chatgpt browser command registration', () => {
|
|
|
29
29
|
expect(cmd.domain).toBe('chatgpt.com');
|
|
30
30
|
expect(cmd.strategy).toBe('cookie');
|
|
31
31
|
expect(cmd.browser).toBe(true);
|
|
32
|
-
expect(cmd.
|
|
32
|
+
expect(cmd.siteSession).toBe('persistent');
|
|
33
33
|
expect(cmd.navigateBefore).toBe(false);
|
|
34
34
|
expect(cmd.access).toBe(access);
|
|
35
35
|
}
|
package/clis/chatgpt/detail.js
CHANGED
|
@@ -3,6 +3,7 @@ import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
|
3
3
|
import {
|
|
4
4
|
CHATGPT_DOMAIN,
|
|
5
5
|
CHATGPT_URL,
|
|
6
|
+
CONVERSATION_MESSAGE_SELECTOR,
|
|
6
7
|
ensureChatGPTLogin,
|
|
7
8
|
getVisibleMessages,
|
|
8
9
|
messageHtmlToMarkdown,
|
|
@@ -18,7 +19,7 @@ export const detailCommand = cli({
|
|
|
18
19
|
domain: CHATGPT_DOMAIN,
|
|
19
20
|
strategy: Strategy.COOKIE,
|
|
20
21
|
browser: true,
|
|
21
|
-
|
|
22
|
+
siteSession: 'persistent',
|
|
22
23
|
navigateBefore: false,
|
|
23
24
|
args: [
|
|
24
25
|
{ name: 'id', positional: true, required: true, help: 'Conversation ID or full /c/<id> URL' },
|
|
@@ -29,7 +30,11 @@ export const detailCommand = cli({
|
|
|
29
30
|
const id = parseChatGPTConversationId(kwargs.id);
|
|
30
31
|
const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
|
|
31
32
|
await page.goto(`${CHATGPT_URL}/c/${id}`, { settleMs: 2000 });
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
await page.wait({ selector: CONVERSATION_MESSAGE_SELECTOR, timeout: 10 });
|
|
35
|
+
} catch {
|
|
36
|
+
// Empty conversation, missing access, or login redirect — handled by ensureChatGPTLogin / EmptyResultError below.
|
|
37
|
+
}
|
|
33
38
|
await ensureChatGPTLogin(page, 'ChatGPT detail requires a logged-in ChatGPT session.');
|
|
34
39
|
const messages = await getVisibleMessages(page);
|
|
35
40
|
if (!messages.length) {
|
package/clis/chatgpt/history.js
CHANGED
|
@@ -16,7 +16,7 @@ export const historyCommand = cli({
|
|
|
16
16
|
domain: CHATGPT_DOMAIN,
|
|
17
17
|
strategy: Strategy.COOKIE,
|
|
18
18
|
browser: true,
|
|
19
|
-
|
|
19
|
+
siteSession: 'persistent',
|
|
20
20
|
navigateBefore: false,
|
|
21
21
|
args: [
|
|
22
22
|
{ name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
|
package/clis/chatgpt/image.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from 'node:fs';
|
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { saveBase64ToFile } from '@jackwener/opencli/utils';
|
|
6
6
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
7
|
-
import { getChatGPTVisibleImageUrls, normalizeBooleanFlag, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets } from './utils.js';
|
|
7
|
+
import { clearChatGPTDraft, getChatGPTVisibleImageUrls, normalizeBooleanFlag, prepareChatGPTImagePaths, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets, uploadChatGPTImages } from './utils.js';
|
|
8
8
|
|
|
9
9
|
const CHATGPT_DOMAIN = 'chatgpt.com';
|
|
10
10
|
|
|
@@ -36,6 +36,23 @@ export function nextAvailablePath(dir, baseName, ext, existsSync = fs.existsSync
|
|
|
36
36
|
return candidate;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function parseImagePaths(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.flatMap(item => parseImagePaths(item));
|
|
42
|
+
}
|
|
43
|
+
return String(value ?? '')
|
|
44
|
+
.split(',')
|
|
45
|
+
.map(item => item.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildPrompt(prompt, imageCount) {
|
|
50
|
+
if (imageCount > 0) {
|
|
51
|
+
return `Edit the attached image${imageCount === 1 ? '' : 's'}: ${prompt}`;
|
|
52
|
+
}
|
|
53
|
+
return `Generate an image of: ${prompt}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
async function currentChatGPTLink(page) {
|
|
40
57
|
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
41
58
|
return typeof url === 'string' && url ? url : 'https://chatgpt.com';
|
|
@@ -49,11 +66,12 @@ export const imageCommand = cli({
|
|
|
49
66
|
domain: CHATGPT_DOMAIN,
|
|
50
67
|
strategy: Strategy.COOKIE,
|
|
51
68
|
browser: true,
|
|
52
|
-
|
|
69
|
+
siteSession: 'persistent',
|
|
53
70
|
navigateBefore: false,
|
|
54
71
|
defaultFormat: 'plain',
|
|
55
72
|
args: [
|
|
56
73
|
{ name: 'prompt', positional: true, required: true, help: 'Image prompt to send to ChatGPT' },
|
|
74
|
+
{ name: 'image', help: 'Local image path to attach before prompting; comma-separated paths are supported' },
|
|
57
75
|
{ name: 'op', help: 'Output directory (default: ~/Pictures/chatgpt)' },
|
|
58
76
|
{ name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show ChatGPT link' },
|
|
59
77
|
{ name: 'timeout', type: 'int', required: false, default: 240, help: 'Max seconds for the overall command (default: 240)' },
|
|
@@ -61,6 +79,7 @@ export const imageCommand = cli({
|
|
|
61
79
|
columns: ['status', 'file', 'link'],
|
|
62
80
|
func: async (page, kwargs) => {
|
|
63
81
|
const prompt = kwargs.prompt;
|
|
82
|
+
const imagePaths = parseImagePaths(kwargs.image);
|
|
64
83
|
const outputDir = resolveOutputDir(kwargs.op);
|
|
65
84
|
const skipDownloadRaw = kwargs.sd;
|
|
66
85
|
const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
|
|
@@ -68,14 +87,29 @@ export const imageCommand = cli({
|
|
|
68
87
|
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
69
88
|
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
70
89
|
}
|
|
90
|
+
const preparedImages = imagePaths.length ? await prepareChatGPTImagePaths(imagePaths) : { ok: true, paths: [] };
|
|
91
|
+
if (!preparedImages.ok) {
|
|
92
|
+
throw new ArgumentError(preparedImages.reason);
|
|
93
|
+
}
|
|
71
94
|
|
|
72
95
|
// Navigate to chatgpt.com/new with full reload to clear React sidebar state
|
|
73
96
|
await page.goto(`https://${CHATGPT_DOMAIN}/new`, { settleMs: 2000 });
|
|
97
|
+
await clearChatGPTDraft(page);
|
|
98
|
+
|
|
99
|
+
if (imagePaths.length) {
|
|
100
|
+
let upload;
|
|
101
|
+
try {
|
|
102
|
+
upload = await uploadChatGPTImages(page, preparedImages.paths);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw new CommandExecutionError(`Failed to upload image to ChatGPT: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
}
|
|
106
|
+
if (!upload?.ok) throw new CommandExecutionError(upload?.reason || 'Failed to upload image to ChatGPT');
|
|
107
|
+
}
|
|
74
108
|
|
|
75
109
|
const beforeUrls = await getChatGPTVisibleImageUrls(page);
|
|
76
110
|
|
|
77
|
-
// Send
|
|
78
|
-
const sent = await sendChatGPTMessage(page,
|
|
111
|
+
// Send an explicit generation/editing prompt so ChatGPT returns image assets.
|
|
112
|
+
const sent = await sendChatGPTMessage(page, buildPrompt(prompt, imagePaths.length));
|
|
79
113
|
if (!sent) {
|
|
80
114
|
throw new CommandExecutionError(
|
|
81
115
|
'Failed to send image prompt to ChatGPT',
|
|
@@ -4,13 +4,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
|
|
5
5
|
const mocks = vi.hoisted(() => ({
|
|
6
6
|
getChatGPTVisibleImageUrls: vi.fn(),
|
|
7
|
+
clearChatGPTDraft: vi.fn(),
|
|
8
|
+
prepareChatGPTImagePaths: vi.fn(),
|
|
7
9
|
sendChatGPTMessage: vi.fn(),
|
|
10
|
+
uploadChatGPTImages: vi.fn(),
|
|
8
11
|
waitForChatGPTImages: vi.fn(),
|
|
9
12
|
getChatGPTImageAssets: vi.fn(),
|
|
10
13
|
saveBase64ToFile: vi.fn(),
|
|
11
14
|
}));
|
|
12
15
|
|
|
13
16
|
vi.mock('./utils.js', () => ({
|
|
17
|
+
clearChatGPTDraft: mocks.clearChatGPTDraft,
|
|
14
18
|
getChatGPTVisibleImageUrls: mocks.getChatGPTVisibleImageUrls,
|
|
15
19
|
normalizeBooleanFlag: (value, fallback = false) => {
|
|
16
20
|
if (typeof value === 'boolean') return value;
|
|
@@ -18,7 +22,9 @@ vi.mock('./utils.js', () => ({
|
|
|
18
22
|
const normalized = String(value).trim().toLowerCase();
|
|
19
23
|
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
20
24
|
},
|
|
25
|
+
prepareChatGPTImagePaths: mocks.prepareChatGPTImagePaths,
|
|
21
26
|
sendChatGPTMessage: mocks.sendChatGPTMessage,
|
|
27
|
+
uploadChatGPTImages: mocks.uploadChatGPTImages,
|
|
22
28
|
waitForChatGPTImages: mocks.waitForChatGPTImages,
|
|
23
29
|
getChatGPTImageAssets: mocks.getChatGPTImageAssets,
|
|
24
30
|
}));
|
|
@@ -27,7 +33,7 @@ vi.mock('@jackwener/opencli/utils', () => ({
|
|
|
27
33
|
saveBase64ToFile: mocks.saveBase64ToFile,
|
|
28
34
|
}));
|
|
29
35
|
|
|
30
|
-
const { imageCommand, nextAvailablePath, resolveOutputDir } = await import('./image.js');
|
|
36
|
+
const { imageCommand, nextAvailablePath, parseImagePaths, resolveOutputDir } = await import('./image.js');
|
|
31
37
|
|
|
32
38
|
function createPage() {
|
|
33
39
|
return {
|
|
@@ -39,8 +45,11 @@ function createPage() {
|
|
|
39
45
|
|
|
40
46
|
beforeEach(() => {
|
|
41
47
|
vi.restoreAllMocks();
|
|
48
|
+
mocks.clearChatGPTDraft.mockReset().mockResolvedValue(undefined);
|
|
49
|
+
mocks.prepareChatGPTImagePaths.mockReset().mockImplementation(async (paths) => ({ ok: true, paths }));
|
|
42
50
|
mocks.getChatGPTVisibleImageUrls.mockReset().mockResolvedValue([]);
|
|
43
51
|
mocks.sendChatGPTMessage.mockReset().mockResolvedValue(true);
|
|
52
|
+
mocks.uploadChatGPTImages.mockReset().mockResolvedValue({ ok: true });
|
|
44
53
|
mocks.waitForChatGPTImages.mockReset().mockResolvedValue(['https://images.example/generated.png']);
|
|
45
54
|
mocks.getChatGPTImageAssets.mockReset().mockResolvedValue([{
|
|
46
55
|
url: 'https://images.example/generated.png',
|
|
@@ -66,6 +75,64 @@ describe('chatgpt image output paths', () => {
|
|
|
66
75
|
|
|
67
76
|
expect(nextAvailablePath(dir, 'chatgpt_123', '.png', (file) => taken.has(file))).toBe(path.join(dir, 'chatgpt_123_2.png'));
|
|
68
77
|
});
|
|
78
|
+
|
|
79
|
+
it('parses comma-separated image paths', () => {
|
|
80
|
+
expect(parseImagePaths('/tmp/a.png, /tmp/b.jpg')).toEqual(['/tmp/a.png', '/tmp/b.jpg']);
|
|
81
|
+
expect(parseImagePaths([' /tmp/a.png ', '/tmp/b.jpg,/tmp/c.webp'])).toEqual(['/tmp/a.png', '/tmp/b.jpg', '/tmp/c.webp']);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('chatgpt image upload flow', () => {
|
|
86
|
+
it('uploads local images before sending an edit prompt', async () => {
|
|
87
|
+
mocks.prepareChatGPTImagePaths.mockResolvedValue({ ok: true, paths: ['/abs/cat.png', '/abs/dog.jpg'] });
|
|
88
|
+
await imageCommand.func(createPage(), {
|
|
89
|
+
prompt: 'make the background blue',
|
|
90
|
+
image: '/tmp/cat.png,/tmp/dog.jpg',
|
|
91
|
+
op: '',
|
|
92
|
+
sd: true,
|
|
93
|
+
timeout: 240,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(mocks.clearChatGPTDraft).toHaveBeenCalled();
|
|
97
|
+
expect(mocks.uploadChatGPTImages).toHaveBeenCalledWith(expect.anything(), ['/abs/cat.png', '/abs/dog.jpg']);
|
|
98
|
+
expect(mocks.uploadChatGPTImages.mock.invocationCallOrder[0]).toBeLessThan(
|
|
99
|
+
mocks.getChatGPTVisibleImageUrls.mock.invocationCallOrder[0],
|
|
100
|
+
);
|
|
101
|
+
expect(mocks.sendChatGPTMessage).toHaveBeenCalledWith(expect.anything(), 'Edit the attached images: make the background blue');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects invalid local image paths before browser navigation', async () => {
|
|
105
|
+
mocks.prepareChatGPTImagePaths.mockResolvedValue({ ok: false, reason: 'Image not found: /tmp/missing.png' });
|
|
106
|
+
const page = createPage();
|
|
107
|
+
|
|
108
|
+
await expect(imageCommand.func(page, {
|
|
109
|
+
prompt: 'make the background blue',
|
|
110
|
+
image: '/tmp/missing.png',
|
|
111
|
+
op: '',
|
|
112
|
+
sd: false,
|
|
113
|
+
timeout: 240,
|
|
114
|
+
})).rejects.toMatchObject({
|
|
115
|
+
code: 'ARGUMENT',
|
|
116
|
+
message: expect.stringContaining('Image not found'),
|
|
117
|
+
});
|
|
118
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
119
|
+
expect(mocks.uploadChatGPTImages).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('surfaces upload failures as command execution errors', async () => {
|
|
123
|
+
mocks.uploadChatGPTImages.mockResolvedValue({ ok: false, reason: 'image upload preview did not appear' });
|
|
124
|
+
|
|
125
|
+
await expect(imageCommand.func(createPage(), {
|
|
126
|
+
prompt: 'make the background blue',
|
|
127
|
+
image: '/tmp/cat.png',
|
|
128
|
+
op: '',
|
|
129
|
+
sd: false,
|
|
130
|
+
timeout: 240,
|
|
131
|
+
})).rejects.toMatchObject({
|
|
132
|
+
code: 'COMMAND_EXEC',
|
|
133
|
+
message: expect.stringContaining('image upload preview did not appear'),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
69
136
|
});
|
|
70
137
|
|
|
71
138
|
describe('chatgpt image failure contracts', () => {
|
package/clis/chatgpt/new.js
CHANGED
package/clis/chatgpt/read.js
CHANGED
|
@@ -17,7 +17,7 @@ export const readCommand = cli({
|
|
|
17
17
|
domain: CHATGPT_DOMAIN,
|
|
18
18
|
strategy: Strategy.COOKIE,
|
|
19
19
|
browser: true,
|
|
20
|
-
|
|
20
|
+
siteSession: 'persistent',
|
|
21
21
|
navigateBefore: false,
|
|
22
22
|
args: [
|
|
23
23
|
{ name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
|
|
@@ -25,8 +25,9 @@ export const readCommand = cli({
|
|
|
25
25
|
columns: ['Index', 'Role', 'Text'],
|
|
26
26
|
func: async (page, kwargs) => {
|
|
27
27
|
const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
|
|
28
|
+
// ensureOnChatGPT now waits for the composer selector after navigating,
|
|
29
|
+
// so the previous standalone 2 s settle is redundant.
|
|
28
30
|
await ensureOnChatGPT(page);
|
|
29
|
-
await page.wait(2);
|
|
30
31
|
await ensureChatGPTLogin(page, 'ChatGPT read requires a logged-in ChatGPT session.');
|
|
31
32
|
const messages = await getVisibleMessages(page);
|
|
32
33
|
if (!messages.length) {
|
package/clis/chatgpt/send.js
CHANGED
|
@@ -19,7 +19,7 @@ export const sendCommand = cli({
|
|
|
19
19
|
domain: CHATGPT_DOMAIN,
|
|
20
20
|
strategy: Strategy.COOKIE,
|
|
21
21
|
browser: true,
|
|
22
|
-
|
|
22
|
+
siteSession: 'persistent',
|
|
23
23
|
navigateBefore: false,
|
|
24
24
|
args: [
|
|
25
25
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
@@ -34,7 +34,8 @@ export const sendCommand = cli({
|
|
|
34
34
|
} else {
|
|
35
35
|
await ensureOnChatGPT(page);
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// startNewChat / ensureOnChatGPT now wait for the composer selector
|
|
38
|
+
// after navigating, so the previous standalone 2 s settle is redundant.
|
|
38
39
|
await ensureChatGPTComposer(page, 'ChatGPT send requires a logged-in ChatGPT session with a visible composer.');
|
|
39
40
|
|
|
40
41
|
const sent = await sendChatGPTMessage(page, prompt);
|
package/clis/chatgpt/status.js
CHANGED