@jackwener/opencli 1.5.4 → 1.5.6

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 (256) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
@@ -0,0 +1,351 @@
1
+ import { ArgumentError, AuthRequiredError, CliError } from '../../errors.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export interface ZsxqUser {
5
+ user_id?: number;
6
+ name?: string;
7
+ avatar_url?: string;
8
+ }
9
+
10
+ export interface ZsxqGroup {
11
+ group_id?: number;
12
+ name?: string;
13
+ description?: string;
14
+ background_url?: string;
15
+ owner?: ZsxqUser;
16
+ statistics?: {
17
+ topics_count?: number;
18
+ answers_count?: number;
19
+ comments_count?: number;
20
+ likes_count?: number;
21
+ subscriptions_count?: number;
22
+ };
23
+ category?: {
24
+ title?: string;
25
+ };
26
+ user_specific?: {
27
+ join_time?: string;
28
+ validity?: {
29
+ end_time?: string;
30
+ };
31
+ };
32
+ }
33
+
34
+ export interface ZsxqComment {
35
+ comment_id?: number;
36
+ create_time?: string;
37
+ text?: string;
38
+ owner?: ZsxqUser;
39
+ likes_count?: number;
40
+ rewards_count?: number;
41
+ repliee?: ZsxqUser;
42
+ }
43
+
44
+ export interface ZsxqTopic {
45
+ topic_id?: number;
46
+ create_time?: string;
47
+ comments_count?: number;
48
+ likes_count?: number;
49
+ readers_count?: number;
50
+ reading_count?: number;
51
+ rewards_count?: number;
52
+ title?: string;
53
+ type?: string;
54
+ group?: ZsxqGroup;
55
+ owner?: ZsxqUser;
56
+ user_specific?: Record<string, unknown>;
57
+ talk?: {
58
+ owner?: ZsxqUser;
59
+ text?: string;
60
+ };
61
+ question?: {
62
+ owner?: ZsxqUser;
63
+ text?: string;
64
+ };
65
+ answer?: {
66
+ owner?: ZsxqUser;
67
+ text?: string;
68
+ };
69
+ task?: {
70
+ owner?: ZsxqUser;
71
+ text?: string;
72
+ };
73
+ solution?: {
74
+ owner?: ZsxqUser;
75
+ text?: string;
76
+ };
77
+ show_comments?: ZsxqComment[];
78
+ comments?: ZsxqComment[];
79
+ }
80
+
81
+ export interface BrowserFetchResult {
82
+ ok: boolean;
83
+ url?: string;
84
+ status?: number;
85
+ error?: string;
86
+ data?: unknown;
87
+ }
88
+
89
+ const SITE_DOMAIN = 'wx.zsxq.com';
90
+ const SITE_URL = 'https://wx.zsxq.com';
91
+
92
+ function asRecord(value: unknown): Record<string, unknown> | null {
93
+ return value && typeof value === 'object' && !Array.isArray(value)
94
+ ? value as Record<string, unknown>
95
+ : null;
96
+ }
97
+
98
+ function pickArray<T>(...values: unknown[]): T[] {
99
+ for (const value of values) {
100
+ if (Array.isArray(value)) {
101
+ return value as T[];
102
+ }
103
+ }
104
+ return [];
105
+ }
106
+
107
+ export async function ensureZsxqPage(page: IPage): Promise<void> {
108
+ await page.goto(SITE_URL);
109
+ }
110
+
111
+ export async function ensureZsxqAuth(page: IPage): Promise<void> {
112
+ // zsxq uses httpOnly cookies that may be on different subdomains.
113
+ // Verify auth by attempting a lightweight API call instead of checking cookies.
114
+ try {
115
+ const result = await page.evaluate(`
116
+ (async () => {
117
+ try {
118
+ const r = await new Promise((resolve, reject) => {
119
+ const xhr = new XMLHttpRequest();
120
+ xhr.open('GET', 'https://api.zsxq.com/v2/groups', true);
121
+ xhr.withCredentials = true;
122
+ xhr.setRequestHeader('accept', 'application/json');
123
+ xhr.onload = () => {
124
+ if (xhr.status >= 200 && xhr.status < 300) {
125
+ try { resolve(JSON.parse(xhr.responseText)); }
126
+ catch { resolve(null); }
127
+ } else { resolve(null); }
128
+ };
129
+ xhr.onerror = () => resolve(null);
130
+ xhr.send();
131
+ });
132
+ return r !== null;
133
+ } catch { return false; }
134
+ })()
135
+ `);
136
+ if (!result) {
137
+ throw new AuthRequiredError('zsxq.com');
138
+ }
139
+ } catch (err) {
140
+ if (err instanceof AuthRequiredError) throw err;
141
+ throw new AuthRequiredError('zsxq.com');
142
+ }
143
+ }
144
+
145
+ export async function getCookieValue(page: IPage, name: string): Promise<string | undefined> {
146
+ const cookies = await page.getCookies({ domain: SITE_DOMAIN });
147
+ return cookies.find(cookie => cookie.name === name)?.value;
148
+ }
149
+
150
+ export async function getActiveGroupId(page: IPage): Promise<string> {
151
+ const groupId = await page.evaluate(`
152
+ (() => {
153
+ const target = localStorage.getItem('target_group');
154
+ if (target) {
155
+ try {
156
+ const parsed = JSON.parse(target);
157
+ if (parsed.group_id) return String(parsed.group_id);
158
+ } catch {}
159
+ }
160
+ return null;
161
+ })()
162
+ `);
163
+ if (groupId) return groupId;
164
+
165
+ throw new ArgumentError(
166
+ 'Cannot determine active group_id',
167
+ 'Pass --group_id <id> or open the target 知识星球 page in Chrome first',
168
+ );
169
+ }
170
+
171
+ export async function browserJsonRequest(page: IPage, path: string): Promise<BrowserFetchResult> {
172
+ return await page.evaluate(`
173
+ (async () => {
174
+ const path = ${JSON.stringify(path)};
175
+
176
+ try {
177
+ return await new Promise((resolve) => {
178
+ const xhr = new XMLHttpRequest();
179
+ xhr.open('GET', path, true);
180
+ xhr.withCredentials = true;
181
+ xhr.setRequestHeader('accept', 'application/json, text/plain, */*');
182
+ xhr.onload = () => {
183
+ let parsed = null;
184
+ if (xhr.responseText) {
185
+ try { parsed = JSON.parse(xhr.responseText); }
186
+ catch {}
187
+ }
188
+
189
+ resolve({
190
+ ok: xhr.status >= 200 && xhr.status < 300,
191
+ url: path,
192
+ status: xhr.status,
193
+ data: parsed,
194
+ error: xhr.status >= 200 && xhr.status < 300 ? undefined : 'HTTP ' + xhr.status,
195
+ });
196
+ };
197
+ xhr.onerror = () => resolve({
198
+ ok: false,
199
+ url: path,
200
+ error: 'Network error',
201
+ });
202
+ xhr.send();
203
+ });
204
+ } catch (error) {
205
+ return {
206
+ ok: false,
207
+ url: path,
208
+ error: error instanceof Error ? error.message : String(error),
209
+ };
210
+ }
211
+ })()
212
+ `) as BrowserFetchResult;
213
+ }
214
+
215
+ export async function fetchFirstJson(page: IPage, paths: string[]): Promise<BrowserFetchResult> {
216
+ let lastFailure: BrowserFetchResult | null = null;
217
+
218
+ for (const path of paths) {
219
+ const result = await browserJsonRequest(page, path);
220
+ if (result.ok) {
221
+ return result;
222
+ }
223
+ lastFailure = result;
224
+ }
225
+
226
+ if (!lastFailure) {
227
+ throw new CliError(
228
+ 'FETCH_ERROR',
229
+ 'No candidate endpoint returned JSON',
230
+ `Checked endpoints: ${paths.join(', ')}`,
231
+ );
232
+ }
233
+
234
+ throw new CliError(
235
+ 'FETCH_ERROR',
236
+ lastFailure.error || 'Failed to fetch ZSXQ API',
237
+ `Checked endpoints: ${paths.join(', ')}`,
238
+ );
239
+ }
240
+
241
+ export function unwrapRespData<T>(payload: unknown): T {
242
+ const record = asRecord(payload);
243
+ if (!record) {
244
+ throw new CliError('PARSE_ERROR', 'Invalid ZSXQ API response');
245
+ }
246
+
247
+ if (record.succeeded === false) {
248
+ const code = typeof record.code === 'number' ? String(record.code) : 'API_ERROR';
249
+ const message = typeof record.info === 'string'
250
+ ? record.info
251
+ : typeof record.error === 'string'
252
+ ? record.error
253
+ : 'ZSXQ API returned an error';
254
+ throw new CliError(code, message);
255
+ }
256
+
257
+ return (record.resp_data ?? record.data ?? payload) as T;
258
+ }
259
+
260
+ export function getTopicsFromResponse(payload: unknown): ZsxqTopic[] {
261
+ const data = unwrapRespData<Record<string, unknown> | ZsxqTopic[]>(payload);
262
+ if (Array.isArray(data)) return data;
263
+ return pickArray<ZsxqTopic>(
264
+ data.topics,
265
+ data.list,
266
+ data.records,
267
+ data.items,
268
+ data.search_result,
269
+ );
270
+ }
271
+
272
+ export function getCommentsFromResponse(payload: unknown): ZsxqComment[] {
273
+ const data = unwrapRespData<Record<string, unknown> | ZsxqComment[]>(payload);
274
+ if (Array.isArray(data)) return data;
275
+ return pickArray<ZsxqComment>(data.comments, data.list, data.items);
276
+ }
277
+
278
+ export function getGroupsFromResponse(payload: unknown): ZsxqGroup[] {
279
+ const data = unwrapRespData<Record<string, unknown> | ZsxqGroup[]>(payload);
280
+ if (Array.isArray(data)) return data;
281
+ return pickArray<ZsxqGroup>(data.groups, data.list, data.items);
282
+ }
283
+
284
+ export function getTopicFromResponse(payload: unknown): ZsxqTopic | null {
285
+ const data = unwrapRespData<Record<string, unknown> | ZsxqTopic>(payload);
286
+ if (Array.isArray(data)) return data[0] ?? null;
287
+ if (typeof data.topic_id === 'number') return data;
288
+ const record = asRecord(data);
289
+ if (!record) return null;
290
+ const topic = record.topic;
291
+ return topic && typeof topic === 'object' ? topic as ZsxqTopic : null;
292
+ }
293
+
294
+ export function getTopicAuthor(topic: ZsxqTopic): string {
295
+ return (
296
+ topic.owner?.name ||
297
+ topic.talk?.owner?.name ||
298
+ topic.question?.owner?.name ||
299
+ topic.answer?.owner?.name ||
300
+ topic.task?.owner?.name ||
301
+ topic.solution?.owner?.name ||
302
+ ''
303
+ );
304
+ }
305
+
306
+ export function getTopicText(topic: ZsxqTopic): string {
307
+ const primary = [
308
+ topic.title,
309
+ topic.talk?.text,
310
+ topic.question?.text,
311
+ topic.answer?.text,
312
+ topic.task?.text,
313
+ topic.solution?.text,
314
+ ].find(value => typeof value === 'string' && value.trim());
315
+ return (primary || '').replace(/\s+/g, ' ').trim();
316
+ }
317
+
318
+ export function getTopicUrl(topicId: number | string | undefined): string {
319
+ return topicId ? `${SITE_URL}/topic/${topicId}` : SITE_URL;
320
+ }
321
+
322
+ export function summarizeComments(comments: ZsxqComment[], limit: number = 3): string {
323
+ return comments
324
+ .slice(0, limit)
325
+ .map((comment) => {
326
+ const author = comment.owner?.name || '匿名';
327
+ const target = comment.repliee?.name ? ` -> ${comment.repliee.name}` : '';
328
+ const text = (comment.text || '').replace(/\s+/g, ' ').trim();
329
+ return `${author}${target}: ${text}`;
330
+ })
331
+ .join(' | ');
332
+ }
333
+
334
+ export function toTopicRow(topic: ZsxqTopic): Record<string, unknown> {
335
+ const topicId = topic.topic_id ?? '';
336
+ const comments = pickArray<ZsxqComment>(topic.show_comments, topic.comments);
337
+ return {
338
+ topic_id: topicId,
339
+ type: topic.type || '',
340
+ group: topic.group?.name || '',
341
+ author: getTopicAuthor(topic),
342
+ title: getTopicText(topic).slice(0, 120),
343
+ content: getTopicText(topic),
344
+ comments: topic.comments_count ?? comments.length ?? 0,
345
+ likes: topic.likes_count ?? 0,
346
+ readers: topic.readers_count ?? topic.reading_count ?? 0,
347
+ time: topic.create_time || '',
348
+ comment_preview: summarizeComments(comments),
349
+ url: getTopicUrl(topicId),
350
+ };
351
+ }
@@ -76,3 +76,50 @@ describe('commanderAdapter arg passing', () => {
76
76
  expect(mockExecuteCommand).not.toHaveBeenCalled();
77
77
  });
78
78
  });
79
+
80
+ describe('commanderAdapter boolean alias support', () => {
81
+ const cmd: CliCommand = {
82
+ site: 'reddit',
83
+ name: 'save',
84
+ description: 'Save a post',
85
+ browser: false,
86
+ args: [
87
+ { name: 'post-id', positional: true, required: true, help: 'Post ID' },
88
+ { name: 'undo', type: 'boolean', default: false, help: 'Unsave instead of save' },
89
+ ],
90
+ func: vi.fn(),
91
+ };
92
+
93
+ beforeEach(() => {
94
+ mockExecuteCommand.mockReset();
95
+ mockExecuteCommand.mockResolvedValue([]);
96
+ mockRenderOutput.mockReset();
97
+ delete process.env.OPENCLI_VERBOSE;
98
+ process.exitCode = undefined;
99
+ });
100
+
101
+ it('coerces default false for boolean args to a real boolean', async () => {
102
+ const program = new Command();
103
+ const siteCmd = program.command('reddit');
104
+ registerCommandToProgram(siteCmd, cmd);
105
+
106
+ await program.parseAsync(['node', 'opencli', 'reddit', 'save', 't3_abc123']);
107
+
108
+ expect(mockExecuteCommand).toHaveBeenCalled();
109
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
110
+ expect(kwargs['post-id']).toBe('t3_abc123');
111
+ expect(kwargs.undo).toBe(false);
112
+ });
113
+
114
+ it('coerces explicit false for boolean args to a real boolean', async () => {
115
+ const program = new Command();
116
+ const siteCmd = program.command('reddit');
117
+ registerCommandToProgram(siteCmd, cmd);
118
+
119
+ await program.parseAsync(['node', 'opencli', 'reddit', 'save', 't3_abc123', '--undo', 'false']);
120
+
121
+ expect(mockExecuteCommand).toHaveBeenCalled();
122
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
123
+ expect(kwargs.undo).toBe(false);
124
+ });
125
+ });
@@ -18,6 +18,7 @@ import { render as renderOutput } from './output.js';
18
18
  import { executeCommand } from './execution.js';
19
19
  import {
20
20
  CliError,
21
+ EXIT_CODES,
21
22
  ERROR_ICONS,
22
23
  getErrorMessage,
23
24
  BrowserConnectError,
@@ -32,7 +33,7 @@ import {
32
33
  import { checkDaemonStatus } from './browser/discover.js';
33
34
 
34
35
  export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
35
- if (argType !== 'bool') return value;
36
+ if (argType !== 'bool' && argType !== 'boolean') return value;
36
37
  if (typeof value === 'boolean') return value;
37
38
  if (value == null || value === '') return false;
38
39
 
@@ -40,7 +41,7 @@ export function normalizeArgValue(argType: string | undefined, value: unknown, n
40
41
  if (normalized === 'true') return true;
41
42
  if (normalized === 'false') return false;
42
43
 
43
- throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
44
+ throw new ArgumentError(`"${name}" must be either "true" or "false".`);
44
45
  }
45
46
 
46
47
  /**
@@ -117,11 +118,33 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
117
118
  });
118
119
  } catch (err) {
119
120
  await renderError(err, fullName(cmd), optionsRecord.verbose === true);
120
- process.exitCode = 1;
121
+ process.exitCode = resolveExitCode(err);
121
122
  }
122
123
  });
123
124
  }
124
125
 
126
+ // ── Exit code resolution ─────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Map any thrown value to a Unix process exit code.
130
+ *
131
+ * - CliError subclasses carry their own exitCode (set in errors.ts).
132
+ * - Generic Error objects are classified by message pattern so that
133
+ * un-typed auth / not-found errors from adapters still produce
134
+ * meaningful exit codes for shell scripts.
135
+ */
136
+ function resolveExitCode(err: unknown): number {
137
+ if (err instanceof CliError) return err.exitCode;
138
+
139
+ // Pattern-based fallback for untyped errors thrown by third-party adapters.
140
+ const msg = getErrorMessage(err);
141
+ const kind = classifyGenericError(msg);
142
+ if (kind === 'auth') return EXIT_CODES.NOPERM;
143
+ if (kind === 'not-found') return EXIT_CODES.EMPTY_RESULT;
144
+ if (kind === 'http') return EXIT_CODES.GENERIC_ERROR; // HTTP 4xx/5xx → generic; renderer shows details
145
+ return EXIT_CODES.GENERIC_ERROR;
146
+ }
147
+
125
148
  // ── Error rendering ──────────────────────────────────────────────────────────
126
149
 
127
150
  const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
package/src/daemon.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
23
23
  import { WebSocketServer, WebSocket, type RawData } from 'ws';
24
24
  import { DEFAULT_DAEMON_PORT } from './constants.js';
25
+ import { EXIT_CODES } from './errors.js';
25
26
 
26
27
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
27
28
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
@@ -53,7 +54,7 @@ function resetIdleTimer(): void {
53
54
  if (idleTimer) clearTimeout(idleTimer);
54
55
  idleTimer = setTimeout(() => {
55
56
  console.error('[daemon] Idle timeout, shutting down');
56
- process.exit(0);
57
+ process.exit(EXIT_CODES.SUCCESS);
57
58
  }, IDLE_TIMEOUT);
58
59
  }
59
60
 
@@ -303,10 +304,10 @@ httpServer.listen(PORT, '127.0.0.1', () => {
303
304
  httpServer.on('error', (err: NodeJS.ErrnoException) => {
304
305
  if (err.code === 'EADDRINUSE') {
305
306
  console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
306
- process.exit(1);
307
+ process.exit(EXIT_CODES.SERVICE_UNAVAIL);
307
308
  }
308
309
  console.error('[daemon] Server error:', err.message);
309
- process.exit(1);
310
+ process.exit(EXIT_CODES.GENERIC_ERROR);
310
311
  });
311
312
 
312
313
  // Graceful shutdown
@@ -319,7 +320,7 @@ function shutdown(): void {
319
320
  pending.clear();
320
321
  if (extensionWs) extensionWs.close();
321
322
  httpServer.close();
322
- process.exit(0);
323
+ process.exit(EXIT_CODES.SUCCESS);
323
324
  }
324
325
 
325
326
  process.on('SIGTERM', shutdown);
package/src/errors.ts CHANGED
@@ -4,48 +4,96 @@
4
4
  * All errors thrown by the framework should extend CliError so that
5
5
  * the top-level handler in commanderAdapter.ts can render consistent,
6
6
  * helpful output with emoji-coded severity and actionable hints.
7
+ *
8
+ * ## Exit codes
9
+ *
10
+ * opencli follows Unix conventions (sysexits.h) for process exit codes:
11
+ *
12
+ * 0 Success
13
+ * 1 Generic / unexpected error
14
+ * 2 Argument / usage error (ArgumentError)
15
+ * 66 No input / empty result (EmptyResultError)
16
+ * 69 Service unavailable (BrowserConnectError, AdapterLoadError)
17
+ * 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
18
+ * 77 Permission denied / auth needed (AuthRequiredError)
19
+ * 78 Configuration error (ConfigError)
20
+ * 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
7
21
  */
8
22
 
23
+ // ── Exit code table ──────────────────────────────────────────────────────────
24
+
25
+ export const EXIT_CODES = {
26
+ SUCCESS: 0,
27
+ GENERIC_ERROR: 1,
28
+ USAGE_ERROR: 2, // Bad arguments / command misuse
29
+ EMPTY_RESULT: 66, // No data / not found (EX_NOINPUT)
30
+ SERVICE_UNAVAIL:69, // Daemon / browser unavailable (EX_UNAVAILABLE)
31
+ TEMPFAIL: 75, // Timeout — try again later (EX_TEMPFAIL)
32
+ NOPERM: 77, // Auth required / permission (EX_NOPERM)
33
+ CONFIG_ERROR: 78, // Missing / invalid config (EX_CONFIG)
34
+ INTERRUPTED: 130, // Ctrl-C / SIGINT
35
+ } as const;
36
+
37
+ export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
38
+
39
+ // ── Base class ───────────────────────────────────────────────────────────────
40
+
9
41
  export class CliError extends Error {
10
42
  /** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
11
43
  readonly code: string;
12
44
  /** Human-readable hint on how to fix the problem */
13
45
  readonly hint?: string;
46
+ /** Unix process exit code — defaults to 1 (generic error) */
47
+ readonly exitCode: ExitCode;
14
48
 
15
- constructor(code: string, message: string, hint?: string) {
49
+ constructor(code: string, message: string, hint?: string, exitCode: ExitCode = EXIT_CODES.GENERIC_ERROR) {
16
50
  super(message);
17
51
  this.name = new.target.name;
18
52
  this.code = code;
19
53
  this.hint = hint;
54
+ this.exitCode = exitCode;
20
55
  }
21
56
  }
22
57
 
58
+ // ── Typed subclasses ─────────────────────────────────────────────────────────
59
+
23
60
  export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
24
61
 
25
62
  export class BrowserConnectError extends CliError {
26
63
  readonly kind: BrowserConnectKind;
27
64
  constructor(message: string, hint?: string, kind: BrowserConnectKind = 'unknown') {
28
- super('BROWSER_CONNECT', message, hint);
65
+ super('BROWSER_CONNECT', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
29
66
  this.kind = kind;
30
67
  }
31
68
  }
32
69
 
33
70
  export class AdapterLoadError extends CliError {
34
- constructor(message: string, hint?: string) { super('ADAPTER_LOAD', message, hint); }
71
+ constructor(message: string, hint?: string) {
72
+ super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
73
+ }
35
74
  }
36
75
 
37
76
  export class CommandExecutionError extends CliError {
38
- constructor(message: string, hint?: string) { super('COMMAND_EXEC', message, hint); }
77
+ constructor(message: string, hint?: string) {
78
+ super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
79
+ }
39
80
  }
40
81
 
41
82
  export class ConfigError extends CliError {
42
- constructor(message: string, hint?: string) { super('CONFIG', message, hint); }
83
+ constructor(message: string, hint?: string) {
84
+ super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
85
+ }
43
86
  }
44
87
 
45
88
  export class AuthRequiredError extends CliError {
46
89
  readonly domain: string;
47
90
  constructor(domain: string, message?: string) {
48
- super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`);
91
+ super(
92
+ 'AUTH_REQUIRED',
93
+ message ?? `Not logged in to ${domain}`,
94
+ `Please open Chrome and log in to https://${domain}`,
95
+ EXIT_CODES.NOPERM,
96
+ );
49
97
  this.domain = domain;
50
98
  }
51
99
  }
@@ -56,27 +104,40 @@ export class TimeoutError extends CliError {
56
104
  'TIMEOUT',
57
105
  `${label} timed out after ${seconds}s`,
58
106
  hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var',
107
+ EXIT_CODES.TEMPFAIL,
59
108
  );
60
109
  }
61
110
  }
62
111
 
63
112
  export class ArgumentError extends CliError {
64
- constructor(message: string, hint?: string) { super('ARGUMENT', message, hint); }
113
+ constructor(message: string, hint?: string) {
114
+ super('ARGUMENT', message, hint, EXIT_CODES.USAGE_ERROR);
115
+ }
65
116
  }
66
117
 
67
118
  export class EmptyResultError extends CliError {
68
119
  constructor(command: string, hint?: string) {
69
- super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
120
+ super(
121
+ 'EMPTY_RESULT',
122
+ `${command} returned no data`,
123
+ hint ?? 'The page structure may have changed, or you may need to log in',
124
+ EXIT_CODES.EMPTY_RESULT,
125
+ );
70
126
  }
71
127
  }
72
128
 
73
129
  export class SelectorError extends CliError {
74
130
  constructor(selector: string, hint?: string) {
75
- super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
131
+ super(
132
+ 'SELECTOR',
133
+ `Could not find element: ${selector}`,
134
+ hint ?? 'The page UI may have changed. Please report this issue.',
135
+ EXIT_CODES.GENERIC_ERROR,
136
+ );
76
137
  }
77
138
  }
78
139
 
79
- // ── Utilities ───────────────────────────────────────────────────────────
140
+ // ── Utilities ───────────────────────────────────────────────────────────────
80
141
 
81
142
  /** Extract a human-readable message from an unknown caught value. */
82
143
  export function getErrorMessage(error: unknown): string {
@@ -30,6 +30,23 @@
30
30
  install:
31
31
  default: "npm install -g @larksuite/cli"
32
32
 
33
+ - name: dws
34
+ binary: dws
35
+ description: "DingTalk Workspace CLI — messages, docs, calendar, contacts and more for humans and AI agents"
36
+ homepage: "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
37
+ tags: [dingtalk, collaboration, productivity, ai-agent]
38
+ install:
39
+ mac: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
40
+ linux: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
41
+
42
+ - name: wecom-cli
43
+ binary: wecom-cli
44
+ description: "WeCom/企业微信 CLI — contacts, todos, meetings, messages, calendar, docs and smart sheets for AI agents"
45
+ homepage: "https://github.com/WecomTeam/wecom-cli"
46
+ tags: [wecom, wechat-work, collaboration, productivity, ai-agent]
47
+ install:
48
+ default: "npm install -g @wecom/cli"
49
+
33
50
  - name: vercel
34
51
  binary: vercel
35
52
  description: "Vercel CLI — deploy projects, manage domains, env vars, logs and serverless functions"