@jackwener/opencli 1.7.1 → 1.7.3

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 (122) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +6 -3
  3. package/cli-manifest.json +1085 -73
  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/feed.js +202 -48
  9. package/clis/binance/asks.js +21 -0
  10. package/clis/binance/commands.test.js +70 -0
  11. package/clis/binance/depth.js +21 -0
  12. package/clis/binance/gainers.js +22 -0
  13. package/clis/binance/klines.js +21 -0
  14. package/clis/binance/losers.js +22 -0
  15. package/clis/binance/pairs.js +21 -0
  16. package/clis/binance/price.js +18 -0
  17. package/clis/binance/prices.js +19 -0
  18. package/clis/binance/ticker.js +21 -0
  19. package/clis/binance/top.js +21 -0
  20. package/clis/binance/trades.js +20 -0
  21. package/clis/boss/utils.js +2 -1
  22. package/clis/chatgpt/image.js +97 -0
  23. package/clis/chatgpt/utils.js +297 -0
  24. package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
  25. package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
  26. package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
  27. package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
  28. package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
  29. package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
  30. package/clis/discord-app/delete.js +114 -0
  31. package/clis/douban/utils.js +29 -2
  32. package/clis/douban/utils.test.js +121 -1
  33. package/clis/ke/chengjiao.js +77 -0
  34. package/clis/ke/ershoufang.js +100 -0
  35. package/clis/ke/utils.js +104 -0
  36. package/clis/ke/xiaoqu.js +77 -0
  37. package/clis/ke/zufang.js +94 -0
  38. package/clis/maimai/search-talents.js +172 -0
  39. package/clis/mubu/doc.js +40 -0
  40. package/clis/mubu/docs.js +43 -0
  41. package/clis/mubu/notes.js +244 -0
  42. package/clis/mubu/recent.js +27 -0
  43. package/clis/mubu/search.js +62 -0
  44. package/clis/mubu/utils.js +304 -0
  45. package/clis/reuters/search.js +1 -1
  46. package/clis/twitter/lists-parser.js +77 -0
  47. package/clis/twitter/lists.d.ts +5 -0
  48. package/clis/twitter/lists.js +62 -0
  49. package/clis/twitter/lists.test.js +50 -0
  50. package/clis/weibo/feed.js +18 -5
  51. package/clis/xiaohongshu/comments.js +18 -6
  52. package/clis/xiaohongshu/comments.test.js +36 -0
  53. package/clis/xiaohongshu/creator-note-detail.js +2 -0
  54. package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
  55. package/clis/xiaohongshu/creator-notes-summary.js +4 -0
  56. package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
  57. package/clis/xiaohongshu/creator-notes.js +1 -0
  58. package/clis/xiaohongshu/creator-profile.js +1 -0
  59. package/clis/xiaohongshu/creator-stats.js +1 -0
  60. package/clis/xiaohongshu/download.js +12 -0
  61. package/clis/xiaohongshu/download.test.js +30 -0
  62. package/clis/xiaohongshu/navigation.test.js +34 -0
  63. package/clis/xiaohongshu/note.js +14 -5
  64. package/clis/xiaohongshu/note.test.js +28 -0
  65. package/clis/xiaohongshu/publish.js +1 -0
  66. package/clis/xiaohongshu/search.js +1 -0
  67. package/clis/xiaohongshu/user.js +1 -0
  68. package/clis/yahoo-finance/quote.js +1 -1
  69. package/clis/zsxq/topic.js +5 -3
  70. package/clis/zsxq/topic.test.js +4 -3
  71. package/clis/zsxq/utils.js +1 -1
  72. package/dist/src/browser/base-page.d.ts +9 -0
  73. package/dist/src/browser/base-page.js +19 -0
  74. package/dist/src/browser/cdp.js +10 -2
  75. package/dist/src/browser/daemon-client.d.ts +1 -0
  76. package/dist/src/cli.js +112 -2
  77. package/dist/src/daemon.js +5 -0
  78. package/dist/src/discovery.d.ts +5 -2
  79. package/dist/src/discovery.js +7 -35
  80. package/dist/src/doctor.d.ts +1 -0
  81. package/dist/src/doctor.js +51 -2
  82. package/dist/src/electron-apps.js +1 -1
  83. package/dist/src/engine.test.js +29 -1
  84. package/dist/src/errors.d.ts +1 -0
  85. package/dist/src/errors.js +13 -0
  86. package/dist/src/execution.js +36 -9
  87. package/dist/src/execution.test.js +23 -0
  88. package/dist/src/logger.d.ts +2 -2
  89. package/dist/src/logger.js +4 -9
  90. package/dist/src/main.js +6 -5
  91. package/dist/src/registry.js +3 -4
  92. package/dist/src/types.d.ts +2 -0
  93. package/dist/src/update-check.d.ts +14 -0
  94. package/dist/src/update-check.js +48 -3
  95. package/dist/src/update-check.test.js +31 -0
  96. package/package.json +3 -3
  97. package/scripts/fetch-adapters.js +92 -34
  98. package/dist/src/clis/binance/asks.js +0 -20
  99. package/dist/src/clis/binance/commands.test.d.ts +0 -3
  100. package/dist/src/clis/binance/commands.test.js +0 -58
  101. package/dist/src/clis/binance/depth.d.ts +0 -1
  102. package/dist/src/clis/binance/depth.js +0 -20
  103. package/dist/src/clis/binance/gainers.d.ts +0 -1
  104. package/dist/src/clis/binance/gainers.js +0 -21
  105. package/dist/src/clis/binance/klines.d.ts +0 -1
  106. package/dist/src/clis/binance/klines.js +0 -20
  107. package/dist/src/clis/binance/losers.d.ts +0 -1
  108. package/dist/src/clis/binance/losers.js +0 -21
  109. package/dist/src/clis/binance/pairs.d.ts +0 -1
  110. package/dist/src/clis/binance/pairs.js +0 -20
  111. package/dist/src/clis/binance/price.d.ts +0 -1
  112. package/dist/src/clis/binance/price.js +0 -17
  113. package/dist/src/clis/binance/prices.d.ts +0 -1
  114. package/dist/src/clis/binance/prices.js +0 -18
  115. package/dist/src/clis/binance/ticker.d.ts +0 -1
  116. package/dist/src/clis/binance/ticker.js +0 -20
  117. package/dist/src/clis/binance/top.d.ts +0 -1
  118. package/dist/src/clis/binance/top.js +0 -20
  119. package/dist/src/clis/binance/trades.d.ts +0 -1
  120. package/dist/src/clis/binance/trades.js +0 -19
  121. /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
  122. /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
@@ -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
@@ -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
  });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'asks',
6
+ description: 'Order book ask prices for a trading pair',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
+ { name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
13
+ ],
14
+ columns: ['rank', 'ask_price', 'ask_qty'],
15
+ pipeline: [
16
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
17
+ { select: 'asks' },
18
+ { map: { rank: '${{ index + 1 }}', ask_price: '${{ item.0 }}', ask_qty: '${{ item.1 }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,70 @@
1
+ import { getRegistry } from '@jackwener/opencli/registry';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { executePipeline } from '@jackwener/opencli/pipeline';
4
+
5
+ // Import all binance adapters to register them
6
+ import './top.js';
7
+ import './gainers.js';
8
+ import './pairs.js';
9
+
10
+ function loadPipeline(name) {
11
+ const cmd = getRegistry().get(`binance/${name}`);
12
+ if (!cmd?.pipeline) throw new Error(`Command binance/${name} not found or has no pipeline`);
13
+ return cmd.pipeline;
14
+ }
15
+
16
+ function mockJsonOnce(payload) {
17
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
18
+ ok: true,
19
+ status: 200,
20
+ statusText: 'OK',
21
+ json: vi.fn().mockResolvedValue(payload),
22
+ }));
23
+ }
24
+
25
+ afterEach(() => {
26
+ vi.unstubAllGlobals();
27
+ vi.restoreAllMocks();
28
+ });
29
+
30
+ describe('binance adapters', () => {
31
+ it('sorts top pairs by numeric quote volume', async () => {
32
+ mockJsonOnce([
33
+ { symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' },
34
+ { symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' },
35
+ { symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' },
36
+ ]);
37
+
38
+ const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } });
39
+
40
+ expect(result.map((item) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']);
41
+ expect(result.map((item) => item.rank)).toEqual([1, 2, 3]);
42
+ });
43
+
44
+ it('sorts gainers by numeric percent change', async () => {
45
+ mockJsonOnce([
46
+ { symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' },
47
+ { symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' },
48
+ { symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' },
49
+ ]);
50
+
51
+ const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } });
52
+
53
+ expect(result.map((item) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']);
54
+ });
55
+
56
+ it('keeps only TRADING pairs', async () => {
57
+ mockJsonOnce({
58
+ symbols: [
59
+ { symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' },
60
+ { symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' },
61
+ ],
62
+ });
63
+
64
+ const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } });
65
+
66
+ expect(result).toEqual([
67
+ { symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' },
68
+ ]);
69
+ });
70
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'depth',
6
+ description: 'Order book bid prices for a trading pair',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
+ { name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
13
+ ],
14
+ columns: ['rank', 'bid_price', 'bid_qty'],
15
+ pipeline: [
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 }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,22 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'gainers',
6
+ description: 'Top gaining trading pairs by 24h price change',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
12
+ ],
13
+ columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
16
+ { filter: 'item.priceChangePercent' },
17
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
18
+ { sort: { by: 'sort_change', order: 'desc' } },
19
+ { map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
20
+ { limit: '${{ args.limit }}' },
21
+ ],
22
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'klines',
6
+ description: 'Candlestick/kline data for a trading pair',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
+ { name: 'interval', type: 'str', default: '1d', help: 'Kline interval (1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M)' },
13
+ { name: 'limit', type: 'int', default: 10, help: 'Number of klines (max 1000)' },
14
+ ],
15
+ columns: ['open', 'high', 'low', 'close', 'volume'],
16
+ pipeline: [
17
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/klines?symbol=${{ args.symbol }}&interval=${{ args.interval }}&limit=${{ args.limit }}' } },
18
+ { map: { open: '${{ item.1 }}', high: '${{ item.2 }}', low: '${{ item.3 }}', close: '${{ item.4 }}', volume: '${{ item.5 }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,22 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'losers',
6
+ description: 'Top losing trading pairs by 24h price change',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
12
+ ],
13
+ columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
16
+ { filter: 'item.priceChangePercent' },
17
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
18
+ { sort: { by: 'sort_change' } },
19
+ { map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
20
+ { limit: '${{ args.limit }}' },
21
+ ],
22
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'pairs',
6
+ description: 'List active trading pairs on Binance',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: 'Number of trading pairs' },
12
+ ],
13
+ columns: ['symbol', 'base', 'quote', 'status'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/exchangeInfo' } },
16
+ { select: 'symbols' },
17
+ { filter: 'item.status === \'TRADING\'' },
18
+ { map: { symbol: '${{ item.symbol }}', base: '${{ item.baseAsset }}', quote: '${{ item.quoteAsset }}', status: '${{ item.status }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,18 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'price',
6
+ description: 'Quick price check for a trading pair',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
+ ],
13
+ columns: ['symbol', 'price', 'change', 'change_pct', 'high', 'low', 'volume', 'quote_volume', 'trades'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr?symbol=${{ args.symbol }}' } },
16
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change: '${{ item.priceChange }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_volume: '${{ item.quoteVolume }}', trades: '${{ item.count }}' } },
17
+ ],
18
+ });
@@ -0,0 +1,19 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'prices',
6
+ description: 'Latest prices for all trading pairs',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: 'Number of prices' },
12
+ ],
13
+ columns: ['rank', 'symbol', 'price'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/price' } },
16
+ { map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.price }}' } },
17
+ { limit: '${{ args.limit }}' },
18
+ ],
19
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'ticker',
6
+ description: '24h ticker statistics for top trading pairs by volume',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: 'Number of tickers' },
12
+ ],
13
+ columns: ['symbol', 'price', 'change_pct', 'high', 'low', 'volume', 'quote_vol', 'trades'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
16
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_vol: '${{ item.quoteVolume }}', trades: '${{ item.count }}', sort_volume: '${{ Number(item.quoteVolume) }}' } },
17
+ { sort: { by: 'sort_volume', order: 'desc' } },
18
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_vol: '${{ item.quoteVolume }}', trades: '${{ item.count }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'top',
6
+ description: 'Top trading pairs by 24h volume on Binance',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: 'Number of trading pairs' },
12
+ ],
13
+ columns: ['rank', 'symbol', 'price', 'change_24h', 'high', 'low', 'volume'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
16
+ { map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.quoteVolume }}', sort_volume: '${{ Number(item.quoteVolume) }}' } },
17
+ { sort: { by: 'sort_volume', order: 'desc' } },
18
+ { map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.quoteVolume }}' } },
19
+ { limit: '${{ args.limit }}' },
20
+ ],
21
+ });
@@ -0,0 +1,20 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'binance',
5
+ name: 'trades',
6
+ description: 'Recent trades for a trading pair',
7
+ domain: 'data-api.binance.vision',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
+ { name: 'limit', type: 'int', default: 20, help: 'Number of trades (max 1000)' },
13
+ ],
14
+ columns: ['id', 'price', 'qty', 'quote_qty', 'buyer_maker'],
15
+ pipeline: [
16
+ { fetch: { url: 'https://data-api.binance.vision/api/v3/trades?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
17
+ { map: { id: '${{ item.id }}', price: '${{ item.price }}', qty: '${{ item.qty }}', quote_qty: '${{ item.quoteQty }}', buyer_maker: '${{ item.isBuyerMaker }}' } },
18
+ { limit: '${{ args.limit }}' },
19
+ ],
20
+ });
@@ -214,7 +214,8 @@ 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, with DEBUG=opencli
218
+ * kept as a compatibility fallback.
218
219
  */
219
220
  export function verbose(msg) {
220
221
  if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {