@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -1,13 +1,18 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { fetchDaemonStatusMock, requestDaemonShutdownMock, } = vi.hoisted(() => ({
2
+ const { fetchDaemonStatusMock, requestDaemonShutdownMock, restartDaemonMock, } = vi.hoisted(() => ({
3
3
  fetchDaemonStatusMock: vi.fn(),
4
4
  requestDaemonShutdownMock: vi.fn(),
5
+ restartDaemonMock: vi.fn(),
5
6
  }));
6
7
  vi.mock('../browser/daemon-client.js', () => ({
7
8
  fetchDaemonStatus: fetchDaemonStatusMock,
8
9
  requestDaemonShutdown: requestDaemonShutdownMock,
9
10
  }));
10
- import { daemonStatus, daemonStop } from './daemon.js';
11
+ vi.mock('../browser/daemon-lifecycle.js', () => ({
12
+ restartDaemon: restartDaemonMock,
13
+ }));
14
+ import { daemonRestart, daemonStatus, daemonStop } from './daemon.js';
15
+ import { PKG_VERSION } from '../version.js';
11
16
  describe('daemonStatus', () => {
12
17
  let stdoutSpy;
13
18
  beforeEach(() => {
@@ -28,6 +33,7 @@ describe('daemonStatus', () => {
28
33
  ok: true,
29
34
  pid: 12345,
30
35
  uptime: 3661,
36
+ daemonVersion: PKG_VERSION,
31
37
  extensionConnected: true,
32
38
  extensionVersion: '1.6.8',
33
39
  pending: 0,
@@ -37,6 +43,7 @@ describe('daemonStatus', () => {
37
43
  await daemonStatus();
38
44
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
39
45
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
46
+ expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining(`v${PKG_VERSION}`));
40
47
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
41
48
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
42
49
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
@@ -48,6 +55,7 @@ describe('daemonStatus', () => {
48
55
  ok: true,
49
56
  pid: 99,
50
57
  uptime: 120,
58
+ daemonVersion: PKG_VERSION,
51
59
  extensionConnected: false,
52
60
  pending: 0,
53
61
  memoryMB: 32,
@@ -61,6 +69,7 @@ describe('daemonStatus', () => {
61
69
  ok: true,
62
70
  pid: 99,
63
71
  uptime: 120,
72
+ daemonVersion: PKG_VERSION,
64
73
  extensionConnected: true,
65
74
  extensionVersion: undefined,
66
75
  pending: 0,
@@ -91,6 +100,7 @@ describe('daemonStop', () => {
91
100
  ok: true,
92
101
  pid: 12345,
93
102
  uptime: 100,
103
+ daemonVersion: PKG_VERSION,
94
104
  extensionConnected: true,
95
105
  pending: 0,
96
106
  memoryMB: 50,
@@ -106,6 +116,7 @@ describe('daemonStop', () => {
106
116
  ok: true,
107
117
  pid: 12345,
108
118
  uptime: 100,
119
+ daemonVersion: PKG_VERSION,
109
120
  extensionConnected: true,
110
121
  pending: 0,
111
122
  memoryMB: 50,
@@ -116,3 +127,93 @@ describe('daemonStop', () => {
116
127
  expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
117
128
  });
118
129
  });
130
+ describe('daemonRestart', () => {
131
+ let stderrSpy;
132
+ beforeEach(() => {
133
+ stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
134
+ fetchDaemonStatusMock.mockReset();
135
+ requestDaemonShutdownMock.mockReset();
136
+ restartDaemonMock.mockReset();
137
+ process.exitCode = undefined;
138
+ });
139
+ afterEach(() => {
140
+ vi.restoreAllMocks();
141
+ process.exitCode = undefined;
142
+ });
143
+ it('restarts a running daemon and reports the new version', async () => {
144
+ fetchDaemonStatusMock.mockResolvedValue({
145
+ ok: true,
146
+ pid: 12345,
147
+ uptime: 100,
148
+ daemonVersion: '1.7.6',
149
+ extensionConnected: true,
150
+ profiles: [{ contextId: 'work', extensionConnected: true, pending: 0 }],
151
+ pending: 0,
152
+ memoryMB: 50,
153
+ port: 19825,
154
+ });
155
+ restartDaemonMock.mockResolvedValue({
156
+ previousStatus: { daemonVersion: '1.7.6' },
157
+ stopped: true,
158
+ spawned: true,
159
+ status: {
160
+ ok: true,
161
+ pid: 12346,
162
+ uptime: 1,
163
+ daemonVersion: PKG_VERSION,
164
+ extensionConnected: true,
165
+ profiles: [{ contextId: 'work', extensionConnected: true, pending: 0 }],
166
+ pending: 0,
167
+ memoryMB: 51,
168
+ port: 19825,
169
+ },
170
+ });
171
+ await daemonRestart();
172
+ expect(restartDaemonMock).toHaveBeenCalledTimes(1);
173
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('will disconnect 1 browser profile'));
174
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 19825 (v${PKG_VERSION})`));
175
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Extension connected; profiles connected: 1'));
176
+ });
177
+ it('starts a new daemon when none was running', async () => {
178
+ fetchDaemonStatusMock.mockResolvedValue(null);
179
+ restartDaemonMock.mockResolvedValue({
180
+ previousStatus: null,
181
+ stopped: true,
182
+ spawned: true,
183
+ status: {
184
+ ok: true,
185
+ pid: 12346,
186
+ uptime: 1,
187
+ daemonVersion: PKG_VERSION,
188
+ extensionConnected: false,
189
+ pending: 0,
190
+ memoryMB: 51,
191
+ port: 19825,
192
+ },
193
+ });
194
+ await daemonRestart();
195
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 19825 (v${PKG_VERSION})`));
196
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('extension has not connected yet'));
197
+ });
198
+ it('reports failure when the daemon cannot stop', async () => {
199
+ fetchDaemonStatusMock.mockResolvedValue({
200
+ ok: true,
201
+ pid: 12345,
202
+ uptime: 100,
203
+ daemonVersion: '1.7.6',
204
+ extensionConnected: true,
205
+ pending: 0,
206
+ memoryMB: 50,
207
+ port: 19825,
208
+ });
209
+ restartDaemonMock.mockResolvedValue({
210
+ previousStatus: { daemonVersion: '1.7.6' },
211
+ status: { daemonVersion: '1.7.6' },
212
+ stopped: false,
213
+ spawned: false,
214
+ });
215
+ await daemonRestart();
216
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon before restart'));
217
+ expect(process.exitCode).toBe(1);
218
+ });
219
+ });
@@ -15,8 +15,7 @@ export const BUILTIN_COMMANDS = [
15
15
  'tab',
16
16
  'doctor',
17
17
  'plugin',
18
- 'install',
19
- 'register',
18
+ 'external',
20
19
  'completion',
21
20
  ];
22
21
  // ── Shell script generators ────────────────────────────────────────────────
@@ -12,8 +12,9 @@ describe('getCompletions', () => {
12
12
  it('includes top-level built-ins that are registered outside the site registry', () => {
13
13
  const completions = getCompletions([], 1);
14
14
  expect(completions).toContain('plugin');
15
- expect(completions).toContain('install');
16
- expect(completions).toContain('register');
15
+ expect(completions).toContain('external');
16
+ expect(completions).not.toContain('install');
17
+ expect(completions).not.toContain('register');
17
18
  expect(completions).not.toContain('setup');
18
19
  });
19
20
  it('still includes discovered site names', () => {
@@ -25,19 +25,96 @@ import { DEFAULT_DAEMON_PORT } from './constants.js';
25
25
  import { EXIT_CODES } from './errors.js';
26
26
  import { log } from './logger.js';
27
27
  import { PKG_VERSION } from './version.js';
28
+ import { DEFAULT_CONTEXT_ID } from './browser/profile.js';
29
+ import { recordExtensionVersion } from './update-check.js';
28
30
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
29
- // ─── State ───────────────────────────────────────────────────────────
30
- let extensionWs = null;
31
- let extensionVersion = null;
32
- let extensionCompatRange = null;
31
+ const extensionProfiles = new Map();
33
32
  const pending = new Map();
34
33
  const LOG_BUFFER_SIZE = 200;
35
34
  const logBuffer = [];
35
+ class DaemonCommandFailure extends Error {
36
+ errorCode;
37
+ errorHint;
38
+ status;
39
+ constructor(message, errorCode, errorHint, status = 400) {
40
+ super(message);
41
+ this.errorCode = errorCode;
42
+ this.errorHint = errorHint;
43
+ this.status = status;
44
+ this.name = 'DaemonCommandFailure';
45
+ }
46
+ }
36
47
  function pushLog(entry) {
37
48
  logBuffer.push(entry);
38
49
  if (logBuffer.length > LOG_BUFFER_SIZE)
39
50
  logBuffer.shift();
40
51
  }
52
+ function activeProfiles() {
53
+ return [...extensionProfiles.values()].filter((entry) => entry.ws.readyState === WebSocket.OPEN);
54
+ }
55
+ function resolveExtensionConnection(contextId) {
56
+ const requestedContextId = typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined;
57
+ if (requestedContextId) {
58
+ const connection = extensionProfiles.get(requestedContextId);
59
+ if (connection?.ws.readyState === WebSocket.OPEN)
60
+ return { connection };
61
+ return {
62
+ errorCode: 'profile_disconnected',
63
+ error: `Browser profile "${requestedContextId}" is not connected.`,
64
+ errorHint: 'Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.',
65
+ };
66
+ }
67
+ const connected = activeProfiles();
68
+ if (connected.length === 1)
69
+ return { connection: connected[0] };
70
+ if (connected.length > 1) {
71
+ return {
72
+ errorCode: 'profile_required',
73
+ error: 'Multiple Browser Bridge profiles are connected; choose one with --profile.',
74
+ errorHint: 'Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>.',
75
+ };
76
+ }
77
+ return {
78
+ errorCode: 'extension_not_connected',
79
+ error: 'Extension not connected. Please install the opencli Browser Bridge extension.',
80
+ };
81
+ }
82
+ function registerExtensionConnection(ws, rawContextId) {
83
+ const contextId = typeof rawContextId === 'string' && rawContextId.trim()
84
+ ? rawContextId.trim()
85
+ : DEFAULT_CONTEXT_ID;
86
+ const previous = extensionProfiles.get(contextId);
87
+ if (previous && previous.ws !== ws) {
88
+ previous.ws.close();
89
+ }
90
+ const existing = [...extensionProfiles.entries()].find(([, entry]) => entry.ws === ws);
91
+ if (existing && existing[0] !== contextId)
92
+ extensionProfiles.delete(existing[0]);
93
+ const current = extensionProfiles.get(contextId);
94
+ const connection = {
95
+ contextId,
96
+ ws,
97
+ extensionVersion: current?.ws === ws ? current.extensionVersion : null,
98
+ extensionCompatRange: current?.ws === ws ? current.extensionCompatRange : null,
99
+ lastSeenAt: Date.now(),
100
+ };
101
+ extensionProfiles.set(contextId, connection);
102
+ return connection;
103
+ }
104
+ function unregisterExtensionConnection(ws) {
105
+ for (const [contextId, connection] of extensionProfiles.entries()) {
106
+ if (connection.ws !== ws)
107
+ continue;
108
+ extensionProfiles.delete(contextId);
109
+ for (const [id, p] of pending) {
110
+ if (p.contextId !== contextId)
111
+ continue;
112
+ clearTimeout(p.timer);
113
+ p.reject(new DaemonCommandFailure(`Browser profile "${contextId}" disconnected`, 'profile_disconnected', 'Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 503));
114
+ pending.delete(id);
115
+ }
116
+ }
117
+ }
41
118
  // ─── HTTP Server ─────────────────────────────────────────────────────
42
119
  const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
43
120
  function readBody(req) {
@@ -118,14 +195,29 @@ async function handleRequest(req, res) {
118
195
  if (req.method === 'GET' && pathname === '/status') {
119
196
  const uptime = process.uptime();
120
197
  const mem = process.memoryUsage();
198
+ const params = new URL(url, `http://localhost:${PORT}`).searchParams;
199
+ const requestedContextId = params.get('contextId')?.trim() || undefined;
200
+ const route = resolveExtensionConnection(requestedContextId);
201
+ const profiles = activeProfiles().map((profile) => ({
202
+ contextId: profile.contextId,
203
+ extensionConnected: true,
204
+ extensionVersion: profile.extensionVersion ?? undefined,
205
+ extensionCompatRange: profile.extensionCompatRange ?? undefined,
206
+ pending: [...pending.values()].filter((entry) => entry.contextId === profile.contextId).length,
207
+ lastSeenAt: profile.lastSeenAt,
208
+ }));
121
209
  jsonResponse(res, 200, {
122
210
  ok: true,
123
211
  pid: process.pid,
124
212
  uptime,
125
213
  daemonVersion: PKG_VERSION,
126
- extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
127
- extensionVersion,
128
- extensionCompatRange,
214
+ extensionConnected: !!route.connection,
215
+ extensionVersion: route.connection?.extensionVersion ?? undefined,
216
+ extensionCompatRange: route.connection?.extensionCompatRange ?? undefined,
217
+ contextId: route.connection?.contextId ?? requestedContextId,
218
+ profileRequired: route.errorCode === 'profile_required',
219
+ profileDisconnected: route.errorCode === 'profile_disconnected',
220
+ profiles,
129
221
  pending: pending.size,
130
222
  memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
131
223
  port: PORT,
@@ -158,8 +250,15 @@ async function handleRequest(req, res) {
158
250
  jsonResponse(res, 400, { ok: false, error: 'Missing command id' });
159
251
  return;
160
252
  }
161
- if (!extensionWs || extensionWs.readyState !== WebSocket.OPEN) {
162
- jsonResponse(res, 503, { id: body.id, ok: false, error: 'Extension not connected. Please install the opencli Browser Bridge extension.' });
253
+ const route = resolveExtensionConnection(typeof body.contextId === 'string' ? body.contextId : undefined);
254
+ if (!route.connection) {
255
+ jsonResponse(res, route.errorCode === 'profile_required' ? 409 : 503, {
256
+ id: body.id,
257
+ ok: false,
258
+ errorCode: route.errorCode,
259
+ error: route.error,
260
+ ...(route.errorHint ? { errorHint: route.errorHint } : {}),
261
+ });
163
262
  return;
164
263
  }
165
264
  const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
@@ -178,15 +277,18 @@ async function handleRequest(req, res) {
178
277
  pending.delete(body.id);
179
278
  reject(new Error(`Command timeout (${timeoutMs / 1000}s)`));
180
279
  }, timeoutMs);
181
- pending.set(body.id, { resolve, reject, timer });
182
- extensionWs.send(JSON.stringify(body));
280
+ pending.set(body.id, { contextId: route.connection.contextId, resolve, reject, timer });
281
+ route.connection.ws.send(JSON.stringify(body));
183
282
  });
184
283
  jsonResponse(res, 200, result);
185
284
  }
186
285
  catch (err) {
187
- jsonResponse(res, err instanceof Error && err.message.includes('timeout') ? 408 : 400, {
286
+ const commandFailure = err instanceof DaemonCommandFailure ? err : null;
287
+ jsonResponse(res, commandFailure?.status ?? (err instanceof Error && err.message.includes('timeout') ? 408 : 400), {
188
288
  ok: false,
189
289
  error: err instanceof Error ? err.message : 'Invalid request',
290
+ ...(commandFailure?.errorCode ? { errorCode: commandFailure.errorCode } : {}),
291
+ ...(commandFailure?.errorHint ? { errorHint: commandFailure.errorHint } : {}),
190
292
  });
191
293
  }
192
294
  return;
@@ -209,9 +311,6 @@ const wss = new WebSocketServer({
209
311
  });
210
312
  wss.on('connection', (ws) => {
211
313
  log.info('[daemon] Extension connected');
212
- extensionWs = ws;
213
- extensionVersion = null; // cleared until hello message arrives
214
- extensionCompatRange = null;
215
314
  // ── Heartbeat: ping every 15s, close if 2 pongs missed ──
216
315
  let missedPongs = 0;
217
316
  const heartbeatInterval = setInterval(() => {
@@ -236,8 +335,13 @@ wss.on('connection', (ws) => {
236
335
  const msg = JSON.parse(data.toString());
237
336
  // Handle hello message from extension (version handshake)
238
337
  if (msg.type === 'hello') {
239
- extensionVersion = typeof msg.version === 'string' ? msg.version : null;
240
- extensionCompatRange = typeof msg.compatRange === 'string' ? msg.compatRange : null;
338
+ const connection = registerExtensionConnection(ws, msg.contextId);
339
+ connection.extensionVersion = typeof msg.version === 'string' ? msg.version : null;
340
+ connection.extensionCompatRange = typeof msg.compatRange === 'string' ? msg.compatRange : null;
341
+ connection.lastSeenAt = Date.now();
342
+ if (connection.extensionVersion)
343
+ recordExtensionVersion(connection.extensionVersion);
344
+ log.info(`[daemon] Extension profile connected: ${connection.contextId}`);
241
345
  return;
242
346
  }
243
347
  // Handle log messages from extension
@@ -266,31 +370,11 @@ wss.on('connection', (ws) => {
266
370
  ws.on('close', () => {
267
371
  log.info('[daemon] Extension disconnected');
268
372
  clearInterval(heartbeatInterval);
269
- if (extensionWs === ws) {
270
- extensionWs = null;
271
- extensionVersion = null;
272
- extensionCompatRange = null;
273
- // Reject all pending requests since the extension is gone
274
- for (const [id, p] of pending) {
275
- clearTimeout(p.timer);
276
- p.reject(new Error('Extension disconnected'));
277
- }
278
- pending.clear();
279
- }
373
+ unregisterExtensionConnection(ws);
280
374
  });
281
375
  ws.on('error', () => {
282
376
  clearInterval(heartbeatInterval);
283
- if (extensionWs === ws) {
284
- extensionWs = null;
285
- extensionVersion = null;
286
- extensionCompatRange = null;
287
- // Reject pending requests in case 'close' does not follow this 'error'
288
- for (const [, p] of pending) {
289
- clearTimeout(p.timer);
290
- p.reject(new Error('Extension disconnected'));
291
- }
292
- pending.clear();
293
- }
377
+ unregisterExtensionConnection(ws);
294
378
  });
295
379
  });
296
380
  // ─── Start ───────────────────────────────────────────────────────────
@@ -313,8 +397,8 @@ function shutdown() {
313
397
  p.reject(new Error('Daemon shutting down'));
314
398
  }
315
399
  pending.clear();
316
- if (extensionWs)
317
- extensionWs.close();
400
+ for (const profile of extensionProfiles.values())
401
+ profile.ws.close();
318
402
  httpServer.close();
319
403
  process.exit(EXIT_CODES.SUCCESS);
320
404
  }
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Simplified for the daemon-based architecture.
5
5
  */
6
+ import type { BrowserSessionInfo } from './types.js';
7
+ import type { BrowserProfileStatus } from './browser/daemon-client.js';
6
8
  export type DoctorOptions = {
7
9
  yes?: boolean;
8
10
  live?: boolean;
@@ -18,18 +20,15 @@ export type DoctorReport = {
18
20
  cliVersion?: string;
19
21
  daemonRunning: boolean;
20
22
  daemonFlaky?: boolean;
23
+ daemonStale?: boolean;
21
24
  daemonVersion?: string;
22
25
  extensionConnected: boolean;
23
26
  extensionFlaky?: boolean;
24
27
  extensionVersion?: string;
25
28
  latestExtensionVersion?: string;
26
29
  connectivity?: ConnectivityResult;
27
- sessions?: Array<{
28
- workspace: string;
29
- windowId: number;
30
- tabCount: number;
31
- idleMsRemaining: number;
32
- }>;
30
+ sessions?: BrowserSessionInfo[];
31
+ profiles?: BrowserProfileStatus[];
33
32
  issues: string[];
34
33
  };
35
34
  /**
@@ -10,6 +10,8 @@ import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
10
10
  import { getErrorMessage } from './errors.js';
11
11
  import { getRuntimeLabel } from './runtime-detect.js';
12
12
  import { getCachedLatestExtensionVersion } from './update-check.js';
13
+ import { aliasForContextId, loadProfileConfig } from './browser/profile.js';
14
+ import { formatDaemonVersion, isDaemonStale, staleDaemonIssue } from './browser/daemon-version.js';
13
15
  const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
14
16
  /** Parse a semver string into [major, minor, patch]. Returns null on invalid input. */
15
17
  function parseSemver(v) {
@@ -48,9 +50,14 @@ export async function checkConnectivity(opts) {
48
50
  try {
49
51
  const bridge = new BrowserBridge();
50
52
  const page = await bridge.connect({ timeout: opts?.timeout ?? DOCTOR_LIVE_TIMEOUT_SECONDS });
51
- // Try a simple eval to verify end-to-end connectivity
52
- await page.evaluate('1 + 1');
53
- await bridge.close();
53
+ try {
54
+ // Try a simple eval to verify end-to-end connectivity.
55
+ await page.evaluate('1 + 1');
56
+ await page.closeWindow?.();
57
+ }
58
+ finally {
59
+ await bridge.close();
60
+ }
54
61
  return { ok: true, durationMs: Date.now() - start };
55
62
  }
56
63
  catch (err) {
@@ -84,9 +91,21 @@ export async function runBrowserDoctor(opts = {}) {
84
91
  const extensionConnected = health.state === 'ready';
85
92
  const daemonFlaky = !!(connectivity?.ok && !daemonRunning);
86
93
  const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected);
87
- const sessions = opts.sessions && health.state === 'ready'
88
- ? await listSessions()
89
- : undefined;
94
+ const daemonStale = isDaemonStale(health.status, opts.cliVersion);
95
+ const profiles = health.status?.profiles;
96
+ let sessions;
97
+ if (opts.sessions) {
98
+ if (profiles && profiles.length > 0) {
99
+ const grouped = await Promise.all(profiles.map(async (profile) => {
100
+ const rows = await listSessions({ contextId: profile.contextId }).catch(() => []);
101
+ return rows.map((row) => ({ ...row, contextId: row.contextId ?? profile.contextId }));
102
+ }));
103
+ sessions = grouped.flat();
104
+ }
105
+ else if (health.state === 'ready') {
106
+ sessions = await listSessions();
107
+ }
108
+ }
90
109
  const extensionVersion = health.status?.extensionVersion;
91
110
  const issues = [];
92
111
  if (daemonFlaky) {
@@ -96,24 +115,25 @@ export async function runBrowserDoctor(opts = {}) {
96
115
  else if (!daemonRunning) {
97
116
  issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
98
117
  }
118
+ if (daemonStale && opts.cliVersion) {
119
+ issues.push(staleDaemonIssue(health.status, opts.cliVersion));
120
+ }
99
121
  if (extensionFlaky) {
100
122
  issues.push('Extension connection is unstable. The live browser test succeeded, but the daemon reported the extension disconnected immediately afterward.\n' +
101
123
  'This usually means the Browser Bridge service worker is reconnecting slowly or Chrome suspended it.');
102
124
  }
103
125
  else if (daemonRunning && !extensionConnected) {
104
- const daemonVersion = health.status?.daemonVersion;
105
- const isStale = opts.cliVersion && (!daemonVersion || daemonVersion !== opts.cliVersion);
106
- if (isStale) {
107
- const reason = daemonVersion
108
- ? `daemon v${daemonVersion} CLI v${opts.cliVersion}`
109
- : `daemon predates version reporting, CLI is v${opts.cliVersion}`;
110
- issues.push(`Stale daemon detected: ${reason}.\n` +
111
- 'The daemon was started by an older CLI version and may have missed the extension registration.\n' +
112
- ' Quick fix: opencli daemon stop && opencli doctor');
126
+ if (health.state === 'profile-required') {
127
+ issues.push('Multiple Chrome profiles are connected to the daemon, but no default profile was selected.\n' +
128
+ ' Run opencli profile list, then opencli profile use <name>, or pass --profile <name>.');
129
+ }
130
+ else if (health.state === 'profile-disconnected') {
131
+ issues.push(`Selected browser profile is not connected: ${health.status?.contextId ?? 'unknown'}.\n` +
132
+ ' Open that Chrome profile and make sure the OpenCLI extension is enabled.');
113
133
  }
114
134
  else {
115
135
  issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
116
- 'If the extension is already installed, try: opencli daemon stop && opencli doctor\n' +
136
+ 'If the extension is already installed, try: opencli daemon restart\n' +
117
137
  'If the extension is not installed:\n' +
118
138
  ' 1. Download from https://github.com/jackwener/opencli/releases\n' +
119
139
  ' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
@@ -155,6 +175,7 @@ export async function runBrowserDoctor(opts = {}) {
155
175
  cliVersion: opts.cliVersion,
156
176
  daemonRunning,
157
177
  daemonFlaky,
178
+ daemonStale,
158
179
  daemonVersion: health.status?.daemonVersion,
159
180
  extensionConnected,
160
181
  extensionFlaky,
@@ -162,6 +183,7 @@ export async function runBrowserDoctor(opts = {}) {
162
183
  latestExtensionVersion,
163
184
  connectivity,
164
185
  sessions,
186
+ profiles,
165
187
  issues,
166
188
  };
167
189
  }
@@ -170,10 +192,16 @@ export function renderBrowserDoctorReport(report) {
170
192
  // Daemon status
171
193
  const daemonIcon = report.daemonFlaky
172
194
  ? styleText('yellow', '[WARN]')
173
- : report.daemonRunning ? styleText('green', '[OK]') : styleText('red', '[MISSING]');
195
+ : report.daemonStale
196
+ ? styleText('yellow', '[WARN]')
197
+ : report.daemonRunning ? styleText('green', '[OK]') : styleText('red', '[MISSING]');
174
198
  const daemonLabel = report.daemonFlaky
175
199
  ? 'unstable (running during live check, then stopped)'
176
- : report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` + (report.daemonVersion ? ` (v${report.daemonVersion})` : '') : 'not running';
200
+ : report.daemonRunning
201
+ ? `running on port ${DEFAULT_DAEMON_PORT} (${report.daemonStale
202
+ ? `${formatDaemonVersion(report)}, stale; CLI v${report.cliVersion ?? 'unknown'}`
203
+ : formatDaemonVersion(report)})`
204
+ : 'not running';
177
205
  lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);
178
206
  // Extension status
179
207
  const extIcon = report.extensionFlaky || (report.extensionConnected && !report.extensionVersion)
@@ -191,6 +219,17 @@ export function renderBrowserDoctorReport(report) {
191
219
  ? 'unstable (connected during live check, then disconnected)'
192
220
  : report.extensionConnected ? 'connected' : 'not connected';
193
221
  lines.push(`${extIcon} Extension: ${extLabel}${extVersion}`);
222
+ if (report.profiles && report.profiles.length > 0) {
223
+ const config = loadProfileConfig();
224
+ lines.push('', styleText('bold', 'Profiles:'));
225
+ for (const profile of report.profiles) {
226
+ const alias = aliasForContextId(config, profile.contextId);
227
+ const aliasText = alias ? ` (${alias})` : '';
228
+ const defaultText = config.defaultContextId === profile.contextId ? ', default' : '';
229
+ const version = profile.extensionVersion ? `v${profile.extensionVersion}` : 'version unknown';
230
+ lines.push(styleText('dim', ` • ${profile.contextId}${aliasText}: connected ${version}${defaultText}`));
231
+ }
232
+ }
194
233
  // Connectivity
195
234
  if (report.connectivity) {
196
235
  const connIcon = report.connectivity.ok ? styleText('green', '[OK]') : styleText('red', '[FAIL]');
@@ -208,8 +247,27 @@ export function renderBrowserDoctorReport(report) {
208
247
  lines.push(styleText('dim', ' • no active automation sessions'));
209
248
  }
210
249
  else {
250
+ const byContext = new Map();
211
251
  for (const session of report.sessions) {
212
- lines.push(styleText('dim', ` • ${session.workspace} window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
252
+ const contextId = typeof session.contextId === 'string' && session.contextId ? session.contextId : 'default';
253
+ const rows = byContext.get(contextId) ?? [];
254
+ rows.push(session);
255
+ byContext.set(contextId, rows);
256
+ }
257
+ for (const [contextId, rows] of byContext) {
258
+ if (byContext.size > 1)
259
+ lines.push(styleText('dim', ` [profile: ${contextId}]`));
260
+ for (const session of rows) {
261
+ const idle = session.idleMsRemaining == null
262
+ ? 'none'
263
+ : `${Math.ceil(session.idleMsRemaining / 1000)}s`;
264
+ const target = session.preferredTabId != null
265
+ ? `tab ${session.preferredTabId}`
266
+ : `window ${session.windowId ?? 'unknown'}`;
267
+ const mode = session.ownership ?? (session.owned === false ? 'borrowed' : 'owned');
268
+ const surface = session.surface ? `, surface=${session.surface}` : '';
269
+ lines.push(styleText('dim', ` • ${session.workspace ?? 'default'} → ${target}, mode=${mode}${surface}, tabs=${session.tabCount ?? 0}, idle=${idle}`));
270
+ }
213
271
  }
214
272
  }
215
273
  }