@jackwener/opencli 1.3.3 → 1.4.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 (496) hide show
  1. package/.github/pull_request_template.md +3 -1
  2. package/.github/workflows/build-extension.yml +7 -1
  3. package/.github/workflows/ci.yml +29 -3
  4. package/.github/workflows/docs.yml +1 -1
  5. package/.github/workflows/e2e-headed.yml +20 -0
  6. package/.github/workflows/release.yml +1 -1
  7. package/.github/workflows/security.yml +0 -3
  8. package/CHANGELOG.md +55 -0
  9. package/CONTRIBUTING.md +6 -3
  10. package/README.md +30 -3
  11. package/README.zh-CN.md +30 -3
  12. package/SKILL.md +7 -1
  13. package/TESTING.md +1 -0
  14. package/chatwise-opencli.ps1 +82 -0
  15. package/dist/analysis.d.ts +38 -0
  16. package/dist/analysis.js +166 -0
  17. package/dist/browser/cdp.d.ts +0 -4
  18. package/dist/browser/cdp.js +53 -41
  19. package/dist/browser/cdp.test.d.ts +1 -0
  20. package/dist/browser/cdp.test.js +52 -0
  21. package/dist/browser/dom-snapshot.d.ts +2 -2
  22. package/dist/browser/dom-snapshot.js +54 -1
  23. package/dist/browser/dom-snapshot.test.js +36 -0
  24. package/dist/browser/index.d.ts +2 -2
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/mcp.d.ts +0 -2
  27. package/dist/browser/mcp.js +2 -3
  28. package/dist/browser/page.d.ts +4 -3
  29. package/dist/browser/page.js +34 -37
  30. package/dist/browser/stealth.d.ts +0 -2
  31. package/dist/browser/stealth.js +24 -9
  32. package/dist/browser.test.js +2 -2
  33. package/dist/build-manifest.js +15 -9
  34. package/dist/build-manifest.test.js +12 -0
  35. package/dist/cascade.js +4 -2
  36. package/dist/cli-manifest.json +639 -258
  37. package/dist/cli.js +57 -29
  38. package/dist/clis/_shared/desktop-commands.d.ts +22 -0
  39. package/dist/clis/_shared/desktop-commands.js +108 -0
  40. package/dist/clis/antigravity/serve.js +5 -2
  41. package/dist/clis/arxiv/search.js +1 -1
  42. package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
  43. package/dist/clis/bilibili/dynamic.test.js +68 -0
  44. package/dist/clis/bilibili/favorite.js +4 -2
  45. package/dist/clis/bilibili/following.js +3 -2
  46. package/dist/clis/bilibili/subtitle.js +8 -7
  47. package/dist/clis/bilibili/utils.js +2 -2
  48. package/dist/clis/boss/batchgreet.js +1 -1
  49. package/dist/clis/boss/chatlist.js +1 -1
  50. package/dist/clis/boss/chatmsg.js +1 -1
  51. package/dist/clis/boss/detail.js +1 -1
  52. package/dist/clis/boss/exchange.js +1 -1
  53. package/dist/clis/boss/greet.js +1 -1
  54. package/dist/clis/boss/invite.js +1 -1
  55. package/dist/clis/boss/joblist.js +1 -1
  56. package/dist/clis/boss/mark.js +4 -3
  57. package/dist/clis/boss/recommend.js +1 -1
  58. package/dist/clis/boss/resume.js +1 -1
  59. package/dist/clis/boss/search.js +1 -1
  60. package/dist/clis/boss/send.js +5 -4
  61. package/dist/clis/boss/stats.js +1 -1
  62. package/dist/clis/chatgpt/ask.js +4 -0
  63. package/dist/clis/chatgpt/new.js +5 -1
  64. package/dist/clis/chatgpt/read.js +5 -1
  65. package/dist/clis/chatgpt/send.js +2 -1
  66. package/dist/clis/chatgpt/status.js +5 -1
  67. package/dist/clis/chatwise/ask.js +8 -2
  68. package/dist/clis/chatwise/export.js +2 -0
  69. package/dist/clis/chatwise/history.js +2 -0
  70. package/dist/clis/chatwise/model.js +8 -3
  71. package/dist/clis/chatwise/new.js +3 -18
  72. package/dist/clis/chatwise/read.js +2 -0
  73. package/dist/clis/chatwise/screenshot.js +3 -27
  74. package/dist/clis/chatwise/send.js +8 -2
  75. package/dist/clis/chatwise/shared.d.ts +2 -0
  76. package/dist/clis/chatwise/shared.js +6 -0
  77. package/dist/clis/chatwise/status.js +3 -22
  78. package/dist/clis/codex/ask.js +6 -2
  79. package/dist/clis/codex/dump.js +2 -25
  80. package/dist/clis/codex/new.js +2 -25
  81. package/dist/clis/codex/screenshot.js +2 -27
  82. package/dist/clis/codex/send.js +6 -4
  83. package/dist/clis/codex/status.js +2 -22
  84. package/dist/clis/cursor/ask.js +2 -1
  85. package/dist/clis/cursor/composer.js +2 -1
  86. package/dist/clis/cursor/dump.js +2 -25
  87. package/dist/clis/cursor/new.js +2 -18
  88. package/dist/clis/cursor/read.js +2 -1
  89. package/dist/clis/cursor/screenshot.js +1 -30
  90. package/dist/clis/cursor/send.js +2 -1
  91. package/dist/clis/cursor/status.js +2 -21
  92. package/dist/clis/dictionary/examples.yaml +25 -0
  93. package/dist/clis/dictionary/search.yaml +27 -0
  94. package/dist/clis/dictionary/synonyms.yaml +25 -0
  95. package/dist/clis/douban/book-hot.js +1 -1
  96. package/dist/clis/douban/movie-hot.js +1 -1
  97. package/dist/clis/douban/search.js +1 -1
  98. package/dist/clis/douban/utils.d.ts +4 -1
  99. package/dist/clis/douban/utils.js +156 -1
  100. package/dist/clis/doubao/ask.js +1 -1
  101. package/dist/clis/doubao/new.js +1 -1
  102. package/dist/clis/doubao/read.js +1 -1
  103. package/dist/clis/doubao/send.js +1 -1
  104. package/dist/clis/doubao/status.js +1 -1
  105. package/dist/clis/doubao-app/ask.js +1 -1
  106. package/dist/clis/doubao-app/new.js +1 -1
  107. package/dist/clis/doubao-app/read.js +1 -1
  108. package/dist/clis/doubao-app/send.js +1 -1
  109. package/dist/clis/grok/ask.d.ts +4 -0
  110. package/dist/clis/grok/ask.js +28 -10
  111. package/dist/clis/grok/ask.test.js +18 -0
  112. package/dist/clis/jd/item.d.ts +1 -0
  113. package/dist/clis/jd/item.js +96 -0
  114. package/dist/clis/jd/item.test.d.ts +1 -0
  115. package/dist/clis/jd/item.test.js +28 -0
  116. package/dist/clis/jike/feed.js +1 -1
  117. package/dist/clis/jike/search.js +1 -1
  118. package/dist/clis/linkedin/search.js +5 -4
  119. package/dist/clis/linkedin/timeline.d.ts +21 -0
  120. package/dist/clis/linkedin/timeline.js +503 -0
  121. package/dist/clis/linkedin/timeline.test.d.ts +1 -0
  122. package/dist/clis/linkedin/timeline.test.js +81 -0
  123. package/dist/clis/medium/feed.js +1 -1
  124. package/dist/clis/medium/search.js +1 -1
  125. package/dist/clis/medium/user.js +1 -1
  126. package/dist/clis/medium/{shared.js → utils.js} +2 -1
  127. package/dist/clis/pixiv/detail.yaml +49 -0
  128. package/dist/clis/pixiv/download.d.ts +7 -0
  129. package/dist/clis/pixiv/download.js +78 -0
  130. package/dist/clis/pixiv/download.test.d.ts +1 -0
  131. package/dist/clis/pixiv/download.test.js +87 -0
  132. package/dist/clis/pixiv/illusts.d.ts +8 -0
  133. package/dist/clis/pixiv/illusts.js +65 -0
  134. package/dist/clis/pixiv/illusts.test.d.ts +1 -0
  135. package/dist/clis/pixiv/illusts.test.js +99 -0
  136. package/dist/clis/pixiv/ranking.yaml +53 -0
  137. package/dist/clis/pixiv/search.d.ts +6 -0
  138. package/dist/clis/pixiv/search.js +43 -0
  139. package/dist/clis/pixiv/search.test.d.ts +1 -0
  140. package/dist/clis/pixiv/search.test.js +83 -0
  141. package/dist/clis/pixiv/test-utils.d.ts +12 -0
  142. package/dist/clis/pixiv/test-utils.js +23 -0
  143. package/dist/clis/pixiv/user.yaml +46 -0
  144. package/dist/clis/pixiv/utils.d.ts +27 -0
  145. package/dist/clis/pixiv/utils.js +49 -0
  146. package/dist/clis/reddit/comment.js +2 -1
  147. package/dist/clis/reddit/read.js +4 -3
  148. package/dist/clis/reddit/read.test.d.ts +1 -0
  149. package/dist/clis/reddit/read.test.js +28 -0
  150. package/dist/clis/reddit/save.js +2 -1
  151. package/dist/clis/reddit/saved.js +7 -3
  152. package/dist/clis/reddit/subscribe.js +2 -1
  153. package/dist/clis/reddit/upvote.js +2 -1
  154. package/dist/clis/reddit/upvoted.js +7 -3
  155. package/dist/clis/sinablog/article.js +1 -1
  156. package/dist/clis/sinablog/hot.js +1 -1
  157. package/dist/clis/sinablog/user.js +1 -1
  158. package/dist/clis/substack/feed.js +1 -1
  159. package/dist/clis/substack/publication.js +1 -1
  160. package/dist/clis/substack/search.js +3 -2
  161. package/dist/clis/substack/{shared.js → utils.js} +3 -2
  162. package/dist/clis/tiktok/search.yaml +2 -1
  163. package/dist/clis/twitter/accept.js +2 -1
  164. package/dist/clis/twitter/article.js +4 -1
  165. package/dist/clis/twitter/block.js +2 -1
  166. package/dist/clis/twitter/bookmark.js +2 -1
  167. package/dist/clis/twitter/bookmarks.js +3 -2
  168. package/dist/clis/twitter/delete.js +2 -1
  169. package/dist/clis/twitter/follow.js +2 -1
  170. package/dist/clis/twitter/followers.js +3 -2
  171. package/dist/clis/twitter/following.js +3 -2
  172. package/dist/clis/twitter/hide-reply.js +2 -1
  173. package/dist/clis/twitter/like.js +2 -1
  174. package/dist/clis/twitter/notifications.js +2 -1
  175. package/dist/clis/twitter/post.js +2 -1
  176. package/dist/clis/twitter/profile.js +5 -2
  177. package/dist/clis/twitter/reply-dm.js +2 -1
  178. package/dist/clis/twitter/reply.js +2 -1
  179. package/dist/clis/twitter/search.js +30 -13
  180. package/dist/clis/twitter/search.test.d.ts +1 -0
  181. package/dist/clis/twitter/search.test.js +104 -0
  182. package/dist/clis/twitter/thread.js +2 -2
  183. package/dist/clis/twitter/timeline.js +3 -2
  184. package/dist/clis/twitter/trending.js +3 -2
  185. package/dist/clis/twitter/unblock.js +2 -1
  186. package/dist/clis/twitter/unbookmark.js +2 -1
  187. package/dist/clis/twitter/unfollow.js +2 -1
  188. package/dist/clis/v2ex/daily.js +3 -2
  189. package/dist/clis/v2ex/me.js +3 -2
  190. package/dist/clis/v2ex/notifications.js +4 -4
  191. package/dist/clis/web/read.d.ts +16 -0
  192. package/dist/clis/web/read.js +202 -0
  193. package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
  194. package/dist/clis/xueqiu/danjuan-utils.js +126 -0
  195. package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
  196. package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
  197. package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
  198. package/dist/clis/xueqiu/fund-holdings.js +28 -0
  199. package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
  200. package/dist/clis/xueqiu/fund-snapshot.js +25 -0
  201. package/dist/clis/youtube/transcript.js +5 -4
  202. package/dist/clis/youtube/video.js +3 -2
  203. package/dist/daemon.js +7 -3
  204. package/dist/discovery.js +11 -10
  205. package/dist/doctor.js +2 -1
  206. package/dist/download/index.d.ts +4 -12
  207. package/dist/download/index.js +33 -12
  208. package/dist/download/index.test.js +79 -2
  209. package/dist/download/media-download.js +4 -2
  210. package/dist/engine.test.js +76 -4
  211. package/dist/execution.d.ts +1 -9
  212. package/dist/execution.js +56 -46
  213. package/dist/explore.js +12 -111
  214. package/dist/external-clis.yaml +0 -8
  215. package/dist/external.js +7 -5
  216. package/dist/external.test.js +4 -0
  217. package/dist/generate.d.ts +0 -9
  218. package/dist/generate.js +4 -20
  219. package/dist/hooks.d.ts +46 -0
  220. package/dist/hooks.js +56 -0
  221. package/dist/hooks.test.d.ts +4 -0
  222. package/dist/hooks.test.js +92 -0
  223. package/dist/interceptor.js +70 -23
  224. package/dist/main.js +2 -0
  225. package/dist/output.js +12 -6
  226. package/dist/pipeline/executor.js +1 -1
  227. package/dist/pipeline/steps/browser.js +1 -3
  228. package/dist/pipeline/steps/download.js +42 -26
  229. package/dist/pipeline/steps/download.test.d.ts +1 -0
  230. package/dist/pipeline/steps/download.test.js +101 -0
  231. package/dist/pipeline/steps/fetch.js +40 -22
  232. package/dist/pipeline/steps/fetch.test.d.ts +1 -0
  233. package/dist/pipeline/steps/fetch.test.js +123 -0
  234. package/dist/pipeline/steps/transform.js +2 -6
  235. package/dist/pipeline/template.js +66 -52
  236. package/dist/pipeline/template.test.js +28 -0
  237. package/dist/pipeline/transform.test.js +18 -0
  238. package/dist/plugin.d.ts +40 -1
  239. package/dist/plugin.js +214 -17
  240. package/dist/plugin.test.d.ts +1 -1
  241. package/dist/plugin.test.js +219 -3
  242. package/dist/record.js +6 -98
  243. package/dist/registry-api.d.ts +2 -0
  244. package/dist/registry-api.js +1 -0
  245. package/dist/registry.d.ts +5 -2
  246. package/dist/registry.js +1 -2
  247. package/dist/runtime.d.ts +0 -1
  248. package/dist/runtime.js +14 -4
  249. package/dist/snapshotFormatter.d.ts +7 -14
  250. package/dist/snapshotFormatter.js +38 -78
  251. package/dist/utils.d.ts +9 -0
  252. package/dist/utils.js +29 -0
  253. package/dist/validate.js +3 -5
  254. package/dist/yaml-schema.d.ts +26 -0
  255. package/dist/yaml-schema.js +5 -0
  256. package/docs/.vitepress/config.mts +3 -0
  257. package/docs/adapters/browser/dictionary.md +27 -0
  258. package/docs/adapters/browser/jd.md +27 -0
  259. package/docs/adapters/browser/linkedin.md +6 -0
  260. package/docs/adapters/browser/pixiv.md +92 -0
  261. package/docs/adapters/browser/web.md +30 -0
  262. package/docs/adapters/browser/xueqiu.md +27 -9
  263. package/docs/adapters/index.md +3 -1
  264. package/docs/comparison.md +125 -0
  265. package/docs/developer/contributing.md +21 -2
  266. package/docs/developer/testing.md +14 -8
  267. package/docs/developer/ts-adapter.md +18 -0
  268. package/docs/developer/yaml-adapter.md +16 -0
  269. package/docs/guide/plugins.md +10 -0
  270. package/docs/zh/guide/plugins.md +10 -0
  271. package/extension/dist/background.js +519 -444
  272. package/extension/manifest.json +1 -1
  273. package/extension/package.json +1 -1
  274. package/extension/src/background.test.ts +46 -1
  275. package/extension/src/background.ts +108 -33
  276. package/extension/src/cdp.ts +9 -9
  277. package/package.json +3 -2
  278. package/scripts/check-doc-coverage.sh +2 -0
  279. package/src/analysis.ts +170 -0
  280. package/src/browser/cdp.test.ts +66 -0
  281. package/src/browser/cdp.ts +59 -44
  282. package/src/browser/dom-snapshot.test.ts +42 -0
  283. package/src/browser/dom-snapshot.ts +56 -3
  284. package/src/browser/index.ts +2 -2
  285. package/src/browser/mcp.ts +2 -4
  286. package/src/browser/page.ts +34 -37
  287. package/src/browser/stealth.ts +24 -10
  288. package/src/browser.test.ts +2 -2
  289. package/src/build-manifest.test.ts +14 -0
  290. package/src/build-manifest.ts +13 -31
  291. package/src/cascade.ts +5 -3
  292. package/src/cli.ts +66 -34
  293. package/src/clis/_shared/desktop-commands.ts +121 -0
  294. package/src/clis/antigravity/serve.ts +6 -3
  295. package/src/clis/arxiv/search.ts +1 -1
  296. package/src/clis/bilibili/dynamic.test.ts +79 -0
  297. package/src/clis/bilibili/favorite.ts +5 -2
  298. package/src/clis/bilibili/following.ts +3 -2
  299. package/src/clis/bilibili/subtitle.ts +8 -7
  300. package/src/clis/bilibili/utils.ts +2 -2
  301. package/src/clis/boss/batchgreet.ts +1 -1
  302. package/src/clis/boss/chatlist.ts +1 -1
  303. package/src/clis/boss/chatmsg.ts +1 -1
  304. package/src/clis/boss/detail.ts +1 -1
  305. package/src/clis/boss/exchange.ts +1 -1
  306. package/src/clis/boss/greet.ts +1 -1
  307. package/src/clis/boss/invite.ts +1 -1
  308. package/src/clis/boss/joblist.ts +1 -1
  309. package/src/clis/boss/mark.ts +4 -3
  310. package/src/clis/boss/recommend.ts +1 -1
  311. package/src/clis/boss/resume.ts +1 -1
  312. package/src/clis/boss/search.ts +1 -1
  313. package/src/clis/boss/send.ts +5 -4
  314. package/src/clis/boss/stats.ts +1 -1
  315. package/src/clis/chatgpt/ask.ts +5 -0
  316. package/src/clis/chatgpt/new.ts +7 -2
  317. package/src/clis/chatgpt/read.ts +7 -2
  318. package/src/clis/chatgpt/send.ts +3 -2
  319. package/src/clis/chatgpt/status.ts +6 -1
  320. package/src/clis/chatwise/ask.ts +7 -2
  321. package/src/clis/chatwise/export.ts +2 -0
  322. package/src/clis/chatwise/history.ts +2 -0
  323. package/src/clis/chatwise/model.ts +7 -3
  324. package/src/clis/chatwise/new.ts +3 -20
  325. package/src/clis/chatwise/read.ts +2 -0
  326. package/src/clis/chatwise/screenshot.ts +3 -32
  327. package/src/clis/chatwise/send.ts +7 -2
  328. package/src/clis/chatwise/shared.ts +8 -0
  329. package/src/clis/chatwise/status.ts +3 -24
  330. package/src/clis/codex/ask.ts +5 -2
  331. package/src/clis/codex/dump.ts +2 -27
  332. package/src/clis/codex/new.ts +2 -28
  333. package/src/clis/codex/screenshot.ts +2 -32
  334. package/src/clis/codex/send.ts +5 -4
  335. package/src/clis/codex/status.ts +2 -24
  336. package/src/clis/cursor/ask.ts +2 -1
  337. package/src/clis/cursor/composer.ts +2 -1
  338. package/src/clis/cursor/dump.ts +2 -27
  339. package/src/clis/cursor/new.ts +2 -20
  340. package/src/clis/cursor/read.ts +2 -1
  341. package/src/clis/cursor/screenshot.ts +1 -36
  342. package/src/clis/cursor/send.ts +2 -1
  343. package/src/clis/cursor/status.ts +2 -22
  344. package/src/clis/dictionary/examples.yaml +25 -0
  345. package/src/clis/dictionary/search.yaml +27 -0
  346. package/src/clis/dictionary/synonyms.yaml +25 -0
  347. package/src/clis/douban/book-hot.ts +1 -1
  348. package/src/clis/douban/movie-hot.ts +1 -1
  349. package/src/clis/douban/search.ts +1 -1
  350. package/src/clis/douban/utils.ts +165 -1
  351. package/src/clis/doubao/ask.ts +1 -1
  352. package/src/clis/doubao/new.ts +1 -1
  353. package/src/clis/doubao/read.ts +1 -1
  354. package/src/clis/doubao/send.ts +1 -1
  355. package/src/clis/doubao/status.ts +1 -1
  356. package/src/clis/doubao-app/ask.ts +1 -1
  357. package/src/clis/doubao-app/new.ts +1 -1
  358. package/src/clis/doubao-app/read.ts +1 -1
  359. package/src/clis/doubao-app/send.ts +1 -1
  360. package/src/clis/grok/ask.test.ts +25 -0
  361. package/src/clis/grok/ask.ts +25 -12
  362. package/src/clis/jd/item.test.ts +35 -0
  363. package/src/clis/jd/item.ts +101 -0
  364. package/src/clis/jike/feed.ts +1 -1
  365. package/src/clis/jike/search.ts +1 -1
  366. package/src/clis/linkedin/search.ts +5 -4
  367. package/src/clis/linkedin/timeline.test.ts +99 -0
  368. package/src/clis/linkedin/timeline.ts +532 -0
  369. package/src/clis/medium/feed.ts +1 -1
  370. package/src/clis/medium/search.ts +1 -1
  371. package/src/clis/medium/user.ts +1 -1
  372. package/src/clis/medium/{shared.ts → utils.ts} +2 -1
  373. package/src/clis/pixiv/detail.yaml +49 -0
  374. package/src/clis/pixiv/download.test.ts +114 -0
  375. package/src/clis/pixiv/download.ts +91 -0
  376. package/src/clis/pixiv/illusts.test.ts +115 -0
  377. package/src/clis/pixiv/illusts.ts +78 -0
  378. package/src/clis/pixiv/ranking.yaml +53 -0
  379. package/src/clis/pixiv/search.test.ts +97 -0
  380. package/src/clis/pixiv/search.ts +53 -0
  381. package/src/clis/pixiv/test-utils.ts +29 -0
  382. package/src/clis/pixiv/user.yaml +46 -0
  383. package/src/clis/pixiv/utils.ts +62 -0
  384. package/src/clis/reddit/comment.ts +2 -1
  385. package/src/clis/reddit/read.test.ts +34 -0
  386. package/src/clis/reddit/read.ts +4 -3
  387. package/src/clis/reddit/save.ts +2 -1
  388. package/src/clis/reddit/saved.ts +6 -2
  389. package/src/clis/reddit/subscribe.ts +2 -1
  390. package/src/clis/reddit/upvote.ts +2 -1
  391. package/src/clis/reddit/upvoted.ts +6 -2
  392. package/src/clis/sinablog/article.ts +1 -1
  393. package/src/clis/sinablog/hot.ts +1 -1
  394. package/src/clis/sinablog/user.ts +1 -1
  395. package/src/clis/substack/feed.ts +1 -1
  396. package/src/clis/substack/publication.ts +1 -1
  397. package/src/clis/substack/search.ts +3 -2
  398. package/src/clis/substack/{shared.ts → utils.ts} +3 -2
  399. package/src/clis/tiktok/search.yaml +2 -1
  400. package/src/clis/twitter/accept.ts +2 -1
  401. package/src/clis/twitter/article.ts +3 -1
  402. package/src/clis/twitter/block.ts +2 -1
  403. package/src/clis/twitter/bookmark.ts +2 -1
  404. package/src/clis/twitter/bookmarks.ts +3 -2
  405. package/src/clis/twitter/delete.ts +2 -1
  406. package/src/clis/twitter/follow.ts +2 -1
  407. package/src/clis/twitter/followers.ts +3 -2
  408. package/src/clis/twitter/following.ts +3 -2
  409. package/src/clis/twitter/hide-reply.ts +2 -1
  410. package/src/clis/twitter/like.ts +2 -1
  411. package/src/clis/twitter/notifications.ts +2 -1
  412. package/src/clis/twitter/post.ts +2 -1
  413. package/src/clis/twitter/profile.ts +4 -2
  414. package/src/clis/twitter/reply-dm.ts +2 -1
  415. package/src/clis/twitter/reply.ts +2 -1
  416. package/src/clis/twitter/search.test.ts +113 -0
  417. package/src/clis/twitter/search.ts +38 -14
  418. package/src/clis/twitter/thread.ts +2 -2
  419. package/src/clis/twitter/timeline.ts +3 -2
  420. package/src/clis/twitter/trending.ts +3 -2
  421. package/src/clis/twitter/unblock.ts +2 -1
  422. package/src/clis/twitter/unbookmark.ts +2 -1
  423. package/src/clis/twitter/unfollow.ts +2 -1
  424. package/src/clis/v2ex/daily.ts +3 -2
  425. package/src/clis/v2ex/me.ts +3 -2
  426. package/src/clis/v2ex/notifications.ts +3 -4
  427. package/src/clis/web/read.ts +210 -0
  428. package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
  429. package/src/clis/xueqiu/danjuan-utils.ts +176 -0
  430. package/src/clis/xueqiu/fund-holdings.ts +32 -0
  431. package/src/clis/xueqiu/fund-snapshot.ts +27 -0
  432. package/src/clis/youtube/transcript.ts +5 -4
  433. package/src/clis/youtube/video.ts +3 -2
  434. package/src/daemon.ts +5 -4
  435. package/src/discovery.ts +12 -34
  436. package/src/doctor.ts +3 -2
  437. package/src/download/index.test.ts +93 -2
  438. package/src/download/index.ts +44 -23
  439. package/src/download/media-download.ts +5 -3
  440. package/src/engine.test.ts +84 -3
  441. package/src/execution.ts +62 -46
  442. package/src/explore.ts +21 -90
  443. package/src/external-clis.yaml +0 -8
  444. package/src/external.test.ts +9 -0
  445. package/src/external.ts +12 -10
  446. package/src/generate.ts +4 -41
  447. package/src/hooks.test.ts +126 -0
  448. package/src/hooks.ts +90 -0
  449. package/src/interceptor.ts +73 -23
  450. package/src/main.ts +2 -0
  451. package/src/output.ts +14 -6
  452. package/src/pipeline/executor.ts +1 -1
  453. package/src/pipeline/steps/browser.ts +1 -3
  454. package/src/pipeline/steps/download.test.ts +136 -0
  455. package/src/pipeline/steps/download.ts +47 -34
  456. package/src/pipeline/steps/fetch.test.ts +179 -0
  457. package/src/pipeline/steps/fetch.ts +39 -23
  458. package/src/pipeline/steps/transform.ts +2 -6
  459. package/src/pipeline/template.test.ts +28 -0
  460. package/src/pipeline/template.ts +67 -79
  461. package/src/pipeline/transform.test.ts +20 -0
  462. package/src/plugin.test.ts +251 -3
  463. package/src/plugin.ts +265 -21
  464. package/src/record.ts +12 -84
  465. package/src/registry-api.ts +2 -0
  466. package/src/registry.ts +7 -4
  467. package/src/runtime.ts +14 -4
  468. package/src/snapshotFormatter.ts +43 -121
  469. package/src/utils.ts +39 -0
  470. package/src/validate.ts +3 -5
  471. package/src/yaml-schema.ts +28 -0
  472. package/tests/e2e/browser-auth.test.ts +25 -0
  473. package/tests/e2e/plugin-management.test.ts +137 -0
  474. package/tests/e2e/public-commands.test.ts +34 -1
  475. package/vitest.config.ts +19 -1
  476. package/.github/workflows/pkg-pr-new.yml +0 -30
  477. package/dist/clis/douban/shared.d.ts +0 -4
  478. package/dist/clis/douban/shared.js +0 -155
  479. package/src/clis/douban/shared.ts +0 -165
  480. /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
  481. /package/dist/clis/boss/{common.js → utils.js} +0 -0
  482. /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
  483. /package/dist/clis/doubao/{common.js → utils.js} +0 -0
  484. /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
  485. /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
  486. /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
  487. /package/dist/clis/jike/{shared.js → utils.js} +0 -0
  488. /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
  489. /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
  490. /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
  491. /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
  492. /package/src/clis/boss/{common.ts → utils.ts} +0 -0
  493. /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
  494. /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
  495. /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
  496. /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCLI",
4
- "version": "1.2.6",
4
+ "version": "1.4.0",
5
5
  "description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencli-extension",
3
- "version": "1.2.6",
3
+ "version": "1.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  type Listener<T extends (...args: any[]) => void> = { addListener: (fn: T) => void };
4
4
 
@@ -96,9 +96,15 @@ function createChromeMock() {
96
96
  describe('background tab isolation', () => {
97
97
  beforeEach(() => {
98
98
  vi.resetModules();
99
+ vi.useRealTimers();
99
100
  vi.stubGlobal('WebSocket', MockWebSocket);
100
101
  });
101
102
 
103
+ afterEach(() => {
104
+ vi.useRealTimers();
105
+ vi.unstubAllGlobals();
106
+ });
107
+
102
108
  it('lists only automation-window web tabs', async () => {
103
109
  const { chrome } = createChromeMock();
104
110
  vi.stubGlobal('chrome', chrome);
@@ -133,6 +139,45 @@ describe('background tab isolation', () => {
133
139
  expect(create).toHaveBeenCalledWith({ windowId: 1, url: 'https://new.example', active: true });
134
140
  });
135
141
 
142
+ it('treats normalized same-url navigate as already complete', async () => {
143
+ const { chrome, tabs, update } = createChromeMock();
144
+ tabs[0].url = 'https://www.bilibili.com/';
145
+ tabs[0].title = 'bilibili';
146
+ tabs[0].status = 'complete';
147
+ vi.stubGlobal('chrome', chrome);
148
+
149
+ const mod = await import('./background');
150
+ mod.__test__.setAutomationWindowId('site:bilibili', 1);
151
+
152
+ const result = await mod.__test__.handleNavigate(
153
+ { id: 'same-url', action: 'navigate', url: 'https://www.bilibili.com', workspace: 'site:bilibili' },
154
+ 'site:bilibili',
155
+ );
156
+
157
+ expect(result).toEqual({
158
+ id: 'same-url',
159
+ ok: true,
160
+ data: {
161
+ title: 'bilibili',
162
+ url: 'https://www.bilibili.com/',
163
+ tabId: 1,
164
+ timedOut: false,
165
+ },
166
+ });
167
+ expect(update).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it('keeps hash routes distinct when comparing target URLs', async () => {
171
+ const { chrome } = createChromeMock();
172
+ vi.stubGlobal('chrome', chrome);
173
+
174
+ const mod = await import('./background');
175
+
176
+ expect(mod.__test__.isTargetUrl('https://example.com/', 'https://example.com')).toBe(true);
177
+ expect(mod.__test__.isTargetUrl('https://example.com/#feed', 'https://example.com/#settings')).toBe(false);
178
+ expect(mod.__test__.isTargetUrl('https://example.com/app/', 'https://example.com/app')).toBe(false);
179
+ });
180
+
136
181
  it('reports sessions per workspace', async () => {
137
182
  const { chrome } = createChromeMock();
138
183
  vi.stubGlobal('chrome', chrome);
@@ -138,7 +138,7 @@ async function getAutomationWindow(workspace: string): Promise<number> {
138
138
  // Create a new window with a data: URI that New Tab Override extensions cannot intercept.
139
139
  // Using about:blank would be hijacked by extensions like "New Tab Override".
140
140
  const win = await chrome.windows.create({
141
- url: 'data:text/html,<html></html>',
141
+ url: BLANK_PAGE,
142
142
  focused: false,
143
143
  width: 1280,
144
144
  height: 900,
@@ -229,10 +229,37 @@ async function handleCommand(cmd: Command): Promise<Result> {
229
229
 
230
230
  // ─── Action handlers ─────────────────────────────────────────────────
231
231
 
232
- /** Check if a URL can be attached via CDP (not chrome:// or chrome-extension://) */
232
+ /** Internal blank page used when no user URL is provided. */
233
+ const BLANK_PAGE = 'data:text/html,<html></html>';
234
+
235
+ /** Check if a URL can be attached via CDP — only allow http(s) and our internal blank page. */
233
236
  function isDebuggableUrl(url?: string): boolean {
234
237
  if (!url) return true; // empty/undefined = tab still loading, allow it
235
- return !url.startsWith('chrome://') && !url.startsWith('chrome-extension://');
238
+ return url.startsWith('http://') || url.startsWith('https://') || url === BLANK_PAGE;
239
+ }
240
+
241
+ /** Check if a URL is safe for user-facing navigation (http/https only). */
242
+ function isSafeNavigationUrl(url: string): boolean {
243
+ return url.startsWith('http://') || url.startsWith('https://');
244
+ }
245
+
246
+ /** Minimal URL normalization for same-page comparison: root slash + default port only. */
247
+ function normalizeUrlForComparison(url?: string): string {
248
+ if (!url) return '';
249
+ try {
250
+ const parsed = new URL(url);
251
+ if ((parsed.protocol === 'https:' && parsed.port === '443') || (parsed.protocol === 'http:' && parsed.port === '80')) {
252
+ parsed.port = '';
253
+ }
254
+ const pathname = parsed.pathname === '/' ? '' : parsed.pathname;
255
+ return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
256
+ } catch {
257
+ return url;
258
+ }
259
+ }
260
+
261
+ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean {
262
+ return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
236
263
  }
237
264
 
238
265
  /**
@@ -247,9 +274,14 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
247
274
  if (tabId !== undefined) {
248
275
  try {
249
276
  const tab = await chrome.tabs.get(tabId);
250
- if (isDebuggableUrl(tab.url)) return tabId;
251
- // Tab exists but URL is not debuggable fall through to auto-resolve
252
- console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
277
+ const session = automationSessions.get(workspace);
278
+ if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
279
+ if (session && tab.windowId !== session.windowId) {
280
+ console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
281
+ } else if (!isDebuggableUrl(tab.url)) {
282
+ // Tab exists but URL is not debuggable — fall through to auto-resolve
283
+ console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
284
+ }
253
285
  } catch {
254
286
  // Tab was closed — fall through to auto-resolve
255
287
  console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
@@ -268,7 +300,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
268
300
  // Try to reuse by navigating to a data: URI (not interceptable by New Tab Override).
269
301
  const reuseTab = tabs.find(t => t.id);
270
302
  if (reuseTab?.id) {
271
- await chrome.tabs.update(reuseTab.id, { url: 'data:text/html,<html></html>' });
303
+ await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
272
304
  await new Promise(resolve => setTimeout(resolve, 300));
273
305
  try {
274
306
  const updated = await chrome.tabs.get(reuseTab.id);
@@ -280,7 +312,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
280
312
  }
281
313
 
282
314
  // Fallback: create a new tab
283
- const newTab = await chrome.tabs.create({ windowId, url: 'data:text/html,<html></html>', active: true });
315
+ const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
284
316
  if (!newTab.id) throw new Error('Failed to create tab in automation window');
285
317
  return newTab.id;
286
318
  }
@@ -314,54 +346,79 @@ async function handleExec(cmd: Command, workspace: string): Promise<Result> {
314
346
 
315
347
  async function handleNavigate(cmd: Command, workspace: string): Promise<Result> {
316
348
  if (!cmd.url) return { id: cmd.id, ok: false, error: 'Missing url' };
349
+ if (!isSafeNavigationUrl(cmd.url)) {
350
+ return { id: cmd.id, ok: false, error: 'Blocked URL scheme -- only http:// and https:// are allowed' };
351
+ }
317
352
  const tabId = await resolveTabId(cmd.tabId, workspace);
318
353
 
319
- // Capture the current URL before navigation to detect actual URL change
320
354
  const beforeTab = await chrome.tabs.get(tabId);
321
- const beforeUrl = beforeTab.url ?? '';
355
+ const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
322
356
  const targetUrl = cmd.url;
323
357
 
358
+ // Fast-path: tab is already at the target URL and fully loaded.
359
+ if (beforeTab.status === 'complete' && isTargetUrl(beforeTab.url, targetUrl)) {
360
+ return {
361
+ id: cmd.id,
362
+ ok: true,
363
+ data: { title: beforeTab.title, url: beforeTab.url, tabId, timedOut: false },
364
+ };
365
+ }
366
+
367
+ // Detach any existing debugger before top-level navigation.
368
+ // Some sites (observed on creator.xiaohongshu.com flows) can invalidate the
369
+ // current inspected target during navigation, which leaves a stale CDP attach
370
+ // state and causes the next Runtime.evaluate to fail with
371
+ // "Inspected target navigated or closed". Resetting here forces a clean
372
+ // re-attach after navigation.
373
+ await executor.detach(tabId);
374
+
324
375
  await chrome.tabs.update(tabId, { url: targetUrl });
325
376
 
326
- // Wait for: 1) URL to change from the old URL, 2) tab.status === 'complete'
327
- // This avoids the race where 'complete' fires for the OLD URL (e.g. about:blank)
377
+ // Wait until navigation completes. Resolve when status is 'complete' AND either:
378
+ // - the URL matches the target (handles same-URL / canonicalized navigations), OR
379
+ // - the URL differs from the pre-navigation URL (handles redirects).
328
380
  let timedOut = false;
329
381
  await new Promise<void>((resolve) => {
330
- let urlChanged = false;
382
+ let settled = false;
383
+ let checkTimer: ReturnType<typeof setTimeout> | null = null;
384
+ let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
331
385
 
332
- const listener = (id: number, info: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
333
- if (id !== tabId) return;
386
+ const finish = () => {
387
+ if (settled) return;
388
+ settled = true;
389
+ chrome.tabs.onUpdated.removeListener(listener);
390
+ if (checkTimer) clearTimeout(checkTimer);
391
+ if (timeoutTimer) clearTimeout(timeoutTimer);
392
+ resolve();
393
+ };
334
394
 
335
- // Track URL change (new URL differs from the one before navigation)
336
- if (info.url && info.url !== beforeUrl) {
337
- urlChanged = true;
338
- }
395
+ const isNavigationDone = (url: string | undefined): boolean => {
396
+ return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
397
+ };
339
398
 
340
- // Only resolve when both URL has changed AND status is complete
341
- if (urlChanged && info.status === 'complete') {
342
- chrome.tabs.onUpdated.removeListener(listener);
343
- resolve();
399
+ const listener = (id: number, info: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
400
+ if (id !== tabId) return;
401
+ if (info.status === 'complete' && isNavigationDone(tab.url ?? info.url)) {
402
+ finish();
344
403
  }
345
404
  };
346
405
  chrome.tabs.onUpdated.addListener(listener);
347
406
 
348
407
  // Also check if the tab already navigated (e.g. instant cache hit)
349
- setTimeout(async () => {
408
+ checkTimer = setTimeout(async () => {
350
409
  try {
351
410
  const currentTab = await chrome.tabs.get(tabId);
352
- if (currentTab.url !== beforeUrl && currentTab.status === 'complete') {
353
- chrome.tabs.onUpdated.removeListener(listener);
354
- resolve();
411
+ if (currentTab.status === 'complete' && isNavigationDone(currentTab.url)) {
412
+ finish();
355
413
  }
356
414
  } catch { /* tab gone */ }
357
415
  }, 100);
358
416
 
359
417
  // Timeout fallback with warning
360
- setTimeout(() => {
361
- chrome.tabs.onUpdated.removeListener(listener);
418
+ timeoutTimer = setTimeout(() => {
362
419
  timedOut = true;
363
420
  console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
364
- resolve();
421
+ finish();
365
422
  }, 15000);
366
423
  });
367
424
 
@@ -388,8 +445,11 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
388
445
  return { id: cmd.id, ok: true, data };
389
446
  }
390
447
  case 'new': {
448
+ if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
449
+ return { id: cmd.id, ok: false, error: 'Blocked URL scheme -- only http:// and https:// are allowed' };
450
+ }
391
451
  const windowId = await getAutomationWindow(workspace);
392
- const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? 'data:text/html,<html></html>', active: true });
452
+ const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, active: true });
393
453
  return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
394
454
  }
395
455
  case 'close': {
@@ -398,18 +458,28 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
398
458
  const target = tabs[cmd.index];
399
459
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
400
460
  await chrome.tabs.remove(target.id);
401
- executor.detach(target.id);
461
+ await executor.detach(target.id);
402
462
  return { id: cmd.id, ok: true, data: { closed: target.id } };
403
463
  }
404
464
  const tabId = await resolveTabId(cmd.tabId, workspace);
405
465
  await chrome.tabs.remove(tabId);
406
- executor.detach(tabId);
466
+ await executor.detach(tabId);
407
467
  return { id: cmd.id, ok: true, data: { closed: tabId } };
408
468
  }
409
469
  case 'select': {
410
470
  if (cmd.index === undefined && cmd.tabId === undefined)
411
471
  return { id: cmd.id, ok: false, error: 'Missing index or tabId' };
412
472
  if (cmd.tabId !== undefined) {
473
+ const session = automationSessions.get(workspace);
474
+ let tab: chrome.tabs.Tab;
475
+ try {
476
+ tab = await chrome.tabs.get(cmd.tabId);
477
+ } catch {
478
+ return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} no longer exists` };
479
+ }
480
+ if (!session || tab.windowId !== session.windowId) {
481
+ return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} is not in the automation window` };
482
+ }
413
483
  await chrome.tabs.update(cmd.tabId, { active: true });
414
484
  return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
415
485
  }
@@ -425,6 +495,9 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
425
495
  }
426
496
 
427
497
  async function handleCookies(cmd: Command): Promise<Result> {
498
+ if (!cmd.domain && !cmd.url) {
499
+ return { id: cmd.id, ok: false, error: 'Cookie scope required: provide domain or url to avoid dumping all cookies' };
500
+ }
428
501
  const details: chrome.cookies.GetAllDetails = {};
429
502
  if (cmd.domain) details.domain = cmd.domain;
430
503
  if (cmd.url) details.url = cmd.url;
@@ -481,6 +554,8 @@ async function handleSessions(cmd: Command): Promise<Result> {
481
554
  }
482
555
 
483
556
  export const __test__ = {
557
+ handleNavigate,
558
+ isTargetUrl,
484
559
  handleTabs,
485
560
  handleSessions,
486
561
  getAutomationWindowId: (workspace: string = 'default') => automationSessions.get(workspace)?.windowId ?? null,
@@ -8,10 +8,13 @@
8
8
 
9
9
  const attached = new Set<number>();
10
10
 
11
- /** Check if a URL can be attached via CDP */
11
+ /** Internal blank page used when no user URL is provided. */
12
+ const BLANK_PAGE = 'data:text/html,<html></html>';
13
+
14
+ /** Check if a URL can be attached via CDP — only allow http(s) and our internal blank page. */
12
15
  function isDebuggableUrl(url?: string): boolean {
13
16
  if (!url) return true; // empty/undefined = tab still loading, allow it
14
- return !url.startsWith('chrome://') && !url.startsWith('chrome-extension://');
17
+ return url.startsWith('http://') || url.startsWith('https://') || url === BLANK_PAGE;
15
18
  }
16
19
 
17
20
  async function ensureAttached(tabId: number): Promise<void> {
@@ -144,10 +147,10 @@ export async function screenshot(
144
147
  }
145
148
  }
146
149
 
147
- export function detach(tabId: number): void {
150
+ export async function detach(tabId: number): Promise<void> {
148
151
  if (!attached.has(tabId)) return;
149
152
  attached.delete(tabId);
150
- try { chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
153
+ try { await chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
151
154
  }
152
155
 
153
156
  export function registerListeners(): void {
@@ -158,12 +161,9 @@ export function registerListeners(): void {
158
161
  if (source.tabId) attached.delete(source.tabId);
159
162
  });
160
163
  // Invalidate attached cache when tab URL changes to non-debuggable
161
- chrome.tabs.onUpdated.addListener((tabId, info) => {
164
+ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
162
165
  if (info.url && !isDebuggableUrl(info.url)) {
163
- if (attached.has(tabId)) {
164
- attached.delete(tabId);
165
- try { chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
166
- }
166
+ await detach(tabId);
167
167
  }
168
168
  });
169
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,6 +30,7 @@
30
30
  "lint": "tsc --noEmit",
31
31
  "prepublishOnly": "npm run build",
32
32
  "test": "vitest run --project unit",
33
+ "test:adapter": "vitest run --project adapter",
33
34
  "test:all": "vitest run",
34
35
  "test:e2e": "vitest run --project e2e",
35
36
  "docs:dev": "vitepress dev docs",
@@ -62,7 +63,7 @@
62
63
  "@types/turndown": "^5.0.6",
63
64
  "@types/ws": "^8.5.13",
64
65
  "tsx": "^4.19.3",
65
- "typescript": "^5.8.2",
66
+ "typescript": "^6.0.2",
66
67
  "vitepress": "^1.6.4",
67
68
  "vitest": "^4.1.0"
68
69
  }
@@ -28,6 +28,8 @@ total=0
28
28
 
29
29
  for adapter_dir in "$SRC_DIR"/*/; do
30
30
  adapter_name="$(basename "$adapter_dir")"
31
+ # Skip internal directories (e.g., _shared)
32
+ [[ "$adapter_name" == _* ]] && continue
31
33
  total=$((total + 1))
32
34
 
33
35
  # Check if doc exists in browser/ or desktop/ subdirectories
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Shared API analysis helpers used by both explore.ts and record.ts.
3
+ *
4
+ * Extracts common logic for:
5
+ * - URL pattern normalization
6
+ * - Array path discovery in JSON responses
7
+ * - Field role detection
8
+ * - Auth indicator inference
9
+ * - Capability name inference
10
+ * - Strategy inference
11
+ */
12
+
13
+ import {
14
+ VOLATILE_PARAMS,
15
+ SEARCH_PARAMS,
16
+ PAGINATION_PARAMS,
17
+ LIMIT_PARAMS,
18
+ FIELD_ROLES,
19
+ } from './constants.js';
20
+
21
+ // ── URL pattern normalization ───────────────────────────────────────────────
22
+
23
+ /** Normalize a full URL into a pattern (replace IDs, strip volatile params). */
24
+ export function urlToPattern(url: string): string {
25
+ try {
26
+ const p = new URL(url);
27
+ const pathNorm = p.pathname
28
+ .replace(/\/\d+/g, '/{id}')
29
+ .replace(/\/[0-9a-fA-F]{8,}/g, '/{hex}')
30
+ .replace(/\/BV[a-zA-Z0-9]{10}/g, '/{bvid}');
31
+ const params: string[] = [];
32
+ p.searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) params.push(k); });
33
+ return `${p.host}${pathNorm}${params.length ? '?' + params.sort().map(k => `${k}={}`).join('&') : ''}`;
34
+ } catch { return url; }
35
+ }
36
+
37
+ // ── Array discovery in JSON responses ───────────────────────────────────────
38
+
39
+ export interface ArrayDiscovery {
40
+ path: string;
41
+ items: unknown[];
42
+ }
43
+
44
+ /** Find the best (largest) array of objects in a JSON response body. */
45
+ export function findArrayPath(obj: unknown, depth = 0): ArrayDiscovery | null {
46
+ if (depth > 5 || !obj || typeof obj !== 'object') return null;
47
+ if (Array.isArray(obj)) {
48
+ if (obj.length >= 2 && obj.some(i => i && typeof i === 'object' && !Array.isArray(i))) {
49
+ return { path: '', items: obj };
50
+ }
51
+ return null;
52
+ }
53
+ let best: ArrayDiscovery | null = null;
54
+ for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
55
+ const found = findArrayPath(val, depth + 1);
56
+ if (found) {
57
+ const fullPath = found.path ? `${key}.${found.path}` : key;
58
+ const candidate = { path: fullPath, items: found.items };
59
+ if (!best || candidate.items.length > best.items.length) best = candidate;
60
+ }
61
+ }
62
+ return best;
63
+ }
64
+
65
+ // ── Field flattening & role detection ───────────────────────────────────────
66
+
67
+ /** Flatten nested object keys up to maxDepth. */
68
+ export function flattenFields(obj: unknown, prefix: string, maxDepth: number): string[] {
69
+ if (maxDepth <= 0 || !obj || typeof obj !== 'object') return [];
70
+ const names: string[] = [];
71
+ const record = obj as Record<string, unknown>;
72
+ for (const key of Object.keys(record)) {
73
+ const full = prefix ? `${prefix}.${key}` : key;
74
+ names.push(full);
75
+ const val = record[key];
76
+ if (val && typeof val === 'object' && !Array.isArray(val)) names.push(...flattenFields(val, full, maxDepth - 1));
77
+ }
78
+ return names;
79
+ }
80
+
81
+ /** Detect semantic field roles (title, url, author, etc.) from sample fields. */
82
+ export function detectFieldRoles(sampleFields: string[]): Record<string, string> {
83
+ const detectedFields: Record<string, string> = {};
84
+ for (const [role, aliases] of Object.entries(FIELD_ROLES)) {
85
+ for (const f of sampleFields) {
86
+ if (aliases.includes(f.split('.').pop()?.toLowerCase() ?? '')) {
87
+ detectedFields[role] = f;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ return detectedFields;
93
+ }
94
+
95
+ // ── Capability name inference ───────────────────────────────────────────────
96
+
97
+ /** Infer a CLI capability name from a URL. */
98
+ export function inferCapabilityName(url: string, goal?: string): string {
99
+ if (goal) return goal;
100
+ const u = url.toLowerCase();
101
+ if (u.includes('hot') || u.includes('popular') || u.includes('ranking') || u.includes('trending')) return 'hot';
102
+ if (u.includes('search')) return 'search';
103
+ if (u.includes('feed') || u.includes('timeline') || u.includes('dynamic')) return 'feed';
104
+ if (u.includes('comment') || u.includes('reply')) return 'comments';
105
+ if (u.includes('history')) return 'history';
106
+ if (u.includes('profile') || u.includes('userinfo') || u.includes('/me')) return 'me';
107
+ if (u.includes('favorite') || u.includes('collect') || u.includes('bookmark')) return 'favorite';
108
+ try {
109
+ const segs = new URL(url).pathname
110
+ .split('/')
111
+ .filter(s => s && !s.match(/^\d+$/) && !s.match(/^[0-9a-f]{8,}$/i) && !s.match(/^v\d+$/));
112
+ if (segs.length) return segs[segs.length - 1].replace(/[^a-z0-9]/gi, '_').toLowerCase();
113
+ } catch {}
114
+ return 'data';
115
+ }
116
+
117
+ // ── Strategy inference ──────────────────────────────────────────────────────
118
+
119
+ /** Infer auth strategy from detected indicators. */
120
+ export function inferStrategy(authIndicators: string[]): string {
121
+ if (authIndicators.includes('signature')) return 'intercept';
122
+ if (authIndicators.includes('bearer') || authIndicators.includes('csrf')) return 'header';
123
+ return 'cookie';
124
+ }
125
+
126
+ // ── Auth indicator detection ────────────────────────────────────────────────
127
+
128
+ /** Detect auth indicators from HTTP headers. */
129
+ export function detectAuthFromHeaders(headers?: Record<string, string>): string[] {
130
+ if (!headers) return [];
131
+ const indicators: string[] = [];
132
+ const keys = Object.keys(headers).map(k => k.toLowerCase());
133
+ if (keys.some(k => k === 'authorization')) indicators.push('bearer');
134
+ if (keys.some(k => k.startsWith('x-csrf') || k.startsWith('x-xsrf'))) indicators.push('csrf');
135
+ if (keys.some(k => k.startsWith('x-s') || k === 'x-t' || k === 'x-s-common')) indicators.push('signature');
136
+ return indicators;
137
+ }
138
+
139
+ /** Detect auth indicators from URL and response body (heuristic). */
140
+ export function detectAuthFromContent(url: string, body: unknown): string[] {
141
+ const indicators: string[] = [];
142
+ if (body && typeof body === 'object') {
143
+ const keys = Object.keys(body as object).map(k => k.toLowerCase());
144
+ if (keys.some(k => k.includes('sign') || k === 'w_rid' || k.includes('token'))) {
145
+ indicators.push('signature');
146
+ }
147
+ }
148
+ if (url.includes('/wbi/') || url.includes('w_rid=')) indicators.push('signature');
149
+ if (url.includes('bearer') || url.includes('access_token')) indicators.push('bearer');
150
+ return indicators;
151
+ }
152
+
153
+ // ── Query param classification ──────────────────────────────────────────────
154
+
155
+ /** Extract non-volatile query params and classify them. */
156
+ export function classifyQueryParams(url: string): {
157
+ params: string[];
158
+ hasSearch: boolean;
159
+ hasPagination: boolean;
160
+ hasLimit: boolean;
161
+ } {
162
+ const params: string[] = [];
163
+ try { new URL(url).searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) params.push(k); }); } catch {}
164
+ return {
165
+ params,
166
+ hasSearch: params.some(p => SEARCH_PARAMS.has(p)),
167
+ hasPagination: params.some(p => PAGINATION_PARAMS.has(p)),
168
+ hasLimit: params.some(p => LIMIT_PARAMS.has(p)),
169
+ };
170
+ }
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { MockWebSocket } = vi.hoisted(() => {
4
+ class MockWebSocket {
5
+ static OPEN = 1;
6
+ readyState = 1;
7
+ private handlers = new Map<string, Array<(...args: any[]) => void>>();
8
+
9
+ constructor(_url: string) {
10
+ queueMicrotask(() => this.emit('open'));
11
+ }
12
+
13
+ on(event: string, handler: (...args: any[]) => void): void {
14
+ const handlers = this.handlers.get(event) ?? [];
15
+ handlers.push(handler);
16
+ this.handlers.set(event, handlers);
17
+ }
18
+
19
+ send(_message: string): void {}
20
+
21
+ close(): void {
22
+ this.readyState = 3;
23
+ }
24
+
25
+ private emit(event: string, ...args: any[]): void {
26
+ for (const handler of this.handlers.get(event) ?? []) {
27
+ handler(...args);
28
+ }
29
+ }
30
+ }
31
+
32
+ return { MockWebSocket };
33
+ });
34
+
35
+ vi.mock('ws', () => ({
36
+ WebSocket: MockWebSocket,
37
+ }));
38
+
39
+ import { CDPBridge } from './cdp.js';
40
+
41
+ describe('CDPBridge cookies', () => {
42
+ beforeEach(() => {
43
+ vi.unstubAllEnvs();
44
+ });
45
+
46
+ it('filters cookies by actual domain match instead of substring match', async () => {
47
+ vi.stubEnv('OPENCLI_CDP_ENDPOINT', 'ws://127.0.0.1:9222/devtools/page/1');
48
+
49
+ const bridge = new CDPBridge();
50
+ vi.spyOn(bridge, 'send').mockResolvedValue({
51
+ cookies: [
52
+ { name: 'good', value: '1', domain: '.example.com' },
53
+ { name: 'exact', value: '2', domain: 'example.com' },
54
+ { name: 'bad', value: '3', domain: 'notexample.com' },
55
+ ],
56
+ });
57
+
58
+ const page = await bridge.connect();
59
+ const cookies = await page.getCookies({ domain: 'example.com' });
60
+
61
+ expect(cookies).toEqual([
62
+ { name: 'good', value: '1', domain: '.example.com' },
63
+ { name: 'exact', value: '2', domain: 'example.com' },
64
+ ]);
65
+ });
66
+ });