@jackwener/opencli 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.github/workflows/build-extension.yml +2 -6
  2. package/.github/workflows/ci.yml +21 -1
  3. package/README.md +35 -6
  4. package/README.zh-CN.md +12 -5
  5. package/SKILL.md +2 -0
  6. package/dist/browser/cdp.d.ts +2 -1
  7. package/dist/browser/discover.d.ts +4 -1
  8. package/dist/browser/discover.js +6 -2
  9. package/dist/browser/errors.d.ts +2 -2
  10. package/dist/browser/errors.js +4 -12
  11. package/dist/browser/mcp.d.ts +2 -1
  12. package/dist/build-manifest.d.ts +2 -0
  13. package/dist/build-manifest.js +39 -14
  14. package/dist/build-manifest.test.js +21 -0
  15. package/dist/capabilityRouting.d.ts +2 -0
  16. package/dist/capabilityRouting.js +2 -1
  17. package/dist/cli-manifest.json +1111 -112
  18. package/dist/cli.js +34 -3
  19. package/dist/clis/36kr/article.d.ts +1 -0
  20. package/dist/clis/36kr/article.js +62 -0
  21. package/dist/clis/36kr/hot.d.ts +3 -0
  22. package/dist/clis/36kr/hot.js +80 -0
  23. package/dist/clis/36kr/hot.test.d.ts +1 -0
  24. package/dist/clis/36kr/hot.test.js +15 -0
  25. package/dist/clis/36kr/news.d.ts +1 -0
  26. package/dist/clis/36kr/news.js +51 -0
  27. package/dist/clis/36kr/news.test.d.ts +1 -0
  28. package/dist/clis/36kr/news.test.js +85 -0
  29. package/dist/clis/36kr/search.d.ts +1 -0
  30. package/dist/clis/36kr/search.js +72 -0
  31. package/dist/clis/bilibili/comments.d.ts +5 -0
  32. package/dist/clis/bilibili/comments.js +40 -0
  33. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  34. package/dist/clis/bilibili/comments.test.js +82 -0
  35. package/dist/clis/chatgpt/ask.js +29 -14
  36. package/dist/clis/chatgpt/ax.d.ts +6 -0
  37. package/dist/clis/chatgpt/ax.js +172 -1
  38. package/dist/clis/chatgpt/model.d.ts +1 -0
  39. package/dist/clis/chatgpt/model.js +24 -0
  40. package/dist/clis/chatgpt/send.js +12 -3
  41. package/dist/clis/douban/download.d.ts +1 -0
  42. package/dist/clis/douban/download.js +67 -0
  43. package/dist/clis/douban/download.test.d.ts +1 -0
  44. package/dist/clis/douban/download.test.js +170 -0
  45. package/dist/clis/douban/photos.d.ts +1 -0
  46. package/dist/clis/douban/photos.js +34 -0
  47. package/dist/clis/douban/utils.d.ts +25 -0
  48. package/dist/clis/douban/utils.js +190 -1
  49. package/dist/clis/douban/utils.test.d.ts +1 -0
  50. package/dist/clis/douban/utils.test.js +64 -0
  51. package/dist/clis/imdb/person.d.ts +1 -0
  52. package/dist/clis/imdb/person.js +203 -0
  53. package/dist/clis/imdb/reviews.d.ts +1 -0
  54. package/dist/clis/imdb/reviews.js +88 -0
  55. package/dist/clis/imdb/search.d.ts +1 -0
  56. package/dist/clis/imdb/search.js +161 -0
  57. package/dist/clis/imdb/title.d.ts +1 -0
  58. package/dist/clis/imdb/title.js +93 -0
  59. package/dist/clis/imdb/top.d.ts +1 -0
  60. package/dist/clis/imdb/top.js +53 -0
  61. package/dist/clis/imdb/trending.d.ts +1 -0
  62. package/dist/clis/imdb/trending.js +52 -0
  63. package/dist/clis/imdb/utils.d.ts +46 -0
  64. package/dist/clis/imdb/utils.js +285 -0
  65. package/dist/clis/imdb/utils.test.d.ts +1 -0
  66. package/dist/clis/imdb/utils.test.js +88 -0
  67. package/dist/clis/jd/item.d.ts +4 -0
  68. package/dist/clis/jd/item.js +16 -15
  69. package/dist/clis/jd/item.test.js +16 -1
  70. package/dist/clis/linux-do/categories.yaml +38 -9
  71. package/dist/clis/linux-do/category.d.ts +1 -0
  72. package/dist/clis/linux-do/category.js +36 -0
  73. package/dist/clis/linux-do/feed.d.ts +45 -0
  74. package/dist/clis/linux-do/feed.js +397 -0
  75. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  76. package/dist/clis/linux-do/feed.test.js +118 -0
  77. package/dist/clis/linux-do/hot.d.ts +1 -0
  78. package/dist/clis/linux-do/hot.js +25 -0
  79. package/dist/clis/linux-do/latest.d.ts +1 -0
  80. package/dist/clis/linux-do/latest.js +18 -0
  81. package/dist/clis/linux-do/tags.yaml +41 -0
  82. package/dist/clis/linux-do/topic.yaml +41 -3
  83. package/dist/clis/linux-do/user-posts.yaml +67 -0
  84. package/dist/clis/linux-do/user-topics.yaml +54 -0
  85. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  86. package/dist/clis/paperreview/commands.test.js +243 -0
  87. package/dist/clis/paperreview/feedback.d.ts +1 -0
  88. package/dist/clis/paperreview/feedback.js +52 -0
  89. package/dist/clis/paperreview/review.d.ts +1 -0
  90. package/dist/clis/paperreview/review.js +37 -0
  91. package/dist/clis/paperreview/submit.d.ts +1 -0
  92. package/dist/clis/paperreview/submit.js +85 -0
  93. package/dist/clis/paperreview/utils.d.ts +46 -0
  94. package/dist/clis/paperreview/utils.js +197 -0
  95. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  96. package/dist/clis/paperreview/utils.test.js +49 -0
  97. package/dist/clis/producthunt/browse.d.ts +1 -0
  98. package/dist/clis/producthunt/browse.js +99 -0
  99. package/dist/clis/producthunt/hot.d.ts +1 -0
  100. package/dist/clis/producthunt/hot.js +110 -0
  101. package/dist/clis/producthunt/posts.d.ts +1 -0
  102. package/dist/clis/producthunt/posts.js +28 -0
  103. package/dist/clis/producthunt/today.d.ts +1 -0
  104. package/dist/clis/producthunt/today.js +35 -0
  105. package/dist/clis/producthunt/utils.d.ts +29 -0
  106. package/dist/clis/producthunt/utils.js +99 -0
  107. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  108. package/dist/clis/producthunt/utils.test.js +64 -0
  109. package/dist/clis/twitter/article.js +4 -28
  110. package/dist/clis/twitter/likes.d.ts +24 -0
  111. package/dist/clis/twitter/likes.js +217 -0
  112. package/dist/clis/twitter/likes.test.d.ts +1 -0
  113. package/dist/clis/twitter/likes.test.js +85 -0
  114. package/dist/clis/twitter/profile.js +4 -28
  115. package/dist/clis/twitter/search.js +2 -1
  116. package/dist/clis/twitter/search.test.js +2 -0
  117. package/dist/clis/twitter/shared.d.ts +6 -0
  118. package/dist/clis/twitter/shared.js +35 -0
  119. package/dist/clis/twitter/timeline.js +2 -13
  120. package/dist/clis/weixin/download.d.ts +17 -0
  121. package/dist/clis/weixin/download.js +88 -20
  122. package/dist/clis/weread/book.js +2 -2
  123. package/dist/clis/weread/commands.test.d.ts +3 -0
  124. package/dist/clis/weread/commands.test.js +43 -0
  125. package/dist/clis/weread/highlights.js +2 -2
  126. package/dist/clis/weread/notebooks.js +2 -2
  127. package/dist/clis/weread/notes.js +3 -3
  128. package/dist/clis/weread/shelf.js +2 -2
  129. package/dist/clis/weread/utils.d.ts +4 -4
  130. package/dist/clis/weread/utils.js +32 -14
  131. package/dist/clis/weread/utils.test.js +1 -28
  132. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  133. package/dist/clis/xiaohongshu/comments.js +74 -0
  134. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  135. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  136. package/dist/clis/xiaohongshu/publish.js +114 -18
  137. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  138. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  139. package/dist/commanderAdapter.d.ts +1 -0
  140. package/dist/commanderAdapter.js +176 -29
  141. package/dist/commanderAdapter.test.d.ts +1 -0
  142. package/dist/commanderAdapter.test.js +62 -0
  143. package/dist/daemon.js +17 -1
  144. package/dist/discovery.js +8 -14
  145. package/dist/doctor.d.ts +1 -0
  146. package/dist/doctor.js +9 -2
  147. package/dist/download/index.js +63 -51
  148. package/dist/download/index.test.js +17 -4
  149. package/dist/errors.d.ts +3 -1
  150. package/dist/errors.js +15 -32
  151. package/dist/execution.d.ts +1 -3
  152. package/dist/execution.js +21 -1
  153. package/dist/hooks.js +2 -0
  154. package/dist/main.js +5 -0
  155. package/dist/output.js +5 -1
  156. package/dist/pipeline/executor.js +3 -4
  157. package/dist/plugin-manifest.d.ts +70 -0
  158. package/dist/plugin-manifest.js +160 -0
  159. package/dist/plugin-manifest.test.d.ts +4 -0
  160. package/dist/plugin-manifest.test.js +179 -0
  161. package/dist/plugin.d.ts +38 -5
  162. package/dist/plugin.js +267 -33
  163. package/dist/plugin.test.js +220 -3
  164. package/dist/registry.d.ts +4 -0
  165. package/dist/registry.js +2 -0
  166. package/dist/runtime-detect.d.ts +21 -0
  167. package/dist/runtime-detect.js +32 -0
  168. package/dist/runtime-detect.test.d.ts +1 -0
  169. package/dist/runtime-detect.test.js +27 -0
  170. package/dist/runtime.js +1 -1
  171. package/dist/serialization.d.ts +2 -0
  172. package/dist/serialization.js +6 -0
  173. package/dist/types.d.ts +1 -0
  174. package/dist/update-check.d.ts +22 -0
  175. package/dist/update-check.js +112 -0
  176. package/dist/weixin-download.test.d.ts +1 -0
  177. package/dist/weixin-download.test.js +30 -0
  178. package/dist/weread-private-api-regression.test.d.ts +1 -0
  179. package/dist/weread-private-api-regression.test.js +122 -0
  180. package/dist/yaml-schema.d.ts +3 -0
  181. package/dist/yaml-schema.js +18 -1
  182. package/docs/.vitepress/config.mts +4 -0
  183. package/docs/adapters/browser/36kr.md +47 -0
  184. package/docs/adapters/browser/douban.md +14 -0
  185. package/docs/adapters/browser/imdb.md +47 -0
  186. package/docs/adapters/browser/jd.md +2 -2
  187. package/docs/adapters/browser/linux-do.md +181 -20
  188. package/docs/adapters/browser/paperreview.md +43 -0
  189. package/docs/adapters/browser/producthunt.md +49 -0
  190. package/docs/adapters/desktop/chatgpt.md +5 -0
  191. package/docs/adapters/index.md +6 -2
  192. package/docs/advanced/download.md +4 -0
  193. package/docs/advanced/rate-limiter-plugin.md +99 -0
  194. package/docs/guide/electron-app-cli.md +200 -0
  195. package/docs/guide/getting-started.md +1 -0
  196. package/docs/guide/plugins.md +87 -0
  197. package/docs/zh/guide/electron-app-cli.md +188 -0
  198. package/docs/zh/guide/getting-started.md +1 -0
  199. package/docs/zh/guide/plugins.md +65 -0
  200. package/extension/package.json +1 -0
  201. package/extension/scripts/package-release.mjs +179 -0
  202. package/extension/src/background.ts +2 -0
  203. package/package.json +4 -1
  204. package/scripts/postinstall.js +10 -0
  205. package/src/browser/cdp.ts +2 -1
  206. package/src/browser/discover.ts +8 -3
  207. package/src/browser/errors.ts +13 -14
  208. package/src/browser/mcp.ts +2 -1
  209. package/src/build-manifest.test.ts +23 -0
  210. package/src/build-manifest.ts +40 -15
  211. package/src/capabilityRouting.ts +2 -1
  212. package/src/cli.ts +35 -3
  213. package/src/clis/36kr/article.ts +69 -0
  214. package/src/clis/36kr/hot.test.ts +19 -0
  215. package/src/clis/36kr/hot.ts +100 -0
  216. package/src/clis/36kr/news.test.ts +90 -0
  217. package/src/clis/36kr/news.ts +54 -0
  218. package/src/clis/36kr/search.ts +78 -0
  219. package/src/clis/bilibili/comments.test.ts +102 -0
  220. package/src/clis/bilibili/comments.ts +44 -0
  221. package/src/clis/chatgpt/ask.ts +28 -14
  222. package/src/clis/chatgpt/ax.ts +180 -1
  223. package/src/clis/chatgpt/model.ts +27 -0
  224. package/src/clis/chatgpt/send.ts +16 -6
  225. package/src/clis/douban/download.test.ts +196 -0
  226. package/src/clis/douban/download.ts +78 -0
  227. package/src/clis/douban/photos.ts +36 -0
  228. package/src/clis/douban/utils.test.ts +97 -0
  229. package/src/clis/douban/utils.ts +232 -1
  230. package/src/clis/imdb/person.ts +232 -0
  231. package/src/clis/imdb/reviews.ts +111 -0
  232. package/src/clis/imdb/search.ts +179 -0
  233. package/src/clis/imdb/title.ts +121 -0
  234. package/src/clis/imdb/top.ts +67 -0
  235. package/src/clis/imdb/trending.ts +66 -0
  236. package/src/clis/imdb/utils.test.ts +117 -0
  237. package/src/clis/imdb/utils.ts +305 -0
  238. package/src/clis/jd/item.test.ts +18 -1
  239. package/src/clis/jd/item.ts +18 -15
  240. package/src/clis/linux-do/categories.yaml +38 -9
  241. package/src/clis/linux-do/category.ts +37 -0
  242. package/src/clis/linux-do/feed.test.ts +132 -0
  243. package/src/clis/linux-do/feed.ts +501 -0
  244. package/src/clis/linux-do/hot.ts +26 -0
  245. package/src/clis/linux-do/latest.ts +19 -0
  246. package/src/clis/linux-do/tags.yaml +41 -0
  247. package/src/clis/linux-do/topic.yaml +41 -3
  248. package/src/clis/linux-do/user-posts.yaml +67 -0
  249. package/src/clis/linux-do/user-topics.yaml +54 -0
  250. package/src/clis/paperreview/commands.test.ts +283 -0
  251. package/src/clis/paperreview/feedback.ts +64 -0
  252. package/src/clis/paperreview/review.ts +47 -0
  253. package/src/clis/paperreview/submit.ts +119 -0
  254. package/src/clis/paperreview/utils.test.ts +68 -0
  255. package/src/clis/paperreview/utils.ts +276 -0
  256. package/src/clis/producthunt/browse.ts +109 -0
  257. package/src/clis/producthunt/hot.ts +127 -0
  258. package/src/clis/producthunt/posts.ts +29 -0
  259. package/src/clis/producthunt/today.ts +37 -0
  260. package/src/clis/producthunt/utils.test.ts +72 -0
  261. package/src/clis/producthunt/utils.ts +122 -0
  262. package/src/clis/twitter/article.ts +5 -28
  263. package/src/clis/twitter/likes.test.ts +91 -0
  264. package/src/clis/twitter/likes.ts +256 -0
  265. package/src/clis/twitter/profile.ts +5 -28
  266. package/src/clis/twitter/search.test.ts +2 -0
  267. package/src/clis/twitter/search.ts +3 -1
  268. package/src/clis/twitter/shared.ts +45 -0
  269. package/src/clis/twitter/timeline.ts +2 -13
  270. package/src/clis/weixin/download.ts +114 -20
  271. package/src/clis/weread/book.ts +2 -2
  272. package/src/clis/weread/commands.test.ts +57 -0
  273. package/src/clis/weread/highlights.ts +2 -2
  274. package/src/clis/weread/notebooks.ts +2 -2
  275. package/src/clis/weread/notes.ts +3 -3
  276. package/src/clis/weread/shelf.ts +2 -2
  277. package/src/clis/weread/utils.test.ts +1 -32
  278. package/src/clis/weread/utils.ts +41 -16
  279. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  280. package/src/clis/xiaohongshu/comments.ts +81 -0
  281. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  282. package/src/clis/xiaohongshu/publish.ts +129 -18
  283. package/src/commanderAdapter.test.ts +78 -0
  284. package/src/commanderAdapter.ts +188 -24
  285. package/src/daemon.ts +19 -1
  286. package/src/discovery.ts +8 -15
  287. package/src/doctor.ts +13 -2
  288. package/src/download/index.test.ts +14 -4
  289. package/src/download/index.ts +67 -55
  290. package/src/errors.ts +25 -66
  291. package/src/execution.ts +28 -3
  292. package/src/hooks.ts +1 -0
  293. package/src/main.ts +6 -0
  294. package/src/output.ts +3 -1
  295. package/src/pipeline/executor.ts +4 -6
  296. package/src/plugin-manifest.test.ts +223 -0
  297. package/src/plugin-manifest.ts +206 -0
  298. package/src/plugin.test.ts +246 -2
  299. package/src/plugin.ts +338 -36
  300. package/src/registry.ts +6 -1
  301. package/src/runtime-detect.test.ts +30 -0
  302. package/src/runtime-detect.ts +36 -0
  303. package/src/runtime.ts +1 -1
  304. package/src/serialization.ts +4 -0
  305. package/src/types.ts +1 -0
  306. package/src/update-check.ts +114 -0
  307. package/src/weixin-download.test.ts +64 -0
  308. package/src/weread-private-api-regression.test.ts +150 -0
  309. package/src/yaml-schema.ts +20 -0
  310. package/tests/e2e/browser-auth.test.ts +13 -9
  311. package/tests/e2e/browser-public-extended.test.ts +1 -1
  312. package/tests/e2e/browser-public.test.ts +62 -4
  313. package/tests/e2e/helpers.ts +2 -1
  314. package/tests/e2e/public-commands.test.ts +37 -3
  315. package/tests/smoke/api-health.test.ts +1 -1
  316. package/vitest.config.ts +10 -0
  317. package/dist/clis/linux-do/category.yaml +0 -51
  318. package/dist/clis/linux-do/hot.yaml +0 -50
  319. package/dist/clis/linux-do/latest.yaml +0 -40
  320. package/src/clis/linux-do/category.yaml +0 -51
  321. package/src/clis/linux-do/hot.yaml +0 -50
  322. package/src/clis/linux-do/latest.yaml +0 -40
@@ -32,11 +32,98 @@ Plugins live in `~/.opencli/plugins/<name>/`. Each subdirectory is scanned at st
32
32
 
33
33
  ```bash
34
34
  opencli plugin install github:user/repo
35
+ opencli plugin install github:user/repo/subplugin # install specific sub-plugin from monorepo
35
36
  opencli plugin install https://github.com/user/repo
36
37
  ```
37
38
 
38
39
  The repo name prefix `opencli-plugin-` is automatically stripped for the local directory name. For example, `opencli-plugin-hot-digest` becomes `hot-digest`.
39
40
 
41
+ ## Plugin Manifest (`opencli-plugin.json`)
42
+
43
+ Plugins can include an `opencli-plugin.json` manifest file at the repo root to declare metadata:
44
+
45
+ ```json
46
+ {
47
+ "name": "my-plugin",
48
+ "version": "1.0.0",
49
+ "opencli": ">=1.0.0",
50
+ "description": "My awesome plugin"
51
+ }
52
+ ```
53
+
54
+ | Field | Description |
55
+ |-------|-------------|
56
+ | `name` | Plugin name (overrides repo-derived name) |
57
+ | `version` | Semantic version |
58
+ | `opencli` | Required opencli version range (e.g. `>=1.0.0`, `^1.2.0`) |
59
+ | `description` | Human-readable description |
60
+ | `plugins` | Monorepo sub-plugin declarations (see below) |
61
+
62
+ The manifest is optional — plugins without one continue to work exactly as before.
63
+
64
+ ## Monorepo Plugins
65
+
66
+ A single repository can contain multiple plugins by declaring a `plugins` field in `opencli-plugin.json`:
67
+
68
+ ```json
69
+ {
70
+ "version": "1.0.0",
71
+ "opencli": ">=1.0.0",
72
+ "description": "My plugin collection",
73
+ "plugins": {
74
+ "polymarket": {
75
+ "path": "packages/polymarket",
76
+ "description": "Prediction market analysis",
77
+ "version": "1.2.0"
78
+ },
79
+ "defi": {
80
+ "path": "packages/defi",
81
+ "description": "DeFi protocol data",
82
+ "version": "0.8.0",
83
+ "opencli": ">=1.2.0"
84
+ },
85
+ "experimental": {
86
+ "path": "packages/experimental",
87
+ "disabled": true
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### Installing
94
+
95
+ ```bash
96
+ # Install ALL enabled sub-plugins from a monorepo
97
+ opencli plugin install github:user/opencli-plugins
98
+
99
+ # Install a SPECIFIC sub-plugin
100
+ opencli plugin install github:user/opencli-plugins/polymarket
101
+ ```
102
+
103
+ ### How It Works
104
+
105
+ - The monorepo is cloned once to `~/.opencli/monorepos/<repo>/`
106
+ - Each sub-plugin gets a symlink in `~/.opencli/plugins/<name>/` pointing to its subdirectory
107
+ - Command discovery works transparently — symlinks are scanned just like regular directories
108
+ - Disabled sub-plugins (with `"disabled": true`) are skipped during install
109
+ - Sub-plugins can specify their own `opencli` compatibility range
110
+
111
+ ### Updating
112
+
113
+ Updating any sub-plugin from a monorepo pulls the entire repo and refreshes all sub-plugins:
114
+
115
+ ```bash
116
+ opencli plugin update polymarket # updates the monorepo, refreshes all
117
+ ```
118
+
119
+ ### Uninstalling
120
+
121
+ ```bash
122
+ opencli plugin uninstall polymarket # removes just this sub-plugin's symlink
123
+ ```
124
+
125
+ When the last sub-plugin from a monorepo is uninstalled, the monorepo clone is automatically cleaned up.
126
+
40
127
  ## Version Tracking
41
128
 
42
129
  OpenCLI records installed plugin versions in `~/.opencli/plugins.lock.json`. Each entry stores the plugin source, current git commit hash, install time, and last update time. `opencli plugin list` shows the short commit hash when version metadata is available.
@@ -0,0 +1,188 @@
1
+ # 给新 Electron 应用生成 CLI
2
+
3
+ 这篇文档是把一个新的 Electron 桌面应用接入 OpenCLI 的**中文入口指南**。
4
+
5
+ 如果你需要更完整的背景和标准流程,继续看:
6
+ - [Chrome DevTools Protocol(中文)](/zh/advanced/cdp)
7
+ - [CLI-ifying Electron Applications(英文深度版)](/advanced/electron)
8
+ - [TypeScript 适配器开发指南(英文)](/developer/ts-adapter)
9
+
10
+ ## 这篇文档适合什么场景
11
+
12
+ 当目标应用满足下面条件时,用这套流程:
13
+ - 应用是 **Electron**,或者至少能暴露可用的 **CDP(Chrome DevTools Protocol)** 端口
14
+ - 可以通过 `--remote-debugging-port=<port>` 启动
15
+ - 你希望控制的是桌面应用本身,而不是它背后的公开 HTTP API
16
+
17
+ 如果应用**不是** Electron,或者不暴露 CDP,就不要硬套这套方案。那种情况应改用原生桌面自动化方案。可参考 [英文版说明](/advanced/electron#non-electron-pattern-applescript)。
18
+
19
+ ## 最短落地路径
20
+
21
+ ### 1. 先确认它是不是 Electron
22
+
23
+ macOS 下常见检查方式:
24
+
25
+ ```bash
26
+ ls /Applications/AppName.app/Contents/Frameworks/Electron\ Framework.framework
27
+ ```
28
+
29
+ 如果存在,通常就可以继续尝试 CDP。
30
+
31
+ ### 2. 带 CDP 端口启动应用
32
+
33
+ ```bash
34
+ /Applications/AppName.app/Contents/MacOS/AppName --remote-debugging-port=9222
35
+ ```
36
+
37
+ 然后把 OpenCLI 指到这个端口:
38
+
39
+ ```bash
40
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9222"
41
+ ```
42
+
43
+ ### 3. 先做 5 个基础命令
44
+
45
+ 建议一个新 Electron 适配器先实现这 5 个命令:
46
+
47
+ - `status.ts` —— 确认 CDP 连通
48
+ - `dump.ts` —— 导出 DOM / snapshot,先做逆向再写逻辑
49
+ - `read.ts` —— 读取当前上下文
50
+ - `send.ts` —— 往真实编辑器里输入并发送
51
+ - `new.ts` —— 新建会话 / 标签页 / 文档
52
+
53
+ 这是最稳妥的基线,因为它先把“能连上、能看见、能读、能写、能重置状态”这 5 件核心事情打通了。
54
+
55
+ ## 推荐开发顺序
56
+
57
+ ### 第一步:先做 `status`
58
+
59
+ 目标不是功能,而是先证明:
60
+ - CDP 真的连上了
61
+ - 你连到的是对的窗口/标签页
62
+ - 应用当前页面确实可读
63
+
64
+ 如果 `status` 都不稳定,先不要继续往下做。
65
+
66
+ ### 第二步:做 `dump`
67
+
68
+ **不要猜 selector。**
69
+
70
+ 先把这些导出来:
71
+ - `document.body.innerHTML`
72
+ - accessibility snapshot
73
+ - 稳定属性:`data-testid`、`role`、`aria-*` 等
74
+
75
+ 然后再决定:
76
+ - 消息列表在哪
77
+ - 输入框在哪
78
+ - 按钮在哪
79
+ - 当前会话容器在哪
80
+
81
+ ### 第三步:做 `read`
82
+
83
+ 只读真正需要的区域,不要把整个页面文本都塞出来。
84
+
85
+ 常见目标:
86
+ - 对话消息区
87
+ - 当前线程内容
88
+ - 当前编辑器历史
89
+ - 当前文档主区域
90
+
91
+ ### 第四步:做 `send`
92
+
93
+ 很多 Electron 应用的输入框是 React 控制组件,直接改 `.value` 往往没用。
94
+
95
+ 更稳妥的方式通常是:
96
+ - 先 focus 到可编辑区域
97
+ - 能用时优先 `document.execCommand('insertText', false, text)`
98
+ - 最后用真实按键提交,比如 `Enter`、`Meta+Enter`
99
+
100
+ ### 第五步:做 `new`
101
+
102
+ 很多桌面应用的新建动作其实更适合走快捷键,而不是点按钮。
103
+
104
+ 典型模式:
105
+
106
+ ```ts
107
+ const isMac = process.platform === 'darwin';
108
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
109
+ await page.wait(1);
110
+ ```
111
+
112
+ ## 文件一般怎么放
113
+
114
+ 一个 TypeScript 桌面适配器,通常结构是:
115
+
116
+ ```text
117
+ src/clis/<app>/status.ts
118
+ src/clis/<app>/dump.ts
119
+ src/clis/<app>/read.ts
120
+ src/clis/<app>/send.ts
121
+ src/clis/<app>/new.ts
122
+ src/clis/<app>/utils.ts
123
+ ```
124
+
125
+ 当基础能力稳定后,再继续加:
126
+ - `ask`
127
+ - `history`
128
+ - `model`
129
+ - `screenshot`
130
+ - `export`
131
+
132
+ ## 加完适配器后,还应该补什么文档
133
+
134
+ 至少补这几项:
135
+ - `docs/adapters/desktop/` 下的适配器说明页
136
+ - 命令列表和示例
137
+ - 如何带 `--remote-debugging-port` 启动
138
+ - 需要哪些环境变量
139
+ - 平台限制和注意事项
140
+
141
+ 可以参考这些现成文档:
142
+ - `docs/adapters/desktop/codex.md`
143
+ - `docs/adapters/desktop/chatwise.md`
144
+ - `docs/adapters/desktop/notion.md`
145
+ - `docs/adapters/desktop/discord.md`
146
+
147
+ ## 常见问题
148
+
149
+ ### CDP 能连,但命令不稳定
150
+
151
+ 常见原因:
152
+ - 连错窗口或标签页
153
+ - 页面还没渲染完
154
+ - selector 是猜的,不是从 `dump` 里找出来的
155
+ - 输入框是受控组件,直接赋值不生效
156
+
157
+ ### 应用看起来像 Chromium,但就是不好控
158
+
159
+ 有些桌面应用虽然嵌了 Chromium,但并不真正暴露可用的 CDP 接口。
160
+ 这种情况不要强行走 Electron 方案,应该换到非 Electron 的桌面自动化方案。
161
+
162
+ ### 这个应用其实也有网页版本,还要不要做 Electron 适配器
163
+
164
+ 如果网页版本已经足够稳定,浏览器适配器通常更简单。
165
+ 只有当**桌面应用才是真正的集成面**时,再优先做 Electron 适配器。
166
+
167
+ ## 推荐阅读顺序
168
+
169
+ 如果你从零开始:
170
+
171
+ 1. 先看这篇
172
+ 2. 再看 [CLI-ifying Electron Applications(英文深度版)](/advanced/electron)
173
+ 3. 再看 [Chrome DevTools Protocol(中文)](/zh/advanced/cdp)
174
+ 4. 再看 [TypeScript Adapter Guide(英文)](/developer/ts-adapter)
175
+ 5. 最后找一个现成桌面适配器文档照着做
176
+
177
+ ## 最后一个实践建议
178
+
179
+ 不要一上来就做很大的命令面。
180
+
181
+ 先把下面 5 个做稳:
182
+ - `status`
183
+ - `dump`
184
+ - `read`
185
+ - `send`
186
+ - `new`
187
+
188
+ 这 5 个稳定了,再往外扩,成本最低,返工也最少。
@@ -38,3 +38,4 @@ opencli bilibili hot -f csv # CSV
38
38
  - [Browser Bridge 设置](/zh/guide/browser-bridge)
39
39
  - [所有适配器](/zh/adapters/)
40
40
  - [开发者指南](/zh/developer/contributing)
41
+ - [给新 Electron 应用生成 CLI](/zh/guide/electron-app-cli)
@@ -32,11 +32,76 @@ Plugins 存放在 `~/.opencli/plugins/<name>/`。每个子目录都会在启动
32
32
 
33
33
  ```bash
34
34
  opencli plugin install github:user/repo
35
+ opencli plugin install github:user/repo/subplugin # 安装 monorepo 中的指定子插件
35
36
  opencli plugin install https://github.com/user/repo
36
37
  ```
37
38
 
38
39
  如果仓库名带 `opencli-plugin-` 前缀,本地目录会自动去掉这个前缀。例如 `opencli-plugin-hot-digest` 会变成 `hot-digest`。
39
40
 
41
+ ## 插件清单 (`opencli-plugin.json`)
42
+
43
+ 插件可以在仓库根目录放置 `opencli-plugin.json` 来声明元数据:
44
+
45
+ ```json
46
+ {
47
+ "name": "my-plugin",
48
+ "version": "1.0.0",
49
+ "opencli": ">=1.0.0",
50
+ "description": "我的插件"
51
+ }
52
+ ```
53
+
54
+ | 字段 | 说明 |
55
+ |------|------|
56
+ | `name` | 插件名称(覆盖从仓库名推导的名称) |
57
+ | `version` | 语义化版本 |
58
+ | `opencli` | 所需的 opencli 版本范围(如 `>=1.0.0`、`^1.2.0`) |
59
+ | `description` | 描述 |
60
+ | `plugins` | Monorepo 子插件声明(见下文) |
61
+
62
+ 清单文件是可选的——没有它的插件依然可以正常工作。
63
+
64
+ ## Monorepo 插件
65
+
66
+ 一个仓库可以通过在 `opencli-plugin.json` 中声明 `plugins` 字段来包含多个插件:
67
+
68
+ ```json
69
+ {
70
+ "version": "1.0.0",
71
+ "opencli": ">=1.0.0",
72
+ "description": "我的插件合集",
73
+ "plugins": {
74
+ "polymarket": {
75
+ "path": "packages/polymarket",
76
+ "description": "预测市场分析",
77
+ "version": "1.2.0"
78
+ },
79
+ "defi": {
80
+ "path": "packages/defi",
81
+ "description": "DeFi 协议数据",
82
+ "version": "0.8.0"
83
+ },
84
+ "experimental": {
85
+ "path": "packages/experimental",
86
+ "disabled": true
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ```bash
93
+ # 安装 monorepo 中的全部子插件
94
+ opencli plugin install github:user/opencli-plugins
95
+
96
+ # 安装指定子插件
97
+ opencli plugin install github:user/opencli-plugins/polymarket
98
+ ```
99
+
100
+ - Monorepo 只 clone 一次到 `~/.opencli/monorepos/<repo>/`
101
+ - 每个子插件通过 symlink 出现在 `~/.opencli/plugins/<name>/`
102
+ - 更新任何子插件会拉取整个 monorepo 并刷新所有子插件
103
+ - 卸载最后一个子插件时,monorepo 目录会被自动清理
104
+
40
105
  ## 版本追踪
41
106
 
42
107
  OpenCLI 会把已安装 plugin 的版本记录到 `~/.opencli/plugins.lock.json`。每条记录会保存 plugin source、当前 git commit hash、安装时间,以及最近一次更新时间。只要有这份元数据,`opencli plugin list` 就会显示对应的短 commit hash。
@@ -6,6 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "vite build --watch",
8
8
  "build": "vite build",
9
+ "package:release": "node scripts/package-release.mjs",
9
10
  "typecheck": "tsc --noEmit"
10
11
  },
11
12
  "devDependencies": {
@@ -0,0 +1,179 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const extensionDir = path.resolve(__dirname, '..');
7
+ const repoRoot = path.resolve(extensionDir, '..');
8
+
9
+ function parseArgs(argv) {
10
+ const args = { outDir: path.join(repoRoot, 'extension-package') };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const arg = argv[i];
13
+ if (arg === '--out' && argv[i + 1]) {
14
+ const outDir = argv[++i];
15
+ args.outDir = path.isAbsolute(outDir)
16
+ ? outDir
17
+ : path.resolve(process.cwd(), outDir);
18
+ }
19
+ }
20
+ return args;
21
+ }
22
+
23
+ async function exists(targetPath) {
24
+ try {
25
+ await fs.access(targetPath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function isLocalAsset(ref) {
33
+ return typeof ref === 'string'
34
+ && ref.length > 0
35
+ && !ref.startsWith('http://')
36
+ && !ref.startsWith('https://')
37
+ && !ref.startsWith('//')
38
+ && !ref.startsWith('chrome://')
39
+ && !ref.startsWith('chrome-extension://')
40
+ && !ref.startsWith('data:')
41
+ && !ref.startsWith('#');
42
+ }
43
+
44
+ function addLocalAsset(files, ref) {
45
+ if (isLocalAsset(ref)) files.add(ref);
46
+ }
47
+
48
+ function collectManifestEntrypoints(manifest) {
49
+ const files = new Set(['manifest.json']);
50
+
51
+ addLocalAsset(files, manifest.background?.service_worker);
52
+ addLocalAsset(files, manifest.action?.default_popup);
53
+ addLocalAsset(files, manifest.options_page);
54
+ addLocalAsset(files, manifest.devtools_page);
55
+ addLocalAsset(files, manifest.side_panel?.default_path);
56
+
57
+ for (const ref of Object.values(manifest.icons ?? {})) addLocalAsset(files, ref);
58
+ for (const ref of Object.values(manifest.action?.default_icon ?? {})) addLocalAsset(files, ref);
59
+ for (const contentScript of manifest.content_scripts ?? []) {
60
+ for (const jsFile of contentScript.js ?? []) addLocalAsset(files, jsFile);
61
+ for (const cssFile of contentScript.css ?? []) addLocalAsset(files, cssFile);
62
+ }
63
+ for (const page of manifest.sandbox?.pages ?? []) addLocalAsset(files, page);
64
+ for (const overridePage of Object.values(manifest.chrome_url_overrides ?? {})) addLocalAsset(files, overridePage);
65
+ for (const entry of manifest.web_accessible_resources ?? []) {
66
+ for (const resource of entry.resources ?? []) addLocalAsset(files, resource);
67
+ }
68
+ if (manifest.default_locale) files.add('_locales');
69
+
70
+ return [...files];
71
+ }
72
+
73
+ async function collectHtmlDependencies(relativeHtmlPath, files, visited) {
74
+ if (visited.has(relativeHtmlPath)) return;
75
+ visited.add(relativeHtmlPath);
76
+
77
+ const htmlPath = path.join(extensionDir, relativeHtmlPath);
78
+ const html = await fs.readFile(htmlPath, 'utf8');
79
+ const attrRe = /\b(?:src|href)=["']([^"'#?]+(?:\?[^"']*)?)["']/gi;
80
+
81
+ for (const match of html.matchAll(attrRe)) {
82
+ const rawRef = match[1];
83
+ const cleanRef = rawRef.split('?')[0];
84
+ if (!isLocalAsset(cleanRef)) continue;
85
+
86
+ const resolvedRelativePath = cleanRef.startsWith('/')
87
+ ? cleanRef.slice(1)
88
+ : path.posix.normalize(path.posix.join(path.posix.dirname(relativeHtmlPath), cleanRef));
89
+
90
+ addLocalAsset(files, resolvedRelativePath);
91
+ if (resolvedRelativePath.endsWith('.html')) {
92
+ await collectHtmlDependencies(resolvedRelativePath, files, visited);
93
+ }
94
+ }
95
+ }
96
+
97
+ async function collectManifestAssets(manifest) {
98
+ const files = new Set(collectManifestEntrypoints(manifest));
99
+ const htmlPages = [];
100
+
101
+ if (manifest.action?.default_popup) {
102
+ htmlPages.push(manifest.action.default_popup);
103
+ }
104
+ if (manifest.options_page) htmlPages.push(manifest.options_page);
105
+ if (manifest.devtools_page) htmlPages.push(manifest.devtools_page);
106
+ if (manifest.side_panel?.default_path) htmlPages.push(manifest.side_panel.default_path);
107
+ for (const page of manifest.sandbox?.pages ?? []) htmlPages.push(page);
108
+ for (const overridePage of Object.values(manifest.chrome_url_overrides ?? {})) htmlPages.push(overridePage);
109
+
110
+ const visited = new Set();
111
+ for (const htmlPage of htmlPages) {
112
+ if (isLocalAsset(htmlPage)) {
113
+ await collectHtmlDependencies(htmlPage, files, visited);
114
+ }
115
+ }
116
+
117
+ return [...files];
118
+ }
119
+
120
+ async function copyEntry(relativePath, outDir) {
121
+ const fromPath = path.join(extensionDir, relativePath);
122
+ const toPath = path.join(outDir, relativePath);
123
+ const stats = await fs.stat(fromPath);
124
+
125
+ if (stats.isDirectory()) {
126
+ await fs.cp(fromPath, toPath, { recursive: true });
127
+ return;
128
+ }
129
+
130
+ await fs.mkdir(path.dirname(toPath), { recursive: true });
131
+ await fs.copyFile(fromPath, toPath);
132
+ }
133
+
134
+ async function findMissingEntries(baseDir, entries) {
135
+ const missingEntries = [];
136
+ for (const relativePath of entries) {
137
+ const absolutePath = path.join(baseDir, relativePath);
138
+ if (!(await exists(absolutePath))) {
139
+ missingEntries.push(relativePath);
140
+ }
141
+ }
142
+ return missingEntries;
143
+ }
144
+
145
+ async function main() {
146
+ const { outDir } = parseArgs(process.argv.slice(2));
147
+ const manifestPath = path.join(extensionDir, 'manifest.json');
148
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
149
+
150
+ const requiredEntries = await collectManifestAssets(manifest);
151
+ const missingEntries = await findMissingEntries(extensionDir, requiredEntries);
152
+
153
+ if (missingEntries.length > 0) {
154
+ console.error('Missing files referenced by the extension package:');
155
+ for (const missingEntry of missingEntries) console.error(`- ${missingEntry}`);
156
+ process.exit(1);
157
+ }
158
+
159
+ await fs.rm(outDir, { recursive: true, force: true });
160
+ await fs.mkdir(outDir, { recursive: true });
161
+
162
+ for (const relativePath of requiredEntries) {
163
+ await copyEntry(relativePath, outDir);
164
+ }
165
+
166
+ // Guard against regressions where manifest entry files (e.g. action.default_popup)
167
+ // are accidentally omitted from the packaged directory.
168
+ const packagedEntrypoints = collectManifestEntrypoints(manifest);
169
+ const missingPackagedEntrypoints = await findMissingEntries(outDir, packagedEntrypoints);
170
+ if (missingPackagedEntrypoints.length > 0) {
171
+ console.error('Packaged extension is missing files referenced by manifest.json:');
172
+ for (const missingEntry of missingPackagedEntrypoints) console.error(`- ${missingEntry}`);
173
+ process.exit(1);
174
+ }
175
+
176
+ console.log(`Extension package prepared at ${path.relative(repoRoot, outDir) || outDir}`);
177
+ }
178
+
179
+ await main();
@@ -51,6 +51,8 @@ function connect(): void {
51
51
  clearTimeout(reconnectTimer);
52
52
  reconnectTimer = null;
53
53
  }
54
+ // Send version so the daemon can report mismatches to the CLI
55
+ ws?.send(JSON.stringify({ type: 'hello', version: chrome.runtime.getManifest().version }));
54
56
  };
55
57
 
56
58
  ws.onmessage = async (event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,17 +19,20 @@
19
19
  },
20
20
  "scripts": {
21
21
  "dev": "tsx src/main.ts",
22
+ "dev:bun": "bun src/main.ts",
22
23
  "build": "npm run clean-dist && tsc && npm run clean-yaml && npm run copy-yaml && npm run build-manifest",
23
24
  "build-manifest": "node dist/build-manifest.js",
24
25
  "clean-dist": "node scripts/clean-dist.cjs",
25
26
  "clean-yaml": "node scripts/clean-yaml.cjs",
26
27
  "copy-yaml": "node scripts/copy-yaml.cjs",
27
28
  "start": "node dist/main.js",
29
+ "start:bun": "bun dist/main.js",
28
30
  "postinstall": "node scripts/postinstall.js || true",
29
31
  "typecheck": "tsc --noEmit",
30
32
  "lint": "tsc --noEmit",
31
33
  "prepublishOnly": "npm run build",
32
34
  "test": "vitest run --project unit",
35
+ "test:bun": "bun vitest run --project unit",
33
36
  "test:adapter": "vitest run --project adapter",
34
37
  "test:all": "vitest run",
35
38
  "test:e2e": "vitest run --project e2e",
@@ -195,6 +195,16 @@ function main() {
195
195
  console.error(`Warning: Could not install shell completion: ${err.message}`);
196
196
  }
197
197
  }
198
+
199
+ // ── Browser Bridge setup hint ───────────────────────────────────────
200
+ console.log('');
201
+ console.log(' \x1b[1mNext step — Browser Bridge setup\x1b[0m');
202
+ console.log(' Browser commands (bilibili, zhihu, twitter...) require the extension:');
203
+ console.log(' 1. Download: https://github.com/jackwener/opencli/releases');
204
+ console.log(' 2. Open chrome://extensions → enable Developer Mode → Load unpacked');
205
+ console.log('');
206
+ console.log(' Then run \x1b[36mopencli doctor\x1b[0m to verify.');
207
+ console.log('');
198
208
  }
199
209
 
200
210
  main();
@@ -12,6 +12,7 @@ import { WebSocket, type RawData } from 'ws';
12
12
  import { request as httpRequest } from 'node:http';
13
13
  import { request as httpsRequest } from 'node:https';
14
14
  import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
15
+ import type { IBrowserFactory } from '../runtime.js';
15
16
  import { wrapForEval } from './utils.js';
16
17
  import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
17
18
  import { generateStealthJs } from './stealth.js';
@@ -47,7 +48,7 @@ interface RuntimeEvaluateResult {
47
48
 
48
49
  const CDP_SEND_TIMEOUT = 30_000;
49
50
 
50
- export class CDPBridge {
51
+ export class CDPBridge implements IBrowserFactory {
51
52
  private _ws: WebSocket | null = null;
52
53
  private _idCounter = 0;
53
54
  private _pending = new Map<number, { resolve: (val: unknown) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
@@ -13,17 +13,22 @@ export { isDaemonRunning };
13
13
  /**
14
14
  * Check daemon status and return connection info.
15
15
  */
16
- export async function checkDaemonStatus(): Promise<{
16
+ export async function checkDaemonStatus(opts?: { timeout?: number }): Promise<{
17
17
  running: boolean;
18
18
  extensionConnected: boolean;
19
+ extensionVersion?: string;
19
20
  }> {
20
21
  try {
21
22
  const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
23
+ const controller = new AbortController();
24
+ const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
22
25
  const res = await fetch(`http://127.0.0.1:${port}/status`, {
23
26
  headers: { 'X-OpenCLI': '1' },
27
+ signal: controller.signal,
24
28
  });
25
- const data = await res.json() as { ok: boolean; extensionConnected: boolean };
26
- return { running: true, extensionConnected: data.extensionConnected };
29
+ const data = await res.json() as { ok: boolean; extensionConnected: boolean; extensionVersion?: string };
30
+ clearTimeout(timer);
31
+ return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
27
32
  } catch {
28
33
  return { running: false, extensionConnected: false };
29
34
  }