@jackwener/opencli 1.7.7 → 1.7.9
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './ax.js';
|
|
3
|
+
|
|
4
|
+
describe('chatgpt-app AX send script', () => {
|
|
5
|
+
it('prefers the focused composer before falling back to the last editable input', () => {
|
|
6
|
+
expect(__test__.AX_SEND_SCRIPT).toContain('kAXFocusedUIElementAttribute');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('fails fast when the AX set does not round-trip into the composer value', () => {
|
|
10
|
+
expect(__test__.AX_SEND_SCRIPT).toContain('Failed to verify input value after AX set');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not report success until the prompt leaves the composer after send', () => {
|
|
14
|
+
expect(__test__.AX_SEND_SCRIPT).toContain('Prompt did not leave input after pressing send');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('supports english, zh-CN, and zh-TW send button labels', () => {
|
|
18
|
+
expect(__test__.AX_SEND_SCRIPT).toContain('["发送", "傳送", "Send"]');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('chatgpt-app AX model script', () => {
|
|
23
|
+
it('supports english, zh-CN, and zh-TW options button labels', () => {
|
|
24
|
+
expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "Options")');
|
|
25
|
+
expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "选项")');
|
|
26
|
+
expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "選項")');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('chatgpt-app generating detection', () => {
|
|
31
|
+
it('supports both english and zh-CN stop-generating labels', () => {
|
|
32
|
+
expect(__test__.AX_GENERATING_SCRIPT).toContain('Stop generating');
|
|
33
|
+
expect(__test__.AX_GENERATING_SCRIPT).toContain('停止生成');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -12,7 +12,7 @@ export const modelCommand = cli({
|
|
|
12
12
|
{ name: 'model', required: true, positional: true, help: 'Model to switch to', choices: MODEL_CHOICES },
|
|
13
13
|
],
|
|
14
14
|
columns: ['Status', 'Model'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (kwargs) => {
|
|
16
16
|
if (process.platform !== 'darwin') {
|
|
17
17
|
throw new ConfigError('ChatGPT Desktop integration requires macOS');
|
|
18
18
|
}
|
package/clis/chatgpt-app/new.js
CHANGED
|
@@ -10,7 +10,7 @@ export const newCommand = cli({
|
|
|
10
10
|
browser: false,
|
|
11
11
|
args: [],
|
|
12
12
|
columns: ['Status'],
|
|
13
|
-
func: async (
|
|
13
|
+
func: async () => {
|
|
14
14
|
if (process.platform !== 'darwin') {
|
|
15
15
|
throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
|
|
16
16
|
}
|
package/clis/chatgpt-app/read.js
CHANGED
|
@@ -11,7 +11,7 @@ export const readCommand = cli({
|
|
|
11
11
|
browser: false,
|
|
12
12
|
args: [],
|
|
13
13
|
columns: ['Role', 'Text'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async () => {
|
|
15
15
|
if (process.platform !== 'darwin') {
|
|
16
16
|
throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
|
|
17
17
|
}
|
package/clis/chatgpt-app/send.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { execSync, spawnSync } from 'node:child_process';
|
|
2
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
2
|
import { getErrorMessage } from '@jackwener/opencli/errors';
|
|
4
|
-
import { activateChatGPT, selectModel, MODEL_CHOICES } from './ax.js';
|
|
3
|
+
import { activateChatGPT, selectModel, MODEL_CHOICES, sendPrompt } from './ax.js';
|
|
5
4
|
export const sendCommand = cli({
|
|
6
5
|
site: 'chatgpt-app',
|
|
7
6
|
name: 'send',
|
|
@@ -14,7 +13,7 @@ export const sendCommand = cli({
|
|
|
14
13
|
{ name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
|
|
15
14
|
],
|
|
16
15
|
columns: ['Status'],
|
|
17
|
-
func: async (
|
|
16
|
+
func: async (kwargs) => {
|
|
18
17
|
const text = kwargs.text;
|
|
19
18
|
const model = kwargs.model;
|
|
20
19
|
try {
|
|
@@ -23,26 +22,8 @@ export const sendCommand = cli({
|
|
|
23
22
|
activateChatGPT();
|
|
24
23
|
selectModel(model);
|
|
25
24
|
}
|
|
26
|
-
// Backup current clipboard content
|
|
27
|
-
let clipBackup = '';
|
|
28
|
-
try {
|
|
29
|
-
clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
|
|
30
|
-
}
|
|
31
|
-
catch { /* clipboard may be empty */ }
|
|
32
|
-
// Copy text to clipboard
|
|
33
|
-
spawnSync('pbcopy', { input: text });
|
|
34
25
|
activateChatGPT();
|
|
35
|
-
|
|
36
|
-
"-e 'tell application \"System Events\"' " +
|
|
37
|
-
"-e 'keystroke \"v\" using command down' " +
|
|
38
|
-
"-e 'delay 0.2' " +
|
|
39
|
-
"-e 'keystroke return' " +
|
|
40
|
-
"-e 'end tell'";
|
|
41
|
-
execSync(cmd);
|
|
42
|
-
// Restore original clipboard content
|
|
43
|
-
if (clipBackup) {
|
|
44
|
-
spawnSync('pbcopy', { input: clipBackup });
|
|
45
|
-
}
|
|
26
|
+
sendPrompt(text);
|
|
46
27
|
return [{ Status: 'Success' }];
|
|
47
28
|
}
|
|
48
29
|
catch (err) {
|
|
@@ -10,7 +10,7 @@ export const statusCommand = cli({
|
|
|
10
10
|
browser: false,
|
|
11
11
|
args: [],
|
|
12
12
|
columns: ['Status'],
|
|
13
|
-
func: async (
|
|
13
|
+
func: async () => {
|
|
14
14
|
if (process.platform !== 'darwin') {
|
|
15
15
|
throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
|
|
16
16
|
}
|
package/clis/chatwise/ask.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const askCommand = cli({
|
|
4
4
|
site: 'chatwise',
|
|
5
5
|
name: 'ask',
|
|
@@ -43,7 +43,7 @@ export const askCommand = cli({
|
|
|
43
43
|
})(${JSON.stringify(text)})
|
|
44
44
|
`);
|
|
45
45
|
if (!injected)
|
|
46
|
-
throw
|
|
46
|
+
throw selectorError('ChatWise input element');
|
|
47
47
|
await page.wait(0.5);
|
|
48
48
|
await page.pressKey('Enter');
|
|
49
49
|
// Poll for response
|
package/clis/chatwise/model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const modelCommand = cli({
|
|
4
4
|
site: 'chatwise',
|
|
5
5
|
name: 'model',
|
|
@@ -58,7 +58,7 @@ export const modelCommand = cli({
|
|
|
58
58
|
})(${JSON.stringify(desiredModel)})
|
|
59
59
|
`);
|
|
60
60
|
if (!opened)
|
|
61
|
-
throw
|
|
61
|
+
throw selectorError('ChatWise model selector');
|
|
62
62
|
await page.wait(0.5);
|
|
63
63
|
// Find and click the target model in the dropdown
|
|
64
64
|
const found = await page.evaluate(`
|
package/clis/chatwise/send.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const sendCommand = cli({
|
|
4
4
|
site: 'chatwise',
|
|
5
5
|
name: 'send',
|
|
@@ -36,7 +36,7 @@ export const sendCommand = cli({
|
|
|
36
36
|
})(${JSON.stringify(text)})
|
|
37
37
|
`);
|
|
38
38
|
if (!injected)
|
|
39
|
-
throw
|
|
39
|
+
throw selectorError('ChatWise input element');
|
|
40
40
|
await page.wait(0.5);
|
|
41
41
|
await page.pressKey('Enter');
|
|
42
42
|
return [
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
CLAUDE_DOMAIN, CLAUDE_URL, ensureOnClaude, selectModel, setAdaptiveThinking,
|
|
5
|
+
sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
|
|
6
|
+
ensureClaudeComposer, requireNonEmptyPrompt, requirePositiveInt,
|
|
7
|
+
} from './utils.js';
|
|
8
|
+
|
|
9
|
+
export const askCommand = cli({
|
|
10
|
+
site: 'claude',
|
|
11
|
+
name: 'ask',
|
|
12
|
+
description: 'Send a prompt to Claude and get the response',
|
|
13
|
+
domain: CLAUDE_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
browser: true,
|
|
16
|
+
navigateBefore: false,
|
|
17
|
+
timeoutSeconds: 180,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
20
|
+
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
|
|
21
|
+
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
|
|
22
|
+
{ name: 'model', default: 'sonnet', choices: ['sonnet', 'opus', 'haiku'], help: 'Model to use: sonnet, opus, or haiku' },
|
|
23
|
+
{ name: 'think', type: 'boolean', default: false, help: 'Enable Adaptive thinking' },
|
|
24
|
+
{ name: 'file', help: 'Attach a file (image, PDF, text) with the prompt' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['response'],
|
|
27
|
+
|
|
28
|
+
func: async (page, kwargs) => {
|
|
29
|
+
const prompt = requireNonEmptyPrompt(kwargs.prompt, 'claude ask');
|
|
30
|
+
const timeoutSeconds = requirePositiveInt(
|
|
31
|
+
Number(kwargs.timeout ?? 120),
|
|
32
|
+
'claude ask --timeout',
|
|
33
|
+
'Example: opencli claude ask "hello" --timeout 120',
|
|
34
|
+
);
|
|
35
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
36
|
+
const wantThink = parseBoolFlag(kwargs.think);
|
|
37
|
+
|
|
38
|
+
if (parseBoolFlag(kwargs.new)) {
|
|
39
|
+
await page.goto(CLAUDE_URL);
|
|
40
|
+
await page.wait(3);
|
|
41
|
+
} else {
|
|
42
|
+
const navigated = await ensureOnClaude(page);
|
|
43
|
+
if (navigated) {
|
|
44
|
+
// Workspace was recycled; try to resume the most recent
|
|
45
|
+
// conversation instead of starting a new one.
|
|
46
|
+
await page.evaluate(`(() => {
|
|
47
|
+
var link = document.querySelector('a[href*="/chat/"]');
|
|
48
|
+
if (link) link.click();
|
|
49
|
+
})()`);
|
|
50
|
+
await page.wait(2);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await page.wait(2);
|
|
55
|
+
await withRetry(() => ensureClaudeComposer(page, 'Claude ask requires a visible composer on the current page.'));
|
|
56
|
+
|
|
57
|
+
// Model selector is only available on the new-chat page, not inside
|
|
58
|
+
// an existing conversation. Skip it when we resumed a prior thread.
|
|
59
|
+
const currentUrl = await page.evaluate('window.location.href') || '';
|
|
60
|
+
const inConversation = currentUrl.includes('/chat/');
|
|
61
|
+
const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli';
|
|
62
|
+
|
|
63
|
+
const wantModel = kwargs.model || 'sonnet';
|
|
64
|
+
if (inConversation && modelExplicit) {
|
|
65
|
+
throw new ArgumentError(
|
|
66
|
+
`Cannot switch to ${wantModel} model inside an existing conversation.`,
|
|
67
|
+
'Re-run with --new to start a fresh chat before selecting a model.',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!inConversation) {
|
|
72
|
+
const modelResult = await withRetry(() => selectModel(page, wantModel));
|
|
73
|
+
if (!modelResult?.ok) {
|
|
74
|
+
if (modelResult?.upgrade) {
|
|
75
|
+
throw new ArgumentError(
|
|
76
|
+
`${wantModel} model requires a paid Claude plan.`,
|
|
77
|
+
'Pick --model sonnet or --model haiku, or upgrade your account.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
|
|
81
|
+
}
|
|
82
|
+
if (modelResult?.toggled) await page.wait(0.5);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const thinkResult = await withRetry(() => setAdaptiveThinking(page, wantThink));
|
|
86
|
+
if (!thinkResult?.ok && wantThink) {
|
|
87
|
+
throw new CommandExecutionError('Could not enable Adaptive thinking');
|
|
88
|
+
}
|
|
89
|
+
if (thinkResult?.toggled) await page.wait(0.5);
|
|
90
|
+
|
|
91
|
+
if (kwargs.file) {
|
|
92
|
+
const baseline = await withRetry(() => getBubbleCount(page));
|
|
93
|
+
try {
|
|
94
|
+
const fileResult = await sendWithFile(page, kwargs.file, prompt);
|
|
95
|
+
if (fileResult && !fileResult.ok) {
|
|
96
|
+
throw new CommandExecutionError(fileResult.reason || 'Failed to attach file');
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// SPA navigates after send; "Promise was collected" means send succeeded
|
|
100
|
+
if (!String(err?.message || err).includes('Promise was collected')) throw err;
|
|
101
|
+
}
|
|
102
|
+
await page.wait(3);
|
|
103
|
+
const result = await waitForResponse(page, baseline, prompt, timeoutMs);
|
|
104
|
+
if (!result) {
|
|
105
|
+
throw new EmptyResultError(
|
|
106
|
+
'claude ask',
|
|
107
|
+
`No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return [{ response: result }];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const baseline = await withRetry(() => getBubbleCount(page));
|
|
114
|
+
const sendResult = await withRetry(() => sendMessage(page, prompt));
|
|
115
|
+
if (!sendResult?.ok) {
|
|
116
|
+
throw new CommandExecutionError(sendResult?.reason || 'Failed to send message');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await waitForResponse(page, baseline, prompt, timeoutMs);
|
|
120
|
+
if (!result) {
|
|
121
|
+
throw new EmptyResultError(
|
|
122
|
+
'claude ask',
|
|
123
|
+
`No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return [{ response: result }];
|
|
127
|
+
},
|
|
128
|
+
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
mockEnsureOnClaude,
|
|
6
|
+
mockEnsureClaudeComposer,
|
|
7
|
+
mockSelectModel,
|
|
8
|
+
mockSetAdaptiveThinking,
|
|
9
|
+
mockSendMessage,
|
|
10
|
+
mockSendWithFile,
|
|
11
|
+
mockGetBubbleCount,
|
|
12
|
+
mockWaitForResponse,
|
|
13
|
+
mockParseBoolFlag,
|
|
14
|
+
mockRequireNonEmptyPrompt,
|
|
15
|
+
mockRequirePositiveInt,
|
|
16
|
+
mockWithRetry,
|
|
17
|
+
} = vi.hoisted(() => ({
|
|
18
|
+
mockEnsureOnClaude: vi.fn(),
|
|
19
|
+
mockEnsureClaudeComposer: vi.fn(),
|
|
20
|
+
mockSelectModel: vi.fn(),
|
|
21
|
+
mockSetAdaptiveThinking: vi.fn(),
|
|
22
|
+
mockSendMessage: vi.fn(),
|
|
23
|
+
mockSendWithFile: vi.fn(),
|
|
24
|
+
mockGetBubbleCount: vi.fn(),
|
|
25
|
+
mockWaitForResponse: vi.fn(),
|
|
26
|
+
mockParseBoolFlag: vi.fn((v) => v === true || v === 'true'),
|
|
27
|
+
mockRequireNonEmptyPrompt: vi.fn((v) => String(v ?? '')),
|
|
28
|
+
mockRequirePositiveInt: vi.fn((v) => Number(v)),
|
|
29
|
+
mockWithRetry: vi.fn(async (fn) => fn()),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('./utils.js', () => ({
|
|
33
|
+
CLAUDE_DOMAIN: 'claude.ai',
|
|
34
|
+
CLAUDE_URL: 'https://claude.ai/new',
|
|
35
|
+
ensureOnClaude: mockEnsureOnClaude,
|
|
36
|
+
ensureClaudeComposer: mockEnsureClaudeComposer,
|
|
37
|
+
selectModel: mockSelectModel,
|
|
38
|
+
setAdaptiveThinking: mockSetAdaptiveThinking,
|
|
39
|
+
sendMessage: mockSendMessage,
|
|
40
|
+
sendWithFile: mockSendWithFile,
|
|
41
|
+
getBubbleCount: mockGetBubbleCount,
|
|
42
|
+
waitForResponse: mockWaitForResponse,
|
|
43
|
+
parseBoolFlag: mockParseBoolFlag,
|
|
44
|
+
requireNonEmptyPrompt: mockRequireNonEmptyPrompt,
|
|
45
|
+
requirePositiveInt: mockRequirePositiveInt,
|
|
46
|
+
withRetry: mockWithRetry,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { askCommand } from './ask.js';
|
|
50
|
+
|
|
51
|
+
describe('claude ask basic flow', () => {
|
|
52
|
+
const page = {
|
|
53
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
evaluate: vi.fn().mockResolvedValue('https://claude.ai/new'),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
page.evaluate.mockResolvedValue('https://claude.ai/new');
|
|
61
|
+
mockEnsureOnClaude.mockResolvedValue(false);
|
|
62
|
+
mockEnsureClaudeComposer.mockResolvedValue({ isLoggedIn: true, hasComposer: true });
|
|
63
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
64
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: true, toggled: false });
|
|
65
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
66
|
+
mockSendWithFile.mockResolvedValue({ ok: true });
|
|
67
|
+
mockGetBubbleCount.mockResolvedValue(0);
|
|
68
|
+
mockWaitForResponse.mockResolvedValue('hello there');
|
|
69
|
+
mockRequireNonEmptyPrompt.mockImplementation((v) => String(v ?? ''));
|
|
70
|
+
mockRequirePositiveInt.mockImplementation((v) => Number(v));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns the assistant response on a fresh chat', async () => {
|
|
74
|
+
const rows = await askCommand.func(page, {
|
|
75
|
+
prompt: 'hi',
|
|
76
|
+
timeout: 120,
|
|
77
|
+
new: false,
|
|
78
|
+
model: 'sonnet',
|
|
79
|
+
think: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(rows).toEqual([{ response: 'hello there' }]);
|
|
83
|
+
expect(mockSendMessage).toHaveBeenCalledWith(page, 'hi');
|
|
84
|
+
expect(mockWaitForResponse).toHaveBeenCalledWith(page, 0, 'hi', 120000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('navigates to /new when --new is set', async () => {
|
|
88
|
+
await askCommand.func(page, {
|
|
89
|
+
prompt: 'hi',
|
|
90
|
+
timeout: 120,
|
|
91
|
+
new: true,
|
|
92
|
+
model: 'sonnet',
|
|
93
|
+
think: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(page.goto).toHaveBeenCalledWith('https://claude.ai/new');
|
|
97
|
+
expect(mockEnsureOnClaude).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws EmptyResultError when waitForResponse yields nothing', async () => {
|
|
101
|
+
mockWaitForResponse.mockResolvedValue(null);
|
|
102
|
+
|
|
103
|
+
await expect(askCommand.func(page, {
|
|
104
|
+
prompt: 'hi',
|
|
105
|
+
timeout: 60,
|
|
106
|
+
new: false,
|
|
107
|
+
model: 'sonnet',
|
|
108
|
+
think: false,
|
|
109
|
+
})).rejects.toThrow(EmptyResultError);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('throws CommandExecutionError when send fails', async () => {
|
|
113
|
+
mockSendMessage.mockResolvedValue({ ok: false, reason: 'composer not found' });
|
|
114
|
+
|
|
115
|
+
await expect(askCommand.func(page, {
|
|
116
|
+
prompt: 'hi',
|
|
117
|
+
timeout: 120,
|
|
118
|
+
new: false,
|
|
119
|
+
model: 'sonnet',
|
|
120
|
+
think: false,
|
|
121
|
+
})).rejects.toThrow(/composer not found/);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('claude ask --model handling', () => {
|
|
126
|
+
const page = {
|
|
127
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
128
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
129
|
+
evaluate: vi.fn(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
mockEnsureOnClaude.mockResolvedValue(false);
|
|
135
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: true, toggled: false });
|
|
136
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
137
|
+
mockGetBubbleCount.mockResolvedValue(0);
|
|
138
|
+
mockWaitForResponse.mockResolvedValue('reply');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects --model opus on free tier with usage-error guidance', async () => {
|
|
142
|
+
page.evaluate.mockResolvedValue('https://claude.ai/new');
|
|
143
|
+
mockSelectModel.mockResolvedValue({ ok: false, upgrade: true });
|
|
144
|
+
|
|
145
|
+
await expect(askCommand.func(page, {
|
|
146
|
+
prompt: 'hi',
|
|
147
|
+
timeout: 120,
|
|
148
|
+
new: false,
|
|
149
|
+
model: 'opus',
|
|
150
|
+
think: false,
|
|
151
|
+
})).rejects.toMatchObject(new ArgumentError(
|
|
152
|
+
'opus model requires a paid Claude plan.',
|
|
153
|
+
'Pick --model sonnet or --model haiku, or upgrade your account.',
|
|
154
|
+
));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('skips model selection inside an existing conversation', async () => {
|
|
158
|
+
page.evaluate.mockResolvedValue('https://claude.ai/chat/abc-123');
|
|
159
|
+
|
|
160
|
+
const rows = await askCommand.func(page, {
|
|
161
|
+
prompt: 'continue',
|
|
162
|
+
timeout: 120,
|
|
163
|
+
new: false,
|
|
164
|
+
model: 'sonnet',
|
|
165
|
+
think: false,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(rows).toEqual([{ response: 'reply' }]);
|
|
169
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('fails fast when --model is explicit inside an existing conversation', async () => {
|
|
173
|
+
page.evaluate.mockResolvedValue('https://claude.ai/chat/abc-123');
|
|
174
|
+
|
|
175
|
+
await expect(askCommand.func(page, {
|
|
176
|
+
prompt: 'continue',
|
|
177
|
+
timeout: 120,
|
|
178
|
+
new: false,
|
|
179
|
+
model: 'opus',
|
|
180
|
+
think: false,
|
|
181
|
+
__opencliOptionSources: { model: 'cli' },
|
|
182
|
+
})).rejects.toMatchObject(new ArgumentError(
|
|
183
|
+
'Cannot switch to opus model inside an existing conversation.',
|
|
184
|
+
'Re-run with --new to start a fresh chat before selecting a model.',
|
|
185
|
+
));
|
|
186
|
+
|
|
187
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('claude ask --think', () => {
|
|
192
|
+
const page = {
|
|
193
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
194
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
195
|
+
evaluate: vi.fn().mockResolvedValue('https://claude.ai/new'),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
vi.clearAllMocks();
|
|
200
|
+
mockEnsureOnClaude.mockResolvedValue(false);
|
|
201
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
202
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
203
|
+
mockGetBubbleCount.mockResolvedValue(0);
|
|
204
|
+
mockWaitForResponse.mockResolvedValue('reply');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('toggles Adaptive thinking when --think is set', async () => {
|
|
208
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: true, toggled: true });
|
|
209
|
+
|
|
210
|
+
await askCommand.func(page, {
|
|
211
|
+
prompt: 'reason carefully',
|
|
212
|
+
timeout: 120,
|
|
213
|
+
new: false,
|
|
214
|
+
model: 'sonnet',
|
|
215
|
+
think: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(mockSetAdaptiveThinking).toHaveBeenCalledWith(page, true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('throws when --think requested but toggle fails', async () => {
|
|
222
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: false });
|
|
223
|
+
|
|
224
|
+
await expect(askCommand.func(page, {
|
|
225
|
+
prompt: 'reason carefully',
|
|
226
|
+
timeout: 120,
|
|
227
|
+
new: false,
|
|
228
|
+
model: 'sonnet',
|
|
229
|
+
think: true,
|
|
230
|
+
})).rejects.toThrow(/Adaptive thinking/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('does not throw when --think is false and toggle returns ok=false', async () => {
|
|
234
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: false });
|
|
235
|
+
|
|
236
|
+
await expect(askCommand.func(page, {
|
|
237
|
+
prompt: 'hi',
|
|
238
|
+
timeout: 120,
|
|
239
|
+
new: false,
|
|
240
|
+
model: 'sonnet',
|
|
241
|
+
think: false,
|
|
242
|
+
})).resolves.toEqual([{ response: 'reply' }]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('fails fast when prompt validation rejects an empty prompt', async () => {
|
|
246
|
+
mockRequireNonEmptyPrompt.mockImplementation(() => {
|
|
247
|
+
throw new ArgumentError('claude ask prompt cannot be empty');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await expect(askCommand.func(page, {
|
|
251
|
+
prompt: '',
|
|
252
|
+
timeout: 120,
|
|
253
|
+
new: false,
|
|
254
|
+
model: 'sonnet',
|
|
255
|
+
think: false,
|
|
256
|
+
})).rejects.toThrow(ArgumentError);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('fails fast when timeout validation rejects a non-positive value', async () => {
|
|
260
|
+
mockRequirePositiveInt.mockImplementation(() => {
|
|
261
|
+
throw new ArgumentError('claude ask --timeout must be a positive integer');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await expect(askCommand.func(page, {
|
|
265
|
+
prompt: 'hi',
|
|
266
|
+
timeout: 0,
|
|
267
|
+
new: false,
|
|
268
|
+
model: 'sonnet',
|
|
269
|
+
think: false,
|
|
270
|
+
})).rejects.toThrow(ArgumentError);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('claude ask --file', () => {
|
|
275
|
+
const page = {
|
|
276
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
277
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
278
|
+
evaluate: vi.fn().mockResolvedValue('https://claude.ai/new'),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
vi.clearAllMocks();
|
|
283
|
+
mockEnsureOnClaude.mockResolvedValue(false);
|
|
284
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
285
|
+
mockSetAdaptiveThinking.mockResolvedValue({ ok: true, toggled: false });
|
|
286
|
+
mockSendWithFile.mockResolvedValue({ ok: true });
|
|
287
|
+
mockGetBubbleCount.mockResolvedValue(3);
|
|
288
|
+
mockWaitForResponse.mockResolvedValue('the image shows a cat');
|
|
289
|
+
mockEnsureClaudeComposer.mockResolvedValue({ isLoggedIn: true, hasComposer: true });
|
|
290
|
+
mockRequireNonEmptyPrompt.mockImplementation((v) => String(v ?? ''));
|
|
291
|
+
mockRequirePositiveInt.mockImplementation((v) => Number(v));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('routes through sendWithFile and captures baseline before sending', async () => {
|
|
295
|
+
const rows = await askCommand.func(page, {
|
|
296
|
+
prompt: 'describe this',
|
|
297
|
+
timeout: 120,
|
|
298
|
+
new: false,
|
|
299
|
+
model: 'sonnet',
|
|
300
|
+
think: false,
|
|
301
|
+
file: '/tmp/cat.png',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(rows).toEqual([{ response: 'the image shows a cat' }]);
|
|
305
|
+
expect(mockGetBubbleCount).toHaveBeenCalledTimes(1);
|
|
306
|
+
expect(mockSendWithFile).toHaveBeenCalledWith(page, '/tmp/cat.png', 'describe this');
|
|
307
|
+
expect(mockSendMessage).not.toHaveBeenCalled();
|
|
308
|
+
expect(mockWaitForResponse).toHaveBeenCalledWith(page, 3, 'describe this', 120000);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('surfaces file upload failure as CommandExecutionError', async () => {
|
|
312
|
+
mockSendWithFile.mockResolvedValue({ ok: false, reason: 'file preview did not appear' });
|
|
313
|
+
|
|
314
|
+
await expect(askCommand.func(page, {
|
|
315
|
+
prompt: 'describe this',
|
|
316
|
+
timeout: 120,
|
|
317
|
+
new: false,
|
|
318
|
+
model: 'sonnet',
|
|
319
|
+
think: false,
|
|
320
|
+
file: '/tmp/cat.png',
|
|
321
|
+
})).rejects.toThrow(/file preview did not appear/);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('absorbs "Promise was collected" SPA navigation error after send', async () => {
|
|
325
|
+
mockSendWithFile.mockRejectedValue(new Error('Promise was collected'));
|
|
326
|
+
|
|
327
|
+
const rows = await askCommand.func(page, {
|
|
328
|
+
prompt: 'describe this',
|
|
329
|
+
timeout: 120,
|
|
330
|
+
new: false,
|
|
331
|
+
model: 'sonnet',
|
|
332
|
+
think: false,
|
|
333
|
+
file: '/tmp/cat.png',
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(rows).toEqual([{ response: 'the image shows a cat' }]);
|
|
337
|
+
});
|
|
338
|
+
});
|