@jackwener/opencli 1.7.2 → 1.7.4

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.
Files changed (144) hide show
  1. package/README.md +18 -15
  2. package/README.zh-CN.md +31 -15
  3. package/cli-manifest.json +1265 -101
  4. package/clis/barchart/flow.js +1 -1
  5. package/clis/barchart/greeks.js +2 -2
  6. package/clis/barchart/options.js +2 -2
  7. package/clis/barchart/quote.js +1 -1
  8. package/clis/bilibili/favorite.js +18 -13
  9. package/clis/bilibili/feed.js +202 -48
  10. package/clis/binance/depth.js +3 -4
  11. package/clis/boss/utils.js +2 -2
  12. package/clis/chatgpt/image.js +97 -0
  13. package/clis/chatgpt/utils.js +297 -0
  14. package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
  15. package/clis/{chatgpt → chatgpt-app}/ax.js +6 -3
  16. package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
  17. package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
  18. package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
  19. package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
  20. package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
  21. package/clis/discord-app/delete.js +114 -0
  22. package/clis/douban/search.js +1 -0
  23. package/clis/douban/search.test.js +11 -0
  24. package/clis/douban/subject.js +20 -93
  25. package/clis/douban/subject.test.js +11 -0
  26. package/clis/douban/utils.js +279 -10
  27. package/clis/douban/utils.test.js +296 -1
  28. package/clis/doubao/utils.js +319 -130
  29. package/clis/doubao/utils.test.js +241 -2
  30. package/clis/eastmoney/hot-rank.js +50 -0
  31. package/clis/eastmoney/hot-rank.test.js +59 -0
  32. package/clis/grok/image.test.ts +107 -0
  33. package/clis/grok/image.ts +356 -0
  34. package/clis/ke/chengjiao.js +77 -0
  35. package/clis/ke/ershoufang.js +100 -0
  36. package/clis/ke/utils.js +104 -0
  37. package/clis/ke/xiaoqu.js +77 -0
  38. package/clis/ke/zufang.js +94 -0
  39. package/clis/maimai/search-talents.js +172 -0
  40. package/clis/mubu/doc.js +40 -0
  41. package/clis/mubu/docs.js +43 -0
  42. package/clis/mubu/notes.js +244 -0
  43. package/clis/mubu/recent.js +27 -0
  44. package/clis/mubu/search.js +62 -0
  45. package/clis/mubu/utils.js +304 -0
  46. package/clis/reuters/search.js +1 -1
  47. package/clis/tdx/hot-rank.js +47 -0
  48. package/clis/tdx/hot-rank.test.js +59 -0
  49. package/clis/ths/hot-rank.js +49 -0
  50. package/clis/ths/hot-rank.test.js +64 -0
  51. package/clis/twitter/bookmarks.js +2 -1
  52. package/clis/uiverse/_shared.js +368 -0
  53. package/clis/uiverse/_shared.test.js +55 -0
  54. package/clis/uiverse/code.js +47 -0
  55. package/clis/uiverse/preview.js +71 -0
  56. package/clis/xiaohongshu/comments.js +20 -8
  57. package/clis/xiaohongshu/comments.test.js +69 -12
  58. package/clis/xiaohongshu/creator-note-detail.js +2 -0
  59. package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
  60. package/clis/xiaohongshu/creator-notes-summary.js +4 -0
  61. package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
  62. package/clis/xiaohongshu/creator-notes.js +1 -0
  63. package/clis/xiaohongshu/creator-profile.js +1 -0
  64. package/clis/xiaohongshu/creator-stats.js +1 -0
  65. package/clis/xiaohongshu/download.js +18 -7
  66. package/clis/xiaohongshu/download.test.js +42 -0
  67. package/clis/xiaohongshu/navigation.test.js +34 -0
  68. package/clis/xiaohongshu/note-helpers.js +46 -12
  69. package/clis/xiaohongshu/note.js +17 -10
  70. package/clis/xiaohongshu/note.test.js +66 -11
  71. package/clis/xiaohongshu/publish.js +1 -0
  72. package/clis/xiaohongshu/search.js +1 -0
  73. package/clis/xiaohongshu/user.js +1 -0
  74. package/clis/xiaoyuzhou/auth.js +303 -0
  75. package/clis/xiaoyuzhou/auth.test.js +124 -0
  76. package/clis/xiaoyuzhou/download.js +49 -0
  77. package/clis/xiaoyuzhou/download.test.js +125 -0
  78. package/clis/xiaoyuzhou/transcript.js +76 -0
  79. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  80. package/clis/yahoo-finance/quote.js +1 -1
  81. package/clis/youtube/feed.js +120 -0
  82. package/clis/youtube/history.js +118 -0
  83. package/clis/youtube/like.js +62 -0
  84. package/clis/youtube/playlist.js +97 -0
  85. package/clis/youtube/subscribe.js +71 -0
  86. package/clis/youtube/subscriptions.js +57 -0
  87. package/clis/youtube/unlike.js +62 -0
  88. package/clis/youtube/unsubscribe.js +71 -0
  89. package/clis/youtube/utils.js +122 -0
  90. package/clis/youtube/utils.test.js +32 -1
  91. package/clis/youtube/watch-later.js +76 -0
  92. package/dist/src/browser/base-page.d.ts +9 -0
  93. package/dist/src/browser/base-page.js +44 -5
  94. package/dist/src/browser/bridge.d.ts +2 -0
  95. package/dist/src/browser/bridge.js +51 -14
  96. package/dist/src/browser/cdp.js +11 -2
  97. package/dist/src/browser/daemon-client.d.ts +2 -0
  98. package/dist/src/browser/dom-snapshot.js +13 -1
  99. package/dist/src/browser/page.d.ts +4 -1
  100. package/dist/src/browser/page.js +48 -8
  101. package/dist/src/browser/page.test.js +61 -1
  102. package/dist/src/browser/target-errors.d.ts +23 -0
  103. package/dist/src/browser/target-errors.js +29 -0
  104. package/dist/src/browser/target-errors.test.d.ts +1 -0
  105. package/dist/src/browser/target-errors.test.js +61 -0
  106. package/dist/src/browser/target-resolver.d.ts +57 -0
  107. package/dist/src/browser/target-resolver.js +298 -0
  108. package/dist/src/browser/target-resolver.test.d.ts +1 -0
  109. package/dist/src/browser/target-resolver.test.js +43 -0
  110. package/dist/src/browser.test.js +38 -1
  111. package/dist/src/cli.js +45 -35
  112. package/dist/src/commands/daemon.d.ts +4 -2
  113. package/dist/src/commands/daemon.js +22 -2
  114. package/dist/src/commands/daemon.test.js +65 -2
  115. package/dist/src/daemon.js +7 -0
  116. package/dist/src/doctor.d.ts +2 -0
  117. package/dist/src/doctor.js +82 -10
  118. package/dist/src/doctor.test.js +28 -12
  119. package/dist/src/electron-apps.js +1 -1
  120. package/dist/src/errors.d.ts +1 -0
  121. package/dist/src/errors.js +13 -0
  122. package/dist/src/execution.js +36 -9
  123. package/dist/src/execution.test.js +23 -0
  124. package/dist/src/external-clis.yaml +2 -2
  125. package/dist/src/logger.d.ts +2 -2
  126. package/dist/src/logger.js +3 -8
  127. package/dist/src/output.js +1 -5
  128. package/dist/src/output.test.js +0 -21
  129. package/dist/src/pipeline/steps/transform.js +1 -1
  130. package/dist/src/pipeline/template.d.ts +1 -0
  131. package/dist/src/pipeline/template.js +11 -3
  132. package/dist/src/pipeline/template.test.js +3 -0
  133. package/dist/src/pipeline/transform.test.js +14 -0
  134. package/dist/src/plugin.d.ts +7 -1
  135. package/dist/src/plugin.js +23 -1
  136. package/dist/src/plugin.test.js +15 -1
  137. package/dist/src/registry.js +3 -4
  138. package/dist/src/types.d.ts +3 -1
  139. package/dist/src/update-check.d.ts +14 -0
  140. package/dist/src/update-check.js +48 -3
  141. package/dist/src/update-check.test.d.ts +1 -0
  142. package/dist/src/update-check.test.js +31 -0
  143. package/package.json +1 -1
  144. package/scripts/fetch-adapters.js +35 -8
@@ -26,7 +26,7 @@ cli({
26
26
  const data = await page.evaluate(`
27
27
  (async () => {
28
28
  const limit = ${limit};
29
- const typeFilter = '${optionType}'.toLowerCase();
29
+ const typeFilter = ${JSON.stringify(optionType)}.toLowerCase();
30
30
 
31
31
  // Wait for CSRF token to appear (Angular may inject it after initial render)
32
32
  let csrf = '';
@@ -27,8 +27,8 @@ cli({
27
27
  await page.wait(4);
28
28
  const data = await page.evaluate(`
29
29
  (async () => {
30
- const sym = '${symbol}';
31
- const expDate = '${expiration}';
30
+ const sym = ${JSON.stringify(symbol)};
31
+ const expDate = ${JSON.stringify(expiration)};
32
32
  const limit = ${limit};
33
33
  const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
34
34
  const headers = { 'X-CSRF-TOKEN': csrf };
@@ -26,8 +26,8 @@ cli({
26
26
  await page.wait(4);
27
27
  const data = await page.evaluate(`
28
28
  (async () => {
29
- const sym = '${symbol}';
30
- const type = '${optType}';
29
+ const sym = ${JSON.stringify(symbol)};
30
+ const type = ${JSON.stringify(optType)};
31
31
  const limit = ${limit};
32
32
  const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
33
33
  const headers = { 'X-CSRF-TOKEN': csrf };
@@ -24,7 +24,7 @@ cli({
24
24
  await page.wait(4);
25
25
  const data = await page.evaluate(`
26
26
  (async () => {
27
- const sym = '${symbol}';
27
+ const sym = ${JSON.stringify(symbol)};
28
28
  const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
29
29
 
30
30
  // Strategy 1: internal proxy API with CSRF token
@@ -3,27 +3,32 @@ import { apiGet, payloadData, getSelfUid } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'favorite',
6
- description: '我的默认收藏夹',
6
+ description: '我的收藏夹',
7
7
  domain: 'www.bilibili.com',
8
8
  strategy: Strategy.COOKIE,
9
9
  args: [
10
+ { name: 'fid', type: 'int', required: false, help: 'Favorite folder ID (defaults to first folder)' },
10
11
  { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
11
12
  { name: 'page', type: 'int', default: 1, help: 'Page number' },
12
13
  ],
13
14
  columns: ['rank', 'title', 'author', 'plays', 'url'],
14
15
  func: async (page, kwargs) => {
15
- const { limit = 20, page: pageNum = 1 } = kwargs;
16
- // Get current user's UID
17
- const uid = await getSelfUid(page);
18
- // Get default favorite folder ID
19
- const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
20
- params: { up_mid: uid },
21
- signed: true,
22
- });
23
- const folders = payloadData(foldersPayload)?.list ?? [];
24
- if (!folders.length)
25
- return [];
26
- const fid = folders[0].id;
16
+ const { fid: favoriteId, limit = 20, page: pageNum = 1 } = kwargs;
17
+ let fid;
18
+ if (favoriteId) {
19
+ fid = Number(favoriteId);
20
+ } else {
21
+ // Fall back to the default (first) favorite folder
22
+ const uid = await getSelfUid(page);
23
+ const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
24
+ params: { up_mid: uid },
25
+ signed: true,
26
+ });
27
+ const folders = payloadData(foldersPayload)?.list ?? [];
28
+ if (!folders.length)
29
+ return [];
30
+ fid = folders[0].id;
31
+ }
27
32
  // Fetch favorite items
28
33
  const payload = await apiGet(page, '/x/v3/fav/resource/list', {
29
34
  params: { media_id: fid, pn: pageNum, ps: Math.min(Number(limit), 40) },
@@ -1,64 +1,218 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { apiGet, payloadData, stripHtml } from './utils.js';
2
+ import { apiGet, payloadData, resolveUid, stripHtml } from './utils.js';
3
+
4
+ /** Map bilibili dynamic type to readable short name */
5
+ const TYPE_MAP = {
6
+ DYNAMIC_TYPE_AV: 'video',
7
+ DYNAMIC_TYPE_DRAW: 'draw',
8
+ DYNAMIC_TYPE_ARTICLE: 'article',
9
+ DYNAMIC_TYPE_FORWARD: 'forward',
10
+ DYNAMIC_TYPE_WORD: 'text',
11
+ DYNAMIC_TYPE_LIVE_RCMD: 'live',
12
+ DYNAMIC_TYPE_PGC: 'bangumi',
13
+ };
14
+
15
+ function parseItem(item) {
16
+ const modules = item.modules ?? {};
17
+ const authorModule = modules.module_author ?? {};
18
+ const dynamicModule = modules.module_dynamic ?? {};
19
+ const major = dynamicModule.major ?? {};
20
+ const stat = modules.module_stat ?? {};
21
+
22
+ let title = '';
23
+ let url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
24
+ const itemType = TYPE_MAP[item.type] ?? item.type ?? '';
25
+
26
+ // video
27
+ if (major.archive) {
28
+ title = major.archive.title ?? '';
29
+ url = major.archive.jump_url ? `https:${major.archive.jump_url}` : url;
30
+ }
31
+ // article
32
+ if (!title && major.article) {
33
+ title = major.article.title ?? '';
34
+ url = major.article.jump_url ? `https:${major.article.jump_url}` : url;
35
+ }
36
+ // text content in desc
37
+ if (!title && dynamicModule.desc?.text) {
38
+ title = stripHtml(dynamicModule.desc.text).slice(0, 60);
39
+ }
40
+ // draw (图文) — use opus or draw items count as hint
41
+ if (!title && major.draw) {
42
+ const imgCount = major.draw.items?.length ?? 0;
43
+ title = imgCount > 0 ? `[图片x${imgCount}]` : '[图文动态]';
44
+ }
45
+ // VIP only content
46
+ if (!title && item.basic?.is_only_fans) {
47
+ title = '[充电专属]';
48
+ }
49
+ // forward
50
+ if (!title && item.type === 'DYNAMIC_TYPE_FORWARD') {
51
+ title = '[转发动态]';
52
+ }
53
+ // final fallback
54
+ if (!title) {
55
+ title = `[${itemType || '动态'}]`;
56
+ }
57
+
58
+ const time = authorModule.pub_time ?? '';
59
+ const likes = stat.like?.count ?? 0;
60
+ const comments = stat.comment?.count ?? 0;
61
+
62
+ return { title, url, itemType, author: authorModule.name ?? '', time, likes, comments };
63
+ }
64
+
3
65
  cli({
4
66
  site: 'bilibili',
5
67
  name: 'feed',
6
- description: '关注的人的动态时间线',
68
+ description: '动态时间线(不传 uid 查关注时间线,传 uid 查指定用户动态)',
7
69
  domain: 'www.bilibili.com',
8
70
  strategy: Strategy.COOKIE,
9
71
  args: [
10
- { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
11
- { name: 'type', default: 'all', help: 'Filter: all, video, article' },
72
+ { name: 'uid', positional: true, required: false, help: '用户 UID 或用户名(不传则显示关注时间线)' },
73
+ { name: 'limit', type: 'int', default: 20, help: 'Max results to return' },
74
+ { name: 'type', default: 'all', help: 'Filter: all, video, article, draw, text' },
75
+ { name: 'pages', type: 'int', default: 1, help: 'Number of pages to fetch (each ~20 items)' },
12
76
  ],
13
- columns: ['rank', 'author', 'title', 'type', 'url'],
77
+ columns: ['rank', 'time', 'author', 'title', 'type', 'likes', 'url'],
14
78
  func: async (page, kwargs) => {
15
- const { limit = 20, type = 'all' } = kwargs;
16
- const typeMap = { all: 'all', video: 'video', article: 'article' };
17
- const updateBaseline = '';
18
- const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', {
19
- params: {
20
- timezone_offset: -480,
21
- type: typeMap[type] ?? 'all',
22
- page: 1,
23
- ...(updateBaseline ? { update_baseline: updateBaseline } : {}),
24
- },
25
- });
26
- const items = payloadData(payload)?.items ?? [];
79
+ const maxResults = Number(kwargs.limit) || 20;
80
+ const maxPages = Number(kwargs.pages) || 1;
81
+ const filterType = kwargs.type === 'all' ? '' : (kwargs.type ?? '');
82
+
83
+ const isUserFeed = !!kwargs.uid;
84
+ const uid = isUserFeed ? await resolveUid(page, String(kwargs.uid)) : null;
85
+
27
86
  const rows = [];
28
- for (let i = 0; i < Math.min(items.length, Number(limit)); i++) {
29
- const item = items[i];
30
- const modules = item.modules ?? {};
31
- const authorModule = modules.module_author ?? {};
32
- const dynamicModule = modules.module_dynamic ?? {};
33
- const major = dynamicModule.major ?? {};
34
- let title = '';
35
- let url = '';
36
- let itemType = item.type ?? '';
37
- if (major.archive) {
38
- title = major.archive.title ?? '';
39
- url = major.archive.jump_url ? `https:${major.archive.jump_url}` : '';
40
- itemType = 'video';
41
- }
42
- else if (major.article) {
43
- title = major.article.title ?? '';
44
- url = major.article.jump_url ? `https:${major.article.jump_url}` : '';
45
- itemType = 'article';
87
+ let offset = '';
88
+
89
+ for (let p = 0; p < maxPages; p++) {
90
+ if (rows.length >= maxResults) break;
91
+
92
+ let payload;
93
+ if (isUserFeed) {
94
+ const params = { host_mid: uid, timezone_offset: -480 };
95
+ if (offset) params.offset = offset;
96
+ payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/space', { params });
97
+ } else {
98
+ const params = {
99
+ timezone_offset: -480,
100
+ type: filterType || 'all',
101
+ page: p + 1,
102
+ };
103
+ if (offset) params.offset = offset;
104
+ payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', { params });
46
105
  }
47
- else if (dynamicModule.desc) {
48
- title = stripHtml(dynamicModule.desc.text ?? '').slice(0, 60);
49
- url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
50
- itemType = 'dynamic';
106
+
107
+ const data = payloadData(payload) ?? {};
108
+ const items = data.items ?? [];
109
+ if (items.length === 0) break;
110
+
111
+ for (const item of items) {
112
+ if (rows.length >= maxResults) break;
113
+ const parsed = parseItem(item);
114
+ if (filterType && parsed.itemType !== filterType) continue;
115
+ rows.push({
116
+ rank: rows.length + 1,
117
+ time: parsed.time,
118
+ author: parsed.author,
119
+ title: parsed.title,
120
+ type: parsed.itemType,
121
+ likes: parsed.likes,
122
+ url: parsed.url,
123
+ });
51
124
  }
52
- if (!title)
53
- continue;
54
- rows.push({
55
- rank: rows.length + 1,
56
- author: authorModule.name ?? '',
57
- title,
58
- type: itemType,
59
- url,
60
- });
125
+
126
+ offset = data.offset ?? items[items.length - 1]?.id_str ?? '';
127
+ if (!offset || !data.has_more) break;
128
+ }
129
+
130
+ return rows;
131
+ },
132
+ });
133
+
134
+ cli({
135
+ site: 'bilibili',
136
+ name: 'feed-detail',
137
+ description: '查看 Bilibili 动态详情(支持充电专属内容)',
138
+ domain: 'www.bilibili.com',
139
+ strategy: Strategy.COOKIE,
140
+ args: [
141
+ { name: 'id', positional: true, required: true, help: '动态 ID(从 feed 命令的 url 中获取)' },
142
+ ],
143
+ columns: ['field', 'value'],
144
+ func: async (page, kwargs) => {
145
+ const id = String(kwargs.id);
146
+ const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/detail', {
147
+ params: { id, timezone_offset: -480 },
148
+ });
149
+
150
+ const rows = [];
151
+ const data = payloadData(payload);
152
+ const item = data?.item;
153
+ if (!item) {
154
+ rows.push({ field: 'error', value: '动态不存在或无权查看'});
155
+ return rows;
156
+ }
157
+
158
+ const modules = item.modules ?? {};
159
+ const author = modules.module_author ?? {};
160
+ const dynamicModule = modules.module_dynamic ?? {};
161
+ const major = dynamicModule.major ?? {};
162
+ const stat = modules.module_stat ?? {};
163
+
164
+ rows.push({ field: 'id', value: item.id_str ?? id });
165
+ rows.push({ field: 'author', value: author.name ?? '' });
166
+ rows.push({ field: 'time', value: author.pub_time ?? '' });
167
+ rows.push({ field: 'type', value: TYPE_MAP[item.type] ?? item.type ?? '' });
168
+
169
+ // text content
170
+ if (dynamicModule.desc?.text) {
171
+ rows.push({ field: 'text', value: stripHtml(dynamicModule.desc.text) });
172
+ }
173
+
174
+ // video
175
+ if (major.archive) {
176
+ rows.push({ field: 'video_title', value: major.archive.title ?? '' });
177
+ rows.push({ field: 'video_desc', value: major.archive.desc ?? '' });
178
+ rows.push({ field: 'video_url', value: major.archive.jump_url ? `https:${major.archive.jump_url}` : '' });
179
+ rows.push({ field: 'play', value: String(major.archive.stat?.play ?? '') });
180
+ rows.push({ field: 'danmaku', value: String(major.archive.stat?.danmaku ?? '') });
181
+ }
182
+
183
+ // article
184
+ if (major.article) {
185
+ rows.push({ field: 'article_title', value: major.article.title ?? '' });
186
+ rows.push({ field: 'article_url', value: major.article.jump_url ? `https:${major.article.jump_url}` : '' });
187
+ }
188
+
189
+ // draw (images)
190
+ if (major.draw?.items?.length) {
191
+ rows.push({ field: 'images', value: major.draw.items.map((img) => img.src).join('\n') });
192
+ }
193
+
194
+ // opus (rich text, some dynamics use this)
195
+ if (major.opus?.summary?.text) {
196
+ rows.push({ field: 'opus_text', value: stripHtml(major.opus.summary.text) });
197
+ }
198
+ if (major.opus?.title) {
199
+ rows.push({ field: 'opus_title', value: major.opus.title });
200
+ }
201
+
202
+ // forward - show original dynamic info
203
+ if (item.orig) {
204
+ const origAuthor = item.orig.modules?.module_author?.name ?? '';
205
+ const origDesc = item.orig.modules?.module_dynamic?.desc?.text ?? '';
206
+ rows.push({ field: 'forward_from', value: origAuthor });
207
+ if (origDesc) rows.push({ field: 'forward_text', value: stripHtml(origDesc).slice(0, 200) });
61
208
  }
209
+
210
+ // stats
211
+ rows.push({ field: 'likes', value: String(stat.like?.count ?? 0) });
212
+ rows.push({ field: 'comments', value: String(stat.comment?.count ?? 0) });
213
+ rows.push({ field: 'forwards', value: String(stat.forward?.count ?? 0) });
214
+ rows.push({ field: 'url', value: `https://t.bilibili.com/${item.id_str ?? id}` });
215
+
62
216
  return rows;
63
217
  },
64
218
  });
@@ -3,7 +3,7 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
3
3
  cli({
4
4
  site: 'binance',
5
5
  name: 'depth',
6
- description: 'Order book bid prices for a trading pair',
6
+ description: 'Order book bid and ask prices for a trading pair',
7
7
  domain: 'data-api.binance.vision',
8
8
  strategy: Strategy.PUBLIC,
9
9
  browser: false,
@@ -11,11 +11,10 @@ cli({
11
11
  { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
12
  { name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
13
13
  ],
14
- columns: ['rank', 'bid_price', 'bid_qty'],
14
+ columns: ['rank', 'bid_price', 'bid_qty', 'ask_price', 'ask_qty'],
15
15
  pipeline: [
16
16
  { fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
17
- { select: 'bids' },
18
- { map: { rank: '${{ index + 1 }}', bid_price: '${{ item.0 }}', bid_qty: '${{ item.1 }}' } },
17
+ { map: { select: 'bids', rank: '${{ index + 1 }}', bid_price: '${{ item[0] }}', bid_qty: '${{ item[1] }}', ask_price: '${{ root.asks[index]?.[0] ?? "" }}', ask_qty: '${{ root.asks[index]?.[1] ?? "" }}' } },
19
18
  { limit: '${{ args.limit }}' },
20
19
  ],
21
20
  });
@@ -214,10 +214,10 @@ export async function typeAndSendMessage(page, text) {
214
214
  return true;
215
215
  }
216
216
  /**
217
- * Verbose log helper — prints when OPENCLI_VERBOSE or DEBUG=opencli is set.
217
+ * Verbose log helper — prints when OPENCLI_VERBOSE is set.
218
218
  */
219
219
  export function verbose(msg) {
220
- if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
220
+ if (process.env.OPENCLI_VERBOSE) {
221
221
  console.error(`[opencli:boss] ${msg}`);
222
222
  }
223
223
  }
@@ -0,0 +1,97 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ import { cli, Strategy } from '@jackwener/opencli/registry';
4
+ import { saveBase64ToFile } from '@jackwener/opencli/utils';
5
+ import { getChatGPTVisibleImageUrls, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets } from './utils.js';
6
+
7
+ const CHATGPT_DOMAIN = 'chatgpt.com';
8
+
9
+ function extFromMime(mime) {
10
+ if (mime.includes('png')) return '.png';
11
+ if (mime.includes('webp')) return '.webp';
12
+ if (mime.includes('gif')) return '.gif';
13
+ return '.jpg';
14
+ }
15
+
16
+ function normalizeBooleanFlag(value) {
17
+ if (typeof value === 'boolean') return value;
18
+ const normalized = String(value ?? '').trim().toLowerCase();
19
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
20
+ }
21
+
22
+ function displayPath(filePath) {
23
+ const home = os.homedir();
24
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
25
+ }
26
+
27
+ async function currentChatGPTLink(page) {
28
+ const url = await page.evaluate('window.location.href').catch(() => '');
29
+ return typeof url === 'string' && url ? url : 'https://chatgpt.com';
30
+ }
31
+
32
+ export const imageCommand = cli({
33
+ site: 'chatgpt',
34
+ name: 'image',
35
+ description: 'Generate images with ChatGPT web and save them locally',
36
+ domain: CHATGPT_DOMAIN,
37
+ strategy: Strategy.COOKIE,
38
+ browser: true,
39
+ navigateBefore: false,
40
+ defaultFormat: 'plain',
41
+ timeoutSeconds: 240,
42
+ args: [
43
+ { name: 'prompt', positional: true, required: true, help: 'Image prompt to send to ChatGPT' },
44
+ { name: 'op', default: path.join(os.homedir(), 'Pictures', 'chatgpt'), help: 'Output directory' },
45
+ { name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show ChatGPT link' },
46
+ ],
47
+ columns: ['status', 'file', 'link'],
48
+ func: async (page, kwargs) => {
49
+ const prompt = kwargs.prompt;
50
+ const outputDir = kwargs.op || path.join(os.homedir(), 'Pictures', 'chatgpt');
51
+ const skipDownloadRaw = kwargs.sd;
52
+ const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
53
+ const timeout = 120;
54
+
55
+ // Navigate to chatgpt.com/new with full reload to clear React sidebar state
56
+ await page.goto(`https://${CHATGPT_DOMAIN}/new`, { settleMs: 2000 });
57
+
58
+ const beforeUrls = await getChatGPTVisibleImageUrls(page);
59
+
60
+ // Send the image generation prompt - must be explicit
61
+ const sent = await sendChatGPTMessage(page, `Generate an image of: ${prompt}`);
62
+ if (!sent) {
63
+ return [{ status: '⚠️ send-failed', file: '📁 -', link: `🔗 ${await currentChatGPTLink(page)}` }];
64
+ }
65
+
66
+ // Wait for response and images
67
+ const urls = await waitForChatGPTImages(page, beforeUrls, timeout);
68
+ const link = await currentChatGPTLink(page);
69
+
70
+ if (!urls.length) {
71
+ return [{ status: '⚠️ no-images', file: '📁 -', link: `🔗 ${link}` }];
72
+ }
73
+
74
+ if (skipDownload) {
75
+ return [{ status: '🎨 generated', file: '📁 -', link: `🔗 ${link}` }];
76
+ }
77
+
78
+ // Export and save images
79
+ const assets = await getChatGPTImageAssets(page, urls);
80
+ if (!assets.length) {
81
+ return [{ status: '⚠️ export-failed', file: '📁 -', link: `🔗 ${link}` }];
82
+ }
83
+
84
+ const stamp = Date.now();
85
+ const results = [];
86
+ for (let index = 0; index < assets.length; index += 1) {
87
+ const asset = assets[index];
88
+ const base64 = asset.dataUrl.replace(/^data:[^;]+;base64,/, '');
89
+ const suffix = assets.length > 1 ? `_${index + 1}` : '';
90
+ const ext = extFromMime(asset.mimeType);
91
+ const filePath = path.join(outputDir, `chatgpt_${stamp}${suffix}${ext}`);
92
+ await saveBase64ToFile(base64, filePath);
93
+ results.push({ status: '✅ saved', file: `📁 ${displayPath(filePath)}`, link: `🔗 ${link}` });
94
+ }
95
+ return results;
96
+ },
97
+ });