@jackwener/opencli 1.7.22 → 1.8.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 (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -0,0 +1,228 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import {
3
+ ArgumentError,
4
+ AuthRequiredError,
5
+ CommandExecutionError,
6
+ EmptyResultError,
7
+ } from '@jackwener/opencli/errors';
8
+ import { createHash } from 'node:crypto';
9
+
10
+ const FLOMO_APP_DOMAIN = 'v.flomoapp.com';
11
+ const FLOMO_API_DOMAIN = 'flomoapp.com';
12
+ const MAX_LIMIT = 200;
13
+
14
+ function unwrapBrowserResult(value) {
15
+ if (value && typeof value === 'object' && 'session' in value && 'data' in value) {
16
+ return value.data;
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function parsePositiveIntArg(value, name, fallback, max) {
22
+ if (value === undefined || value === null || value === '') {
23
+ return fallback;
24
+ }
25
+ const text = String(value).trim();
26
+ if (!/^\d+$/.test(text)) {
27
+ throw new ArgumentError(`flomo memos --${name} must be a positive integer`);
28
+ }
29
+ const parsed = Number(text);
30
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
31
+ throw new ArgumentError(`flomo memos --${name} must be between 1 and ${max}`);
32
+ }
33
+ return parsed;
34
+ }
35
+
36
+ function parseSinceArg(value) {
37
+ if (value === undefined || value === null || value === '') {
38
+ return 0;
39
+ }
40
+ const text = String(value).trim();
41
+ if (!/^\d+$/.test(text)) {
42
+ throw new ArgumentError('flomo memos --since must be a non-negative Unix timestamp in seconds');
43
+ }
44
+ const parsed = Number(text);
45
+ if (!Number.isSafeInteger(parsed)) {
46
+ throw new ArgumentError('flomo memos --since must be a safe integer Unix timestamp in seconds');
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ function parseSlugArg(value) {
52
+ if (value === undefined || value === null || value === '') {
53
+ return '';
54
+ }
55
+ const slug = String(value).trim();
56
+ if (!/^[A-Za-z0-9_-]{1,256}$/.test(slug)) {
57
+ throw new ArgumentError('flomo memos --slug must be an opaque memo cursor containing only letters, numbers, _ or -');
58
+ }
59
+ return slug;
60
+ }
61
+
62
+ function buildSignedUrl(limit, since, slug) {
63
+ const params = {
64
+ limit: String(limit),
65
+ latest_updated_at: String(since),
66
+ tz: '8:0',
67
+ timestamp: String(Math.floor(Date.now() / 1000)),
68
+ api_key: 'flomo_web',
69
+ app_version: '4.0',
70
+ platform: 'web',
71
+ webp: '1',
72
+ };
73
+ if (slug) params.latest_slug = slug;
74
+ const keys = Object.keys(params).sort();
75
+ const signBase = keys.map((key) => `${key}=${params[key]}`).join('&');
76
+ params.sign = createHash('md5').update(signBase + 'dbbc3dd73364b4084c3a69346e0ce2b2').digest('hex');
77
+ return 'https://flomoapp.com/api/v1/memo/updated/?' + new URLSearchParams(params).toString();
78
+ }
79
+
80
+ function buildGetTokenJs() {
81
+ return `
82
+ (() => {
83
+ try {
84
+ const raw = localStorage.getItem('me');
85
+ if (!raw) return null;
86
+ const me = JSON.parse(raw);
87
+ const token = me?.access_token || me?.data?.access_token || '';
88
+ return typeof token === 'string' && token.trim() ? token.trim() : null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ })()
93
+ `;
94
+ }
95
+
96
+ function isAuthFailureMessage(message) {
97
+ return /auth|unauth|login|token|permission|forbidden|unauthorized|登录|登陆|鉴权|权限/i.test(String(message || ''));
98
+ }
99
+
100
+ function normalizeTags(tags) {
101
+ if (!Array.isArray(tags)) return '';
102
+ return tags
103
+ .map((tag) => {
104
+ if (typeof tag === 'string') return tag;
105
+ return tag?.name || tag?.tag || tag?.content || '';
106
+ })
107
+ .map((tag) => String(tag).trim())
108
+ .filter(Boolean)
109
+ .join(', ');
110
+ }
111
+
112
+ function normalizeImages(files) {
113
+ if (!Array.isArray(files)) return '';
114
+ return files
115
+ .map((file) => file?.thumbnail_url || file?.url || '')
116
+ .map((url) => String(url).trim())
117
+ .filter(Boolean)
118
+ .join(' | ');
119
+ }
120
+
121
+ function memoUrl(slug) {
122
+ return slug ? `https://${FLOMO_APP_DOMAIN}/mine/?memo_id=${encodeURIComponent(slug)}` : '';
123
+ }
124
+
125
+ function normalizeMemo(memo) {
126
+ if (!memo || typeof memo !== 'object' || Array.isArray(memo)) {
127
+ throw new CommandExecutionError('Flomo API returned a malformed memo entry');
128
+ }
129
+ const slug = String(memo.slug || memo.id || '').trim();
130
+ if (!slug) {
131
+ throw new CommandExecutionError('Flomo API returned a memo without slug/id');
132
+ }
133
+ return {
134
+ id: slug,
135
+ url: memoUrl(slug),
136
+ content: String(memo.content || '').trim(),
137
+ slug,
138
+ tags: normalizeTags(memo.tags),
139
+ images: normalizeImages(memo.files),
140
+ created_at: String(memo.created_at || ''),
141
+ updated_at: String(memo.updated_at || ''),
142
+ };
143
+ }
144
+
145
+ async function fetchFlomoJson(url, token) {
146
+ let resp;
147
+ try {
148
+ resp = await fetch(url, {
149
+ headers: {
150
+ Authorization: 'Bearer ' + token,
151
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
152
+ Accept: 'application/json',
153
+ },
154
+ });
155
+ } catch (err) {
156
+ throw new CommandExecutionError(`Failed to fetch Flomo memos: ${err instanceof Error ? err.message : String(err)}`);
157
+ }
158
+ if (resp.status === 401 || resp.status === 403) {
159
+ throw new AuthRequiredError(FLOMO_API_DOMAIN, `Flomo API returned HTTP ${resp.status}; please refresh your Flomo login session`);
160
+ }
161
+ if (!resp.ok) {
162
+ throw new CommandExecutionError(`Flomo API returned HTTP ${resp.status}`);
163
+ }
164
+ try {
165
+ return await resp.json();
166
+ } catch (err) {
167
+ throw new CommandExecutionError(`Flomo API returned malformed JSON: ${err instanceof Error ? err.message : String(err)}`);
168
+ }
169
+ }
170
+
171
+ async function readAccessToken(page) {
172
+ const token = unwrapBrowserResult(await page.evaluate(buildGetTokenJs()));
173
+ if (typeof token !== 'string' || !token.trim()) {
174
+ throw new AuthRequiredError(FLOMO_API_DOMAIN, 'Flomo memos requires an active signed-in Flomo browser session');
175
+ }
176
+ return token.trim();
177
+ }
178
+
179
+ const command = cli({
180
+ site: 'flomo',
181
+ name: 'memos',
182
+ access: 'read',
183
+ description: 'List your Flomo memos',
184
+ domain: FLOMO_API_DOMAIN,
185
+ strategy: Strategy.COOKIE,
186
+ browser: true,
187
+ navigateBefore: `https://${FLOMO_APP_DOMAIN}/`,
188
+ args: [
189
+ { name: 'limit', type: 'int', default: 20, help: 'Number of memos to fetch (1-200)' },
190
+ { name: 'since', type: 'int', help: 'Only memos updated after this Unix timestamp in seconds' },
191
+ { name: 'slug', help: 'Pagination cursor from a previous memo page' },
192
+ ],
193
+ columns: ['id', 'url', 'content', 'slug', 'tags', 'images', 'created_at', 'updated_at'],
194
+ func: async (page, kwargs) => {
195
+ const limit = parsePositiveIntArg(kwargs.limit, 'limit', 20, MAX_LIMIT);
196
+ const since = parseSinceArg(kwargs.since);
197
+ const slug = parseSlugArg(kwargs.slug);
198
+ await page.wait(3).catch(() => {});
199
+ const token = await readAccessToken(page);
200
+ const body = await fetchFlomoJson(buildSignedUrl(limit, since, slug), token);
201
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
202
+ throw new CommandExecutionError('Flomo API returned a malformed response');
203
+ }
204
+ if (body.code !== 0) {
205
+ const message = body.message || `Flomo API error code ${body.code}`;
206
+ if (isAuthFailureMessage(message)) {
207
+ throw new AuthRequiredError(FLOMO_API_DOMAIN, message);
208
+ }
209
+ throw new CommandExecutionError(message);
210
+ }
211
+ if (!Array.isArray(body.data)) {
212
+ throw new CommandExecutionError('Flomo API returned malformed memo data');
213
+ }
214
+ if (body.data.length === 0) {
215
+ throw new EmptyResultError('flomo memos', 'No Flomo memos matched the requested filters.');
216
+ }
217
+ return body.data.map(normalizeMemo);
218
+ },
219
+ });
220
+
221
+ export const __test__ = {
222
+ buildSignedUrl,
223
+ command,
224
+ normalizeMemo,
225
+ parsePositiveIntArg,
226
+ parseSinceArg,
227
+ parseSlugArg,
228
+ };
@@ -0,0 +1,144 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ ArgumentError,
4
+ AuthRequiredError,
5
+ CommandExecutionError,
6
+ EmptyResultError,
7
+ } from '@jackwener/opencli/errors';
8
+
9
+ const { __test__ } = await import('./memos.js');
10
+ const { command, normalizeMemo, parsePositiveIntArg, parseSinceArg, parseSlugArg } = __test__;
11
+
12
+ function createPage(token = 'token-123') {
13
+ return {
14
+ wait: vi.fn().mockResolvedValue(undefined),
15
+ evaluate: vi.fn().mockResolvedValue({ session: 'browser:default', data: token }),
16
+ };
17
+ }
18
+
19
+ function mockFetchJson(body, status = 200) {
20
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
21
+ ok: status >= 200 && status < 300,
22
+ status,
23
+ json: vi.fn().mockResolvedValue(body),
24
+ }));
25
+ }
26
+
27
+ describe('flomo memos registration', () => {
28
+ it('registers as a browser cookie read command with stable columns', () => {
29
+ expect(command.site).toBe('flomo');
30
+ expect(command.name).toBe('memos');
31
+ expect(command.access).toBe('read');
32
+ expect(command.browser).toBe(true);
33
+ expect(command.strategy).toBe('cookie');
34
+ expect(command.columns).toEqual(['id', 'url', 'content', 'slug', 'tags', 'images', 'created_at', 'updated_at']);
35
+ });
36
+ });
37
+
38
+ describe('flomo memos argument validation', () => {
39
+ it('rejects invalid limits instead of silently clamping', () => {
40
+ expect(() => parsePositiveIntArg('0', 'limit', 20, 200)).toThrow(ArgumentError);
41
+ expect(() => parsePositiveIntArg('201', 'limit', 20, 200)).toThrow(ArgumentError);
42
+ expect(() => parsePositiveIntArg('10.5', 'limit', 20, 200)).toThrow(ArgumentError);
43
+ expect(() => parsePositiveIntArg('abc', 'limit', 20, 200)).toThrow(ArgumentError);
44
+ expect(parsePositiveIntArg(undefined, 'limit', 20, 200)).toBe(20);
45
+ expect(parsePositiveIntArg('200', 'limit', 20, 200)).toBe(200);
46
+ });
47
+
48
+ it('rejects invalid since and slug arguments', () => {
49
+ expect(parseSinceArg(undefined)).toBe(0);
50
+ expect(parseSinceArg('1735689600')).toBe(1735689600);
51
+ expect(() => parseSinceArg('-1')).toThrow(ArgumentError);
52
+ expect(() => parseSinceArg('1.5')).toThrow(ArgumentError);
53
+ expect(parseSlugArg(undefined)).toBe('');
54
+ expect(parseSlugArg('abc_DEF-123')).toBe('abc_DEF-123');
55
+ expect(() => parseSlugArg('bad/slash')).toThrow(ArgumentError);
56
+ expect(() => parseSlugArg('bad space')).toThrow(ArgumentError);
57
+ });
58
+ });
59
+
60
+ describe('flomo memo normalization', () => {
61
+ it('emits string-safe id/url fields and normalizes tags/images', () => {
62
+ expect(normalizeMemo({
63
+ slug: 'memo_12345678901234567890',
64
+ content: ' <p>Hello</p> ',
65
+ tags: [{ name: 'work' }, 'idea'],
66
+ files: [{ thumbnail_url: 'https://img/thumb.jpg' }, { url: 'https://img/full.jpg' }],
67
+ created_at: '2026-01-01T00:00:00+08:00',
68
+ updated_at: '2026-01-02T00:00:00+08:00',
69
+ })).toEqual({
70
+ id: 'memo_12345678901234567890',
71
+ url: 'https://v.flomoapp.com/mine/?memo_id=memo_12345678901234567890',
72
+ content: '<p>Hello</p>',
73
+ slug: 'memo_12345678901234567890',
74
+ tags: 'work, idea',
75
+ images: 'https://img/thumb.jpg | https://img/full.jpg',
76
+ created_at: '2026-01-01T00:00:00+08:00',
77
+ updated_at: '2026-01-02T00:00:00+08:00',
78
+ });
79
+ });
80
+
81
+ it('fails typed on malformed memo entries', () => {
82
+ expect(() => normalizeMemo(null)).toThrow(CommandExecutionError);
83
+ expect(() => normalizeMemo({ content: 'missing slug' })).toThrow(CommandExecutionError);
84
+ });
85
+ });
86
+
87
+ describe('flomo memos command', () => {
88
+ beforeEach(() => {
89
+ vi.unstubAllGlobals();
90
+ });
91
+
92
+ it('reads token from Browser Bridge envelope and returns memo rows', async () => {
93
+ mockFetchJson({
94
+ code: 0,
95
+ data: [{
96
+ slug: 'memo_1',
97
+ content: 'hello',
98
+ tags: ['tag'],
99
+ files: [],
100
+ created_at: '2026-01-01',
101
+ updated_at: '2026-01-02',
102
+ }],
103
+ });
104
+
105
+ const rows = await command.func(createPage(), { limit: '1' });
106
+
107
+ expect(globalThis.fetch).toHaveBeenCalledWith(expect.stringContaining('limit=1'), expect.objectContaining({
108
+ headers: expect.objectContaining({ Authorization: 'Bearer token-123' }),
109
+ }));
110
+ expect(rows).toEqual([{
111
+ id: 'memo_1',
112
+ url: 'https://v.flomoapp.com/mine/?memo_id=memo_1',
113
+ content: 'hello',
114
+ slug: 'memo_1',
115
+ tags: 'tag',
116
+ images: '',
117
+ created_at: '2026-01-01',
118
+ updated_at: '2026-01-02',
119
+ }]);
120
+ });
121
+
122
+ it('throws AuthRequiredError when the browser session has no token', async () => {
123
+ await expect(command.func(createPage(null), {})).rejects.toBeInstanceOf(AuthRequiredError);
124
+ });
125
+
126
+ it('maps Flomo auth failures to AuthRequiredError', async () => {
127
+ mockFetchJson({ code: 401, message: 'unauthorized' });
128
+ await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(AuthRequiredError);
129
+ });
130
+
131
+ it('maps HTTP, malformed JSON, malformed data, and empty results to typed errors', async () => {
132
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, json: vi.fn() }));
133
+ await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
134
+
135
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, json: vi.fn().mockRejectedValue(new Error('bad json')) }));
136
+ await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
137
+
138
+ mockFetchJson({ code: 0, data: {} });
139
+ await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
140
+
141
+ mockFetchJson({ code: 0, data: [] });
142
+ await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(EmptyResultError);
143
+ });
144
+ });
@@ -0,0 +1,46 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
+ import { ensureApplet, ggbEval, normalizeLabel, normalizeNumber, requireGgbSuccess } from './utils.js';
4
+
5
+ cli({
6
+ site: 'geogebra',
7
+ name: 'add-circle',
8
+ access: 'write',
9
+ description: 'Create a circle by center+radius or center+point',
10
+ domain: 'www.geogebra.org',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ example: 'opencli geogebra add-circle --center A --radius 3',
15
+ args: [
16
+ { name: 'center', required: true, help: 'Center point label (e.g. A)' },
17
+ { name: 'radius', required: false, help: 'Radius value (number) or a point label on the circle' },
18
+ { name: 'point', required: false, help: 'Alternative: a point label on the circle (use instead of --radius for Circle(center,point))' },
19
+ ],
20
+ columns: ['label', 'center', 'radius'],
21
+ func: async (page, kwargs) => {
22
+ const center = normalizeLabel(kwargs.center, 'center');
23
+ if (kwargs.point && kwargs.radius !== undefined) {
24
+ throw new ArgumentError('Use either --point or --radius, not both');
25
+ }
26
+ const pointOnCircle = kwargs.point ? normalizeLabel(kwargs.point, 'point') : '';
27
+ const radiusValue = kwargs.radius;
28
+
29
+ let cmd;
30
+ if (pointOnCircle) {
31
+ cmd = `Circle(${center},${pointOnCircle})`;
32
+ } else if (radiusValue !== undefined) {
33
+ const raw = String(radiusValue).trim();
34
+ const num = Number(raw);
35
+ cmd = Number.isFinite(num)
36
+ ? `Circle(${center},${normalizeNumber(raw, 'radius', { positive: true })})`
37
+ : `Circle(${center},${normalizeLabel(raw, 'radius point')})`;
38
+ } else {
39
+ throw new ArgumentError('Provide --radius (number or point label) or --point (point on circle)');
40
+ }
41
+
42
+ await ensureApplet(page);
43
+ const result = requireGgbSuccess(await ggbEval(page, cmd), `Failed to create circle: ${cmd}`);
44
+ return [{ label: result.label, center, radius: pointOnCircle || radiusValue }];
45
+ },
46
+ });
@@ -0,0 +1,35 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
+ import { ensureApplet, ggbEval, normalizeLabelList, requireGgbSuccess } from './utils.js';
4
+
5
+ cli({
6
+ site: 'geogebra',
7
+ name: 'add-line',
8
+ access: 'write',
9
+ description: 'Create a line through two points or a segment between two points',
10
+ domain: 'www.geogebra.org',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ example: 'opencli geogebra add-line --points A,B --type segment',
15
+ args: [
16
+ { name: 'points', required: true, help: 'Two point labels separated by comma (e.g. "A,B")' },
17
+ { name: 'type', required: false, choices: ['line', 'segment', 'ray'], default: 'line', help: 'Type: line, segment, or ray (default: line)' },
18
+ ],
19
+ columns: ['label', 'type', 'points'],
20
+ func: async (page, kwargs) => {
21
+ const [a, b] = normalizeLabelList(kwargs.points, 'points', 2, 2);
22
+ const type = kwargs.type || 'line';
23
+ const geogebraCmd = {
24
+ line: `Line(${a},${b})`,
25
+ segment: `Segment(${a},${b})`,
26
+ ray: `Ray(${a},${b})`,
27
+ }[type];
28
+ if (!geogebraCmd) {
29
+ throw new ArgumentError('type must be one of: line, segment, ray');
30
+ }
31
+ await ensureApplet(page);
32
+ const result = requireGgbSuccess(await ggbEval(page, geogebraCmd), `Failed to create ${type}: ${geogebraCmd}`);
33
+ return [{ label: result.label, type, points: `${a},${b}` }];
34
+ },
35
+ });
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ensureApplet, ggbEval, normalizeCoords, normalizeLabel, requireGgbSuccess } from './utils.js';
3
+
4
+ cli({
5
+ site: 'geogebra',
6
+ name: 'add-point',
7
+ access: 'write',
8
+ description: 'Create a point with given label and coordinates',
9
+ domain: 'www.geogebra.org',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ example: 'opencli geogebra add-point --name A --coords 1,2',
14
+ args: [
15
+ { name: 'name', required: true, help: 'Point label (e.g. A, B, P1)' },
16
+ { name: 'coords', required: true, help: 'Coordinates as x,y (e.g. "1,2")' },
17
+ ],
18
+ columns: ['name', 'x', 'y'],
19
+ func: async (page, kwargs) => {
20
+ const name = normalizeLabel(kwargs.name, 'name');
21
+ const [x, y] = normalizeCoords(kwargs.coords);
22
+ const cmd = `${name}=(${x},${y})`;
23
+ await ensureApplet(page);
24
+ const result = requireGgbSuccess(await ggbEval(page, cmd), `Failed to create point: ${cmd}`);
25
+ return [{ name, x, y }];
26
+ },
27
+ });
@@ -0,0 +1,25 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ensureApplet, ggbEval, normalizeLabelList, requireGgbSuccess } from './utils.js';
3
+
4
+ cli({
5
+ site: 'geogebra',
6
+ name: 'add-polygon',
7
+ access: 'write',
8
+ description: 'Create a polygon from a list of point labels',
9
+ domain: 'www.geogebra.org',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ example: 'opencli geogebra add-polygon --points A,B,C',
14
+ args: [
15
+ { name: 'points', required: true, help: 'Comma-separated point labels (e.g. "A,B,C" or "A,B,C,D")' },
16
+ ],
17
+ columns: ['label', 'vertices'],
18
+ func: async (page, kwargs) => {
19
+ const points = normalizeLabelList(kwargs.points, 'points', 3, 50);
20
+ const cmd = `Polygon(${points.join(',')})`;
21
+ await ensureApplet(page);
22
+ const result = requireGgbSuccess(await ggbEval(page, cmd), `Failed to create polygon: ${cmd}`);
23
+ return [{ label: result.label, vertices: points.join(',') }];
24
+ },
25
+ });
@@ -0,0 +1,35 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
+ import { ensureApplet, ggbEval, requireGgbSuccess } from './utils.js';
4
+
5
+ cli({
6
+ site: 'geogebra',
7
+ name: 'eval',
8
+ access: 'write',
9
+ description: 'Execute one or more GeoGebra command strings (semicolon-separated)',
10
+ domain: 'www.geogebra.org',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ example: 'opencli geogebra eval "A=(0,0);B=(4,0);c=Circle(A,B);d=Circle(B,A);C=Intersect(c,d,1);Polygon(A,B,C)"',
15
+ args: [
16
+ { name: 'command', positional: true, required: true, help: 'GeoGebra command string (use ; to chain multiple commands)' },
17
+ ],
18
+ columns: ['command', 'result'],
19
+ func: async (page, kwargs) => {
20
+ const commands = String(kwargs.command).split(';').map(s => s.trim()).filter(Boolean);
21
+ if (commands.length === 0) {
22
+ throw new ArgumentError('command must contain at least one GeoGebra command');
23
+ }
24
+ await ensureApplet(page);
25
+ const results = [];
26
+ for (const command of commands) {
27
+ const result = requireGgbSuccess(await ggbEval(page, command), `Failed to execute GeoGebra command: ${command}`);
28
+ results.push({
29
+ command,
30
+ result: `ok (${result.label || 'no label'})`,
31
+ });
32
+ }
33
+ return results;
34
+ },
35
+ });