@jackwener/opencli 0.9.6 → 1.0.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.
Files changed (307) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CDP.md +1 -1
  14. package/CDP.zh-CN.md +1 -1
  15. package/CLI-ELECTRON.md +89 -36
  16. package/CLI-EXPLORER.md +4 -4
  17. package/CONTRIBUTING.md +167 -0
  18. package/README.md +113 -89
  19. package/README.zh-CN.md +114 -91
  20. package/SKILL.md +10 -8
  21. package/TESTING.md +7 -7
  22. package/dist/browser/daemon-client.d.ts +37 -0
  23. package/dist/browser/daemon-client.js +82 -0
  24. package/dist/browser/discover.d.ts +11 -34
  25. package/dist/browser/discover.js +15 -190
  26. package/dist/browser/errors.d.ts +6 -20
  27. package/dist/browser/errors.js +24 -63
  28. package/dist/browser/index.d.ts +2 -11
  29. package/dist/browser/index.js +5 -11
  30. package/dist/browser/mcp.d.ts +9 -18
  31. package/dist/browser/mcp.js +70 -284
  32. package/dist/browser/page.d.ts +28 -6
  33. package/dist/browser/page.js +210 -85
  34. package/dist/browser.test.js +4 -202
  35. package/dist/build-manifest.d.ts +26 -0
  36. package/dist/build-manifest.js +132 -60
  37. package/dist/build-manifest.test.d.ts +1 -0
  38. package/dist/build-manifest.test.js +26 -0
  39. package/dist/cli-manifest.json +1582 -29
  40. package/dist/clis/bilibili/download.d.ts +10 -0
  41. package/dist/clis/bilibili/download.js +135 -0
  42. package/dist/clis/chatwise/ask.d.ts +1 -0
  43. package/dist/clis/chatwise/ask.js +76 -0
  44. package/dist/clis/chatwise/export.d.ts +1 -0
  45. package/dist/clis/chatwise/export.js +46 -0
  46. package/dist/clis/chatwise/history.d.ts +1 -0
  47. package/dist/clis/chatwise/history.js +43 -0
  48. package/dist/clis/chatwise/model.d.ts +1 -0
  49. package/dist/clis/chatwise/model.js +81 -0
  50. package/dist/clis/chatwise/new.d.ts +1 -0
  51. package/dist/clis/chatwise/new.js +18 -0
  52. package/dist/clis/chatwise/read.d.ts +1 -0
  53. package/dist/clis/chatwise/read.js +39 -0
  54. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  55. package/dist/clis/chatwise/screenshot.js +27 -0
  56. package/dist/clis/chatwise/send.d.ts +1 -0
  57. package/dist/clis/chatwise/send.js +45 -0
  58. package/dist/clis/chatwise/status.d.ts +1 -0
  59. package/dist/clis/chatwise/status.js +22 -0
  60. package/dist/clis/discord-app/channels.d.ts +1 -0
  61. package/dist/clis/discord-app/channels.js +45 -0
  62. package/dist/clis/discord-app/members.d.ts +1 -0
  63. package/dist/clis/discord-app/members.js +38 -0
  64. package/dist/clis/discord-app/read.d.ts +1 -0
  65. package/dist/clis/discord-app/read.js +45 -0
  66. package/dist/clis/discord-app/search.d.ts +1 -0
  67. package/dist/clis/discord-app/search.js +56 -0
  68. package/dist/clis/discord-app/send.d.ts +1 -0
  69. package/dist/clis/discord-app/send.js +27 -0
  70. package/dist/clis/discord-app/servers.d.ts +1 -0
  71. package/dist/clis/discord-app/servers.js +36 -0
  72. package/dist/clis/discord-app/status.d.ts +1 -0
  73. package/dist/clis/discord-app/status.js +16 -0
  74. package/dist/clis/feishu/new.d.ts +1 -0
  75. package/dist/clis/feishu/new.js +27 -0
  76. package/dist/clis/feishu/read.d.ts +1 -0
  77. package/dist/clis/feishu/read.js +40 -0
  78. package/dist/clis/feishu/search.d.ts +1 -0
  79. package/dist/clis/feishu/search.js +30 -0
  80. package/dist/clis/feishu/send.d.ts +1 -0
  81. package/dist/clis/feishu/send.js +39 -0
  82. package/dist/clis/feishu/status.d.ts +1 -0
  83. package/dist/clis/feishu/status.js +28 -0
  84. package/dist/clis/grok/ask.d.ts +1 -0
  85. package/dist/clis/grok/ask.js +82 -0
  86. package/dist/clis/grok/debug.d.ts +1 -0
  87. package/dist/clis/grok/debug.js +45 -0
  88. package/dist/clis/jimeng/generate.yaml +84 -0
  89. package/dist/clis/jimeng/history.yaml +47 -0
  90. package/dist/clis/linux-do/categories.yaml +41 -0
  91. package/dist/clis/linux-do/category.yaml +49 -0
  92. package/dist/clis/linux-do/hot.yaml +50 -0
  93. package/dist/clis/linux-do/latest.yaml +40 -0
  94. package/dist/clis/linux-do/search.yaml +45 -0
  95. package/dist/clis/linux-do/topic.yaml +38 -0
  96. package/dist/clis/neteasemusic/like.d.ts +1 -0
  97. package/dist/clis/neteasemusic/like.js +25 -0
  98. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  99. package/dist/clis/neteasemusic/lyrics.js +47 -0
  100. package/dist/clis/neteasemusic/next.d.ts +1 -0
  101. package/dist/clis/neteasemusic/next.js +26 -0
  102. package/dist/clis/neteasemusic/play.d.ts +1 -0
  103. package/dist/clis/neteasemusic/play.js +26 -0
  104. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  105. package/dist/clis/neteasemusic/playing.js +59 -0
  106. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  107. package/dist/clis/neteasemusic/playlist.js +46 -0
  108. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  109. package/dist/clis/neteasemusic/prev.js +25 -0
  110. package/dist/clis/neteasemusic/search.d.ts +1 -0
  111. package/dist/clis/neteasemusic/search.js +52 -0
  112. package/dist/clis/neteasemusic/status.d.ts +1 -0
  113. package/dist/clis/neteasemusic/status.js +16 -0
  114. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  115. package/dist/clis/neteasemusic/volume.js +54 -0
  116. package/dist/clis/notion/export.d.ts +1 -0
  117. package/dist/clis/notion/export.js +31 -0
  118. package/dist/clis/notion/favorites.d.ts +1 -0
  119. package/dist/clis/notion/favorites.js +84 -0
  120. package/dist/clis/notion/new.d.ts +1 -0
  121. package/dist/clis/notion/new.js +34 -0
  122. package/dist/clis/notion/read.d.ts +1 -0
  123. package/dist/clis/notion/read.js +30 -0
  124. package/dist/clis/notion/search.d.ts +1 -0
  125. package/dist/clis/notion/search.js +46 -0
  126. package/dist/clis/notion/sidebar.d.ts +1 -0
  127. package/dist/clis/notion/sidebar.js +41 -0
  128. package/dist/clis/notion/status.d.ts +1 -0
  129. package/dist/clis/notion/status.js +16 -0
  130. package/dist/clis/notion/write.d.ts +1 -0
  131. package/dist/clis/notion/write.js +40 -0
  132. package/dist/clis/twitter/download.d.ts +8 -0
  133. package/dist/clis/twitter/download.js +204 -0
  134. package/dist/clis/wechat/chats.d.ts +1 -0
  135. package/dist/clis/wechat/chats.js +28 -0
  136. package/dist/clis/wechat/contacts.d.ts +1 -0
  137. package/dist/clis/wechat/contacts.js +28 -0
  138. package/dist/clis/wechat/read.d.ts +1 -0
  139. package/dist/clis/wechat/read.js +58 -0
  140. package/dist/clis/wechat/search.d.ts +1 -0
  141. package/dist/clis/wechat/search.js +31 -0
  142. package/dist/clis/wechat/send.d.ts +1 -0
  143. package/dist/clis/wechat/send.js +42 -0
  144. package/dist/clis/wechat/status.d.ts +1 -0
  145. package/dist/clis/wechat/status.js +29 -0
  146. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  147. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  148. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  149. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  150. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  151. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  152. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  153. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  154. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  155. package/dist/clis/xiaohongshu/download.js +155 -0
  156. package/dist/clis/xiaohongshu/search.js +1 -1
  157. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  158. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  159. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  160. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  161. package/dist/clis/xiaohongshu/user.js +46 -29
  162. package/dist/clis/zhihu/download.d.ts +11 -0
  163. package/dist/clis/zhihu/download.js +186 -0
  164. package/dist/clis/zhihu/download.test.d.ts +1 -0
  165. package/dist/clis/zhihu/download.test.js +10 -0
  166. package/dist/daemon.d.ts +13 -0
  167. package/dist/daemon.js +187 -0
  168. package/dist/doctor.d.ts +27 -61
  169. package/dist/doctor.js +70 -601
  170. package/dist/doctor.test.js +30 -170
  171. package/dist/download/index.d.ts +79 -0
  172. package/dist/download/index.js +325 -0
  173. package/dist/download/progress.d.ts +36 -0
  174. package/dist/download/progress.js +111 -0
  175. package/dist/engine.test.js +15 -0
  176. package/dist/main.js +22 -28
  177. package/dist/pipeline/executor.test.js +1 -0
  178. package/dist/pipeline/registry.js +2 -0
  179. package/dist/pipeline/steps/browser.js +2 -2
  180. package/dist/pipeline/steps/download.d.ts +34 -0
  181. package/dist/pipeline/steps/download.js +251 -0
  182. package/dist/pipeline/steps/intercept.js +1 -2
  183. package/dist/pipeline/template.js +28 -0
  184. package/dist/setup.d.ts +6 -0
  185. package/dist/setup.js +46 -160
  186. package/dist/types.d.ts +6 -0
  187. package/extension/icons/icon-128.png +0 -0
  188. package/extension/icons/icon-16.png +0 -0
  189. package/extension/icons/icon-32.png +0 -0
  190. package/extension/icons/icon-48.png +0 -0
  191. package/extension/manifest.json +31 -0
  192. package/extension/package.json +16 -0
  193. package/extension/src/background.ts +293 -0
  194. package/extension/src/cdp.ts +125 -0
  195. package/extension/src/protocol.ts +57 -0
  196. package/extension/store-assets/screenshot-1280x800.png +0 -0
  197. package/extension/tsconfig.json +15 -0
  198. package/extension/vite.config.ts +18 -0
  199. package/package.json +8 -7
  200. package/scripts/test-site.mjs +70 -0
  201. package/src/browser/daemon-client.ts +113 -0
  202. package/src/browser/discover.ts +18 -216
  203. package/src/browser/errors.ts +30 -100
  204. package/src/browser/index.ts +6 -12
  205. package/src/browser/mcp.ts +78 -278
  206. package/src/browser/page.ts +222 -88
  207. package/src/browser.test.ts +3 -210
  208. package/src/build-manifest.test.ts +28 -0
  209. package/src/build-manifest.ts +147 -57
  210. package/src/clis/bilibili/download.ts +161 -0
  211. package/src/clis/chatgpt/README.md +1 -1
  212. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  213. package/src/clis/chatwise/README.md +38 -0
  214. package/src/clis/chatwise/README.zh-CN.md +38 -0
  215. package/src/clis/chatwise/ask.ts +87 -0
  216. package/src/clis/chatwise/export.ts +51 -0
  217. package/src/clis/chatwise/history.ts +47 -0
  218. package/src/clis/chatwise/model.ts +87 -0
  219. package/src/clis/chatwise/new.ts +21 -0
  220. package/src/clis/chatwise/read.ts +42 -0
  221. package/src/clis/chatwise/screenshot.ts +33 -0
  222. package/src/clis/chatwise/send.ts +50 -0
  223. package/src/clis/chatwise/status.ts +25 -0
  224. package/src/clis/discord-app/README.md +28 -0
  225. package/src/clis/discord-app/README.zh-CN.md +28 -0
  226. package/src/clis/discord-app/channels.ts +48 -0
  227. package/src/clis/discord-app/members.ts +41 -0
  228. package/src/clis/discord-app/read.ts +49 -0
  229. package/src/clis/discord-app/search.ts +64 -0
  230. package/src/clis/discord-app/send.ts +32 -0
  231. package/src/clis/discord-app/servers.ts +39 -0
  232. package/src/clis/discord-app/status.ts +18 -0
  233. package/src/clis/feishu/README.md +20 -0
  234. package/src/clis/feishu/README.zh-CN.md +20 -0
  235. package/src/clis/feishu/new.ts +32 -0
  236. package/src/clis/feishu/read.ts +48 -0
  237. package/src/clis/feishu/search.ts +35 -0
  238. package/src/clis/feishu/send.ts +46 -0
  239. package/src/clis/feishu/status.ts +34 -0
  240. package/src/clis/grok/ask.ts +90 -0
  241. package/src/clis/grok/debug.ts +49 -0
  242. package/src/clis/jimeng/generate.yaml +84 -0
  243. package/src/clis/jimeng/history.yaml +47 -0
  244. package/src/clis/linux-do/categories.yaml +41 -0
  245. package/src/clis/linux-do/category.yaml +49 -0
  246. package/src/clis/linux-do/hot.yaml +50 -0
  247. package/src/clis/linux-do/latest.yaml +40 -0
  248. package/src/clis/linux-do/search.yaml +45 -0
  249. package/src/clis/linux-do/topic.yaml +38 -0
  250. package/src/clis/neteasemusic/README.md +31 -0
  251. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  252. package/src/clis/neteasemusic/like.ts +28 -0
  253. package/src/clis/neteasemusic/lyrics.ts +53 -0
  254. package/src/clis/neteasemusic/next.ts +30 -0
  255. package/src/clis/neteasemusic/play.ts +30 -0
  256. package/src/clis/neteasemusic/playing.ts +62 -0
  257. package/src/clis/neteasemusic/playlist.ts +51 -0
  258. package/src/clis/neteasemusic/prev.ts +29 -0
  259. package/src/clis/neteasemusic/search.ts +58 -0
  260. package/src/clis/neteasemusic/status.ts +18 -0
  261. package/src/clis/neteasemusic/volume.ts +61 -0
  262. package/src/clis/notion/README.md +29 -0
  263. package/src/clis/notion/README.zh-CN.md +29 -0
  264. package/src/clis/notion/export.ts +36 -0
  265. package/src/clis/notion/favorites.ts +87 -0
  266. package/src/clis/notion/new.ts +39 -0
  267. package/src/clis/notion/read.ts +33 -0
  268. package/src/clis/notion/search.ts +54 -0
  269. package/src/clis/notion/sidebar.ts +44 -0
  270. package/src/clis/notion/status.ts +18 -0
  271. package/src/clis/notion/write.ts +45 -0
  272. package/src/clis/twitter/download.ts +227 -0
  273. package/src/clis/wechat/README.md +28 -0
  274. package/src/clis/wechat/README.zh-CN.md +28 -0
  275. package/src/clis/wechat/chats.ts +33 -0
  276. package/src/clis/wechat/contacts.ts +33 -0
  277. package/src/clis/wechat/read.ts +72 -0
  278. package/src/clis/wechat/search.ts +36 -0
  279. package/src/clis/wechat/send.ts +49 -0
  280. package/src/clis/wechat/status.ts +35 -0
  281. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  282. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  283. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  284. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  285. package/src/clis/xiaohongshu/download.ts +173 -0
  286. package/src/clis/xiaohongshu/search.ts +1 -1
  287. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  288. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  289. package/src/clis/xiaohongshu/user.ts +52 -32
  290. package/src/clis/zhihu/download.test.ts +12 -0
  291. package/src/clis/zhihu/download.ts +223 -0
  292. package/src/daemon.ts +217 -0
  293. package/src/doctor.test.ts +32 -193
  294. package/src/doctor.ts +74 -668
  295. package/src/download/index.ts +395 -0
  296. package/src/download/progress.ts +125 -0
  297. package/src/engine.test.ts +17 -0
  298. package/src/main.ts +18 -26
  299. package/src/pipeline/executor.test.ts +1 -0
  300. package/src/pipeline/registry.ts +2 -0
  301. package/src/pipeline/steps/browser.ts +2 -2
  302. package/src/pipeline/steps/download.ts +310 -0
  303. package/src/pipeline/steps/intercept.ts +1 -2
  304. package/src/pipeline/template.ts +26 -0
  305. package/src/setup.ts +47 -183
  306. package/src/types.ts +1 -0
  307. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Download progress display: terminal progress bars, status updates.
3
+ */
4
+ import chalk from 'chalk';
5
+ /**
6
+ * Format bytes as human-readable string (KB, MB, GB).
7
+ */
8
+ export function formatBytes(bytes) {
9
+ if (bytes === 0)
10
+ return '0 B';
11
+ const k = 1024;
12
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
13
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
14
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
15
+ }
16
+ /**
17
+ * Format milliseconds as human-readable duration.
18
+ */
19
+ export function formatDuration(ms) {
20
+ if (ms < 1000)
21
+ return `${ms}ms`;
22
+ const seconds = Math.floor(ms / 1000);
23
+ if (seconds < 60)
24
+ return `${seconds}s`;
25
+ const minutes = Math.floor(seconds / 60);
26
+ const remainingSeconds = seconds % 60;
27
+ return `${minutes}m ${remainingSeconds}s`;
28
+ }
29
+ /**
30
+ * Create a simple progress bar for terminal display.
31
+ */
32
+ export function createProgressBar(filename, index, total) {
33
+ const prefix = chalk.dim(`[${index + 1}/${total}]`);
34
+ const truncatedName = filename.length > 40 ? filename.slice(0, 37) + '...' : filename;
35
+ return {
36
+ update(current, totalBytes, label) {
37
+ const percent = totalBytes > 0 ? Math.round((current / totalBytes) * 100) : 0;
38
+ const bar = createBar(percent);
39
+ const size = totalBytes > 0 ? formatBytes(totalBytes) : '';
40
+ const extra = label ? ` ${label}` : '';
41
+ process.stderr.write(`\r${prefix} ${truncatedName} ${bar} ${percent}% ${size}${extra}`);
42
+ },
43
+ complete(success, message) {
44
+ const icon = success ? chalk.green('✓') : chalk.red('✗');
45
+ const msg = message ? ` ${chalk.dim(message)}` : '';
46
+ process.stderr.write(`\r${prefix} ${icon} ${truncatedName}${msg}\n`);
47
+ },
48
+ fail(error) {
49
+ process.stderr.write(`\r${prefix} ${chalk.red('✗')} ${truncatedName} ${chalk.red(error)}\n`);
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Create a progress bar string.
55
+ */
56
+ function createBar(percent, width = 20) {
57
+ const filled = Math.round((percent / 100) * width);
58
+ const empty = width - filled;
59
+ return chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
60
+ }
61
+ /**
62
+ * Multi-file download progress tracker.
63
+ */
64
+ export class DownloadProgressTracker {
65
+ completed = 0;
66
+ failed = 0;
67
+ skipped = 0;
68
+ total;
69
+ startTime;
70
+ verbose;
71
+ constructor(total, verbose = true) {
72
+ this.total = total;
73
+ this.startTime = Date.now();
74
+ this.verbose = verbose;
75
+ }
76
+ onFileStart(filename, index) {
77
+ if (!this.verbose)
78
+ return null;
79
+ return createProgressBar(filename, index, this.total);
80
+ }
81
+ onFileComplete(success, skipped = false) {
82
+ if (skipped) {
83
+ this.skipped++;
84
+ }
85
+ else if (success) {
86
+ this.completed++;
87
+ }
88
+ else {
89
+ this.failed++;
90
+ }
91
+ }
92
+ getSummary() {
93
+ const elapsed = formatDuration(Date.now() - this.startTime);
94
+ const parts = [];
95
+ if (this.completed > 0) {
96
+ parts.push(chalk.green(`${this.completed} downloaded`));
97
+ }
98
+ if (this.skipped > 0) {
99
+ parts.push(chalk.yellow(`${this.skipped} skipped`));
100
+ }
101
+ if (this.failed > 0) {
102
+ parts.push(chalk.red(`${this.failed} failed`));
103
+ }
104
+ return `${parts.join(', ')} in ${elapsed}`;
105
+ }
106
+ finish() {
107
+ if (this.verbose) {
108
+ process.stderr.write(`\n${chalk.bold('Download complete:')} ${this.getSummary()}\n`);
109
+ }
110
+ }
111
+ }
@@ -11,6 +11,21 @@ describe('discoverClis', () => {
11
11
  });
12
12
  });
13
13
  describe('executeCommand', () => {
14
+ it('accepts kebab-case option names after Commander camelCases them', async () => {
15
+ const cmd = cli({
16
+ site: 'test-engine',
17
+ name: 'kebab-arg-test',
18
+ description: 'test command with kebab-case arg',
19
+ browser: false,
20
+ strategy: Strategy.PUBLIC,
21
+ args: [
22
+ { name: 'note-id', required: true, help: 'Note ID' },
23
+ ],
24
+ func: async (_page, kwargs) => [{ noteId: kwargs['note-id'] }],
25
+ });
26
+ const result = await executeCommand(cmd, null, { 'note-id': 'abc123' });
27
+ expect(result).toEqual([{ noteId: 'abc123' }]);
28
+ });
14
29
  it('executes a command with func', async () => {
15
30
  const cmd = cli({
16
31
  site: 'test-engine',
package/dist/main.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import { Command } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import { discoverClis, executeCommand } from './engine.js';
11
- import { fullName, getRegistry, strategyLabel } from './registry.js';
11
+ import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
12
12
  import { render as renderOutput } from './output.js';
13
13
  import { PlaywrightMCP } from './browser/index.js';
14
14
  import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
@@ -122,37 +122,18 @@ program.command('cascade').description('Strategy cascade: find simplest working
122
122
  console.log(renderCascadeResult(result));
123
123
  });
124
124
  program.command('doctor')
125
- .description('Diagnose Playwright MCP Bridge, token consistency, and Chrome remote debugging')
126
- .option('--fix', 'Apply suggested fixes to shell rc and detected MCP configs', false)
127
- .option('-y, --yes', 'Skip confirmation prompts when applying fixes', false)
128
- .option('--token <token>', 'Override token to write instead of auto-detecting')
125
+ .description('Diagnose opencli browser bridge connectivity')
129
126
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
130
- .option('--shell-rc <path>', 'Shell startup file to update')
131
- .option('--mcp-config <paths>', 'Comma-separated MCP config paths to scan/update')
132
127
  .action(async (opts) => {
133
- const { runBrowserDoctor, renderBrowserDoctorReport, applyBrowserDoctorFix } = await import('./doctor.js');
134
- const configPaths = opts.mcpConfig ? String(opts.mcpConfig).split(',').map((s) => s.trim()).filter(Boolean) : undefined;
135
- const report = await runBrowserDoctor({ token: opts.token, live: opts.live, shellRc: opts.shellRc, configPaths, cliVersion: PKG_VERSION });
128
+ const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
129
+ const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
136
130
  console.log(renderBrowserDoctorReport(report));
137
- if (opts.fix) {
138
- const written = await applyBrowserDoctorFix(report, { fix: true, yes: opts.yes, token: opts.token, shellRc: opts.shellRc, configPaths });
139
- console.log();
140
- if (written.length > 0) {
141
- console.log(chalk.green('Updated files:'));
142
- for (const filePath of written)
143
- console.log(`- ${filePath}`);
144
- }
145
- else {
146
- console.log(chalk.yellow('No files were changed.'));
147
- }
148
- }
149
131
  });
150
132
  program.command('setup')
151
- .description('Interactive setup: configure Playwright MCP token across all detected tools')
152
- .option('--token <token>', 'Provide token directly instead of auto-detecting')
153
- .action(async (opts) => {
133
+ .description('Interactive setup: verify browser bridge connectivity')
134
+ .action(async () => {
154
135
  const { runSetup } = await import('./setup.js');
155
- await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
136
+ await runSetup({ cliVersion: PKG_VERSION });
156
137
  });
157
138
  program.command('completion')
158
139
  .description('Output shell completion script')
@@ -205,7 +186,8 @@ for (const [, cmd] of registry) {
205
186
  for (const arg of cmd.args) {
206
187
  if (arg.positional)
207
188
  continue;
208
- const v = actionOpts[arg.name];
189
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
190
+ const v = actionOpts[arg.name] ?? actionOpts[camelName];
209
191
  if (v !== undefined)
210
192
  kwargs[arg.name] = v;
211
193
  }
@@ -214,7 +196,19 @@ for (const [, cmd] of registry) {
214
196
  process.env.OPENCLI_VERBOSE = '1';
215
197
  let result;
216
198
  if (cmd.browser) {
217
- result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
199
+ result = await browserSession(PlaywrightMCP, async (page) => {
200
+ // Cookie/header strategies require same-origin context for credentialed fetch.
201
+ // In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
202
+ // Navigate to the command's domain first (mirrors cascade command behavior).
203
+ if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
204
+ try {
205
+ await page.goto(`https://${cmd.domain}`);
206
+ await page.wait(2);
207
+ }
208
+ catch { }
209
+ }
210
+ return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
211
+ });
218
212
  }
219
213
  else {
220
214
  result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
@@ -23,6 +23,7 @@ function createMockPage(overrides = {}) {
23
23
  autoScroll: vi.fn(),
24
24
  installInterceptor: vi.fn(),
25
25
  getInterceptedRequests: vi.fn().mockResolvedValue([]),
26
+ screenshot: vi.fn().mockResolvedValue(''),
26
27
  ...overrides,
27
28
  };
28
29
  }
@@ -8,6 +8,7 @@ import { stepFetch } from './steps/fetch.js';
8
8
  import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
9
9
  import { stepIntercept } from './steps/intercept.js';
10
10
  import { stepTap } from './steps/tap.js';
11
+ import { stepDownload } from './steps/download.js';
11
12
  const _stepRegistry = new Map();
12
13
  /**
13
14
  * Get a registered step handler by name.
@@ -39,3 +40,4 @@ registerStep('sort', stepSort);
39
40
  registerStep('limit', stepLimit);
40
41
  registerStep('intercept', stepIntercept);
41
42
  registerStep('tap', stepTap);
43
+ registerStep('download', stepDownload);
@@ -2,7 +2,7 @@
2
2
  * Pipeline step: navigate, click, type, wait, press, snapshot.
3
3
  * Browser interaction primitives.
4
4
  */
5
- import { render, normalizeEvaluateSource } from '../template.js';
5
+ import { render } from '../template.js';
6
6
  export async function stepNavigate(page, params, data, args) {
7
7
  const url = render(params, { args, data });
8
8
  await page.goto(String(url));
@@ -49,7 +49,7 @@ export async function stepSnapshot(page, params, _data, _args) {
49
49
  }
50
50
  export async function stepEvaluate(page, params, data, args) {
51
51
  const js = String(render(params, { args, data }));
52
- let result = await page.evaluate(normalizeEvaluateSource(js));
52
+ let result = await page.evaluate(js);
53
53
  // MCP may return JSON as a string — auto-parse it
54
54
  if (typeof result === 'string') {
55
55
  const trimmed = result.trim();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pipeline step: download — file download with concurrency and progress.
3
+ *
4
+ * Supports:
5
+ * - Direct HTTP downloads (images, documents)
6
+ * - yt-dlp integration for video platforms
7
+ * - Browser cookie forwarding for authenticated downloads
8
+ * - Filename templating and deduplication
9
+ */
10
+ import type { IPage } from '../../types.js';
11
+ export interface DownloadResult {
12
+ status: 'success' | 'skipped' | 'failed';
13
+ path?: string;
14
+ size?: number;
15
+ error?: string;
16
+ duration?: number;
17
+ }
18
+ /**
19
+ * Download step handler for YAML pipelines.
20
+ *
21
+ * Usage in YAML:
22
+ * ```yaml
23
+ * pipeline:
24
+ * - download:
25
+ * url: ${{ item.imageUrl }}
26
+ * dir: ./downloads
27
+ * filename: ${{ item.title }}.jpg
28
+ * concurrency: 5
29
+ * skip_existing: true
30
+ * use_ytdlp: false
31
+ * type: auto
32
+ * ```
33
+ */
34
+ export declare function stepDownload(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Pipeline step: download — file download with concurrency and progress.
3
+ *
4
+ * Supports:
5
+ * - Direct HTTP downloads (images, documents)
6
+ * - yt-dlp integration for video platforms
7
+ * - Browser cookie forwarding for authenticated downloads
8
+ * - Filename templating and deduplication
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { render } from '../template.js';
13
+ import { httpDownload, ytdlpDownload, saveDocument, detectContentType, requiresYtdlp, sanitizeFilename, generateFilename, exportCookiesToNetscape, getTempDir, } from '../../download/index.js';
14
+ import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
15
+ /**
16
+ * Simple async concurrency limiter for downloads.
17
+ */
18
+ async function mapConcurrent(items, limit, fn) {
19
+ const results = new Array(items.length);
20
+ let index = 0;
21
+ async function worker() {
22
+ while (index < items.length) {
23
+ const i = index++;
24
+ results[i] = await fn(items[i], i);
25
+ }
26
+ }
27
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
28
+ await Promise.all(workers);
29
+ return results;
30
+ }
31
+ /**
32
+ * Extract cookies from browser page.
33
+ */
34
+ async function extractBrowserCookies(page, domain) {
35
+ try {
36
+ // Use browser evaluate to get document.cookie
37
+ const cookieString = await page.evaluate(`(() => document.cookie)()`);
38
+ return typeof cookieString === 'string' ? cookieString : '';
39
+ }
40
+ catch {
41
+ return '';
42
+ }
43
+ }
44
+ /**
45
+ * Extract cookies as array for yt-dlp Netscape format.
46
+ */
47
+ async function extractCookiesArray(page, domain) {
48
+ try {
49
+ const cookieString = await extractBrowserCookies(page);
50
+ if (!cookieString)
51
+ return [];
52
+ return cookieString.split(';').map((c) => {
53
+ const [name, ...rest] = c.trim().split('=');
54
+ return {
55
+ name: name || '',
56
+ value: rest.join('=') || '',
57
+ domain,
58
+ path: '/',
59
+ secure: true,
60
+ httpOnly: false,
61
+ };
62
+ }).filter((c) => c.name);
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
68
+ /**
69
+ * Download step handler for YAML pipelines.
70
+ *
71
+ * Usage in YAML:
72
+ * ```yaml
73
+ * pipeline:
74
+ * - download:
75
+ * url: ${{ item.imageUrl }}
76
+ * dir: ./downloads
77
+ * filename: ${{ item.title }}.jpg
78
+ * concurrency: 5
79
+ * skip_existing: true
80
+ * use_ytdlp: false
81
+ * type: auto
82
+ * ```
83
+ */
84
+ export async function stepDownload(page, params, data, args) {
85
+ // Parse parameters with defaults
86
+ const urlTemplate = typeof params === 'string' ? params : (params?.url ?? '');
87
+ const dirTemplate = params?.dir ?? './downloads';
88
+ const filenameTemplate = params?.filename ?? '';
89
+ const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 3;
90
+ const skipExisting = params?.skip_existing !== false;
91
+ const timeout = typeof params?.timeout === 'number' ? params.timeout * 1000 : 30000;
92
+ const useYtdlp = params?.use_ytdlp ?? false;
93
+ const ytdlpArgs = Array.isArray(params?.ytdlp_args) ? params.ytdlp_args : [];
94
+ const contentType = params?.type ?? 'auto';
95
+ const showProgress = params?.progress !== false;
96
+ const contentTemplate = params?.content;
97
+ const metadataTemplate = params?.metadata;
98
+ // Resolve output directory
99
+ const dir = String(render(dirTemplate, { args, data }));
100
+ fs.mkdirSync(dir, { recursive: true });
101
+ // Normalize data to array
102
+ const items = Array.isArray(data) ? data : data ? [data] : [];
103
+ if (items.length === 0) {
104
+ return [];
105
+ }
106
+ // Create progress tracker
107
+ const tracker = new DownloadProgressTracker(items.length, showProgress);
108
+ // Extract cookies if browser is available
109
+ let cookies = '';
110
+ let cookiesFile;
111
+ if (page) {
112
+ cookies = await extractBrowserCookies(page);
113
+ // For yt-dlp, we need to export cookies to Netscape format
114
+ if (useYtdlp || items.some((item, index) => {
115
+ const url = String(render(urlTemplate, { args, data, item, index }));
116
+ return requiresYtdlp(url);
117
+ })) {
118
+ try {
119
+ // Try to get domain from first URL
120
+ const firstUrl = String(render(urlTemplate, { args, data, item: items[0], index: 0 }));
121
+ const domain = new URL(firstUrl).hostname;
122
+ const cookiesArray = await extractCookiesArray(page, domain);
123
+ if (cookiesArray.length > 0) {
124
+ const tempDir = getTempDir();
125
+ fs.mkdirSync(tempDir, { recursive: true });
126
+ cookiesFile = path.join(tempDir, `cookies_${Date.now()}.txt`);
127
+ exportCookiesToNetscape(cookiesArray, cookiesFile);
128
+ }
129
+ }
130
+ catch {
131
+ // Ignore cookie extraction errors
132
+ }
133
+ }
134
+ }
135
+ // Process downloads with concurrency
136
+ const results = await mapConcurrent(items, concurrency, async (item, index) => {
137
+ const startTime = Date.now();
138
+ // Render URL
139
+ const url = String(render(urlTemplate, { args, data, item, index }));
140
+ if (!url) {
141
+ tracker.onFileComplete(false);
142
+ return {
143
+ ...item,
144
+ _download: { status: 'failed', error: 'Empty URL' },
145
+ };
146
+ }
147
+ // Render filename
148
+ let filename;
149
+ if (filenameTemplate) {
150
+ filename = String(render(filenameTemplate, { args, data, item, index }));
151
+ }
152
+ else {
153
+ filename = generateFilename(url, index);
154
+ }
155
+ filename = sanitizeFilename(filename);
156
+ const destPath = path.join(dir, filename);
157
+ // Check if file exists and skip_existing is true
158
+ if (skipExisting && fs.existsSync(destPath)) {
159
+ tracker.onFileComplete(true, true);
160
+ return {
161
+ ...item,
162
+ _download: {
163
+ status: 'skipped',
164
+ path: destPath,
165
+ size: fs.statSync(destPath).size,
166
+ },
167
+ };
168
+ }
169
+ // Create progress bar for this file
170
+ const progressBar = tracker.onFileStart(filename, index);
171
+ // Determine download method
172
+ const detectedType = contentType === 'auto' ? detectContentType(url) : contentType;
173
+ const shouldUseYtdlp = useYtdlp || (detectedType === 'video' && requiresYtdlp(url));
174
+ let result;
175
+ try {
176
+ if (detectedType === 'document' && contentTemplate) {
177
+ // Save extracted content as document
178
+ const content = String(render(contentTemplate, { args, data, item, index }));
179
+ const metadata = metadataTemplate
180
+ ? Object.fromEntries(Object.entries(metadataTemplate).map(([k, v]) => [k, render(v, { args, data, item, index })]))
181
+ : undefined;
182
+ const ext = path.extname(filename).toLowerCase();
183
+ const format = ext === '.json' ? 'json' : ext === '.html' ? 'html' : 'markdown';
184
+ result = await saveDocument(content, destPath, format, metadata);
185
+ if (progressBar) {
186
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
187
+ }
188
+ }
189
+ else if (shouldUseYtdlp) {
190
+ // Use yt-dlp for video downloads
191
+ result = await ytdlpDownload(url, destPath, {
192
+ cookiesFile,
193
+ extraArgs: ytdlpArgs,
194
+ onProgress: (percent) => {
195
+ if (progressBar) {
196
+ progressBar.update(percent, 100);
197
+ }
198
+ },
199
+ });
200
+ if (progressBar) {
201
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
202
+ }
203
+ }
204
+ else {
205
+ // Direct HTTP download
206
+ result = await httpDownload(url, destPath, {
207
+ cookies,
208
+ timeout,
209
+ onProgress: (received, total) => {
210
+ if (progressBar) {
211
+ progressBar.update(received, total);
212
+ }
213
+ },
214
+ });
215
+ if (progressBar) {
216
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
217
+ }
218
+ }
219
+ }
220
+ catch (err) {
221
+ result = { success: false, size: 0, error: err.message };
222
+ if (progressBar) {
223
+ progressBar.fail(err.message);
224
+ }
225
+ }
226
+ tracker.onFileComplete(result.success);
227
+ const duration = Date.now() - startTime;
228
+ return {
229
+ ...item,
230
+ _download: {
231
+ status: result.success ? 'success' : 'failed',
232
+ path: result.success ? destPath : undefined,
233
+ size: result.size,
234
+ error: result.error,
235
+ duration,
236
+ },
237
+ };
238
+ });
239
+ // Cleanup temp cookie file
240
+ if (cookiesFile && fs.existsSync(cookiesFile)) {
241
+ try {
242
+ fs.unlinkSync(cookiesFile);
243
+ }
244
+ catch {
245
+ // Ignore cleanup errors
246
+ }
247
+ }
248
+ // Show summary
249
+ tracker.finish();
250
+ return results;
251
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pipeline step: intercept — declarative XHR interception.
3
3
  */
4
- import { render } from '../template.js';
4
+ import { render, normalizeEvaluateSource } from '../template.js';
5
5
  import { generateInterceptorJs, generateReadInterceptedJs } from '../../interceptor.js';
6
6
  export async function stepIntercept(page, params, data, args) {
7
7
  const cfg = typeof params === 'object' ? params : {};
@@ -20,7 +20,6 @@ export async function stepIntercept(page, params, data, args) {
20
20
  }
21
21
  else if (trigger.startsWith('evaluate:')) {
22
22
  const js = trigger.slice('evaluate:'.length);
23
- const { normalizeEvaluateSource } = await import('../template.js');
24
23
  await page.evaluate(normalizeEvaluateSource(render(js, { args, data })));
25
24
  }
26
25
  else if (trigger.startsWith('click:')) {
@@ -114,6 +114,34 @@ function applyFilter(filterExpr, value) {
114
114
  return Array.isArray(value) ? value[value.length - 1] : value;
115
115
  case 'json':
116
116
  return JSON.stringify(value ?? null);
117
+ case 'slugify':
118
+ // Convert to URL-safe slug
119
+ return typeof value === 'string'
120
+ ? value
121
+ .toLowerCase()
122
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
123
+ .replace(/^-|-$/g, '')
124
+ : value;
125
+ case 'sanitize':
126
+ // Remove invalid filename characters
127
+ return typeof value === 'string'
128
+ ? value.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
129
+ : value;
130
+ case 'ext': {
131
+ // Extract file extension from URL or path
132
+ if (typeof value !== 'string')
133
+ return value;
134
+ const lastDot = value.lastIndexOf('.');
135
+ const lastSlash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
136
+ return lastDot > lastSlash ? value.slice(lastDot) : '';
137
+ }
138
+ case 'basename': {
139
+ // Extract filename from URL or path
140
+ if (typeof value !== 'string')
141
+ return value;
142
+ const parts = value.split(/[/\\]/);
143
+ return parts[parts.length - 1] || value;
144
+ }
117
145
  default:
118
146
  return value;
119
147
  }
package/dist/setup.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * setup.ts — Interactive browser setup for opencli
3
+ *
4
+ * Simplified for daemon-based architecture. No more token management.
5
+ * Just verifies daemon + extension connectivity.
6
+ */
1
7
  export declare function runSetup(opts?: {
2
8
  cliVersion?: string;
3
9
  token?: string;