@jackwener/opencli 0.9.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) 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/CDP.md +1 -1
  14. package/CDP.zh-CN.md +1 -1
  15. package/CLI-ELECTRON.md +89 -36
  16. package/CLI-EXPLORER.md +4 -4
  17. package/CONTRIBUTING.md +167 -0
  18. package/README.md +113 -89
  19. package/README.zh-CN.md +114 -91
  20. package/SKILL.md +10 -8
  21. package/TESTING.md +7 -7
  22. package/dist/browser/daemon-client.d.ts +37 -0
  23. package/dist/browser/daemon-client.js +82 -0
  24. package/dist/browser/discover.d.ts +11 -34
  25. package/dist/browser/discover.js +15 -190
  26. package/dist/browser/errors.d.ts +6 -20
  27. package/dist/browser/errors.js +24 -63
  28. package/dist/browser/index.d.ts +2 -11
  29. package/dist/browser/index.js +5 -11
  30. package/dist/browser/mcp.d.ts +9 -18
  31. package/dist/browser/mcp.js +70 -284
  32. package/dist/browser/page.d.ts +28 -6
  33. package/dist/browser/page.js +210 -85
  34. package/dist/browser.test.js +4 -202
  35. package/dist/build-manifest.d.ts +26 -0
  36. package/dist/build-manifest.js +132 -60
  37. package/dist/build-manifest.test.d.ts +1 -0
  38. package/dist/build-manifest.test.js +26 -0
  39. package/dist/cli-manifest.json +1582 -29
  40. package/dist/clis/bilibili/download.d.ts +10 -0
  41. package/dist/clis/bilibili/download.js +135 -0
  42. package/dist/clis/chatwise/ask.d.ts +1 -0
  43. package/dist/clis/chatwise/ask.js +76 -0
  44. package/dist/clis/chatwise/export.d.ts +1 -0
  45. package/dist/clis/chatwise/export.js +46 -0
  46. package/dist/clis/chatwise/history.d.ts +1 -0
  47. package/dist/clis/chatwise/history.js +43 -0
  48. package/dist/clis/chatwise/model.d.ts +1 -0
  49. package/dist/clis/chatwise/model.js +81 -0
  50. package/dist/clis/chatwise/new.d.ts +1 -0
  51. package/dist/clis/chatwise/new.js +18 -0
  52. package/dist/clis/chatwise/read.d.ts +1 -0
  53. package/dist/clis/chatwise/read.js +39 -0
  54. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  55. package/dist/clis/chatwise/screenshot.js +27 -0
  56. package/dist/clis/chatwise/send.d.ts +1 -0
  57. package/dist/clis/chatwise/send.js +45 -0
  58. package/dist/clis/chatwise/status.d.ts +1 -0
  59. package/dist/clis/chatwise/status.js +22 -0
  60. package/dist/clis/discord-app/channels.d.ts +1 -0
  61. package/dist/clis/discord-app/channels.js +45 -0
  62. package/dist/clis/discord-app/members.d.ts +1 -0
  63. package/dist/clis/discord-app/members.js +38 -0
  64. package/dist/clis/discord-app/read.d.ts +1 -0
  65. package/dist/clis/discord-app/read.js +45 -0
  66. package/dist/clis/discord-app/search.d.ts +1 -0
  67. package/dist/clis/discord-app/search.js +56 -0
  68. package/dist/clis/discord-app/send.d.ts +1 -0
  69. package/dist/clis/discord-app/send.js +27 -0
  70. package/dist/clis/discord-app/servers.d.ts +1 -0
  71. package/dist/clis/discord-app/servers.js +36 -0
  72. package/dist/clis/discord-app/status.d.ts +1 -0
  73. package/dist/clis/discord-app/status.js +16 -0
  74. package/dist/clis/feishu/new.d.ts +1 -0
  75. package/dist/clis/feishu/new.js +27 -0
  76. package/dist/clis/feishu/read.d.ts +1 -0
  77. package/dist/clis/feishu/read.js +40 -0
  78. package/dist/clis/feishu/search.d.ts +1 -0
  79. package/dist/clis/feishu/search.js +30 -0
  80. package/dist/clis/feishu/send.d.ts +1 -0
  81. package/dist/clis/feishu/send.js +39 -0
  82. package/dist/clis/feishu/status.d.ts +1 -0
  83. package/dist/clis/feishu/status.js +28 -0
  84. package/dist/clis/grok/ask.d.ts +1 -0
  85. package/dist/clis/grok/ask.js +82 -0
  86. package/dist/clis/grok/debug.d.ts +1 -0
  87. package/dist/clis/grok/debug.js +45 -0
  88. package/dist/clis/jimeng/generate.yaml +84 -0
  89. package/dist/clis/jimeng/history.yaml +47 -0
  90. package/dist/clis/linux-do/categories.yaml +41 -0
  91. package/dist/clis/linux-do/category.yaml +49 -0
  92. package/dist/clis/linux-do/hot.yaml +50 -0
  93. package/dist/clis/linux-do/latest.yaml +40 -0
  94. package/dist/clis/linux-do/search.yaml +45 -0
  95. package/dist/clis/linux-do/topic.yaml +38 -0
  96. package/dist/clis/neteasemusic/like.d.ts +1 -0
  97. package/dist/clis/neteasemusic/like.js +25 -0
  98. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  99. package/dist/clis/neteasemusic/lyrics.js +47 -0
  100. package/dist/clis/neteasemusic/next.d.ts +1 -0
  101. package/dist/clis/neteasemusic/next.js +26 -0
  102. package/dist/clis/neteasemusic/play.d.ts +1 -0
  103. package/dist/clis/neteasemusic/play.js +26 -0
  104. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  105. package/dist/clis/neteasemusic/playing.js +59 -0
  106. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  107. package/dist/clis/neteasemusic/playlist.js +46 -0
  108. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  109. package/dist/clis/neteasemusic/prev.js +25 -0
  110. package/dist/clis/neteasemusic/search.d.ts +1 -0
  111. package/dist/clis/neteasemusic/search.js +52 -0
  112. package/dist/clis/neteasemusic/status.d.ts +1 -0
  113. package/dist/clis/neteasemusic/status.js +16 -0
  114. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  115. package/dist/clis/neteasemusic/volume.js +54 -0
  116. package/dist/clis/notion/export.d.ts +1 -0
  117. package/dist/clis/notion/export.js +31 -0
  118. package/dist/clis/notion/favorites.d.ts +1 -0
  119. package/dist/clis/notion/favorites.js +84 -0
  120. package/dist/clis/notion/new.d.ts +1 -0
  121. package/dist/clis/notion/new.js +34 -0
  122. package/dist/clis/notion/read.d.ts +1 -0
  123. package/dist/clis/notion/read.js +30 -0
  124. package/dist/clis/notion/search.d.ts +1 -0
  125. package/dist/clis/notion/search.js +46 -0
  126. package/dist/clis/notion/sidebar.d.ts +1 -0
  127. package/dist/clis/notion/sidebar.js +41 -0
  128. package/dist/clis/notion/status.d.ts +1 -0
  129. package/dist/clis/notion/status.js +16 -0
  130. package/dist/clis/notion/write.d.ts +1 -0
  131. package/dist/clis/notion/write.js +40 -0
  132. package/dist/clis/twitter/download.d.ts +8 -0
  133. package/dist/clis/twitter/download.js +204 -0
  134. package/dist/clis/wechat/chats.d.ts +1 -0
  135. package/dist/clis/wechat/chats.js +28 -0
  136. package/dist/clis/wechat/contacts.d.ts +1 -0
  137. package/dist/clis/wechat/contacts.js +28 -0
  138. package/dist/clis/wechat/read.d.ts +1 -0
  139. package/dist/clis/wechat/read.js +58 -0
  140. package/dist/clis/wechat/search.d.ts +1 -0
  141. package/dist/clis/wechat/search.js +31 -0
  142. package/dist/clis/wechat/send.d.ts +1 -0
  143. package/dist/clis/wechat/send.js +42 -0
  144. package/dist/clis/wechat/status.d.ts +1 -0
  145. package/dist/clis/wechat/status.js +29 -0
  146. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  147. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  148. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  149. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  150. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  151. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  152. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  153. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  154. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  155. package/dist/clis/xiaohongshu/download.js +155 -0
  156. package/dist/clis/xiaohongshu/search.js +1 -1
  157. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  158. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  159. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  160. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  161. package/dist/clis/xiaohongshu/user.js +46 -29
  162. package/dist/clis/zhihu/download.d.ts +11 -0
  163. package/dist/clis/zhihu/download.js +186 -0
  164. package/dist/clis/zhihu/download.test.d.ts +1 -0
  165. package/dist/clis/zhihu/download.test.js +10 -0
  166. package/dist/daemon.d.ts +13 -0
  167. package/dist/daemon.js +187 -0
  168. package/dist/doctor.d.ts +27 -61
  169. package/dist/doctor.js +70 -601
  170. package/dist/doctor.test.js +30 -170
  171. package/dist/download/index.d.ts +79 -0
  172. package/dist/download/index.js +325 -0
  173. package/dist/download/progress.d.ts +36 -0
  174. package/dist/download/progress.js +111 -0
  175. package/dist/engine.test.js +15 -0
  176. package/dist/main.js +22 -28
  177. package/dist/pipeline/executor.test.js +1 -0
  178. package/dist/pipeline/registry.js +2 -0
  179. package/dist/pipeline/steps/browser.js +2 -2
  180. package/dist/pipeline/steps/download.d.ts +34 -0
  181. package/dist/pipeline/steps/download.js +251 -0
  182. package/dist/pipeline/steps/intercept.js +1 -2
  183. package/dist/pipeline/template.js +28 -0
  184. package/dist/setup.d.ts +6 -0
  185. package/dist/setup.js +46 -160
  186. package/dist/types.d.ts +6 -0
  187. package/extension/icons/icon-128.png +0 -0
  188. package/extension/icons/icon-16.png +0 -0
  189. package/extension/icons/icon-32.png +0 -0
  190. package/extension/icons/icon-48.png +0 -0
  191. package/extension/manifest.json +31 -0
  192. package/extension/package.json +16 -0
  193. package/extension/src/background.ts +293 -0
  194. package/extension/src/cdp.ts +125 -0
  195. package/extension/src/protocol.ts +57 -0
  196. package/extension/store-assets/screenshot-1280x800.png +0 -0
  197. package/extension/tsconfig.json +15 -0
  198. package/extension/vite.config.ts +18 -0
  199. package/package.json +8 -7
  200. package/scripts/test-site.mjs +70 -0
  201. package/src/browser/daemon-client.ts +113 -0
  202. package/src/browser/discover.ts +18 -216
  203. package/src/browser/errors.ts +30 -100
  204. package/src/browser/index.ts +6 -12
  205. package/src/browser/mcp.ts +78 -278
  206. package/src/browser/page.ts +222 -88
  207. package/src/browser.test.ts +3 -210
  208. package/src/build-manifest.test.ts +28 -0
  209. package/src/build-manifest.ts +147 -57
  210. package/src/clis/bilibili/download.ts +161 -0
  211. package/src/clis/chatgpt/README.md +1 -1
  212. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  213. package/src/clis/chatwise/README.md +38 -0
  214. package/src/clis/chatwise/README.zh-CN.md +38 -0
  215. package/src/clis/chatwise/ask.ts +87 -0
  216. package/src/clis/chatwise/export.ts +51 -0
  217. package/src/clis/chatwise/history.ts +47 -0
  218. package/src/clis/chatwise/model.ts +87 -0
  219. package/src/clis/chatwise/new.ts +21 -0
  220. package/src/clis/chatwise/read.ts +42 -0
  221. package/src/clis/chatwise/screenshot.ts +33 -0
  222. package/src/clis/chatwise/send.ts +50 -0
  223. package/src/clis/chatwise/status.ts +25 -0
  224. package/src/clis/discord-app/README.md +28 -0
  225. package/src/clis/discord-app/README.zh-CN.md +28 -0
  226. package/src/clis/discord-app/channels.ts +48 -0
  227. package/src/clis/discord-app/members.ts +41 -0
  228. package/src/clis/discord-app/read.ts +49 -0
  229. package/src/clis/discord-app/search.ts +64 -0
  230. package/src/clis/discord-app/send.ts +32 -0
  231. package/src/clis/discord-app/servers.ts +39 -0
  232. package/src/clis/discord-app/status.ts +18 -0
  233. package/src/clis/feishu/README.md +20 -0
  234. package/src/clis/feishu/README.zh-CN.md +20 -0
  235. package/src/clis/feishu/new.ts +32 -0
  236. package/src/clis/feishu/read.ts +48 -0
  237. package/src/clis/feishu/search.ts +35 -0
  238. package/src/clis/feishu/send.ts +46 -0
  239. package/src/clis/feishu/status.ts +34 -0
  240. package/src/clis/grok/ask.ts +90 -0
  241. package/src/clis/grok/debug.ts +49 -0
  242. package/src/clis/jimeng/generate.yaml +84 -0
  243. package/src/clis/jimeng/history.yaml +47 -0
  244. package/src/clis/linux-do/categories.yaml +41 -0
  245. package/src/clis/linux-do/category.yaml +49 -0
  246. package/src/clis/linux-do/hot.yaml +50 -0
  247. package/src/clis/linux-do/latest.yaml +40 -0
  248. package/src/clis/linux-do/search.yaml +45 -0
  249. package/src/clis/linux-do/topic.yaml +38 -0
  250. package/src/clis/neteasemusic/README.md +31 -0
  251. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  252. package/src/clis/neteasemusic/like.ts +28 -0
  253. package/src/clis/neteasemusic/lyrics.ts +53 -0
  254. package/src/clis/neteasemusic/next.ts +30 -0
  255. package/src/clis/neteasemusic/play.ts +30 -0
  256. package/src/clis/neteasemusic/playing.ts +62 -0
  257. package/src/clis/neteasemusic/playlist.ts +51 -0
  258. package/src/clis/neteasemusic/prev.ts +29 -0
  259. package/src/clis/neteasemusic/search.ts +58 -0
  260. package/src/clis/neteasemusic/status.ts +18 -0
  261. package/src/clis/neteasemusic/volume.ts +61 -0
  262. package/src/clis/notion/README.md +29 -0
  263. package/src/clis/notion/README.zh-CN.md +29 -0
  264. package/src/clis/notion/export.ts +36 -0
  265. package/src/clis/notion/favorites.ts +87 -0
  266. package/src/clis/notion/new.ts +39 -0
  267. package/src/clis/notion/read.ts +33 -0
  268. package/src/clis/notion/search.ts +54 -0
  269. package/src/clis/notion/sidebar.ts +44 -0
  270. package/src/clis/notion/status.ts +18 -0
  271. package/src/clis/notion/write.ts +45 -0
  272. package/src/clis/twitter/download.ts +227 -0
  273. package/src/clis/wechat/README.md +28 -0
  274. package/src/clis/wechat/README.zh-CN.md +28 -0
  275. package/src/clis/wechat/chats.ts +33 -0
  276. package/src/clis/wechat/contacts.ts +33 -0
  277. package/src/clis/wechat/read.ts +72 -0
  278. package/src/clis/wechat/search.ts +36 -0
  279. package/src/clis/wechat/send.ts +49 -0
  280. package/src/clis/wechat/status.ts +35 -0
  281. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  282. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  283. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  284. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  285. package/src/clis/xiaohongshu/download.ts +173 -0
  286. package/src/clis/xiaohongshu/search.ts +1 -1
  287. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  288. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  289. package/src/clis/xiaohongshu/user.ts +52 -32
  290. package/src/clis/zhihu/download.test.ts +12 -0
  291. package/src/clis/zhihu/download.ts +223 -0
  292. package/src/daemon.ts +217 -0
  293. package/src/doctor.test.ts +32 -193
  294. package/src/doctor.ts +74 -668
  295. package/src/download/index.ts +395 -0
  296. package/src/download/progress.ts +125 -0
  297. package/src/engine.test.ts +17 -0
  298. package/src/main.ts +18 -26
  299. package/src/pipeline/executor.test.ts +1 -0
  300. package/src/pipeline/registry.ts +2 -0
  301. package/src/pipeline/steps/browser.ts +2 -2
  302. package/src/pipeline/steps/download.ts +310 -0
  303. package/src/pipeline/steps/intercept.ts +1 -2
  304. package/src/pipeline/template.ts +26 -0
  305. package/src/setup.ts +47 -183
  306. package/src/types.ts +1 -0
  307. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -1,191 +1,51 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { readTokenFromShellContent, renderBrowserDoctorReport, upsertShellToken, readTomlConfigToken, upsertTomlConfigToken, upsertJsonConfigToken, } from './doctor.js';
3
- describe('shell token helpers', () => {
4
- it('reads token from shell export', () => {
5
- expect(readTokenFromShellContent('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"\n')).toBe('abc123');
6
- });
7
- it('appends token export when missing', () => {
8
- const next = upsertShellToken('export PATH="/usr/bin"\n', 'abc123');
9
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
10
- });
11
- it('replaces token export when present', () => {
12
- const next = upsertShellToken('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="old"\n', 'new');
13
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="new"');
14
- expect(next).not.toContain('"old"');
15
- });
16
- });
17
- describe('toml token helpers', () => {
18
- it('reads token from playwright env section', () => {
19
- const content = `
20
- [mcp_servers.playwright.env]
21
- PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"
22
- `;
23
- expect(readTomlConfigToken(content)).toBe('abc123');
24
- });
25
- it('updates token inside existing env section', () => {
26
- const content = `
27
- [mcp_servers.playwright.env]
28
- PLAYWRIGHT_MCP_EXTENSION_TOKEN = "old"
29
- `;
30
- const next = upsertTomlConfigToken(content, 'new');
31
- expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "new"');
32
- expect(next).not.toContain('"old"');
33
- });
34
- it('creates env section when missing', () => {
35
- const content = `
36
- [mcp_servers.playwright]
37
- type = "stdio"
38
- `;
39
- const next = upsertTomlConfigToken(content, 'abc123');
40
- expect(next).toContain('[mcp_servers.playwright.env]');
41
- expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"');
42
- });
43
- });
44
- describe('json token helpers', () => {
45
- it('writes token into standard mcpServers config', () => {
46
- const next = upsertJsonConfigToken(JSON.stringify({
47
- mcpServers: {
48
- playwright: {
49
- command: 'npx',
50
- args: ['-y', '@playwright/mcp@latest', '--extension'],
51
- },
52
- },
53
- }), 'abc123');
54
- const parsed = JSON.parse(next);
55
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
56
- });
57
- it('writes token into opencode mcp config', () => {
58
- const next = upsertJsonConfigToken(JSON.stringify({
59
- $schema: 'https://opencode.ai/config.json',
60
- mcp: {
61
- playwright: {
62
- command: ['npx', '-y', '@playwright/mcp@latest', '--extension'],
63
- enabled: true,
64
- type: 'local',
65
- },
66
- },
67
- }), 'abc123');
68
- const parsed = JSON.parse(next);
69
- expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
70
- });
71
- it('creates standard mcpServers format for empty file (not OpenCode)', () => {
72
- const next = upsertJsonConfigToken('', 'abc123');
73
- const parsed = JSON.parse(next);
74
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
75
- expect(parsed.mcp).toBeUndefined();
76
- });
77
- it('creates OpenCode format when filePath contains opencode', () => {
78
- const next = upsertJsonConfigToken('', 'abc123', '/home/user/.config/opencode/opencode.json');
79
- const parsed = JSON.parse(next);
80
- expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
81
- expect(parsed.mcpServers).toBeUndefined();
82
- });
83
- it('creates standard format when filePath is claude.json', () => {
84
- const next = upsertJsonConfigToken('', 'abc123', '/home/user/.claude.json');
85
- const parsed = JSON.parse(next);
86
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
87
- });
88
- });
89
- describe('fish shell support', () => {
90
- it('generates fish set -gx syntax for fish config path', () => {
91
- const next = upsertShellToken('', 'abc123', '/home/user/.config/fish/config.fish');
92
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
93
- expect(next).not.toContain('export');
94
- });
95
- it('replaces existing fish set line', () => {
96
- const content = 'set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "old"\n';
97
- const next = upsertShellToken(content, 'new', '/home/user/.config/fish/config.fish');
98
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "new"');
99
- expect(next).not.toContain('"old"');
100
- });
101
- it('appends fish syntax to existing fish config', () => {
102
- const content = 'set -gx PATH /usr/bin\n';
103
- const next = upsertShellToken(content, 'abc123', '/home/user/.config/fish/config.fish');
104
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
105
- expect(next).toContain('set -gx PATH /usr/bin');
106
- });
107
- it('uses export syntax for zshrc even with filePath', () => {
108
- const next = upsertShellToken('', 'abc123', '/home/user/.zshrc');
109
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
110
- expect(next).not.toContain('set -gx');
111
- });
112
- });
2
+ import { renderBrowserDoctorReport } from './doctor.js';
113
3
  describe('doctor report rendering', () => {
114
4
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
115
- it('renders OK-style report when tokens match', () => {
5
+ it('renders OK-style report when daemon and extension connected', () => {
116
6
  const text = strip(renderBrowserDoctorReport({
117
- envToken: 'abc123',
118
- envFingerprint: 'fp1',
119
- extensionToken: 'abc123',
120
- extensionFingerprint: 'fp1',
121
- extensionInstalled: true,
122
- extensionBrowsers: ['Chrome'],
123
- shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
124
- configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
125
- recommendedToken: 'abc123',
126
- recommendedFingerprint: 'fp1',
127
- warnings: [],
7
+ daemonRunning: true,
8
+ extensionConnected: true,
128
9
  issues: [],
129
10
  }));
130
- expect(text).toContain('[OK] Extension installed (Chrome)');
131
- expect(text).toContain('[OK] Environment token: configured (fp1)');
132
- expect(text).toContain('[OK] /tmp/mcp.json');
133
- expect(text).toContain('configured (fp1)');
11
+ expect(text).toContain('[OK] Daemon: running on port 19825');
12
+ expect(text).toContain('[OK] Extension: connected');
13
+ expect(text).toContain('Everything looks good!');
14
+ });
15
+ it('renders MISSING when daemon not running', () => {
16
+ const text = strip(renderBrowserDoctorReport({
17
+ daemonRunning: false,
18
+ extensionConnected: false,
19
+ issues: ['Daemon is not running.'],
20
+ }));
21
+ expect(text).toContain('[MISSING] Daemon: not running');
22
+ expect(text).toContain('[MISSING] Extension: not connected');
23
+ expect(text).toContain('Daemon is not running.');
134
24
  });
135
- it('renders MISMATCH-style report when fingerprints differ', () => {
25
+ it('renders extension not connected when daemon is running', () => {
136
26
  const text = strip(renderBrowserDoctorReport({
137
- envToken: 'abc123',
138
- envFingerprint: 'fp1',
139
- extensionToken: null,
140
- extensionFingerprint: null,
141
- extensionInstalled: false,
142
- extensionBrowsers: [],
143
- shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
144
- configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
145
- recommendedToken: 'abc123',
146
- recommendedFingerprint: 'fp1',
147
- warnings: [],
148
- issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
27
+ daemonRunning: true,
28
+ extensionConnected: false,
29
+ issues: ['Daemon is running but the Chrome extension is not connected.'],
149
30
  }));
150
- expect(text).toContain('[MISSING] Extension not installed in any browser');
151
- expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
152
- expect(text).toContain('[MISMATCH] /tmp/.zshrc');
153
- expect(text).toContain('configured (fp2)');
154
- expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
31
+ expect(text).toContain('[OK] Daemon: running on port 19825');
32
+ expect(text).toContain('[MISSING] Extension: not connected');
155
33
  });
156
34
  it('renders connectivity OK when live test succeeds', () => {
157
35
  const text = strip(renderBrowserDoctorReport({
158
- envToken: 'abc123',
159
- envFingerprint: 'fp1',
160
- extensionToken: 'abc123',
161
- extensionFingerprint: 'fp1',
162
- extensionInstalled: true,
163
- extensionBrowsers: ['Chrome'],
164
- shellFiles: [],
165
- configs: [],
166
- recommendedToken: 'abc123',
167
- recommendedFingerprint: 'fp1',
36
+ daemonRunning: true,
37
+ extensionConnected: true,
168
38
  connectivity: { ok: true, durationMs: 1234 },
169
- warnings: [],
170
39
  issues: [],
171
40
  }));
172
- expect(text).toContain('[OK] Browser connectivity: connected in 1.2s');
41
+ expect(text).toContain('[OK] Connectivity: connected in 1.2s');
173
42
  });
174
- it('renders connectivity WARN when not tested', () => {
43
+ it('renders connectivity SKIP when not tested', () => {
175
44
  const text = strip(renderBrowserDoctorReport({
176
- envToken: 'abc123',
177
- envFingerprint: 'fp1',
178
- extensionToken: 'abc123',
179
- extensionFingerprint: 'fp1',
180
- extensionInstalled: true,
181
- extensionBrowsers: ['Chrome'],
182
- shellFiles: [],
183
- configs: [],
184
- recommendedToken: 'abc123',
185
- recommendedFingerprint: 'fp1',
186
- warnings: [],
45
+ daemonRunning: true,
46
+ extensionConnected: true,
187
47
  issues: [],
188
48
  }));
189
- expect(text).toContain('[WARN] Browser connectivity: not tested (use --live)');
49
+ expect(text).toContain('[SKIP] Connectivity: not tested (use --live)');
190
50
  });
191
51
  });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
3
+ */
4
+ export interface DownloadOptions {
5
+ cookies?: string;
6
+ headers?: Record<string, string>;
7
+ timeout?: number;
8
+ onProgress?: (received: number, total: number) => void;
9
+ }
10
+ export interface YtdlpOptions {
11
+ cookies?: string;
12
+ cookiesFile?: string;
13
+ format?: string;
14
+ extraArgs?: string[];
15
+ onProgress?: (percent: number) => void;
16
+ }
17
+ /**
18
+ * Check if yt-dlp is available in PATH.
19
+ */
20
+ export declare function checkYtdlp(): boolean;
21
+ /**
22
+ * Check if ffmpeg is available in PATH.
23
+ */
24
+ export declare function checkFfmpeg(): boolean;
25
+ /**
26
+ * Detect content type from URL and optional headers.
27
+ */
28
+ export declare function detectContentType(url: string, contentType?: string): 'image' | 'video' | 'document' | 'binary';
29
+ /**
30
+ * Check if URL requires yt-dlp for download.
31
+ */
32
+ export declare function requiresYtdlp(url: string): boolean;
33
+ /**
34
+ * HTTP download with progress callback.
35
+ */
36
+ export declare function httpDownload(url: string, destPath: string, options?: DownloadOptions): Promise<{
37
+ success: boolean;
38
+ size: number;
39
+ error?: string;
40
+ }>;
41
+ /**
42
+ * Export cookies to Netscape format for yt-dlp.
43
+ */
44
+ export declare function exportCookiesToNetscape(cookies: Array<{
45
+ name: string;
46
+ value: string;
47
+ domain: string;
48
+ path?: string;
49
+ secure?: boolean;
50
+ httpOnly?: boolean;
51
+ }>, filePath: string): void;
52
+ /**
53
+ * Download video using yt-dlp.
54
+ */
55
+ export declare function ytdlpDownload(url: string, destPath: string, options?: YtdlpOptions): Promise<{
56
+ success: boolean;
57
+ size: number;
58
+ error?: string;
59
+ }>;
60
+ /**
61
+ * Save document content to file.
62
+ */
63
+ export declare function saveDocument(content: string, destPath: string, format?: 'json' | 'markdown' | 'html' | 'text', metadata?: Record<string, any>): Promise<{
64
+ success: boolean;
65
+ size: number;
66
+ error?: string;
67
+ }>;
68
+ /**
69
+ * Sanitize filename by removing invalid characters.
70
+ */
71
+ export declare function sanitizeFilename(name: string, maxLength?: number): string;
72
+ /**
73
+ * Generate filename from URL if not provided.
74
+ */
75
+ export declare function generateFilename(url: string, index: number, extension?: string): string;
76
+ /**
77
+ * Get temp directory for cookie files.
78
+ */
79
+ export declare function getTempDir(): string;
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
3
+ */
4
+ import { spawn, execSync } from 'node:child_process';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as https from 'node:https';
8
+ import * as http from 'node:http';
9
+ import * as os from 'node:os';
10
+ import { URL } from 'node:url';
11
+ /**
12
+ * Check if yt-dlp is available in PATH.
13
+ */
14
+ export function checkYtdlp() {
15
+ try {
16
+ execSync('yt-dlp --version', { encoding: 'utf-8', stdio: 'pipe' });
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Check if ffmpeg is available in PATH.
25
+ */
26
+ export function checkFfmpeg() {
27
+ try {
28
+ execSync('ffmpeg -version', { encoding: 'utf-8', stdio: 'pipe' });
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * Detect content type from URL and optional headers.
37
+ */
38
+ export function detectContentType(url, contentType) {
39
+ // Check content-type header first
40
+ if (contentType) {
41
+ if (contentType.startsWith('image/'))
42
+ return 'image';
43
+ if (contentType.startsWith('video/'))
44
+ return 'video';
45
+ if (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('xml'))
46
+ return 'document';
47
+ }
48
+ // Detect from URL
49
+ const urlLower = url.toLowerCase();
50
+ const ext = path.extname(new URL(url).pathname).toLowerCase();
51
+ // Image extensions
52
+ if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif'].includes(ext)) {
53
+ return 'image';
54
+ }
55
+ // Video extensions
56
+ if (['.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.m3u8', '.ts'].includes(ext)) {
57
+ return 'video';
58
+ }
59
+ // Video platforms (need yt-dlp)
60
+ if (urlLower.includes('youtube.com') || urlLower.includes('youtu.be') ||
61
+ urlLower.includes('bilibili.com') || urlLower.includes('twitter.com') ||
62
+ urlLower.includes('x.com') || urlLower.includes('tiktok.com') ||
63
+ urlLower.includes('vimeo.com') || urlLower.includes('twitch.tv')) {
64
+ return 'video';
65
+ }
66
+ // Document extensions
67
+ if (['.html', '.htm', '.json', '.xml', '.txt', '.md', '.markdown'].includes(ext)) {
68
+ return 'document';
69
+ }
70
+ return 'binary';
71
+ }
72
+ /**
73
+ * Check if URL requires yt-dlp for download.
74
+ */
75
+ export function requiresYtdlp(url) {
76
+ const urlLower = url.toLowerCase();
77
+ return (urlLower.includes('youtube.com') ||
78
+ urlLower.includes('youtu.be') ||
79
+ urlLower.includes('bilibili.com/video') ||
80
+ urlLower.includes('twitter.com') ||
81
+ urlLower.includes('x.com') ||
82
+ urlLower.includes('tiktok.com') ||
83
+ urlLower.includes('vimeo.com') ||
84
+ urlLower.includes('twitch.tv'));
85
+ }
86
+ /**
87
+ * HTTP download with progress callback.
88
+ */
89
+ export async function httpDownload(url, destPath, options = {}) {
90
+ const { cookies, headers = {}, timeout = 30000, onProgress } = options;
91
+ return new Promise((resolve) => {
92
+ const parsedUrl = new URL(url);
93
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
94
+ const requestHeaders = {
95
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
96
+ ...headers,
97
+ };
98
+ if (cookies) {
99
+ requestHeaders['Cookie'] = cookies;
100
+ }
101
+ // Ensure directory exists
102
+ const dir = path.dirname(destPath);
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ const tempPath = `${destPath}.tmp`;
105
+ const file = fs.createWriteStream(tempPath);
106
+ const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
107
+ // Handle redirects
108
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
109
+ file.close();
110
+ fs.unlinkSync(tempPath);
111
+ httpDownload(response.headers.location, destPath, options).then(resolve);
112
+ return;
113
+ }
114
+ if (response.statusCode !== 200) {
115
+ file.close();
116
+ fs.unlinkSync(tempPath);
117
+ resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
118
+ return;
119
+ }
120
+ const totalSize = parseInt(response.headers['content-length'] || '0', 10);
121
+ let received = 0;
122
+ response.on('data', (chunk) => {
123
+ received += chunk.length;
124
+ if (onProgress)
125
+ onProgress(received, totalSize);
126
+ });
127
+ response.pipe(file);
128
+ file.on('finish', () => {
129
+ file.close();
130
+ // Rename temp file to final destination
131
+ fs.renameSync(tempPath, destPath);
132
+ resolve({ success: true, size: received });
133
+ });
134
+ });
135
+ request.on('error', (err) => {
136
+ file.close();
137
+ if (fs.existsSync(tempPath))
138
+ fs.unlinkSync(tempPath);
139
+ resolve({ success: false, size: 0, error: err.message });
140
+ });
141
+ request.on('timeout', () => {
142
+ request.destroy();
143
+ file.close();
144
+ if (fs.existsSync(tempPath))
145
+ fs.unlinkSync(tempPath);
146
+ resolve({ success: false, size: 0, error: 'Timeout' });
147
+ });
148
+ });
149
+ }
150
+ /**
151
+ * Export cookies to Netscape format for yt-dlp.
152
+ */
153
+ export function exportCookiesToNetscape(cookies, filePath) {
154
+ const lines = [
155
+ '# Netscape HTTP Cookie File',
156
+ '# https://curl.se/docs/http-cookies.html',
157
+ '# This is a generated file! Do not edit.',
158
+ '',
159
+ ];
160
+ for (const cookie of cookies) {
161
+ const domain = cookie.domain.startsWith('.') ? cookie.domain : `.${cookie.domain}`;
162
+ const includeSubdomains = 'TRUE';
163
+ const cookiePath = cookie.path || '/';
164
+ const secure = cookie.secure ? 'TRUE' : 'FALSE';
165
+ const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now
166
+ lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${cookie.name}\t${cookie.value}`);
167
+ }
168
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
169
+ fs.writeFileSync(filePath, lines.join('\n'));
170
+ }
171
+ /**
172
+ * Download video using yt-dlp.
173
+ */
174
+ export async function ytdlpDownload(url, destPath, options = {}) {
175
+ const { cookiesFile, format = 'best', extraArgs = [], onProgress } = options;
176
+ if (!checkYtdlp()) {
177
+ return { success: false, size: 0, error: 'yt-dlp not installed. Install with: pip install yt-dlp' };
178
+ }
179
+ return new Promise((resolve) => {
180
+ const dir = path.dirname(destPath);
181
+ fs.mkdirSync(dir, { recursive: true });
182
+ // Build yt-dlp arguments
183
+ const args = [
184
+ url,
185
+ '-o', destPath,
186
+ '-f', format,
187
+ '--no-playlist',
188
+ '--progress',
189
+ ];
190
+ if (cookiesFile && fs.existsSync(cookiesFile)) {
191
+ args.push('--cookies', cookiesFile);
192
+ }
193
+ else {
194
+ // Try to use browser cookies
195
+ args.push('--cookies-from-browser', 'chrome');
196
+ }
197
+ args.push(...extraArgs);
198
+ const proc = spawn('yt-dlp', args, {
199
+ stdio: ['ignore', 'pipe', 'pipe'],
200
+ });
201
+ let lastPercent = 0;
202
+ let errorOutput = '';
203
+ proc.stderr.on('data', (data) => {
204
+ const line = data.toString();
205
+ errorOutput += line;
206
+ // Parse progress from yt-dlp output
207
+ const match = line.match(/(\d+\.?\d*)%/);
208
+ if (match && onProgress) {
209
+ const percent = parseFloat(match[1]);
210
+ if (percent > lastPercent) {
211
+ lastPercent = percent;
212
+ onProgress(percent);
213
+ }
214
+ }
215
+ });
216
+ proc.stdout.on('data', (data) => {
217
+ const line = data.toString();
218
+ const match = line.match(/(\d+\.?\d*)%/);
219
+ if (match && onProgress) {
220
+ const percent = parseFloat(match[1]);
221
+ if (percent > lastPercent) {
222
+ lastPercent = percent;
223
+ onProgress(percent);
224
+ }
225
+ }
226
+ });
227
+ proc.on('close', (code) => {
228
+ if (code === 0 && fs.existsSync(destPath)) {
229
+ const stats = fs.statSync(destPath);
230
+ resolve({ success: true, size: stats.size });
231
+ }
232
+ else {
233
+ // Check for common yt-dlp output patterns
234
+ const patterns = fs.readdirSync(dir).filter(f => f.startsWith(path.basename(destPath, path.extname(destPath))));
235
+ if (patterns.length > 0) {
236
+ const actualFile = path.join(dir, patterns[0]);
237
+ const stats = fs.statSync(actualFile);
238
+ resolve({ success: true, size: stats.size });
239
+ }
240
+ else {
241
+ resolve({ success: false, size: 0, error: errorOutput.slice(0, 200) || `Exit code ${code}` });
242
+ }
243
+ }
244
+ });
245
+ proc.on('error', (err) => {
246
+ resolve({ success: false, size: 0, error: err.message });
247
+ });
248
+ });
249
+ }
250
+ /**
251
+ * Save document content to file.
252
+ */
253
+ export async function saveDocument(content, destPath, format = 'markdown', metadata) {
254
+ try {
255
+ const dir = path.dirname(destPath);
256
+ fs.mkdirSync(dir, { recursive: true });
257
+ let output;
258
+ if (format === 'json') {
259
+ output = JSON.stringify({ ...metadata, content }, null, 2);
260
+ }
261
+ else if (format === 'markdown') {
262
+ // Add frontmatter if metadata exists
263
+ const frontmatter = metadata ? `---\n${Object.entries(metadata).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n')}\n---\n\n` : '';
264
+ output = frontmatter + content;
265
+ }
266
+ else {
267
+ output = content;
268
+ }
269
+ fs.writeFileSync(destPath, output, 'utf-8');
270
+ return { success: true, size: Buffer.byteLength(output, 'utf-8') };
271
+ }
272
+ catch (err) {
273
+ return { success: false, size: 0, error: err.message };
274
+ }
275
+ }
276
+ /**
277
+ * Sanitize filename by removing invalid characters.
278
+ */
279
+ export function sanitizeFilename(name, maxLength = 200) {
280
+ return name
281
+ .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') // Remove invalid chars
282
+ .replace(/\s+/g, '_') // Replace spaces with underscores
283
+ .replace(/_+/g, '_') // Collapse multiple underscores
284
+ .replace(/^_|_$/g, '') // Trim underscores
285
+ .slice(0, maxLength);
286
+ }
287
+ /**
288
+ * Generate filename from URL if not provided.
289
+ */
290
+ export function generateFilename(url, index, extension) {
291
+ try {
292
+ const parsedUrl = new URL(url);
293
+ const pathname = parsedUrl.pathname;
294
+ const basename = path.basename(pathname);
295
+ if (basename && basename !== '/' && basename.includes('.')) {
296
+ return sanitizeFilename(basename);
297
+ }
298
+ // Generate from hostname and index
299
+ const ext = extension || detectExtension(url);
300
+ const hostname = parsedUrl.hostname.replace(/^www\./, '');
301
+ return sanitizeFilename(`${hostname}_${index + 1}${ext}`);
302
+ }
303
+ catch {
304
+ const ext = extension || '.bin';
305
+ return `download_${index + 1}${ext}`;
306
+ }
307
+ }
308
+ /**
309
+ * Detect file extension from URL.
310
+ */
311
+ function detectExtension(url) {
312
+ const type = detectContentType(url);
313
+ switch (type) {
314
+ case 'image': return '.jpg';
315
+ case 'video': return '.mp4';
316
+ case 'document': return '.md';
317
+ default: return '.bin';
318
+ }
319
+ }
320
+ /**
321
+ * Get temp directory for cookie files.
322
+ */
323
+ export function getTempDir() {
324
+ return path.join(os.tmpdir(), 'opencli-download');
325
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Download progress display: terminal progress bars, status updates.
3
+ */
4
+ export interface ProgressBar {
5
+ update(current: number, total: number, label?: string): void;
6
+ complete(success: boolean, message?: string): void;
7
+ fail(error: string): void;
8
+ }
9
+ /**
10
+ * Format bytes as human-readable string (KB, MB, GB).
11
+ */
12
+ export declare function formatBytes(bytes: number): string;
13
+ /**
14
+ * Format milliseconds as human-readable duration.
15
+ */
16
+ export declare function formatDuration(ms: number): string;
17
+ /**
18
+ * Create a simple progress bar for terminal display.
19
+ */
20
+ export declare function createProgressBar(filename: string, index: number, total: number): ProgressBar;
21
+ /**
22
+ * Multi-file download progress tracker.
23
+ */
24
+ export declare class DownloadProgressTracker {
25
+ private completed;
26
+ private failed;
27
+ private skipped;
28
+ private total;
29
+ private startTime;
30
+ private verbose;
31
+ constructor(total: number, verbose?: boolean);
32
+ onFileStart(filename: string, index: number): ProgressBar | null;
33
+ onFileComplete(success: boolean, skipped?: boolean): void;
34
+ getSummary(): string;
35
+ finish(): void;
36
+ }