@jackwener/opencli 0.9.6 → 0.9.8

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 (221) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CLI-ELECTRON.md +89 -36
  14. package/CONTRIBUTING.md +167 -0
  15. package/README.md +98 -32
  16. package/README.zh-CN.md +99 -33
  17. package/dist/browser/discover.js +22 -7
  18. package/dist/browser.test.js +23 -0
  19. package/dist/build-manifest.d.ts +26 -0
  20. package/dist/build-manifest.js +132 -60
  21. package/dist/build-manifest.test.d.ts +1 -0
  22. package/dist/build-manifest.test.js +26 -0
  23. package/dist/cli-manifest.json +1415 -29
  24. package/dist/clis/bilibili/download.d.ts +10 -0
  25. package/dist/clis/bilibili/download.js +135 -0
  26. package/dist/clis/chatwise/ask.d.ts +1 -0
  27. package/dist/clis/chatwise/ask.js +76 -0
  28. package/dist/clis/chatwise/export.d.ts +1 -0
  29. package/dist/clis/chatwise/export.js +46 -0
  30. package/dist/clis/chatwise/history.d.ts +1 -0
  31. package/dist/clis/chatwise/history.js +43 -0
  32. package/dist/clis/chatwise/model.d.ts +1 -0
  33. package/dist/clis/chatwise/model.js +81 -0
  34. package/dist/clis/chatwise/new.d.ts +1 -0
  35. package/dist/clis/chatwise/new.js +18 -0
  36. package/dist/clis/chatwise/read.d.ts +1 -0
  37. package/dist/clis/chatwise/read.js +39 -0
  38. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  39. package/dist/clis/chatwise/screenshot.js +27 -0
  40. package/dist/clis/chatwise/send.d.ts +1 -0
  41. package/dist/clis/chatwise/send.js +45 -0
  42. package/dist/clis/chatwise/status.d.ts +1 -0
  43. package/dist/clis/chatwise/status.js +22 -0
  44. package/dist/clis/discord-app/channels.d.ts +1 -0
  45. package/dist/clis/discord-app/channels.js +45 -0
  46. package/dist/clis/discord-app/members.d.ts +1 -0
  47. package/dist/clis/discord-app/members.js +38 -0
  48. package/dist/clis/discord-app/read.d.ts +1 -0
  49. package/dist/clis/discord-app/read.js +45 -0
  50. package/dist/clis/discord-app/search.d.ts +1 -0
  51. package/dist/clis/discord-app/search.js +56 -0
  52. package/dist/clis/discord-app/send.d.ts +1 -0
  53. package/dist/clis/discord-app/send.js +27 -0
  54. package/dist/clis/discord-app/servers.d.ts +1 -0
  55. package/dist/clis/discord-app/servers.js +36 -0
  56. package/dist/clis/discord-app/status.d.ts +1 -0
  57. package/dist/clis/discord-app/status.js +16 -0
  58. package/dist/clis/feishu/new.d.ts +1 -0
  59. package/dist/clis/feishu/new.js +27 -0
  60. package/dist/clis/feishu/read.d.ts +1 -0
  61. package/dist/clis/feishu/read.js +40 -0
  62. package/dist/clis/feishu/search.d.ts +1 -0
  63. package/dist/clis/feishu/search.js +30 -0
  64. package/dist/clis/feishu/send.d.ts +1 -0
  65. package/dist/clis/feishu/send.js +39 -0
  66. package/dist/clis/feishu/status.d.ts +1 -0
  67. package/dist/clis/feishu/status.js +28 -0
  68. package/dist/clis/grok/ask.d.ts +1 -0
  69. package/dist/clis/grok/ask.js +82 -0
  70. package/dist/clis/grok/debug.d.ts +1 -0
  71. package/dist/clis/grok/debug.js +45 -0
  72. package/dist/clis/jimeng/generate.yaml +84 -0
  73. package/dist/clis/jimeng/history.yaml +47 -0
  74. package/dist/clis/linux-do/categories.yaml +41 -0
  75. package/dist/clis/linux-do/category.yaml +49 -0
  76. package/dist/clis/linux-do/hot.yaml +50 -0
  77. package/dist/clis/linux-do/latest.yaml +40 -0
  78. package/dist/clis/linux-do/search.yaml +45 -0
  79. package/dist/clis/linux-do/topic.yaml +38 -0
  80. package/dist/clis/notion/export.d.ts +1 -0
  81. package/dist/clis/notion/export.js +31 -0
  82. package/dist/clis/notion/favorites.d.ts +1 -0
  83. package/dist/clis/notion/favorites.js +84 -0
  84. package/dist/clis/notion/new.d.ts +1 -0
  85. package/dist/clis/notion/new.js +34 -0
  86. package/dist/clis/notion/read.d.ts +1 -0
  87. package/dist/clis/notion/read.js +30 -0
  88. package/dist/clis/notion/search.d.ts +1 -0
  89. package/dist/clis/notion/search.js +46 -0
  90. package/dist/clis/notion/sidebar.d.ts +1 -0
  91. package/dist/clis/notion/sidebar.js +41 -0
  92. package/dist/clis/notion/status.d.ts +1 -0
  93. package/dist/clis/notion/status.js +16 -0
  94. package/dist/clis/notion/write.d.ts +1 -0
  95. package/dist/clis/notion/write.js +40 -0
  96. package/dist/clis/twitter/download.d.ts +8 -0
  97. package/dist/clis/twitter/download.js +204 -0
  98. package/dist/clis/wechat/chats.d.ts +1 -0
  99. package/dist/clis/wechat/chats.js +28 -0
  100. package/dist/clis/wechat/contacts.d.ts +1 -0
  101. package/dist/clis/wechat/contacts.js +28 -0
  102. package/dist/clis/wechat/read.d.ts +1 -0
  103. package/dist/clis/wechat/read.js +58 -0
  104. package/dist/clis/wechat/search.d.ts +1 -0
  105. package/dist/clis/wechat/search.js +31 -0
  106. package/dist/clis/wechat/send.d.ts +1 -0
  107. package/dist/clis/wechat/send.js +42 -0
  108. package/dist/clis/wechat/status.d.ts +1 -0
  109. package/dist/clis/wechat/status.js +29 -0
  110. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  111. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  112. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  113. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  114. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  115. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  116. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  117. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  118. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  119. package/dist/clis/xiaohongshu/download.js +155 -0
  120. package/dist/clis/xiaohongshu/search.js +1 -1
  121. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  122. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  123. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  124. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  125. package/dist/clis/xiaohongshu/user.js +46 -29
  126. package/dist/clis/zhihu/download.d.ts +11 -0
  127. package/dist/clis/zhihu/download.js +186 -0
  128. package/dist/clis/zhihu/download.test.d.ts +1 -0
  129. package/dist/clis/zhihu/download.test.js +10 -0
  130. package/dist/download/index.d.ts +79 -0
  131. package/dist/download/index.js +325 -0
  132. package/dist/download/progress.d.ts +36 -0
  133. package/dist/download/progress.js +111 -0
  134. package/dist/engine.test.js +15 -0
  135. package/dist/main.js +16 -3
  136. package/dist/pipeline/registry.js +2 -0
  137. package/dist/pipeline/steps/download.d.ts +34 -0
  138. package/dist/pipeline/steps/download.js +251 -0
  139. package/dist/pipeline/template.js +28 -0
  140. package/package.json +4 -3
  141. package/scripts/test-site.mjs +70 -0
  142. package/src/browser/discover.ts +23 -7
  143. package/src/browser.test.ts +23 -0
  144. package/src/build-manifest.test.ts +28 -0
  145. package/src/build-manifest.ts +147 -57
  146. package/src/clis/bilibili/download.ts +161 -0
  147. package/src/clis/chatwise/README.md +38 -0
  148. package/src/clis/chatwise/README.zh-CN.md +38 -0
  149. package/src/clis/chatwise/ask.ts +87 -0
  150. package/src/clis/chatwise/export.ts +51 -0
  151. package/src/clis/chatwise/history.ts +47 -0
  152. package/src/clis/chatwise/model.ts +87 -0
  153. package/src/clis/chatwise/new.ts +21 -0
  154. package/src/clis/chatwise/read.ts +42 -0
  155. package/src/clis/chatwise/screenshot.ts +33 -0
  156. package/src/clis/chatwise/send.ts +50 -0
  157. package/src/clis/chatwise/status.ts +25 -0
  158. package/src/clis/discord-app/README.md +28 -0
  159. package/src/clis/discord-app/README.zh-CN.md +28 -0
  160. package/src/clis/discord-app/channels.ts +48 -0
  161. package/src/clis/discord-app/members.ts +41 -0
  162. package/src/clis/discord-app/read.ts +49 -0
  163. package/src/clis/discord-app/search.ts +64 -0
  164. package/src/clis/discord-app/send.ts +32 -0
  165. package/src/clis/discord-app/servers.ts +39 -0
  166. package/src/clis/discord-app/status.ts +18 -0
  167. package/src/clis/feishu/README.md +20 -0
  168. package/src/clis/feishu/README.zh-CN.md +20 -0
  169. package/src/clis/feishu/new.ts +32 -0
  170. package/src/clis/feishu/read.ts +48 -0
  171. package/src/clis/feishu/search.ts +35 -0
  172. package/src/clis/feishu/send.ts +46 -0
  173. package/src/clis/feishu/status.ts +34 -0
  174. package/src/clis/grok/ask.ts +90 -0
  175. package/src/clis/grok/debug.ts +49 -0
  176. package/src/clis/jimeng/generate.yaml +84 -0
  177. package/src/clis/jimeng/history.yaml +47 -0
  178. package/src/clis/linux-do/categories.yaml +41 -0
  179. package/src/clis/linux-do/category.yaml +49 -0
  180. package/src/clis/linux-do/hot.yaml +50 -0
  181. package/src/clis/linux-do/latest.yaml +40 -0
  182. package/src/clis/linux-do/search.yaml +45 -0
  183. package/src/clis/linux-do/topic.yaml +38 -0
  184. package/src/clis/notion/README.md +29 -0
  185. package/src/clis/notion/README.zh-CN.md +29 -0
  186. package/src/clis/notion/export.ts +36 -0
  187. package/src/clis/notion/favorites.ts +87 -0
  188. package/src/clis/notion/new.ts +39 -0
  189. package/src/clis/notion/read.ts +33 -0
  190. package/src/clis/notion/search.ts +54 -0
  191. package/src/clis/notion/sidebar.ts +44 -0
  192. package/src/clis/notion/status.ts +18 -0
  193. package/src/clis/notion/write.ts +45 -0
  194. package/src/clis/twitter/download.ts +227 -0
  195. package/src/clis/wechat/README.md +28 -0
  196. package/src/clis/wechat/README.zh-CN.md +28 -0
  197. package/src/clis/wechat/chats.ts +33 -0
  198. package/src/clis/wechat/contacts.ts +33 -0
  199. package/src/clis/wechat/read.ts +72 -0
  200. package/src/clis/wechat/search.ts +36 -0
  201. package/src/clis/wechat/send.ts +49 -0
  202. package/src/clis/wechat/status.ts +35 -0
  203. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  204. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  205. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  206. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  207. package/src/clis/xiaohongshu/download.ts +173 -0
  208. package/src/clis/xiaohongshu/search.ts +1 -1
  209. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  210. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  211. package/src/clis/xiaohongshu/user.ts +52 -32
  212. package/src/clis/zhihu/download.test.ts +12 -0
  213. package/src/clis/zhihu/download.ts +223 -0
  214. package/src/download/index.ts +395 -0
  215. package/src/download/progress.ts +125 -0
  216. package/src/engine.test.ts +17 -0
  217. package/src/main.ts +12 -3
  218. package/src/pipeline/registry.ts +2 -0
  219. package/src/pipeline/steps/download.ts +310 -0
  220. package/src/pipeline/template.ts +26 -0
  221. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -11,7 +11,7 @@
11
11
 
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
- import { fileURLToPath } from 'node:url';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
  import yaml from 'js-yaml';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -43,6 +43,116 @@ interface ManifestEntry {
43
43
  modulePath?: string;
44
44
  }
45
45
 
46
+ function extractBalancedBlock(
47
+ source: string,
48
+ startIndex: number,
49
+ openChar: string,
50
+ closeChar: string,
51
+ ): string | null {
52
+ let depth = 0;
53
+ let quote: string | null = null;
54
+ let escaped = false;
55
+
56
+ for (let i = startIndex; i < source.length; i++) {
57
+ const ch = source[i];
58
+
59
+ if (quote) {
60
+ if (escaped) {
61
+ escaped = false;
62
+ continue;
63
+ }
64
+ if (ch === '\\') {
65
+ escaped = true;
66
+ continue;
67
+ }
68
+ if (ch === quote) quote = null;
69
+ continue;
70
+ }
71
+
72
+ if (ch === '"' || ch === '\'' || ch === '`') {
73
+ quote = ch;
74
+ continue;
75
+ }
76
+
77
+ if (ch === openChar) {
78
+ depth++;
79
+ } else if (ch === closeChar) {
80
+ depth--;
81
+ if (depth === 0) {
82
+ return source.slice(startIndex + 1, i);
83
+ }
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ function extractTsArgsBlock(source: string): string | null {
91
+ const argsMatch = source.match(/args\s*:/);
92
+ if (!argsMatch || argsMatch.index === undefined) return null;
93
+
94
+ const bracketIndex = source.indexOf('[', argsMatch.index);
95
+ if (bracketIndex === -1) return null;
96
+
97
+ return extractBalancedBlock(source, bracketIndex, '[', ']');
98
+ }
99
+
100
+ function parseInlineChoices(body: string): string[] | undefined {
101
+ const choicesMatch = body.match(/choices\s*:\s*\[([^\]]*)\]/);
102
+ if (!choicesMatch) return undefined;
103
+
104
+ const values = choicesMatch[1]
105
+ .split(',')
106
+ .map(s => s.trim().replace(/^['"`]|['"`]$/g, ''))
107
+ .filter(Boolean);
108
+
109
+ return values.length > 0 ? values : undefined;
110
+ }
111
+
112
+ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] {
113
+ const args: ManifestEntry['args'] = [];
114
+ let cursor = 0;
115
+
116
+ while (cursor < argsBlock.length) {
117
+ const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`](\w+)['"`]/);
118
+ if (!nameMatch || nameMatch.index === undefined) break;
119
+
120
+ const objectStart = cursor + nameMatch.index;
121
+ const body = extractBalancedBlock(argsBlock, objectStart, '{', '}');
122
+ if (body == null) break;
123
+
124
+ const typeMatch = body.match(/type\s*:\s*['"`](\w+)['"`]/);
125
+ const defaultMatch = body.match(/default\s*:\s*([^,}]+)/);
126
+ const requiredMatch = body.match(/required\s*:\s*(true|false)/);
127
+ const helpMatch = body.match(/help\s*:\s*['"`]([^'"`]*)['"`]/);
128
+ const positionalMatch = body.match(/positional\s*:\s*(true|false)/);
129
+
130
+ let defaultVal: any = undefined;
131
+ if (defaultMatch) {
132
+ const raw = defaultMatch[1].trim();
133
+ if (raw === 'true') defaultVal = true;
134
+ else if (raw === 'false') defaultVal = false;
135
+ else if (/^\d+$/.test(raw)) defaultVal = parseInt(raw, 10);
136
+ else if (/^\d+\.\d+$/.test(raw)) defaultVal = parseFloat(raw);
137
+ else defaultVal = raw.replace(/^['"`]|['"`]$/g, '');
138
+ }
139
+
140
+ args.push({
141
+ name: nameMatch[1],
142
+ type: typeMatch?.[1] ?? 'str',
143
+ default: defaultVal,
144
+ required: requiredMatch?.[1] === 'true',
145
+ positional: positionalMatch?.[1] === 'true' || undefined,
146
+ help: helpMatch?.[1] ?? '',
147
+ choices: parseInlineChoices(body),
148
+ });
149
+
150
+ cursor = objectStart + body.length + 2;
151
+ }
152
+
153
+ return args;
154
+ }
155
+
46
156
  function scanYaml(filePath: string, site: string): ManifestEntry | null {
47
157
  try {
48
158
  const raw = fs.readFileSync(filePath, 'utf-8');
@@ -129,39 +239,9 @@ function scanTs(filePath: string, site: string): ManifestEntry {
129
239
  }
130
240
 
131
241
  // Extract args array items: { name: '...', ... }
132
- const argsBlockMatch = src.match(/args\s*:\s*\[([\s\S]*?)\]\s*,/);
133
- if (argsBlockMatch) {
134
- const argsBlock = argsBlockMatch[1];
135
- const argRegex = /\{\s*name\s*:\s*['"`](\w+)['"`]([^}]*)\}/g;
136
- let m;
137
- while ((m = argRegex.exec(argsBlock)) !== null) {
138
- const argName = m[1];
139
- const body = m[2];
140
- const typeMatch = body.match(/type\s*:\s*['"`](\w+)['"`]/);
141
- const defaultMatch = body.match(/default\s*:\s*([^,}]+)/);
142
- const requiredMatch = body.match(/required\s*:\s*(true|false)/);
143
- const helpMatch = body.match(/help\s*:\s*['"`]([^'"`]*)['"`]/);
144
- const positionalMatch = body.match(/positional\s*:\s*(true|false)/);
145
-
146
- let defaultVal: any = undefined;
147
- if (defaultMatch) {
148
- const raw = defaultMatch[1].trim();
149
- if (raw === 'true') defaultVal = true;
150
- else if (raw === 'false') defaultVal = false;
151
- else if (/^\d+$/.test(raw)) defaultVal = parseInt(raw, 10);
152
- else if (/^\d+\.\d+$/.test(raw)) defaultVal = parseFloat(raw);
153
- else defaultVal = raw.replace(/^['"`]|['"`]$/g, '');
154
- }
155
-
156
- entry.args.push({
157
- name: argName,
158
- type: typeMatch?.[1] ?? 'str',
159
- default: defaultVal,
160
- required: requiredMatch?.[1] === 'true',
161
- positional: positionalMatch?.[1] === 'true' || undefined,
162
- help: helpMatch?.[1] ?? '',
163
- });
164
- }
242
+ const argsBlock = extractTsArgsBlock(src);
243
+ if (argsBlock) {
244
+ entry.args = parseTsArgsBlock(argsBlock);
165
245
  }
166
246
  } catch {
167
247
  // If parsing fails, fall back to empty metadata — module will self-register at runtime
@@ -170,32 +250,42 @@ function scanTs(filePath: string, site: string): ManifestEntry {
170
250
  return entry;
171
251
  }
172
252
 
173
- // Main
174
- const manifest: ManifestEntry[] = [];
175
-
176
- if (fs.existsSync(CLIS_DIR)) {
177
- for (const site of fs.readdirSync(CLIS_DIR)) {
178
- const siteDir = path.join(CLIS_DIR, site);
179
- if (!fs.statSync(siteDir).isDirectory()) continue;
180
- for (const file of fs.readdirSync(siteDir)) {
181
- const filePath = path.join(siteDir, file);
182
- if (file.endsWith('.yaml') || file.endsWith('.yml')) {
183
- const entry = scanYaml(filePath, site);
184
- if (entry) manifest.push(entry);
185
- } else if (
186
- (file.endsWith('.ts') && !file.endsWith('.d.ts') && file !== 'index.ts') ||
187
- (file.endsWith('.js') && !file.endsWith('.d.js') && file !== 'index.js')
188
- ) {
189
- manifest.push(scanTs(filePath, site));
253
+ export function buildManifest(): ManifestEntry[] {
254
+ const manifest: ManifestEntry[] = [];
255
+
256
+ if (fs.existsSync(CLIS_DIR)) {
257
+ for (const site of fs.readdirSync(CLIS_DIR)) {
258
+ const siteDir = path.join(CLIS_DIR, site);
259
+ if (!fs.statSync(siteDir).isDirectory()) continue;
260
+ for (const file of fs.readdirSync(siteDir)) {
261
+ const filePath = path.join(siteDir, file);
262
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
263
+ const entry = scanYaml(filePath, site);
264
+ if (entry) manifest.push(entry);
265
+ } else if (
266
+ (file.endsWith('.ts') && !file.endsWith('.d.ts') && file !== 'index.ts') ||
267
+ (file.endsWith('.js') && !file.endsWith('.d.js') && file !== 'index.js')
268
+ ) {
269
+ manifest.push(scanTs(filePath, site));
270
+ }
190
271
  }
191
272
  }
192
273
  }
274
+
275
+ return manifest;
193
276
  }
194
277
 
195
- // Ensure output directory exists
196
- fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
197
- fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
278
+ function main(): void {
279
+ const manifest = buildManifest();
280
+ fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
281
+ fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
198
282
 
199
- const yamlCount = manifest.filter(e => e.type === 'yaml').length;
200
- const tsCount = manifest.filter(e => e.type === 'ts').length;
201
- console.log(`✅ Manifest compiled: ${manifest.length} entries (${yamlCount} YAML, ${tsCount} TS) → ${OUTPUT}`);
283
+ const yamlCount = manifest.filter(e => e.type === 'yaml').length;
284
+ const tsCount = manifest.filter(e => e.type === 'ts').length;
285
+ console.log(`✅ Manifest compiled: ${manifest.length} entries (${yamlCount} YAML, ${tsCount} TS) → ${OUTPUT}`);
286
+ }
287
+
288
+ const entrypoint = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
289
+ if (entrypoint === import.meta.url) {
290
+ main();
291
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Bilibili download — download videos using yt-dlp.
3
+ *
4
+ * Usage:
5
+ * opencli bilibili download --bvid BV1xxx --output ./bilibili
6
+ *
7
+ * Requirements:
8
+ * - yt-dlp must be installed: pip install yt-dlp
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { cli, Strategy } from '../../registry.js';
14
+ import {
15
+ ytdlpDownload,
16
+ checkYtdlp,
17
+ sanitizeFilename,
18
+ getTempDir,
19
+ exportCookiesToNetscape,
20
+ } from '../../download/index.js';
21
+ import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
22
+
23
+ cli({
24
+ site: 'bilibili',
25
+ name: 'download',
26
+ description: '下载B站视频(需要 yt-dlp)',
27
+ domain: 'www.bilibili.com',
28
+ strategy: Strategy.COOKIE,
29
+ args: [
30
+ { name: 'bvid', required: true, help: 'Video BV ID (e.g., BV1xxx)' },
31
+ { name: 'output', default: './bilibili-downloads', help: 'Output directory' },
32
+ { name: 'quality', default: 'best', help: 'Video quality (best, 1080p, 720p, 480p)' },
33
+ ],
34
+ columns: ['bvid', 'title', 'status', 'size'],
35
+ func: async (page, kwargs) => {
36
+ const bvid = kwargs.bvid;
37
+ const output = kwargs.output;
38
+ const quality = kwargs.quality;
39
+
40
+ // Check yt-dlp availability
41
+ if (!checkYtdlp()) {
42
+ return [{
43
+ bvid,
44
+ title: '-',
45
+ status: 'failed',
46
+ size: 'yt-dlp not installed. Run: pip install yt-dlp',
47
+ }];
48
+ }
49
+
50
+ // Navigate to video page to get title and cookies
51
+ await page.goto(`https://www.bilibili.com/video/${bvid}`);
52
+ await page.wait(3);
53
+
54
+ // Extract video info
55
+ const data = await page.evaluate(`
56
+ (() => {
57
+ const title = document.querySelector('h1.video-title, .video-title')?.textContent?.trim() || 'video';
58
+ const author = document.querySelector('.up-name, .username')?.textContent?.trim() || 'unknown';
59
+ return { title, author };
60
+ })()
61
+ `);
62
+
63
+ const title = sanitizeFilename(data?.title || 'video');
64
+
65
+ // Extract cookies for authenticated downloads
66
+ const cookieString = await page.evaluate(`(() => document.cookie)()`);
67
+
68
+ // Create output directory
69
+ fs.mkdirSync(output, { recursive: true });
70
+
71
+ // Export cookies to Netscape format for yt-dlp
72
+ let cookiesFile: string | undefined;
73
+ if (typeof cookieString === 'string' && cookieString) {
74
+ const tempDir = getTempDir();
75
+ fs.mkdirSync(tempDir, { recursive: true });
76
+ cookiesFile = path.join(tempDir, `bilibili_cookies_${Date.now()}.txt`);
77
+
78
+ const cookies = cookieString.split(';').map((c) => {
79
+ const [name, ...rest] = c.trim().split('=');
80
+ return {
81
+ name: name || '',
82
+ value: rest.join('=') || '',
83
+ domain: '.bilibili.com',
84
+ path: '/',
85
+ secure: true,
86
+ httpOnly: false,
87
+ };
88
+ }).filter((c) => c.name);
89
+
90
+ exportCookiesToNetscape(cookies, cookiesFile);
91
+ }
92
+
93
+ // Build yt-dlp format string based on quality
94
+ let format = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best';
95
+ if (quality === '1080p') {
96
+ format = 'bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080]';
97
+ } else if (quality === '720p') {
98
+ format = 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]';
99
+ } else if (quality === '480p') {
100
+ format = 'bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480]';
101
+ }
102
+
103
+ const destPath = path.join(output, `${bvid}_${title}.mp4`);
104
+
105
+ const tracker = new DownloadProgressTracker(1, true);
106
+ const progressBar = tracker.onFileStart(`${bvid}.mp4`, 0);
107
+
108
+ try {
109
+ const result = await ytdlpDownload(
110
+ `https://www.bilibili.com/video/${bvid}`,
111
+ destPath,
112
+ {
113
+ cookiesFile,
114
+ format,
115
+ extraArgs: [
116
+ '--merge-output-format', 'mp4',
117
+ '--embed-thumbnail',
118
+ ],
119
+ onProgress: (percent) => {
120
+ if (progressBar) progressBar.update(percent, 100);
121
+ },
122
+ },
123
+ );
124
+
125
+ if (progressBar) {
126
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
127
+ }
128
+
129
+ tracker.onFileComplete(result.success);
130
+ tracker.finish();
131
+
132
+ // Cleanup cookies file
133
+ if (cookiesFile && fs.existsSync(cookiesFile)) {
134
+ fs.unlinkSync(cookiesFile);
135
+ }
136
+
137
+ return [{
138
+ bvid,
139
+ title: data?.title || 'video',
140
+ status: result.success ? 'success' : 'failed',
141
+ size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
142
+ }];
143
+ } catch (err: any) {
144
+ if (progressBar) progressBar.fail(err.message);
145
+ tracker.onFileComplete(false);
146
+ tracker.finish();
147
+
148
+ // Cleanup cookies file
149
+ if (cookiesFile && fs.existsSync(cookiesFile)) {
150
+ fs.unlinkSync(cookiesFile);
151
+ }
152
+
153
+ return [{
154
+ bvid,
155
+ title: data?.title || 'video',
156
+ status: 'failed',
157
+ size: err.message,
158
+ }];
159
+ }
160
+ },
161
+ });
@@ -0,0 +1,38 @@
1
+ # ChatWise Adapter for OpenCLI
2
+
3
+ Control the **ChatWise Desktop App** from the terminal via Chrome DevTools Protocol (CDP). ChatWise is an Electron-based multi-LLM client supporting GPT-4, Claude, Gemini, and more.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. Install [ChatWise](https://chatwise.app/).
8
+ 2. Launch with remote debugging port:
9
+ ```bash
10
+ /Applications/ChatWise.app/Contents/MacOS/ChatWise \
11
+ --remote-debugging-port=9228
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9228"
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ ### Diagnostics
23
+ - `opencli chatwise status`: Check CDP connection status.
24
+ - `opencli chatwise screenshot`: Export DOM + accessibility snapshot.
25
+
26
+ ### Chat
27
+ - `opencli chatwise new`: Start a new conversation (`Cmd+N`).
28
+ - `opencli chatwise send "message"`: Send a message to the active chat.
29
+ - `opencli chatwise read`: Read the current conversation.
30
+ - `opencli chatwise ask "prompt"`: Send + wait for response + return it (one-shot).
31
+
32
+ ### AI Features
33
+ - `opencli chatwise model`: Get the current AI model.
34
+ - `opencli chatwise model gpt-4`: Switch to a different model.
35
+
36
+ ### Organization
37
+ - `opencli chatwise history`: List conversations from the sidebar.
38
+ - `opencli chatwise export`: Export conversation as Markdown.
@@ -0,0 +1,38 @@
1
+ # ChatWise 适配器
2
+
3
+ 通过 Chrome DevTools Protocol (CDP) 在终端中控制 **ChatWise 桌面应用**。ChatWise 是基于 Electron 的多 LLM 客户端,支持 GPT-4、Claude、Gemini 等。
4
+
5
+ ## 前置条件
6
+
7
+ 1. 安装 [ChatWise](https://chatwise.app/)。
8
+ 2. 通过远程调试端口启动:
9
+ ```bash
10
+ /Applications/ChatWise.app/Contents/MacOS/ChatWise \
11
+ --remote-debugging-port=9228
12
+ ```
13
+
14
+ ## 配置
15
+
16
+ ```bash
17
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9228"
18
+ ```
19
+
20
+ ## 命令
21
+
22
+ ### 诊断
23
+ - `opencli chatwise status`:检查 CDP 连接状态。
24
+ - `opencli chatwise screenshot`:导出 DOM + accessibility 快照。
25
+
26
+ ### 对话
27
+ - `opencli chatwise new`:开始新对话(`Cmd+N`)。
28
+ - `opencli chatwise send "消息"`:发送消息到当前对话。
29
+ - `opencli chatwise read`:读取当前对话内容。
30
+ - `opencli chatwise ask "提示词"`:发送 + 等待回复 + 返回结果(一站式)。
31
+
32
+ ### AI 功能
33
+ - `opencli chatwise model`:获取当前 AI 模型。
34
+ - `opencli chatwise model gpt-4`:切换模型。
35
+
36
+ ### 组织管理
37
+ - `opencli chatwise history`:列出 sidebar 会话列表。
38
+ - `opencli chatwise export`:导出对话为 Markdown 文件。
@@ -0,0 +1,87 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const askCommand = cli({
5
+ site: 'chatwise',
6
+ name: 'ask',
7
+ description: 'Send a prompt and wait for the AI response (send + wait + read)',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'text', required: true, positional: true, help: 'Prompt to send' },
13
+ { name: 'timeout', required: false, help: 'Max seconds to wait (default: 30)', default: '30' },
14
+ ],
15
+ columns: ['Role', 'Text'],
16
+ func: async (page: IPage, kwargs: any) => {
17
+ const text = kwargs.text as string;
18
+ const timeout = parseInt(kwargs.timeout as string, 10) || 30;
19
+
20
+ // Snapshot content length
21
+ const beforeLen = await page.evaluate(`
22
+ (function() {
23
+ const msgs = document.querySelectorAll('[data-message-id], [class*="message"], [class*="bubble"]');
24
+ return msgs.length;
25
+ })()
26
+ `);
27
+
28
+ // Send message
29
+ await page.evaluate(`
30
+ (function(text) {
31
+ let composer = document.querySelector('textarea');
32
+ if (!composer) {
33
+ const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
34
+ composer = editables.length > 0 ? editables[editables.length - 1] : null;
35
+ }
36
+ if (!composer) throw new Error('Could not find input');
37
+ composer.focus();
38
+ if (composer.tagName === 'TEXTAREA') {
39
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
40
+ setter.call(composer, text);
41
+ composer.dispatchEvent(new Event('input', { bubbles: true }));
42
+ } else {
43
+ document.execCommand('insertText', false, text);
44
+ }
45
+ })(${JSON.stringify(text)})
46
+ `);
47
+
48
+ await page.wait(0.5);
49
+ await page.pressKey('Enter');
50
+
51
+ // Poll for response
52
+ const pollInterval = 2;
53
+ const maxPolls = Math.ceil(timeout / pollInterval);
54
+ let response = '';
55
+
56
+ for (let i = 0; i < maxPolls; i++) {
57
+ await page.wait(pollInterval);
58
+
59
+ const result = await page.evaluate(`
60
+ (function(prevLen) {
61
+ const msgs = document.querySelectorAll('[data-message-id], [class*="message"], [class*="bubble"]');
62
+ if (msgs.length <= prevLen) return null;
63
+ const last = msgs[msgs.length - 1];
64
+ const text = last.innerText || last.textContent;
65
+ return text ? text.trim() : null;
66
+ })(${beforeLen})
67
+ `);
68
+
69
+ if (result) {
70
+ response = result;
71
+ break;
72
+ }
73
+ }
74
+
75
+ if (!response) {
76
+ return [
77
+ { Role: 'User', Text: text },
78
+ { Role: 'System', Text: `No response within ${timeout}s.` },
79
+ ];
80
+ }
81
+
82
+ return [
83
+ { Role: 'User', Text: text },
84
+ { Role: 'Assistant', Text: response },
85
+ ];
86
+ },
87
+ });
@@ -0,0 +1,51 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const exportCommand = cli({
6
+ site: 'chatwise',
7
+ name: 'export',
8
+ description: 'Export the current ChatWise conversation to a Markdown file',
9
+ domain: 'localhost',
10
+ strategy: Strategy.UI,
11
+ browser: true,
12
+ args: [
13
+ { name: 'output', required: false, positional: true, help: 'Output file (default: /tmp/chatwise-export.md)' },
14
+ ],
15
+ columns: ['Status', 'File', 'Messages'],
16
+ func: async (page: IPage, kwargs: any) => {
17
+ const outputPath = (kwargs.output as string) || '/tmp/chatwise-export.md';
18
+
19
+ const md = await page.evaluate(`
20
+ (function() {
21
+ const selectors = [
22
+ '[data-message-id]',
23
+ '[class*="message"]',
24
+ '[class*="chat-item"]',
25
+ '[class*="bubble"]',
26
+ ];
27
+
28
+ for (const sel of selectors) {
29
+ const nodes = document.querySelectorAll(sel);
30
+ if (nodes.length > 0) {
31
+ return Array.from(nodes).map((n, i) => '## Message ' + (i + 1) + '\\n\\n' + (n.innerText || n.textContent).trim()).join('\\n\\n---\\n\\n');
32
+ }
33
+ }
34
+
35
+ const main = document.querySelector('main, [role="main"], [class*="chat-container"]');
36
+ if (main) return main.innerText || main.textContent;
37
+ return document.body.innerText;
38
+ })()
39
+ `);
40
+
41
+ fs.writeFileSync(outputPath, '# ChatWise Conversation Export\\n\\n' + md);
42
+
43
+ return [
44
+ {
45
+ Status: 'Success',
46
+ File: outputPath,
47
+ Messages: md.split('## Message').length - 1,
48
+ },
49
+ ];
50
+ },
51
+ });
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const historyCommand = cli({
5
+ site: 'chatwise',
6
+ name: 'history',
7
+ description: 'List conversation history in ChatWise sidebar',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Title'],
13
+ func: async (page: IPage) => {
14
+ const items = await page.evaluate(`
15
+ (function() {
16
+ const results = [];
17
+ const selectors = [
18
+ '[class*="sidebar"] [class*="item"]',
19
+ '[class*="conversation-list"] a',
20
+ '[class*="chat-list"] > *',
21
+ 'nav a',
22
+ 'aside a',
23
+ '[role="listbox"] [role="option"]',
24
+ ];
25
+
26
+ for (const sel of selectors) {
27
+ const nodes = document.querySelectorAll(sel);
28
+ if (nodes.length > 0) {
29
+ nodes.forEach((n, i) => {
30
+ const text = (n.textContent || '').trim().substring(0, 100);
31
+ if (text) results.push({ Index: i + 1, Title: text });
32
+ });
33
+ break;
34
+ }
35
+ }
36
+
37
+ return results;
38
+ })()
39
+ `);
40
+
41
+ if (items.length === 0) {
42
+ return [{ Index: 0, Title: 'No history found. Ensure the sidebar is visible.' }];
43
+ }
44
+
45
+ return items;
46
+ },
47
+ });