@jackwener/opencli 1.4.1 → 1.5.0
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/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1111 -112
- package/dist/cli.js +34 -3
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +114 -18
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +119 -0
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +8 -14
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +9 -2
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +15 -32
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +21 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +5 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin.d.ts +38 -5
- package/dist/plugin.js +267 -33
- package/dist/plugin.test.js +220 -3
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.js +1 -1
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +1 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +87 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +2 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +35 -3
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +137 -0
- package/src/clis/xiaohongshu/publish.ts +129 -18
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +8 -15
- package/src/doctor.ts +13 -2
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/errors.ts +25 -66
- package/src/execution.ts +28 -3
- package/src/hooks.ts +1 -0
- package/src/main.ts +6 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin.test.ts +246 -2
- package/src/plugin.ts +338 -36
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +1 -1
- package/src/serialization.ts +4 -0
- package/src/types.ts +1 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 36kr article search — INTERCEPT strategy.
|
|
3
|
+
*
|
|
4
|
+
* Navigates to the 36kr search results page and scrapes rendered articles.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import type { IPage } from '../../types.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: '36kr',
|
|
12
|
+
name: 'search',
|
|
13
|
+
description: '搜索36氪文章',
|
|
14
|
+
domain: 'www.36kr.com',
|
|
15
|
+
strategy: Strategy.INTERCEPT,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "AI", "OpenAI")' },
|
|
18
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['rank', 'title', 'date', 'url'],
|
|
21
|
+
func: async (page: IPage, args) => {
|
|
22
|
+
const count = Math.min(Number(args.limit) || 20, 50);
|
|
23
|
+
const query = encodeURIComponent(String(args.query ?? ''));
|
|
24
|
+
|
|
25
|
+
await page.installInterceptor('36kr.com/api');
|
|
26
|
+
await page.goto(`https://www.36kr.com/search/articles/${query}`);
|
|
27
|
+
await page.wait(6);
|
|
28
|
+
|
|
29
|
+
const domItems: any = await page.evaluate(`
|
|
30
|
+
(() => {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const results = [];
|
|
33
|
+
// article-item-title contains the clickable title link
|
|
34
|
+
const titleEls = document.querySelectorAll('.article-item-title a[href*="/p/"], .article-item-title[href*="/p/"]');
|
|
35
|
+
for (const el of titleEls) {
|
|
36
|
+
const href = el.getAttribute('href') || '';
|
|
37
|
+
const title = el.textContent?.trim() || '';
|
|
38
|
+
if (!title || seen.has(href)) continue;
|
|
39
|
+
seen.add(href);
|
|
40
|
+
// Look for date near the article item
|
|
41
|
+
const item = el.closest('[class*="article-item"]') || el.parentElement;
|
|
42
|
+
const dateEl = item?.querySelector('[class*="time"], [class*="date"], time');
|
|
43
|
+
const date = dateEl?.textContent?.trim() || '';
|
|
44
|
+
results.push({
|
|
45
|
+
title,
|
|
46
|
+
url: href.startsWith('http') ? href : 'https://36kr.com' + href,
|
|
47
|
+
date,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Fallback: generic /p/ links with meaningful text
|
|
51
|
+
if (results.length === 0) {
|
|
52
|
+
const links = document.querySelectorAll('a[href*="/p/"]');
|
|
53
|
+
for (const el of links) {
|
|
54
|
+
const href = el.getAttribute('href') || '';
|
|
55
|
+
const title = el.textContent?.trim() || '';
|
|
56
|
+
if (!title || title.length < 8 || seen.has(href) || seen.has(title)) continue;
|
|
57
|
+
seen.add(href);
|
|
58
|
+
seen.add(title);
|
|
59
|
+
results.push({ title, url: href.startsWith('http') ? href : 'https://36kr.com' + href, date: '' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
})()
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
const items = Array.isArray(domItems) ? (domItems as any[]) : [];
|
|
67
|
+
if (items.length === 0) {
|
|
68
|
+
throw new CliError('NO_DATA', 'No results found', `Try a different query or check your keyword`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return items.slice(0, count).map((item: any, i: number) => ({
|
|
72
|
+
rank: i + 1,
|
|
73
|
+
title: item.title,
|
|
74
|
+
date: item.date,
|
|
75
|
+
url: item.url,
|
|
76
|
+
}));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockApiGet } = vi.hoisted(() => ({
|
|
4
|
+
mockApiGet: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('./utils.js', () => ({
|
|
8
|
+
apiGet: mockApiGet,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { getRegistry } from '../../registry.js';
|
|
12
|
+
import './comments.js';
|
|
13
|
+
|
|
14
|
+
describe('bilibili comments', () => {
|
|
15
|
+
const command = getRegistry().get('bilibili/comments');
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockApiGet.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('resolves bvid to aid and fetches replies', async () => {
|
|
22
|
+
mockApiGet
|
|
23
|
+
.mockResolvedValueOnce({ data: { aid: 12345 } }) // view endpoint
|
|
24
|
+
.mockResolvedValueOnce({
|
|
25
|
+
data: {
|
|
26
|
+
replies: [
|
|
27
|
+
{
|
|
28
|
+
member: { uname: 'Alice' },
|
|
29
|
+
content: { message: 'Great video!' },
|
|
30
|
+
like: 42,
|
|
31
|
+
rcount: 3,
|
|
32
|
+
ctime: 1700000000,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await command!.func!({} as any, { bvid: 'BV1WtAGzYEBm', limit: 5 });
|
|
39
|
+
|
|
40
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(1, {}, '/x/web-interface/view', { params: { bvid: 'BV1WtAGzYEBm' } });
|
|
41
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(2, {}, '/x/v2/reply/main', {
|
|
42
|
+
params: { oid: 12345, type: 1, mode: 3, ps: 5 },
|
|
43
|
+
signed: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual([
|
|
47
|
+
{
|
|
48
|
+
rank: 1,
|
|
49
|
+
author: 'Alice',
|
|
50
|
+
text: 'Great video!',
|
|
51
|
+
likes: 42,
|
|
52
|
+
replies: 3,
|
|
53
|
+
time: new Date(1700000000 * 1000).toISOString().slice(0, 16).replace('T', ' '),
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('throws when aid cannot be resolved', async () => {
|
|
59
|
+
mockApiGet.mockResolvedValueOnce({ data: {} }); // no aid
|
|
60
|
+
|
|
61
|
+
await expect(command!.func!({} as any, { bvid: 'BV_invalid', limit: 5 })).rejects.toThrow(
|
|
62
|
+
'Cannot resolve aid for bvid: BV_invalid',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns empty array when replies is missing', async () => {
|
|
67
|
+
mockApiGet
|
|
68
|
+
.mockResolvedValueOnce({ data: { aid: 99 } })
|
|
69
|
+
.mockResolvedValueOnce({ data: {} }); // no replies key
|
|
70
|
+
|
|
71
|
+
const result = await command!.func!({} as any, { bvid: 'BV1xxx', limit: 5 });
|
|
72
|
+
expect(result).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('caps limit at 50', async () => {
|
|
76
|
+
mockApiGet
|
|
77
|
+
.mockResolvedValueOnce({ data: { aid: 1 } })
|
|
78
|
+
.mockResolvedValueOnce({ data: { replies: [] } });
|
|
79
|
+
|
|
80
|
+
await command!.func!({} as any, { bvid: 'BV1xxx', limit: 999 });
|
|
81
|
+
|
|
82
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(2, {}, '/x/v2/reply/main', {
|
|
83
|
+
params: { oid: 1, type: 1, mode: 3, ps: 50 },
|
|
84
|
+
signed: true,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('collapses newlines in comment text', async () => {
|
|
89
|
+
mockApiGet
|
|
90
|
+
.mockResolvedValueOnce({ data: { aid: 1 } })
|
|
91
|
+
.mockResolvedValueOnce({
|
|
92
|
+
data: {
|
|
93
|
+
replies: [
|
|
94
|
+
{ member: { uname: 'Bob' }, content: { message: 'line1\nline2\nline3' }, like: 0, rcount: 0, ctime: 0 },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = (await command!.func!({} as any, { bvid: 'BV1xxx', limit: 5 })) as any[];
|
|
100
|
+
expect(result[0].text).toBe('line1 line2 line3');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bilibili comments — fetches top-level replies via the official API with WBI signing.
|
|
3
|
+
* Uses the /x/v2/reply/main endpoint which is stable and doesn't depend on DOM structure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { apiGet } from './utils.js';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'bilibili',
|
|
11
|
+
name: 'comments',
|
|
12
|
+
description: '获取 B站视频评论(使用官方 API + WBI 签名)',
|
|
13
|
+
domain: 'www.bilibili.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'bvid', required: true, positional: true, help: 'Video BV ID (e.g. BV1WtAGzYEBm)' },
|
|
17
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of comments (max 50)' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const bvid = String(kwargs.bvid).trim();
|
|
22
|
+
const limit = Math.min(Number(kwargs.limit) || 20, 50);
|
|
23
|
+
|
|
24
|
+
// Resolve bvid → aid (required by reply API)
|
|
25
|
+
const view = await apiGet(page, '/x/web-interface/view', { params: { bvid } });
|
|
26
|
+
const aid = view?.data?.aid;
|
|
27
|
+
if (!aid) throw new Error(`Cannot resolve aid for bvid: ${bvid}`);
|
|
28
|
+
|
|
29
|
+
const payload = await apiGet(page, '/x/v2/reply/main', {
|
|
30
|
+
params: { oid: aid, type: 1, mode: 3, ps: limit },
|
|
31
|
+
signed: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const replies: any[] = payload?.data?.replies ?? [];
|
|
35
|
+
return replies.slice(0, limit).map((r: any, i: number) => ({
|
|
36
|
+
rank: i + 1,
|
|
37
|
+
author: r.member?.uname ?? '',
|
|
38
|
+
text: (r.content?.message ?? '').replace(/\n/g, ' ').trim(),
|
|
39
|
+
likes: r.like ?? 0,
|
|
40
|
+
replies: r.rcount ?? 0,
|
|
41
|
+
time: new Date(r.ctime * 1000).toISOString().slice(0, 16).replace('T', ' '),
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
});
|
package/src/clis/chatgpt/ask.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
3
|
import { ConfigError } from '../../errors.js';
|
|
4
4
|
import type { IPage } from '../../types.js';
|
|
5
|
-
import { getVisibleChatMessages } from './ax.js';
|
|
5
|
+
import { activateChatGPT, getVisibleChatMessages, selectModel, MODEL_CHOICES, isGenerating } from './ax.js';
|
|
6
6
|
|
|
7
7
|
export const askCommand = cli({
|
|
8
8
|
site: 'chatgpt',
|
|
@@ -13,6 +13,7 @@ export const askCommand = cli({
|
|
|
13
13
|
browser: false,
|
|
14
14
|
args: [
|
|
15
15
|
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
16
|
+
{ name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
|
|
16
17
|
{ name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
|
|
17
18
|
],
|
|
18
19
|
columns: ['Role', 'Text'],
|
|
@@ -22,8 +23,15 @@ export const askCommand = cli({
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const text = kwargs.text as string;
|
|
26
|
+
const model = kwargs.model as string | undefined;
|
|
25
27
|
const timeout = parseInt(kwargs.timeout as string, 10) || 30;
|
|
26
28
|
|
|
29
|
+
// Switch model before sending if requested
|
|
30
|
+
if (model) {
|
|
31
|
+
activateChatGPT();
|
|
32
|
+
selectModel(model);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
// Backup clipboard
|
|
28
36
|
let clipBackup = '';
|
|
29
37
|
try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
|
|
@@ -31,8 +39,7 @@ export const askCommand = cli({
|
|
|
31
39
|
|
|
32
40
|
// Send the message
|
|
33
41
|
spawnSync('pbcopy', { input: text });
|
|
34
|
-
|
|
35
|
-
execSync("osascript -e 'delay 0.5'");
|
|
42
|
+
activateChatGPT();
|
|
36
43
|
|
|
37
44
|
const cmd = "osascript " +
|
|
38
45
|
"-e 'tell application \"System Events\"' " +
|
|
@@ -45,25 +52,32 @@ export const askCommand = cli({
|
|
|
45
52
|
// Restore clipboard after the prompt is sent.
|
|
46
53
|
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
|
|
47
54
|
|
|
48
|
-
// Wait for response
|
|
49
|
-
|
|
55
|
+
// Wait for response: poll until ChatGPT stops generating ("Stop generating" button disappears),
|
|
56
|
+
// then read the final response text.
|
|
57
|
+
const pollInterval = 2;
|
|
50
58
|
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
51
59
|
let response = '';
|
|
60
|
+
let generationStarted = false;
|
|
52
61
|
|
|
53
62
|
for (let i = 0; i < maxPolls; i++) {
|
|
54
63
|
execSync(`sleep ${pollInterval}`);
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
const generating = isGenerating();
|
|
65
|
+
if (generating) {
|
|
66
|
+
generationStarted = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Generation finished (or never started yet)
|
|
70
|
+
if (!generationStarted && i < 3) continue; // give it a moment to start
|
|
57
71
|
|
|
72
|
+
// Read final response
|
|
73
|
+
activateChatGPT(0.3);
|
|
58
74
|
const messagesNow = getVisibleChatMessages();
|
|
59
|
-
if (messagesNow.length
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (candidate) {
|
|
64
|
-
response = candidate;
|
|
65
|
-
break;
|
|
75
|
+
if (messagesNow.length > messagesBefore.length) {
|
|
76
|
+
const newMessages = messagesNow.slice(messagesBefore.length);
|
|
77
|
+
const candidate = [...newMessages].reverse().find((message) => message !== text);
|
|
78
|
+
if (candidate) response = candidate;
|
|
66
79
|
}
|
|
80
|
+
break;
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
if (!response) {
|
package/src/clis/chatgpt/ax.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
const AX_READ_SCRIPT = `
|
|
4
4
|
import Cocoa
|
|
@@ -62,6 +62,185 @@ let data = try! JSONSerialization.data(withJSONObject: best, options: [])
|
|
|
62
62
|
print(String(data: data, encoding: .utf8)!)
|
|
63
63
|
`;
|
|
64
64
|
|
|
65
|
+
const AX_MODEL_SCRIPT = `
|
|
66
|
+
import Cocoa
|
|
67
|
+
import ApplicationServices
|
|
68
|
+
|
|
69
|
+
func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
|
|
70
|
+
var value: CFTypeRef?
|
|
71
|
+
guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
|
|
72
|
+
return value as AnyObject?
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func s(_ el: AXUIElement, _ name: String) -> String? {
|
|
76
|
+
if let v = attr(el, name) as? String, !v.isEmpty { return v }
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func children(_ el: AXUIElement) -> [AXUIElement] {
|
|
81
|
+
(attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func press(_ el: AXUIElement) {
|
|
85
|
+
AXUIElementPerformAction(el, kAXPressAction as CFString)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func findByDesc(_ el: AXUIElement, _ target: String, prefix: Bool = false, depth: Int = 0) -> AXUIElement? {
|
|
89
|
+
guard depth < 20 else { return nil }
|
|
90
|
+
let desc = s(el, kAXDescriptionAttribute as String) ?? ""
|
|
91
|
+
if prefix ? desc.hasPrefix(target) : (desc == target) { return el }
|
|
92
|
+
for c in children(el) {
|
|
93
|
+
if let found = findByDesc(c, target, prefix: prefix, depth: depth + 1) { return found }
|
|
94
|
+
}
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func findPopover(_ el: AXUIElement, depth: Int = 0) -> AXUIElement? {
|
|
99
|
+
guard depth < 20 else { return nil }
|
|
100
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
101
|
+
if role == "AXPopover" { return el }
|
|
102
|
+
for c in children(el) {
|
|
103
|
+
if let found = findPopover(c, depth: depth + 1) { return found }
|
|
104
|
+
}
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func pressEscape() {
|
|
109
|
+
let src = CGEventSource(stateID: .combinedSessionState)
|
|
110
|
+
if let esc = CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: true) { esc.post(tap: .cghidEventTap) }
|
|
111
|
+
if let esc = CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: false) { esc.post(tap: .cghidEventTap) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
|
|
115
|
+
fputs("ChatGPT not running\\n", stderr); exit(1)
|
|
116
|
+
}
|
|
117
|
+
let axApp = AXUIElementCreateApplication(app.processIdentifier)
|
|
118
|
+
guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
|
|
119
|
+
fputs("No focused ChatGPT window\\n", stderr); exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let args = CommandLine.arguments
|
|
123
|
+
let target = args.count > 1 ? args[1] : ""
|
|
124
|
+
let needsLegacy = args.count > 2 && args[2] == "legacy"
|
|
125
|
+
|
|
126
|
+
// Step 1: Click the "Options" button to open the popover
|
|
127
|
+
guard let optionsBtn = findByDesc(win, "Options") else {
|
|
128
|
+
fputs("Could not find Options button\\n", stderr); exit(1)
|
|
129
|
+
}
|
|
130
|
+
press(optionsBtn)
|
|
131
|
+
Thread.sleep(forTimeInterval: 0.8)
|
|
132
|
+
|
|
133
|
+
// Step 2: Find the popover that appeared, search ONLY within it
|
|
134
|
+
guard let popover = findPopover(win) else {
|
|
135
|
+
pressEscape()
|
|
136
|
+
fputs("Popover did not appear\\n", stderr); exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 3: If legacy, click "Legacy models" to expand submenu
|
|
140
|
+
if needsLegacy {
|
|
141
|
+
guard let legacyBtn = findByDesc(popover, "Legacy models") else {
|
|
142
|
+
pressEscape()
|
|
143
|
+
fputs("Could not find Legacy models button\\n", stderr); exit(1)
|
|
144
|
+
}
|
|
145
|
+
press(legacyBtn)
|
|
146
|
+
Thread.sleep(forTimeInterval: 0.8)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step 4: Click the target model button within the popover (prefix match)
|
|
150
|
+
guard let modelBtn = findByDesc(popover, target, prefix: true) else {
|
|
151
|
+
pressEscape()
|
|
152
|
+
fputs("Could not find button starting with '\\(target)' in popover\\n", stderr); exit(1)
|
|
153
|
+
}
|
|
154
|
+
press(modelBtn)
|
|
155
|
+
print("Selected: \\(target)")
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const AX_GENERATING_SCRIPT = `
|
|
159
|
+
import Cocoa
|
|
160
|
+
import ApplicationServices
|
|
161
|
+
|
|
162
|
+
func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
|
|
163
|
+
var value: CFTypeRef?
|
|
164
|
+
guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
|
|
165
|
+
return value as AnyObject?
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func s(_ el: AXUIElement, _ name: String) -> String? {
|
|
169
|
+
if let v = attr(el, name) as? String, !v.isEmpty { return v }
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func children(_ el: AXUIElement) -> [AXUIElement] {
|
|
174
|
+
(attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func hasButton(_ el: AXUIElement, desc target: String, depth: Int = 0) -> Bool {
|
|
178
|
+
guard depth < 15 else { return false }
|
|
179
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
180
|
+
let desc = s(el, kAXDescriptionAttribute as String) ?? ""
|
|
181
|
+
if role == "AXButton" && desc == target { return true }
|
|
182
|
+
for c in children(el) {
|
|
183
|
+
if hasButton(c, desc: target, depth: depth + 1) { return true }
|
|
184
|
+
}
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
|
|
189
|
+
print("false"); exit(0)
|
|
190
|
+
}
|
|
191
|
+
let axApp = AXUIElementCreateApplication(app.processIdentifier)
|
|
192
|
+
guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
|
|
193
|
+
print("false"); exit(0)
|
|
194
|
+
}
|
|
195
|
+
print(hasButton(win, desc: "Stop generating") ? "true" : "false")
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
type ModelChoice = 'auto' | 'instant' | 'thinking' | '5.2-instant' | '5.2-thinking';
|
|
199
|
+
|
|
200
|
+
const MODEL_MAP: Record<ModelChoice, { desc: string; legacy?: boolean }> = {
|
|
201
|
+
'auto': { desc: 'Auto' },
|
|
202
|
+
'instant': { desc: 'Instant' },
|
|
203
|
+
'thinking': { desc: 'Thinking' },
|
|
204
|
+
'5.2-instant': { desc: 'GPT-5.2 Instant', legacy: true },
|
|
205
|
+
'5.2-thinking': { desc: 'GPT-5.2 Thinking', legacy: true },
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const MODEL_CHOICES = Object.keys(MODEL_MAP) as ModelChoice[];
|
|
209
|
+
|
|
210
|
+
export function activateChatGPT(delaySeconds: number = 0.5): void {
|
|
211
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
212
|
+
execSync(`osascript -e 'delay ${delaySeconds}'`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function selectModel(model: string): string {
|
|
216
|
+
const entry = MODEL_MAP[model as ModelChoice];
|
|
217
|
+
if (!entry) {
|
|
218
|
+
throw new Error(`Unknown model "${model}". Choose from: ${MODEL_CHOICES.join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
const swiftArgs = ['-', entry.desc];
|
|
221
|
+
if (entry.legacy) swiftArgs.push('legacy');
|
|
222
|
+
|
|
223
|
+
const output = execFileSync('swift', swiftArgs, {
|
|
224
|
+
input: AX_MODEL_SCRIPT,
|
|
225
|
+
encoding: 'utf-8',
|
|
226
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
227
|
+
}).trim();
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function isGenerating(): boolean {
|
|
232
|
+
try {
|
|
233
|
+
const output = execFileSync('swift', ['-'], {
|
|
234
|
+
input: AX_GENERATING_SCRIPT,
|
|
235
|
+
encoding: 'utf-8',
|
|
236
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
237
|
+
}).trim();
|
|
238
|
+
return output === 'true';
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
65
244
|
export function getVisibleChatMessages(): string[] {
|
|
66
245
|
const output = execFileSync('swift', ['-'], {
|
|
67
246
|
input: AX_READ_SCRIPT,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { ConfigError } from '../../errors.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
import { activateChatGPT, selectModel, MODEL_CHOICES } from './ax.js';
|
|
5
|
+
|
|
6
|
+
export const modelCommand = cli({
|
|
7
|
+
site: 'chatgpt',
|
|
8
|
+
name: 'model',
|
|
9
|
+
description: 'Switch ChatGPT Desktop model/mode (auto, instant, thinking, 5.2-instant, 5.2-thinking)',
|
|
10
|
+
domain: 'localhost',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'model', required: true, positional: true, help: 'Model to switch to', choices: MODEL_CHOICES },
|
|
15
|
+
],
|
|
16
|
+
columns: ['Status', 'Model'],
|
|
17
|
+
func: async (page: IPage | null, kwargs: any) => {
|
|
18
|
+
if (process.platform !== 'darwin') {
|
|
19
|
+
throw new ConfigError('ChatGPT Desktop integration requires macOS');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const model = kwargs.model as string;
|
|
23
|
+
activateChatGPT();
|
|
24
|
+
const result = selectModel(model);
|
|
25
|
+
return [{ Status: 'Success', Model: result }];
|
|
26
|
+
},
|
|
27
|
+
});
|
package/src/clis/chatgpt/send.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
3
|
import type { IPage } from '../../types.js';
|
|
4
4
|
import { getErrorMessage } from '../../errors.js';
|
|
5
|
+
import { activateChatGPT, selectModel, MODEL_CHOICES } from './ax.js';
|
|
5
6
|
|
|
6
7
|
export const sendCommand = cli({
|
|
7
8
|
site: 'chatgpt',
|
|
@@ -10,11 +11,21 @@ export const sendCommand = cli({
|
|
|
10
11
|
domain: 'localhost',
|
|
11
12
|
strategy: Strategy.PUBLIC,
|
|
12
13
|
browser: false,
|
|
13
|
-
args: [
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'text', required: true, positional: true, help: 'Message to send' },
|
|
16
|
+
{ name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
|
|
17
|
+
],
|
|
14
18
|
columns: ['Status'],
|
|
15
19
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
20
|
const text = kwargs.text as string;
|
|
21
|
+
const model = kwargs.model as string | undefined;
|
|
17
22
|
try {
|
|
23
|
+
// Switch model before sending if requested
|
|
24
|
+
if (model) {
|
|
25
|
+
activateChatGPT();
|
|
26
|
+
selectModel(model);
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
// Backup current clipboard content
|
|
19
30
|
let clipBackup = '';
|
|
20
31
|
try {
|
|
@@ -23,17 +34,16 @@ export const sendCommand = cli({
|
|
|
23
34
|
|
|
24
35
|
// Copy text to clipboard
|
|
25
36
|
spawnSync('pbcopy', { input: text });
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
|
|
38
|
+
activateChatGPT();
|
|
39
|
+
|
|
30
40
|
const cmd = "osascript " +
|
|
31
41
|
"-e 'tell application \"System Events\"' " +
|
|
32
42
|
"-e 'keystroke \"v\" using command down' " +
|
|
33
43
|
"-e 'delay 0.2' " +
|
|
34
44
|
"-e 'keystroke return' " +
|
|
35
45
|
"-e 'end tell'";
|
|
36
|
-
|
|
46
|
+
|
|
37
47
|
execSync(cmd);
|
|
38
48
|
|
|
39
49
|
// Restore original clipboard content
|