@jackwener/opencli 1.4.1 → 1.5.1

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 (369) 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/cdp.js +5 -0
  8. package/dist/browser/discover.d.ts +4 -1
  9. package/dist/browser/discover.js +6 -2
  10. package/dist/browser/errors.d.ts +2 -2
  11. package/dist/browser/errors.js +4 -12
  12. package/dist/browser/mcp.d.ts +2 -1
  13. package/dist/browser/page.d.ts +3 -0
  14. package/dist/browser/page.js +24 -1
  15. package/dist/build-manifest.d.ts +2 -0
  16. package/dist/build-manifest.js +39 -14
  17. package/dist/build-manifest.test.js +21 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +2 -1
  20. package/dist/cli-manifest.json +1567 -108
  21. package/dist/cli.js +68 -6
  22. package/dist/clis/36kr/article.d.ts +1 -0
  23. package/dist/clis/36kr/article.js +62 -0
  24. package/dist/clis/36kr/hot.d.ts +3 -0
  25. package/dist/clis/36kr/hot.js +80 -0
  26. package/dist/clis/36kr/hot.test.d.ts +1 -0
  27. package/dist/clis/36kr/hot.test.js +15 -0
  28. package/dist/clis/36kr/news.d.ts +1 -0
  29. package/dist/clis/36kr/news.js +51 -0
  30. package/dist/clis/36kr/news.test.d.ts +1 -0
  31. package/dist/clis/36kr/news.test.js +85 -0
  32. package/dist/clis/36kr/search.d.ts +1 -0
  33. package/dist/clis/36kr/search.js +72 -0
  34. package/dist/clis/bilibili/comments.d.ts +5 -0
  35. package/dist/clis/bilibili/comments.js +40 -0
  36. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  37. package/dist/clis/bilibili/comments.test.js +82 -0
  38. package/dist/clis/bluesky/feeds.yaml +29 -0
  39. package/dist/clis/bluesky/followers.yaml +33 -0
  40. package/dist/clis/bluesky/following.yaml +33 -0
  41. package/dist/clis/bluesky/profile.yaml +27 -0
  42. package/dist/clis/bluesky/search.yaml +34 -0
  43. package/dist/clis/bluesky/starter-packs.yaml +34 -0
  44. package/dist/clis/bluesky/thread.yaml +32 -0
  45. package/dist/clis/bluesky/trending.yaml +27 -0
  46. package/dist/clis/bluesky/user.yaml +34 -0
  47. package/dist/clis/chatgpt/ask.js +29 -14
  48. package/dist/clis/chatgpt/ax.d.ts +6 -0
  49. package/dist/clis/chatgpt/ax.js +172 -1
  50. package/dist/clis/chatgpt/model.d.ts +1 -0
  51. package/dist/clis/chatgpt/model.js +24 -0
  52. package/dist/clis/chatgpt/send.js +12 -3
  53. package/dist/clis/douban/download.d.ts +1 -0
  54. package/dist/clis/douban/download.js +67 -0
  55. package/dist/clis/douban/download.test.d.ts +1 -0
  56. package/dist/clis/douban/download.test.js +170 -0
  57. package/dist/clis/douban/photos.d.ts +1 -0
  58. package/dist/clis/douban/photos.js +34 -0
  59. package/dist/clis/douban/utils.d.ts +25 -0
  60. package/dist/clis/douban/utils.js +190 -1
  61. package/dist/clis/douban/utils.test.d.ts +1 -0
  62. package/dist/clis/douban/utils.test.js +64 -0
  63. package/dist/clis/imdb/person.d.ts +1 -0
  64. package/dist/clis/imdb/person.js +203 -0
  65. package/dist/clis/imdb/reviews.d.ts +1 -0
  66. package/dist/clis/imdb/reviews.js +88 -0
  67. package/dist/clis/imdb/search.d.ts +1 -0
  68. package/dist/clis/imdb/search.js +161 -0
  69. package/dist/clis/imdb/title.d.ts +1 -0
  70. package/dist/clis/imdb/title.js +93 -0
  71. package/dist/clis/imdb/top.d.ts +1 -0
  72. package/dist/clis/imdb/top.js +53 -0
  73. package/dist/clis/imdb/trending.d.ts +1 -0
  74. package/dist/clis/imdb/trending.js +52 -0
  75. package/dist/clis/imdb/utils.d.ts +46 -0
  76. package/dist/clis/imdb/utils.js +285 -0
  77. package/dist/clis/imdb/utils.test.d.ts +1 -0
  78. package/dist/clis/imdb/utils.test.js +88 -0
  79. package/dist/clis/jd/item.d.ts +4 -0
  80. package/dist/clis/jd/item.js +16 -15
  81. package/dist/clis/jd/item.test.js +16 -1
  82. package/dist/clis/linux-do/categories.yaml +38 -9
  83. package/dist/clis/linux-do/category.d.ts +1 -0
  84. package/dist/clis/linux-do/category.js +36 -0
  85. package/dist/clis/linux-do/feed.d.ts +45 -0
  86. package/dist/clis/linux-do/feed.js +397 -0
  87. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  88. package/dist/clis/linux-do/feed.test.js +118 -0
  89. package/dist/clis/linux-do/hot.d.ts +1 -0
  90. package/dist/clis/linux-do/hot.js +25 -0
  91. package/dist/clis/linux-do/latest.d.ts +1 -0
  92. package/dist/clis/linux-do/latest.js +18 -0
  93. package/dist/clis/linux-do/tags.yaml +41 -0
  94. package/dist/clis/linux-do/topic.yaml +41 -3
  95. package/dist/clis/linux-do/user-posts.yaml +67 -0
  96. package/dist/clis/linux-do/user-topics.yaml +54 -0
  97. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  98. package/dist/clis/paperreview/commands.test.js +243 -0
  99. package/dist/clis/paperreview/feedback.d.ts +1 -0
  100. package/dist/clis/paperreview/feedback.js +52 -0
  101. package/dist/clis/paperreview/review.d.ts +1 -0
  102. package/dist/clis/paperreview/review.js +37 -0
  103. package/dist/clis/paperreview/submit.d.ts +1 -0
  104. package/dist/clis/paperreview/submit.js +85 -0
  105. package/dist/clis/paperreview/utils.d.ts +46 -0
  106. package/dist/clis/paperreview/utils.js +197 -0
  107. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  108. package/dist/clis/paperreview/utils.test.js +49 -0
  109. package/dist/clis/producthunt/browse.d.ts +1 -0
  110. package/dist/clis/producthunt/browse.js +99 -0
  111. package/dist/clis/producthunt/hot.d.ts +1 -0
  112. package/dist/clis/producthunt/hot.js +110 -0
  113. package/dist/clis/producthunt/posts.d.ts +1 -0
  114. package/dist/clis/producthunt/posts.js +28 -0
  115. package/dist/clis/producthunt/today.d.ts +1 -0
  116. package/dist/clis/producthunt/today.js +35 -0
  117. package/dist/clis/producthunt/utils.d.ts +29 -0
  118. package/dist/clis/producthunt/utils.js +99 -0
  119. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  120. package/dist/clis/producthunt/utils.test.js +64 -0
  121. package/dist/clis/twitter/article.js +4 -28
  122. package/dist/clis/twitter/likes.d.ts +24 -0
  123. package/dist/clis/twitter/likes.js +217 -0
  124. package/dist/clis/twitter/likes.test.d.ts +1 -0
  125. package/dist/clis/twitter/likes.test.js +85 -0
  126. package/dist/clis/twitter/profile.js +4 -28
  127. package/dist/clis/twitter/search.js +2 -1
  128. package/dist/clis/twitter/search.test.js +2 -0
  129. package/dist/clis/twitter/shared.d.ts +6 -0
  130. package/dist/clis/twitter/shared.js +35 -0
  131. package/dist/clis/twitter/timeline.js +2 -13
  132. package/dist/clis/twitter/trending.js +29 -61
  133. package/dist/clis/v2ex/hot.yaml +17 -3
  134. package/dist/clis/weixin/download.d.ts +17 -0
  135. package/dist/clis/weixin/download.js +88 -20
  136. package/dist/clis/weread/book.js +2 -2
  137. package/dist/clis/weread/commands.test.d.ts +3 -0
  138. package/dist/clis/weread/commands.test.js +43 -0
  139. package/dist/clis/weread/highlights.js +2 -2
  140. package/dist/clis/weread/notebooks.js +2 -2
  141. package/dist/clis/weread/notes.js +3 -3
  142. package/dist/clis/weread/shelf.js +2 -2
  143. package/dist/clis/weread/utils.d.ts +4 -4
  144. package/dist/clis/weread/utils.js +32 -14
  145. package/dist/clis/weread/utils.test.js +1 -28
  146. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  147. package/dist/clis/xiaohongshu/comments.js +74 -0
  148. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  149. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  150. package/dist/clis/xiaohongshu/publish.js +179 -47
  151. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  152. package/dist/clis/xiaohongshu/publish.test.js +131 -0
  153. package/dist/clis/xiaohongshu/search.d.ts +8 -1
  154. package/dist/clis/xiaohongshu/search.js +20 -1
  155. package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
  156. package/dist/clis/xiaohongshu/search.test.js +32 -1
  157. package/dist/commanderAdapter.d.ts +1 -0
  158. package/dist/commanderAdapter.js +176 -29
  159. package/dist/commanderAdapter.test.d.ts +1 -0
  160. package/dist/commanderAdapter.test.js +62 -0
  161. package/dist/daemon.js +17 -1
  162. package/dist/discovery.js +48 -42
  163. package/dist/doctor.d.ts +2 -2
  164. package/dist/doctor.js +11 -4
  165. package/dist/download/index.js +63 -51
  166. package/dist/download/index.test.js +17 -4
  167. package/dist/engine.test.js +42 -0
  168. package/dist/errors.d.ts +4 -2
  169. package/dist/errors.js +17 -34
  170. package/dist/execution.d.ts +1 -3
  171. package/dist/execution.js +66 -8
  172. package/dist/execution.test.d.ts +1 -0
  173. package/dist/execution.test.js +40 -0
  174. package/dist/external.js +6 -1
  175. package/dist/hooks.js +2 -0
  176. package/dist/main.js +6 -0
  177. package/dist/output.js +5 -1
  178. package/dist/pipeline/executor.js +3 -4
  179. package/dist/plugin-manifest.d.ts +70 -0
  180. package/dist/plugin-manifest.js +160 -0
  181. package/dist/plugin-manifest.test.d.ts +4 -0
  182. package/dist/plugin-manifest.test.js +179 -0
  183. package/dist/plugin-scaffold.d.ts +28 -0
  184. package/dist/plugin-scaffold.js +142 -0
  185. package/dist/plugin-scaffold.test.d.ts +4 -0
  186. package/dist/plugin-scaffold.test.js +83 -0
  187. package/dist/plugin.d.ts +82 -11
  188. package/dist/plugin.js +870 -84
  189. package/dist/plugin.test.js +1032 -17
  190. package/dist/registry.d.ts +4 -0
  191. package/dist/registry.js +2 -0
  192. package/dist/runtime-detect.d.ts +21 -0
  193. package/dist/runtime-detect.js +32 -0
  194. package/dist/runtime-detect.test.d.ts +1 -0
  195. package/dist/runtime-detect.test.js +27 -0
  196. package/dist/runtime.d.ts +1 -0
  197. package/dist/runtime.js +2 -2
  198. package/dist/serialization.d.ts +2 -0
  199. package/dist/serialization.js +6 -0
  200. package/dist/types.d.ts +3 -0
  201. package/dist/update-check.d.ts +22 -0
  202. package/dist/update-check.js +112 -0
  203. package/dist/weixin-download.test.d.ts +1 -0
  204. package/dist/weixin-download.test.js +30 -0
  205. package/dist/weread-private-api-regression.test.d.ts +1 -0
  206. package/dist/weread-private-api-regression.test.js +122 -0
  207. package/dist/yaml-schema.d.ts +3 -0
  208. package/dist/yaml-schema.js +18 -1
  209. package/docs/.vitepress/config.mts +4 -0
  210. package/docs/adapters/browser/36kr.md +47 -0
  211. package/docs/adapters/browser/bluesky.md +53 -0
  212. package/docs/adapters/browser/douban.md +14 -0
  213. package/docs/adapters/browser/imdb.md +47 -0
  214. package/docs/adapters/browser/jd.md +2 -2
  215. package/docs/adapters/browser/linux-do.md +181 -20
  216. package/docs/adapters/browser/paperreview.md +43 -0
  217. package/docs/adapters/browser/producthunt.md +49 -0
  218. package/docs/adapters/desktop/chatgpt.md +5 -0
  219. package/docs/adapters/index.md +6 -2
  220. package/docs/advanced/download.md +4 -0
  221. package/docs/advanced/rate-limiter-plugin.md +99 -0
  222. package/docs/guide/electron-app-cli.md +200 -0
  223. package/docs/guide/getting-started.md +1 -0
  224. package/docs/guide/plugins.md +97 -0
  225. package/docs/zh/guide/electron-app-cli.md +188 -0
  226. package/docs/zh/guide/getting-started.md +1 -0
  227. package/docs/zh/guide/plugins.md +65 -0
  228. package/extension/package.json +1 -0
  229. package/extension/scripts/package-release.mjs +179 -0
  230. package/extension/src/background.ts +2 -0
  231. package/package.json +4 -1
  232. package/scripts/postinstall.js +10 -0
  233. package/src/browser/cdp.ts +8 -1
  234. package/src/browser/discover.ts +8 -3
  235. package/src/browser/errors.ts +13 -14
  236. package/src/browser/mcp.ts +2 -1
  237. package/src/browser/page.ts +24 -1
  238. package/src/build-manifest.test.ts +23 -0
  239. package/src/build-manifest.ts +40 -15
  240. package/src/capabilityRouting.ts +2 -1
  241. package/src/cli.ts +69 -6
  242. package/src/clis/36kr/article.ts +69 -0
  243. package/src/clis/36kr/hot.test.ts +19 -0
  244. package/src/clis/36kr/hot.ts +100 -0
  245. package/src/clis/36kr/news.test.ts +90 -0
  246. package/src/clis/36kr/news.ts +54 -0
  247. package/src/clis/36kr/search.ts +78 -0
  248. package/src/clis/bilibili/comments.test.ts +102 -0
  249. package/src/clis/bilibili/comments.ts +44 -0
  250. package/src/clis/bluesky/feeds.yaml +29 -0
  251. package/src/clis/bluesky/followers.yaml +33 -0
  252. package/src/clis/bluesky/following.yaml +33 -0
  253. package/src/clis/bluesky/profile.yaml +27 -0
  254. package/src/clis/bluesky/search.yaml +34 -0
  255. package/src/clis/bluesky/starter-packs.yaml +34 -0
  256. package/src/clis/bluesky/thread.yaml +32 -0
  257. package/src/clis/bluesky/trending.yaml +27 -0
  258. package/src/clis/bluesky/user.yaml +34 -0
  259. package/src/clis/chatgpt/ask.ts +28 -14
  260. package/src/clis/chatgpt/ax.ts +180 -1
  261. package/src/clis/chatgpt/model.ts +27 -0
  262. package/src/clis/chatgpt/send.ts +16 -6
  263. package/src/clis/douban/download.test.ts +196 -0
  264. package/src/clis/douban/download.ts +78 -0
  265. package/src/clis/douban/photos.ts +36 -0
  266. package/src/clis/douban/utils.test.ts +97 -0
  267. package/src/clis/douban/utils.ts +232 -1
  268. package/src/clis/imdb/person.ts +232 -0
  269. package/src/clis/imdb/reviews.ts +111 -0
  270. package/src/clis/imdb/search.ts +179 -0
  271. package/src/clis/imdb/title.ts +121 -0
  272. package/src/clis/imdb/top.ts +67 -0
  273. package/src/clis/imdb/trending.ts +66 -0
  274. package/src/clis/imdb/utils.test.ts +117 -0
  275. package/src/clis/imdb/utils.ts +305 -0
  276. package/src/clis/jd/item.test.ts +18 -1
  277. package/src/clis/jd/item.ts +18 -15
  278. package/src/clis/linux-do/categories.yaml +38 -9
  279. package/src/clis/linux-do/category.ts +37 -0
  280. package/src/clis/linux-do/feed.test.ts +132 -0
  281. package/src/clis/linux-do/feed.ts +501 -0
  282. package/src/clis/linux-do/hot.ts +26 -0
  283. package/src/clis/linux-do/latest.ts +19 -0
  284. package/src/clis/linux-do/tags.yaml +41 -0
  285. package/src/clis/linux-do/topic.yaml +41 -3
  286. package/src/clis/linux-do/user-posts.yaml +67 -0
  287. package/src/clis/linux-do/user-topics.yaml +54 -0
  288. package/src/clis/paperreview/commands.test.ts +283 -0
  289. package/src/clis/paperreview/feedback.ts +64 -0
  290. package/src/clis/paperreview/review.ts +47 -0
  291. package/src/clis/paperreview/submit.ts +119 -0
  292. package/src/clis/paperreview/utils.test.ts +68 -0
  293. package/src/clis/paperreview/utils.ts +276 -0
  294. package/src/clis/producthunt/browse.ts +109 -0
  295. package/src/clis/producthunt/hot.ts +127 -0
  296. package/src/clis/producthunt/posts.ts +29 -0
  297. package/src/clis/producthunt/today.ts +37 -0
  298. package/src/clis/producthunt/utils.test.ts +72 -0
  299. package/src/clis/producthunt/utils.ts +122 -0
  300. package/src/clis/twitter/article.ts +5 -28
  301. package/src/clis/twitter/likes.test.ts +91 -0
  302. package/src/clis/twitter/likes.ts +256 -0
  303. package/src/clis/twitter/profile.ts +5 -28
  304. package/src/clis/twitter/search.test.ts +2 -0
  305. package/src/clis/twitter/search.ts +3 -1
  306. package/src/clis/twitter/shared.ts +45 -0
  307. package/src/clis/twitter/timeline.ts +2 -13
  308. package/src/clis/twitter/trending.ts +29 -77
  309. package/src/clis/v2ex/hot.yaml +17 -3
  310. package/src/clis/weixin/download.ts +114 -20
  311. package/src/clis/weread/book.ts +2 -2
  312. package/src/clis/weread/commands.test.ts +57 -0
  313. package/src/clis/weread/highlights.ts +2 -2
  314. package/src/clis/weread/notebooks.ts +2 -2
  315. package/src/clis/weread/notes.ts +3 -3
  316. package/src/clis/weread/shelf.ts +2 -2
  317. package/src/clis/weread/utils.test.ts +1 -32
  318. package/src/clis/weread/utils.ts +41 -16
  319. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  320. package/src/clis/xiaohongshu/comments.ts +81 -0
  321. package/src/clis/xiaohongshu/publish.test.ts +151 -0
  322. package/src/clis/xiaohongshu/publish.ts +206 -54
  323. package/src/clis/xiaohongshu/search.test.ts +39 -1
  324. package/src/clis/xiaohongshu/search.ts +19 -1
  325. package/src/commanderAdapter.test.ts +78 -0
  326. package/src/commanderAdapter.ts +188 -24
  327. package/src/daemon.ts +19 -1
  328. package/src/discovery.ts +49 -48
  329. package/src/doctor.ts +15 -5
  330. package/src/download/index.test.ts +14 -4
  331. package/src/download/index.ts +67 -55
  332. package/src/engine.test.ts +38 -0
  333. package/src/errors.ts +26 -63
  334. package/src/execution.test.ts +47 -0
  335. package/src/execution.ts +67 -9
  336. package/src/external.ts +6 -1
  337. package/src/hooks.ts +1 -0
  338. package/src/main.ts +7 -0
  339. package/src/output.ts +3 -1
  340. package/src/pipeline/executor.ts +4 -6
  341. package/src/plugin-manifest.test.ts +223 -0
  342. package/src/plugin-manifest.ts +206 -0
  343. package/src/plugin-scaffold.test.ts +98 -0
  344. package/src/plugin-scaffold.ts +170 -0
  345. package/src/plugin.test.ts +1104 -17
  346. package/src/plugin.ts +1101 -86
  347. package/src/registry.ts +6 -1
  348. package/src/runtime-detect.test.ts +30 -0
  349. package/src/runtime-detect.ts +36 -0
  350. package/src/runtime.ts +3 -3
  351. package/src/serialization.ts +4 -0
  352. package/src/types.ts +3 -0
  353. package/src/update-check.ts +114 -0
  354. package/src/weixin-download.test.ts +64 -0
  355. package/src/weread-private-api-regression.test.ts +150 -0
  356. package/src/yaml-schema.ts +20 -0
  357. package/tests/e2e/browser-auth.test.ts +13 -9
  358. package/tests/e2e/browser-public-extended.test.ts +1 -1
  359. package/tests/e2e/browser-public.test.ts +62 -4
  360. package/tests/e2e/helpers.ts +2 -1
  361. package/tests/e2e/public-commands.test.ts +37 -3
  362. package/tests/smoke/api-health.test.ts +1 -1
  363. package/vitest.config.ts +10 -0
  364. package/dist/clis/linux-do/category.yaml +0 -51
  365. package/dist/clis/linux-do/hot.yaml +0 -50
  366. package/dist/clis/linux-do/latest.yaml +0 -40
  367. package/src/clis/linux-do/category.yaml +0 -51
  368. package/src/clis/linux-do/hot.yaml +0 -50
  369. package/src/clis/linux-do/latest.yaml +0 -40
@@ -0,0 +1,67 @@
1
+ site: linux-do
2
+ name: user-posts
3
+ description: linux.do 用户的帖子
4
+ domain: linux.do
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ username:
10
+ positional: true
11
+ type: str
12
+ required: true
13
+ description: Username
14
+ limit:
15
+ type: int
16
+ default: 20
17
+ description: Number of posts
18
+
19
+ pipeline:
20
+ - navigate: https://linux.do
21
+
22
+ - evaluate: |
23
+ (async () => {
24
+ const username = ${{ args.username | json }};
25
+ const toLocalTime = (utcStr) => {
26
+ if (!utcStr) return '';
27
+ const date = new Date(utcStr);
28
+ return Number.isNaN(date.getTime()) ? utcStr : date.toLocaleString();
29
+ };
30
+ const strip = (html) => (html || '')
31
+ .replace(/<br\s*\/?>/gi, ' ')
32
+ .replace(/<\/(p|div|li|blockquote|h[1-6])>/gi, ' ')
33
+ .replace(/<[^>]+>/g, '')
34
+ .replace(/&nbsp;/g, ' ')
35
+ .replace(/&amp;/g, '&')
36
+ .replace(/&lt;/g, '<')
37
+ .replace(/&gt;/g, '>')
38
+ .replace(/&quot;/g, '"')
39
+ .replace(/&#(?:(\d+)|x([0-9a-fA-F]+));/g, (_, dec, hex) => {
40
+ try { return String.fromCodePoint(dec !== undefined ? Number(dec) : parseInt(hex, 16)); } catch { return ''; }
41
+ })
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+ const limit = ${{ args.limit | default(20) }};
45
+ const res = await fetch('/user_actions.json?username=' + encodeURIComponent(username) + '&filter=5&offset=0&limit=' + limit, { credentials: 'include' });
46
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
47
+ let data;
48
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
49
+ const actions = data?.user_actions || [];
50
+ return actions.slice(0, limit).map(a => ({
51
+ author: a.acting_username || a.username || '',
52
+ title: a.title || '',
53
+ content: strip(a.excerpt).slice(0, 200),
54
+ created_at: toLocalTime(a.created_at),
55
+ url: 'https://linux.do/t/topic/' + a.topic_id + '/' + a.post_number,
56
+ }));
57
+ })()
58
+
59
+ - map:
60
+ index: ${{ index + 1 }}
61
+ topic_user: ${{ item.author }}
62
+ topic: ${{ item.title }}
63
+ reply: ${{ item.content }}
64
+ time: ${{ item.created_at }}
65
+ url: ${{ item.url }}
66
+
67
+ columns: [index, topic_user, topic, reply, time, url]
@@ -0,0 +1,54 @@
1
+ site: linux-do
2
+ name: user-topics
3
+ description: linux.do 用户创建的话题
4
+ domain: linux.do
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ username:
10
+ positional: true
11
+ type: str
12
+ required: true
13
+ description: Username
14
+ limit:
15
+ type: int
16
+ default: 20
17
+ description: Number of topics
18
+
19
+ pipeline:
20
+ - navigate: https://linux.do
21
+
22
+ - evaluate: |
23
+ (async () => {
24
+ const username = ${{ args.username | json }};
25
+ const toLocalTime = (utcStr) => {
26
+ if (!utcStr) return '';
27
+ const date = new Date(utcStr);
28
+ return Number.isNaN(date.getTime()) ? utcStr : date.toLocaleString();
29
+ };
30
+ const res = await fetch('/topics/created-by/' + encodeURIComponent(username) + '.json', { credentials: 'include' });
31
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
32
+ let data;
33
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
34
+ const topics = data?.topic_list?.topics || [];
35
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
36
+ title: t.fancy_title || t.title || '',
37
+ replies: t.posts_count || 0,
38
+ created_at: toLocalTime(t.created_at),
39
+ likes: t.like_count || 0,
40
+ views: t.views || 0,
41
+ url: 'https://linux.do/t/topic/' + t.id,
42
+ }));
43
+ })()
44
+
45
+ - map:
46
+ rank: ${{ index + 1 }}
47
+ title: ${{ item.title }}
48
+ replies: ${{ item.replies }}
49
+ created_at: ${{ item.created_at }}
50
+ likes: ${{ item.likes }}
51
+ views: ${{ item.views }}
52
+ url: ${{ item.url }}
53
+
54
+ columns: [rank, title, replies, created_at, likes, views, url]
@@ -0,0 +1,3 @@
1
+ import './submit.js';
2
+ import './review.js';
3
+ import './feedback.js';
@@ -0,0 +1,243 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { mockReadPdfFile, mockRequestJson, mockUploadPresignedPdf, mockValidateHelpfulness, mockParseYesNo, } = vi.hoisted(() => ({
3
+ mockReadPdfFile: vi.fn(),
4
+ mockRequestJson: vi.fn(),
5
+ mockUploadPresignedPdf: vi.fn(),
6
+ mockValidateHelpfulness: vi.fn(),
7
+ mockParseYesNo: vi.fn(),
8
+ }));
9
+ vi.mock('./utils.js', async () => {
10
+ const actual = await vi.importActual('./utils.js');
11
+ return {
12
+ ...actual,
13
+ readPdfFile: mockReadPdfFile,
14
+ requestJson: mockRequestJson,
15
+ uploadPresignedPdf: mockUploadPresignedPdf,
16
+ validateHelpfulness: mockValidateHelpfulness,
17
+ parseYesNo: mockParseYesNo,
18
+ };
19
+ });
20
+ import { getRegistry } from '../../registry.js';
21
+ import './submit.js';
22
+ import './review.js';
23
+ import './feedback.js';
24
+ describe('paperreview submit command', () => {
25
+ beforeEach(() => {
26
+ mockReadPdfFile.mockReset();
27
+ mockRequestJson.mockReset();
28
+ mockUploadPresignedPdf.mockReset();
29
+ mockValidateHelpfulness.mockReset();
30
+ mockParseYesNo.mockReset();
31
+ });
32
+ it('supports dry run without any remote request', async () => {
33
+ const cmd = getRegistry().get('paperreview/submit');
34
+ expect(cmd?.func).toBeTypeOf('function');
35
+ mockReadPdfFile.mockResolvedValue({
36
+ buffer: Buffer.from('%PDF'),
37
+ fileName: 'paper.pdf',
38
+ resolvedPath: '/tmp/paper.pdf',
39
+ sizeBytes: 4096,
40
+ });
41
+ const result = await cmd.func(null, {
42
+ pdf: './paper.pdf',
43
+ email: 'wang2629651228@gmail.com',
44
+ venue: 'RAL',
45
+ 'dry-run': true,
46
+ 'prepare-only': false,
47
+ });
48
+ expect(mockRequestJson).not.toHaveBeenCalled();
49
+ expect(result).toMatchObject({
50
+ status: 'dry-run',
51
+ file: 'paper.pdf',
52
+ email: 'wang2629651228@gmail.com',
53
+ venue: 'RAL',
54
+ });
55
+ });
56
+ it('treats explicit false flags as false and performs the real submission path', async () => {
57
+ const cmd = getRegistry().get('paperreview/submit');
58
+ expect(cmd?.func).toBeTypeOf('function');
59
+ mockReadPdfFile.mockResolvedValue({
60
+ buffer: Buffer.from('%PDF'),
61
+ fileName: 'paper.pdf',
62
+ resolvedPath: '/tmp/paper.pdf',
63
+ sizeBytes: 4096,
64
+ });
65
+ mockRequestJson
66
+ .mockResolvedValueOnce({
67
+ response: { ok: true, status: 200 },
68
+ payload: {
69
+ success: true,
70
+ presigned_url: 'https://upload.example.com',
71
+ presigned_fields: { key: 'uploads/paper.pdf' },
72
+ s3_key: 'uploads/paper.pdf',
73
+ },
74
+ })
75
+ .mockResolvedValueOnce({
76
+ response: { ok: true, status: 200 },
77
+ payload: {
78
+ success: true,
79
+ token: 'tok_false',
80
+ message: 'Submission accepted',
81
+ },
82
+ });
83
+ const result = await cmd.func(null, {
84
+ pdf: './paper.pdf',
85
+ email: 'wang2629651228@gmail.com',
86
+ venue: 'RAL',
87
+ 'dry-run': false,
88
+ 'prepare-only': false,
89
+ });
90
+ expect(mockUploadPresignedPdf).toHaveBeenCalledTimes(1);
91
+ expect(result).toMatchObject({
92
+ status: 'submitted',
93
+ token: 'tok_false',
94
+ review_url: 'https://paperreview.ai/review?token=tok_false',
95
+ });
96
+ });
97
+ it('supports prepare-only without uploading the PDF', async () => {
98
+ const cmd = getRegistry().get('paperreview/submit');
99
+ expect(cmd?.func).toBeTypeOf('function');
100
+ mockReadPdfFile.mockResolvedValue({
101
+ buffer: Buffer.from('%PDF'),
102
+ fileName: 'paper.pdf',
103
+ resolvedPath: '/tmp/paper.pdf',
104
+ sizeBytes: 4096,
105
+ });
106
+ mockRequestJson.mockResolvedValueOnce({
107
+ response: { ok: true, status: 200 },
108
+ payload: {
109
+ success: true,
110
+ presigned_url: 'https://upload.example.com',
111
+ presigned_fields: { key: 'uploads/paper.pdf' },
112
+ s3_key: 'uploads/paper.pdf',
113
+ },
114
+ });
115
+ const result = await cmd.func(null, {
116
+ pdf: './paper.pdf',
117
+ email: 'wang2629651228@gmail.com',
118
+ venue: 'RAL',
119
+ 'dry-run': false,
120
+ 'prepare-only': true,
121
+ });
122
+ expect(mockUploadPresignedPdf).not.toHaveBeenCalled();
123
+ expect(mockRequestJson).toHaveBeenCalledTimes(1);
124
+ expect(result).toMatchObject({
125
+ status: 'prepared',
126
+ s3_key: 'uploads/paper.pdf',
127
+ });
128
+ });
129
+ it('requests an upload URL, uploads the PDF, and confirms the submission', async () => {
130
+ const cmd = getRegistry().get('paperreview/submit');
131
+ expect(cmd?.func).toBeTypeOf('function');
132
+ mockReadPdfFile.mockResolvedValue({
133
+ buffer: Buffer.from('%PDF'),
134
+ fileName: 'paper.pdf',
135
+ resolvedPath: '/tmp/paper.pdf',
136
+ sizeBytes: 4096,
137
+ });
138
+ mockRequestJson
139
+ .mockResolvedValueOnce({
140
+ response: { ok: true, status: 200 },
141
+ payload: {
142
+ success: true,
143
+ presigned_url: 'https://upload.example.com',
144
+ presigned_fields: { key: 'uploads/paper.pdf' },
145
+ s3_key: 'uploads/paper.pdf',
146
+ },
147
+ })
148
+ .mockResolvedValueOnce({
149
+ response: { ok: true, status: 200 },
150
+ payload: {
151
+ success: true,
152
+ token: 'tok_123',
153
+ message: 'Submission accepted',
154
+ },
155
+ });
156
+ const result = await cmd.func(null, {
157
+ pdf: './paper.pdf',
158
+ email: 'wang2629651228@gmail.com',
159
+ venue: 'RAL',
160
+ 'dry-run': false,
161
+ 'prepare-only': false,
162
+ });
163
+ expect(mockRequestJson).toHaveBeenNthCalledWith(1, '/api/get-upload-url', expect.objectContaining({
164
+ method: 'POST',
165
+ body: JSON.stringify({
166
+ filename: 'paper.pdf',
167
+ venue: 'RAL',
168
+ }),
169
+ }));
170
+ expect(mockUploadPresignedPdf).toHaveBeenCalledWith('https://upload.example.com', expect.objectContaining({ fileName: 'paper.pdf' }), expect.objectContaining({ s3_key: 'uploads/paper.pdf' }));
171
+ expect(mockRequestJson).toHaveBeenNthCalledWith(2, '/api/confirm-upload', expect.objectContaining({
172
+ method: 'POST',
173
+ body: expect.any(FormData),
174
+ }));
175
+ expect(result).toMatchObject({
176
+ status: 'submitted',
177
+ token: 'tok_123',
178
+ review_url: 'https://paperreview.ai/review?token=tok_123',
179
+ });
180
+ });
181
+ });
182
+ describe('paperreview review command', () => {
183
+ beforeEach(() => {
184
+ mockRequestJson.mockReset();
185
+ });
186
+ it('returns processing status when the review is not ready yet', async () => {
187
+ const cmd = getRegistry().get('paperreview/review');
188
+ expect(cmd?.func).toBeTypeOf('function');
189
+ mockRequestJson.mockResolvedValue({
190
+ response: { status: 202 },
191
+ payload: { detail: 'Review is still processing.' },
192
+ });
193
+ const result = await cmd.func(null, { token: 'tok_123' });
194
+ expect(result).toMatchObject({
195
+ status: 'processing',
196
+ token: 'tok_123',
197
+ review_url: 'https://paperreview.ai/review?token=tok_123',
198
+ message: 'Review is still processing.',
199
+ });
200
+ });
201
+ });
202
+ describe('paperreview feedback command', () => {
203
+ beforeEach(() => {
204
+ mockRequestJson.mockReset();
205
+ mockValidateHelpfulness.mockReset();
206
+ mockParseYesNo.mockReset();
207
+ });
208
+ it('normalizes feedback inputs and posts them to the API', async () => {
209
+ const cmd = getRegistry().get('paperreview/feedback');
210
+ expect(cmd?.func).toBeTypeOf('function');
211
+ mockValidateHelpfulness.mockReturnValue(4);
212
+ mockParseYesNo.mockReturnValueOnce(true).mockReturnValueOnce(false);
213
+ mockRequestJson.mockResolvedValue({
214
+ response: { ok: true, status: 200 },
215
+ payload: { message: 'Thanks for the feedback.' },
216
+ });
217
+ const result = await cmd.func(null, {
218
+ token: 'tok_123',
219
+ helpfulness: 4,
220
+ 'critical-error': 'yes',
221
+ 'actionable-suggestions': 'no',
222
+ 'additional-comments': 'Helpful summary, but the contribution section needs more detail.',
223
+ });
224
+ expect(mockRequestJson).toHaveBeenCalledWith('/api/feedback/tok_123', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({
228
+ helpfulness: 4,
229
+ has_critical_error: true,
230
+ has_actionable_suggestions: false,
231
+ additional_comments: 'Helpful summary, but the contribution section needs more detail.',
232
+ }),
233
+ });
234
+ expect(result).toMatchObject({
235
+ status: 'submitted',
236
+ token: 'tok_123',
237
+ helpfulness: 4,
238
+ critical_error: true,
239
+ actionable_suggestions: false,
240
+ message: 'Thanks for the feedback.',
241
+ });
242
+ });
243
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { PAPERREVIEW_DOMAIN, ensureSuccess, parseYesNo, requestJson, summarizeFeedback, validateHelpfulness, } from './utils.js';
4
+ cli({
5
+ site: 'paperreview',
6
+ name: 'feedback',
7
+ description: 'Submit feedback for a paperreview.ai review token',
8
+ domain: PAPERREVIEW_DOMAIN,
9
+ strategy: Strategy.PUBLIC,
10
+ browser: false,
11
+ timeoutSeconds: 30,
12
+ args: [
13
+ { name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
14
+ { name: 'helpfulness', required: true, type: 'int', help: 'Helpfulness score from 1 to 5' },
15
+ { name: 'critical-error', required: true, choices: ['yes', 'no'], help: 'Whether the review contains a critical error' },
16
+ { name: 'actionable-suggestions', required: true, choices: ['yes', 'no'], help: 'Whether the review contains actionable suggestions' },
17
+ { name: 'additional-comments', help: 'Optional free-text feedback' },
18
+ ],
19
+ columns: ['status', 'token', 'helpfulness', 'critical_error', 'actionable_suggestions', 'message'],
20
+ func: async (_page, kwargs) => {
21
+ const token = String(kwargs.token ?? '').trim();
22
+ if (!token) {
23
+ throw new CliError('ARGUMENT', 'A review token is required.');
24
+ }
25
+ const helpfulness = validateHelpfulness(kwargs.helpfulness);
26
+ const criticalError = parseYesNo(kwargs['critical-error'], 'critical-error');
27
+ const actionableSuggestions = parseYesNo(kwargs['actionable-suggestions'], 'actionable-suggestions');
28
+ const comments = String(kwargs['additional-comments'] ?? '').trim();
29
+ const payload = {
30
+ helpfulness,
31
+ has_critical_error: criticalError,
32
+ has_actionable_suggestions: actionableSuggestions,
33
+ };
34
+ if (comments) {
35
+ payload.additional_comments = comments;
36
+ }
37
+ const { response, payload: responsePayload } = await requestJson(`/api/feedback/${encodeURIComponent(token)}`, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(payload),
41
+ });
42
+ ensureSuccess(response, responsePayload, 'Failed to submit feedback.', 'Check the token and try again');
43
+ return summarizeFeedback({
44
+ token,
45
+ helpfulness,
46
+ criticalError,
47
+ actionableSuggestions,
48
+ comments,
49
+ payload: responsePayload,
50
+ });
51
+ },
52
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { PAPERREVIEW_DOMAIN, buildReviewUrl, ensureSuccess, requestJson, summarizeReview, } from './utils.js';
4
+ cli({
5
+ site: 'paperreview',
6
+ name: 'review',
7
+ description: 'Fetch a paperreview.ai review by token',
8
+ domain: PAPERREVIEW_DOMAIN,
9
+ strategy: Strategy.PUBLIC,
10
+ browser: false,
11
+ timeoutSeconds: 30,
12
+ args: [
13
+ { name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
14
+ ],
15
+ columns: ['status', 'title', 'venue', 'numerical_score', 'has_feedback', 'review_url'],
16
+ func: async (_page, kwargs) => {
17
+ const token = String(kwargs.token ?? '').trim();
18
+ if (!token) {
19
+ throw new CliError('ARGUMENT', 'A review token is required.');
20
+ }
21
+ const { response, payload } = await requestJson(`/api/review/${encodeURIComponent(token)}`);
22
+ if (response.status === 202) {
23
+ return {
24
+ status: 'processing',
25
+ token,
26
+ review_url: buildReviewUrl(token),
27
+ title: '',
28
+ venue: '',
29
+ numerical_score: '',
30
+ has_feedback: '',
31
+ message: typeof payload === 'object' && payload ? payload.detail ?? 'Review is still processing.' : 'Review is still processing.',
32
+ };
33
+ }
34
+ ensureSuccess(response, payload, 'Failed to fetch the review.', 'Check the token and try again');
35
+ return summarizeReview(token, payload);
36
+ },
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { PAPERREVIEW_DOMAIN, ensureApiSuccess, ensureSuccess, normalizeVenue, readPdfFile, requestJson, summarizeSubmission, uploadPresignedPdf, } from './utils.js';
4
+ cli({
5
+ site: 'paperreview',
6
+ name: 'submit',
7
+ description: 'Submit a PDF to paperreview.ai for review',
8
+ domain: PAPERREVIEW_DOMAIN,
9
+ strategy: Strategy.PUBLIC,
10
+ browser: false,
11
+ timeoutSeconds: 120,
12
+ args: [
13
+ { name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
14
+ { name: 'email', required: true, help: 'Email address for the submission' },
15
+ { name: 'venue', help: 'Optional target venue such as ICLR or NeurIPS' },
16
+ { name: 'dry-run', type: 'bool', default: false, help: 'Validate the input and stop before remote submission' },
17
+ { name: 'prepare-only', type: 'bool', default: false, help: 'Request an upload slot but stop before uploading the PDF' },
18
+ ],
19
+ columns: ['status', 'file', 'email', 'venue', 'token', 'review_url', 'message'],
20
+ footerExtra: (kwargs) => {
21
+ if (kwargs['dry-run'] === true)
22
+ return 'dry run only';
23
+ if (kwargs['prepare-only'] === true)
24
+ return 'prepared only';
25
+ return undefined;
26
+ },
27
+ func: async (_page, kwargs) => {
28
+ const pdfFile = await readPdfFile(kwargs.pdf);
29
+ const email = String(kwargs.email ?? '').trim();
30
+ const venue = normalizeVenue(kwargs.venue);
31
+ const dryRun = kwargs['dry-run'] === true;
32
+ const prepareOnly = kwargs['prepare-only'] === true;
33
+ if (!email) {
34
+ throw new CliError('ARGUMENT', 'An email address is required.', 'Pass --email <address>');
35
+ }
36
+ if (dryRun) {
37
+ return summarizeSubmission({
38
+ pdfFile,
39
+ email,
40
+ venue,
41
+ message: 'Input validation passed. No remote request was sent.',
42
+ dryRun: true,
43
+ });
44
+ }
45
+ const { response: uploadUrlResponse, payload: uploadUrlPayload } = await requestJson('/api/get-upload-url', {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({
49
+ filename: pdfFile.fileName,
50
+ venue,
51
+ }),
52
+ });
53
+ ensureSuccess(uploadUrlResponse, uploadUrlPayload, 'Failed to request an upload URL.', 'Try again in a moment');
54
+ ensureApiSuccess(uploadUrlPayload, 'paperreview.ai did not return a usable upload URL.', 'Try again in a moment');
55
+ if (prepareOnly) {
56
+ return summarizeSubmission({
57
+ pdfFile,
58
+ email,
59
+ venue,
60
+ message: 'Upload slot prepared. The PDF was not uploaded and no submission was confirmed.',
61
+ s3Key: uploadUrlPayload.s3_key,
62
+ status: 'prepared',
63
+ });
64
+ }
65
+ await uploadPresignedPdf(uploadUrlPayload.presigned_url, pdfFile, uploadUrlPayload);
66
+ const confirmForm = new FormData();
67
+ confirmForm.append('s3_key', uploadUrlPayload.s3_key);
68
+ confirmForm.append('venue', venue);
69
+ confirmForm.append('email', email);
70
+ const { response: confirmResponse, payload: confirmPayload } = await requestJson('/api/confirm-upload', {
71
+ method: 'POST',
72
+ body: confirmForm,
73
+ });
74
+ ensureSuccess(confirmResponse, confirmPayload, 'Failed to confirm the upload with paperreview.ai.', 'Try again in a moment');
75
+ ensureApiSuccess(confirmPayload, 'paperreview.ai did not confirm the submission.', 'Try again in a moment');
76
+ return summarizeSubmission({
77
+ pdfFile,
78
+ email,
79
+ venue,
80
+ token: confirmPayload.token,
81
+ message: confirmPayload.message,
82
+ s3Key: uploadUrlPayload.s3_key,
83
+ });
84
+ },
85
+ });
@@ -0,0 +1,46 @@
1
+ export declare const PAPERREVIEW_DOMAIN = "paperreview.ai";
2
+ export declare const PAPERREVIEW_BASE_URL = "https://paperreview.ai";
3
+ export declare const MAX_PDF_BYTES: number;
4
+ export interface PaperreviewPdfFile {
5
+ buffer: Buffer;
6
+ fileName: string;
7
+ resolvedPath: string;
8
+ sizeBytes: number;
9
+ }
10
+ export interface PaperreviewRequestResult {
11
+ response: Response;
12
+ payload: any;
13
+ }
14
+ export declare function buildReviewUrl(token: string): string;
15
+ export declare function parseYesNo(value: unknown, name: string): boolean;
16
+ export declare function normalizeVenue(value: unknown): string;
17
+ export declare function validateHelpfulness(value: unknown): number;
18
+ export declare function readPdfFile(inputPath: unknown): Promise<PaperreviewPdfFile>;
19
+ export declare function requestJson(pathname: string, init?: RequestInit): Promise<PaperreviewRequestResult>;
20
+ export declare function ensureSuccess(response: Response, payload: unknown, fallback: string, hint?: string): void;
21
+ export declare function ensureApiSuccess(payload: unknown, fallback: string, hint?: string): void;
22
+ export declare function createUploadForm(urlData: {
23
+ presigned_fields?: Record<string, string>;
24
+ }, pdfFile: PaperreviewPdfFile): FormData;
25
+ export declare function uploadPresignedPdf(presignedUrl: string, pdfFile: PaperreviewPdfFile, urlData: {
26
+ presigned_fields?: Record<string, string>;
27
+ }): Promise<void>;
28
+ export declare function summarizeSubmission(options: {
29
+ pdfFile: PaperreviewPdfFile;
30
+ email: string;
31
+ venue: string;
32
+ token?: string;
33
+ message?: string;
34
+ s3Key?: string;
35
+ dryRun?: boolean;
36
+ status?: string;
37
+ }): Record<string, unknown>;
38
+ export declare function summarizeReview(token: string, payload: any, status?: string): Record<string, unknown>;
39
+ export declare function summarizeFeedback(options: {
40
+ token: string;
41
+ helpfulness: number;
42
+ criticalError: boolean;
43
+ actionableSuggestions: boolean;
44
+ comments: string;
45
+ payload: any;
46
+ }): Record<string, unknown>;