@jackwener/opencli 1.7.7 → 1.7.9

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 (280) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +782 -55
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/amazon/discussion.js +37 -6
  6. package/clis/amazon/discussion.test.js +147 -32
  7. package/clis/apple-podcasts/commands.test.js +4 -4
  8. package/clis/apple-podcasts/episodes.js +1 -1
  9. package/clis/apple-podcasts/search.js +1 -1
  10. package/clis/apple-podcasts/top.js +1 -1
  11. package/clis/arxiv/paper.js +1 -1
  12. package/clis/arxiv/search.js +1 -1
  13. package/clis/band/mentions.js +3 -3
  14. package/clis/bbc/news.js +1 -1
  15. package/clis/bilibili/subtitle.js +2 -2
  16. package/clis/bloomberg/businessweek.js +1 -1
  17. package/clis/bloomberg/economics.js +1 -1
  18. package/clis/bloomberg/industries.js +1 -1
  19. package/clis/bloomberg/main.js +1 -1
  20. package/clis/bloomberg/markets.js +1 -1
  21. package/clis/bloomberg/opinions.js +1 -1
  22. package/clis/bloomberg/politics.js +1 -1
  23. package/clis/bloomberg/tech.js +1 -1
  24. package/clis/boss/search.js +49 -8
  25. package/clis/boss/search.test.js +78 -0
  26. package/clis/boss/send.js +3 -3
  27. package/clis/chatgpt/image.js +37 -8
  28. package/clis/chatgpt/image.test.js +92 -0
  29. package/clis/chatgpt/utils.js +39 -6
  30. package/clis/chatgpt/utils.test.js +63 -0
  31. package/clis/chatgpt-app/ask.js +4 -20
  32. package/clis/chatgpt-app/ax.js +135 -2
  33. package/clis/chatgpt-app/ax.test.js +35 -0
  34. package/clis/chatgpt-app/model.js +1 -1
  35. package/clis/chatgpt-app/new.js +1 -1
  36. package/clis/chatgpt-app/read.js +1 -1
  37. package/clis/chatgpt-app/send.js +3 -22
  38. package/clis/chatgpt-app/status.js +1 -1
  39. package/clis/chatwise/ask.js +2 -2
  40. package/clis/chatwise/model.js +2 -2
  41. package/clis/chatwise/send.js +2 -2
  42. package/clis/claude/ask.js +128 -0
  43. package/clis/claude/ask.test.js +338 -0
  44. package/clis/claude/commands.test.js +118 -0
  45. package/clis/claude/detail.js +29 -0
  46. package/clis/claude/history.js +31 -0
  47. package/clis/claude/new.js +21 -0
  48. package/clis/claude/read.js +24 -0
  49. package/clis/claude/send.js +41 -0
  50. package/clis/claude/status.js +24 -0
  51. package/clis/claude/utils.js +440 -0
  52. package/clis/claude/utils.test.js +148 -0
  53. package/clis/codex/ask.js +2 -2
  54. package/clis/codex/send.js +2 -2
  55. package/clis/ctrip/search.js +1 -1
  56. package/clis/ctrip/search.test.js +4 -4
  57. package/clis/cursor/ask.js +2 -2
  58. package/clis/cursor/composer.js +2 -2
  59. package/clis/cursor/send.js +2 -2
  60. package/clis/deepseek/ask.js +49 -10
  61. package/clis/deepseek/ask.test.js +150 -3
  62. package/clis/deepseek/utils.js +60 -22
  63. package/clis/deepseek/utils.test.js +124 -5
  64. package/clis/doubao/utils.js +53 -11
  65. package/clis/doubao/utils.test.js +22 -2
  66. package/clis/eastmoney/announcement.js +1 -1
  67. package/clis/eastmoney/convertible.js +1 -1
  68. package/clis/eastmoney/etf.js +1 -1
  69. package/clis/eastmoney/holders.js +1 -1
  70. package/clis/eastmoney/index-board.js +1 -1
  71. package/clis/eastmoney/kline.js +1 -1
  72. package/clis/eastmoney/kuaixun.js +1 -1
  73. package/clis/eastmoney/longhu.js +1 -1
  74. package/clis/eastmoney/money-flow.js +1 -1
  75. package/clis/eastmoney/northbound.js +1 -1
  76. package/clis/eastmoney/quote.js +1 -1
  77. package/clis/eastmoney/rank.js +1 -1
  78. package/clis/eastmoney/sectors.js +1 -1
  79. package/clis/facebook/marketplace-inbox.js +83 -0
  80. package/clis/facebook/marketplace-listings.js +83 -0
  81. package/clis/facebook/marketplace.test.js +91 -0
  82. package/clis/google/news.js +1 -1
  83. package/clis/google/suggest.js +1 -1
  84. package/clis/google/trends.js +1 -1
  85. package/clis/google-scholar/cite.js +74 -0
  86. package/clis/google-scholar/cite.test.js +47 -0
  87. package/clis/google-scholar/profile.js +92 -0
  88. package/clis/google-scholar/profile.test.js +49 -0
  89. package/clis/google-scholar/search.js +1 -1
  90. package/clis/google-scholar/search.test.js +15 -0
  91. package/clis/hf/top.js +1 -1
  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/powerchina/search.js +250 -0
  115. package/clis/powerchina/search.test.js +67 -0
  116. package/clis/producthunt/posts.js +1 -1
  117. package/clis/producthunt/today.js +1 -1
  118. package/clis/sinablog/search.js +1 -1
  119. package/clis/sinafinance/news.js +1 -1
  120. package/clis/sinafinance/stock.js +6 -3
  121. package/clis/sinafinance/stock.test.js +59 -0
  122. package/clis/spotify/spotify.js +6 -6
  123. package/clis/substack/search.js +1 -1
  124. package/clis/toutiao/articles.js +80 -0
  125. package/clis/toutiao/articles.test.js +30 -0
  126. package/clis/twitter/followers.js +2 -2
  127. package/clis/twitter/following.js +224 -73
  128. package/clis/twitter/following.test.js +277 -0
  129. package/clis/twitter/post.js +184 -47
  130. package/clis/twitter/post.test.js +114 -34
  131. package/clis/uiverse/_shared.js +63 -4
  132. package/clis/uiverse/_shared.test.js +7 -0
  133. package/clis/uiverse/code.js +1 -0
  134. package/clis/uiverse/navigation.test.js +12 -0
  135. package/clis/uiverse/preview.js +1 -0
  136. package/clis/web/read.js +319 -81
  137. package/clis/web/read.test.js +221 -5
  138. package/clis/weibo/favorites.js +169 -0
  139. package/clis/weibo/favorites.test.js +114 -0
  140. package/clis/weibo/publish.js +282 -0
  141. package/clis/weibo/publish.test.js +183 -0
  142. package/clis/weixin/create-draft.js +225 -0
  143. package/clis/weixin/drafts.js +65 -0
  144. package/clis/weixin/drafts.test.js +65 -0
  145. package/clis/weread/ranking.js +1 -1
  146. package/clis/weread/search-regression.test.js +8 -8
  147. package/clis/weread/search.js +1 -1
  148. package/clis/wikipedia/random.js +1 -1
  149. package/clis/wikipedia/search.js +1 -1
  150. package/clis/wikipedia/summary.js +1 -1
  151. package/clis/wikipedia/trending.js +1 -1
  152. package/clis/xianyu/chat.js +3 -3
  153. package/clis/xianyu/item.js +2 -2
  154. package/clis/xianyu/item.test.js +3 -3
  155. package/clis/xiaohongshu/search.js +17 -2
  156. package/clis/xiaohongshu/search.test.js +37 -1
  157. package/clis/xiaoyuzhou/download.js +1 -1
  158. package/clis/xiaoyuzhou/download.test.js +3 -3
  159. package/clis/xiaoyuzhou/episode.js +1 -1
  160. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  161. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  162. package/clis/xiaoyuzhou/podcast.js +1 -1
  163. package/clis/xiaoyuzhou/transcript.js +1 -1
  164. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  165. package/clis/yollomi/models.js +1 -1
  166. package/clis/youtube/channel.js +24 -1
  167. package/clis/youtube/channel.test.js +59 -0
  168. package/clis/zhihu/answer.js +21 -162
  169. package/clis/zhihu/answer.test.js +26 -53
  170. package/clis/zhihu/collection.js +197 -0
  171. package/clis/zhihu/collection.test.js +290 -0
  172. package/clis/zhihu/collections.js +127 -0
  173. package/clis/zhihu/collections.test.js +182 -0
  174. package/clis/zhihu/comment.js +24 -305
  175. package/clis/zhihu/comment.test.js +31 -35
  176. package/clis/zhihu/favorite.js +44 -182
  177. package/clis/zhihu/favorite.test.js +30 -167
  178. package/clis/zhihu/follow.js +25 -56
  179. package/clis/zhihu/follow.test.js +20 -23
  180. package/clis/zhihu/like.js +22 -67
  181. package/clis/zhihu/like.test.js +19 -42
  182. package/clis/zhihu/search.js +3 -2
  183. package/clis/zhihu/write-shared.js +8 -1
  184. package/clis/zhihu/write-shared.test.js +1 -0
  185. package/clis/zlibrary/commands.test.js +75 -0
  186. package/clis/zlibrary/info.js +47 -0
  187. package/clis/zlibrary/search.js +46 -0
  188. package/clis/zlibrary/utils.js +136 -0
  189. package/dist/src/adapter-source.d.ts +11 -0
  190. package/dist/src/adapter-source.js +24 -0
  191. package/dist/src/adapter-source.test.js +29 -0
  192. package/dist/src/browser/base-page.d.ts +3 -1
  193. package/dist/src/browser/base-page.js +76 -1
  194. package/dist/src/browser/base-page.test.d.ts +1 -0
  195. package/dist/src/browser/base-page.test.js +74 -0
  196. package/dist/src/browser/bridge.d.ts +1 -0
  197. package/dist/src/browser/bridge.js +36 -9
  198. package/dist/src/browser/cdp.d.ts +1 -0
  199. package/dist/src/browser/cdp.js +3 -3
  200. package/dist/src/browser/daemon-client.d.ts +38 -4
  201. package/dist/src/browser/daemon-client.js +24 -7
  202. package/dist/src/browser/daemon-client.test.js +49 -0
  203. package/dist/src/browser/errors.js +3 -0
  204. package/dist/src/browser/errors.test.js +3 -0
  205. package/dist/src/browser/network-cache.d.ts +1 -0
  206. package/dist/src/browser/page.d.ts +3 -1
  207. package/dist/src/browser/page.js +10 -2
  208. package/dist/src/browser/profile.d.ts +14 -0
  209. package/dist/src/browser/profile.js +85 -0
  210. package/dist/src/build-manifest.d.ts +2 -0
  211. package/dist/src/build-manifest.js +13 -3
  212. package/dist/src/build-manifest.test.js +20 -2
  213. package/dist/src/cli.d.ts +6 -0
  214. package/dist/src/cli.js +462 -32
  215. package/dist/src/cli.test.js +209 -2
  216. package/dist/src/commanderAdapter.js +29 -9
  217. package/dist/src/commanderAdapter.test.js +78 -2
  218. package/dist/src/commands/daemon.js +6 -0
  219. package/dist/src/completion-shared.js +1 -2
  220. package/dist/src/completion.test.js +3 -2
  221. package/dist/src/daemon.js +125 -41
  222. package/dist/src/doctor.d.ts +4 -6
  223. package/dist/src/doctor.js +80 -22
  224. package/dist/src/doctor.test.js +82 -0
  225. package/dist/src/engine.test.js +6 -5
  226. package/dist/src/errors.d.ts +14 -8
  227. package/dist/src/errors.js +36 -30
  228. package/dist/src/errors.test.js +5 -5
  229. package/dist/src/execution.d.ts +4 -0
  230. package/dist/src/execution.js +173 -25
  231. package/dist/src/execution.test.js +171 -1
  232. package/dist/src/main.js +10 -0
  233. package/dist/src/observation/artifact.d.ts +16 -0
  234. package/dist/src/observation/artifact.js +260 -0
  235. package/dist/src/observation/artifact.test.d.ts +1 -0
  236. package/dist/src/observation/artifact.test.js +121 -0
  237. package/dist/src/observation/events.d.ts +89 -0
  238. package/dist/src/observation/events.js +1 -0
  239. package/dist/src/observation/index.d.ts +7 -0
  240. package/dist/src/observation/index.js +7 -0
  241. package/dist/src/observation/manager.d.ts +9 -0
  242. package/dist/src/observation/manager.js +27 -0
  243. package/dist/src/observation/manager.test.d.ts +1 -0
  244. package/dist/src/observation/manager.test.js +13 -0
  245. package/dist/src/observation/redaction.d.ts +11 -0
  246. package/dist/src/observation/redaction.js +81 -0
  247. package/dist/src/observation/redaction.test.d.ts +1 -0
  248. package/dist/src/observation/redaction.test.js +32 -0
  249. package/dist/src/observation/retention.d.ts +32 -0
  250. package/dist/src/observation/retention.js +160 -0
  251. package/dist/src/observation/retention.test.d.ts +1 -0
  252. package/dist/src/observation/retention.test.js +118 -0
  253. package/dist/src/observation/ring-buffer.d.ts +22 -0
  254. package/dist/src/observation/ring-buffer.js +45 -0
  255. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  256. package/dist/src/observation/ring-buffer.test.js +22 -0
  257. package/dist/src/observation/session.d.ts +25 -0
  258. package/dist/src/observation/session.js +50 -0
  259. package/dist/src/pipeline/executor.test.js +1 -0
  260. package/dist/src/pipeline/steps/download.test.js +1 -0
  261. package/dist/src/pipeline/steps/fetch.js +1 -21
  262. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  263. package/dist/src/plugin-scaffold.js +1 -1
  264. package/dist/src/plugin-scaffold.test.js +1 -1
  265. package/dist/src/registry.d.ts +40 -9
  266. package/dist/src/registry.js +3 -1
  267. package/dist/src/runtime-detect.d.ts +10 -0
  268. package/dist/src/runtime-detect.js +19 -0
  269. package/dist/src/runtime-detect.test.js +12 -1
  270. package/dist/src/runtime.d.ts +2 -0
  271. package/dist/src/runtime.js +1 -0
  272. package/dist/src/types.d.ts +22 -0
  273. package/dist/src/update-check.d.ts +31 -1
  274. package/dist/src/update-check.js +62 -16
  275. package/dist/src/update-check.test.js +86 -1
  276. package/package.json +1 -1
  277. package/dist/src/diagnostic.d.ts +0 -63
  278. package/dist/src/diagnostic.js +0 -292
  279. package/dist/src/diagnostic.test.js +0 -302
  280. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -2,10 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
+ import { BrowserCommandError } from './browser/daemon-client.js';
5
6
  import { TargetError } from './browser/target-errors.js';
6
- const { mockBrowserConnect, mockBrowserClose, browserState, } = vi.hoisted(() => ({
7
+ const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, browserState, } = vi.hoisted(() => ({
7
8
  mockBrowserConnect: vi.fn(),
8
9
  mockBrowserClose: vi.fn(),
10
+ mockBindTab: vi.fn(),
11
+ mockSendCommand: vi.fn(),
9
12
  browserState: { page: null },
10
13
  }));
11
14
  vi.mock('./browser/index.js', () => {
@@ -17,7 +20,15 @@ vi.mock('./browser/index.js', () => {
17
20
  },
18
21
  };
19
22
  });
20
- import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation } from './cli.js';
23
+ vi.mock('./browser/daemon-client.js', async () => {
24
+ const actual = await vi.importActual('./browser/daemon-client.js');
25
+ return {
26
+ ...actual,
27
+ bindTab: mockBindTab,
28
+ sendCommand: mockSendCommand,
29
+ };
30
+ });
31
+ import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation, selectFreshByTimestamp } from './cli.js';
21
32
  describe('resolveBrowserVerifyInvocation', () => {
22
33
  it('prefers the built entry declared in package metadata', () => {
23
34
  const projectRoot = path.join('repo-root');
@@ -82,6 +93,22 @@ describe('resolveBrowserVerifyInvocation', () => {
82
93
  });
83
94
  });
84
95
  });
96
+ describe('selectFreshByTimestamp', () => {
97
+ it('uses timestamp watermarks so rolled buffers still emit new messages', () => {
98
+ const first = selectFreshByTimestamp([
99
+ { timestamp: 1, text: 'a' },
100
+ { timestamp: 2, text: 'b' },
101
+ ], 0);
102
+ expect(first.fresh.map((item) => item.text)).toEqual(['a', 'b']);
103
+ expect(first.lastSeenTs).toBe(2);
104
+ const rolled = selectFreshByTimestamp([
105
+ { timestamp: 2, text: 'b' },
106
+ { timestamp: 3, text: 'c' },
107
+ ], first.lastSeenTs);
108
+ expect(rolled.fresh.map((item) => item.text)).toEqual(['c']);
109
+ expect(rolled.lastSeenTs).toBe(3);
110
+ });
111
+ });
85
112
  describe('browser tab targeting commands', () => {
86
113
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
87
114
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
@@ -95,6 +122,13 @@ describe('browser tab targeting commands', () => {
95
122
  stderrSpy.mockClear();
96
123
  mockBrowserConnect.mockClear();
97
124
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
125
+ mockBindTab.mockReset().mockResolvedValue({
126
+ workspace: 'bound:default',
127
+ page: 'tab-2',
128
+ url: 'https://user.example/inbox',
129
+ title: 'Inbox',
130
+ });
131
+ mockSendCommand.mockReset().mockResolvedValue({ closed: true });
98
132
  browserState.page = {
99
133
  goto: vi.fn().mockResolvedValue(undefined),
100
134
  wait: vi.fn().mockResolvedValue(undefined),
@@ -104,6 +138,7 @@ describe('browser tab targeting commands', () => {
104
138
  startNetworkCapture: vi.fn().mockResolvedValue(true),
105
139
  getCookies: vi.fn().mockResolvedValue([]),
106
140
  evaluate: vi.fn().mockResolvedValue({ ok: true }),
141
+ snapshot: vi.fn().mockResolvedValue('snapshot'),
107
142
  tabs: vi.fn().mockResolvedValue([
108
143
  { index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
109
144
  { index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
@@ -127,6 +162,62 @@ describe('browser tab targeting commands', () => {
127
162
  throw new Error(`Expected string arg to console.log, got ${typeof last}`);
128
163
  return JSON.parse(last);
129
164
  }
165
+ it('binds the current Chrome tab into a bound workspace', async () => {
166
+ const program = createProgram('', '');
167
+ await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--domain', 'user.example', '--path-prefix', '/inbox']);
168
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
169
+ expect(mockBindTab).toHaveBeenCalledWith('bound:default', {
170
+ matchDomain: 'user.example',
171
+ matchPathPrefix: '/inbox',
172
+ });
173
+ const out = lastJsonLog();
174
+ expect(out.workspace).toBe('bound:default');
175
+ expect(out.url).toBe('https://user.example/inbox');
176
+ });
177
+ it('rejects bind workspaces outside the bound namespace', async () => {
178
+ const program = createProgram('', '');
179
+ await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--workspace', 'browser:default']);
180
+ expect(mockBrowserConnect).not.toHaveBeenCalled();
181
+ expect(mockBindTab).not.toHaveBeenCalled();
182
+ const out = lastJsonLog();
183
+ expect(out.error.code).toBe('invalid_bind_workspace');
184
+ expect(process.exitCode).toBeDefined();
185
+ });
186
+ it('runs browser commands against an explicit bound workspace', async () => {
187
+ const program = createProgram('', '');
188
+ await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
189
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
190
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
191
+ });
192
+ it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
193
+ browserState.page = {
194
+ ...browserState.page,
195
+ workspace: 'bound:default',
196
+ evaluate: vi.fn(),
197
+ wait: vi.fn(),
198
+ };
199
+ const program = createProgram('', '');
200
+ await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'back']);
201
+ expect(browserState.page?.evaluate).not.toHaveBeenCalled();
202
+ const out = lastJsonLog();
203
+ expect(out.error.code).toBe('bound_navigation_blocked');
204
+ });
205
+ it('unbinds a bound workspace through the daemon close-window command', async () => {
206
+ const program = createProgram('', '');
207
+ await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
208
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
209
+ expect(mockSendCommand).toHaveBeenCalledWith('close-window', { workspace: 'bound:default' });
210
+ const out = lastJsonLog();
211
+ expect(out).toEqual({ unbound: true, workspace: 'bound:default' });
212
+ });
213
+ it('does not print false success when unbind fails', async () => {
214
+ mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Workspace "bound:default" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
215
+ const program = createProgram('', '');
216
+ await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
217
+ const out = lastJsonLog();
218
+ expect(out.error.code).toBe('bound_session_missing');
219
+ expect(process.exitCode).toBeDefined();
220
+ });
130
221
  it('binds browser commands to an explicit target tab via --tab', async () => {
131
222
  const program = createProgram('', '');
132
223
  await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-2', 'document.title']);
@@ -446,6 +537,9 @@ describe('browser network command', () => {
446
537
  function getNetworkCachePath(cacheDir) {
447
538
  return path.join(cacheDir, 'browser-network', 'browser_default.json');
448
539
  }
540
+ function getBoundNetworkCachePath(cacheDir) {
541
+ return path.join(cacheDir, 'browser-network', 'bound_default.json');
542
+ }
449
543
  function lastJsonLog() {
450
544
  const calls = consoleLogSpy.mock.calls;
451
545
  if (calls.length === 0)
@@ -473,6 +567,7 @@ describe('browser network command', () => {
473
567
  responseStatus: 200,
474
568
  responseContentType: 'application/json',
475
569
  responsePreview: JSON.stringify({ data: { user: { rest_id: '42' } } }),
570
+ timestamp: Date.now(),
476
571
  },
477
572
  {
478
573
  url: 'https://cdn.example.com/app.js',
@@ -496,6 +591,19 @@ describe('browser network command', () => {
496
591
  expect(out.entries[0]).not.toHaveProperty('body');
497
592
  expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
498
593
  });
594
+ it('uses the selected browser workspace for network cache scope', async () => {
595
+ const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
596
+ browserState.page = {
597
+ ...browserState.page,
598
+ workspace: 'bound:default',
599
+ };
600
+ const program = createProgram('', '');
601
+ await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'network']);
602
+ const out = lastJsonLog();
603
+ expect(out.workspace).toBe('bound:default');
604
+ expect(fs.existsSync(getBoundNetworkCachePath(cacheDir))).toBe(true);
605
+ expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
606
+ });
499
607
  it('--all includes static resources that the default filter drops', async () => {
500
608
  const program = createProgram('', '');
501
609
  await program.parseAsync(['node', 'opencli', 'browser', 'network', '--all']);
@@ -504,11 +612,73 @@ describe('browser network command', () => {
504
612
  expect(out.entries.map((e) => e.key)).toContain('UserTweets');
505
613
  expect(out.entries.map((e) => e.key)).toContain('GET cdn.example.com/app.js');
506
614
  });
615
+ it('--failed and --since filter captured entries by status and time window', async () => {
616
+ const now = Date.now();
617
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
618
+ {
619
+ url: 'https://api.example.com/new-fail',
620
+ method: 'GET',
621
+ responseStatus: 500,
622
+ responseContentType: 'application/json',
623
+ responsePreview: JSON.stringify({ error: true }),
624
+ timestamp: now,
625
+ },
626
+ {
627
+ url: 'https://api.example.com/old-fail',
628
+ method: 'GET',
629
+ responseStatus: 500,
630
+ responseContentType: 'application/json',
631
+ responsePreview: JSON.stringify({ error: true }),
632
+ timestamp: now - 180_000,
633
+ },
634
+ {
635
+ url: 'https://api.example.com/new-ok',
636
+ method: 'GET',
637
+ responseStatus: 200,
638
+ responseContentType: 'application/json',
639
+ responsePreview: JSON.stringify({ ok: true }),
640
+ timestamp: now,
641
+ },
642
+ ]);
643
+ const program = createProgram('', '');
644
+ await program.parseAsync(['node', 'opencli', 'browser', 'network', '--since', '120s', '--failed']);
645
+ const out = lastJsonLog();
646
+ expect(out.count).toBe(1);
647
+ expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
648
+ expect(out.entries[0].timestamp).toMatch(/T/);
649
+ });
650
+ it('default output keeps text/javascript API responses while dropping static JS files', async () => {
651
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
652
+ {
653
+ url: 'https://hw.mail.163.com/js6/s?sid=abc&func=mbox:listMessages',
654
+ method: 'POST',
655
+ responseStatus: 200,
656
+ responseContentType: 'text/javascript',
657
+ responsePreview: JSON.stringify({ messages: [{ id: 'm1', subject: 'hello' }] }),
658
+ },
659
+ {
660
+ url: 'https://cdn.example.com/app.js',
661
+ method: 'GET',
662
+ responseStatus: 200,
663
+ responseContentType: 'application/javascript',
664
+ responsePreview: '// js',
665
+ },
666
+ ]);
667
+ const program = createProgram('', '');
668
+ await program.parseAsync(['node', 'opencli', 'browser', 'network']);
669
+ const out = lastJsonLog();
670
+ expect(out.count).toBe(1);
671
+ expect(out.filtered_out).toBe(1);
672
+ expect(out.entries[0].key).toBe('POST hw.mail.163.com/js6/s');
673
+ expect(out.entries[0].ct).toBe('text/javascript');
674
+ expect(out.entries[0].shape['$.messages']).toBe('array(1)');
675
+ });
507
676
  it('--raw emits full bodies inline for every entry', async () => {
508
677
  const program = createProgram('', '');
509
678
  await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
510
679
  const out = lastJsonLog();
511
680
  expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
681
+ expect(out.entries[0].timestamp).toMatch(/T/);
512
682
  });
513
683
  it('--detail <key> returns the full body for the requested entry', async () => {
514
684
  const program = createProgram('', '');
@@ -519,6 +689,7 @@ describe('browser network command', () => {
519
689
  expect(out.key).toBe('UserTweets');
520
690
  expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
521
691
  expect(out.shape['$.data.user.rest_id']).toBe('string');
692
+ expect(out.timestamp).toMatch(/T/);
522
693
  });
523
694
  it('--detail reports key_not_found with the list of available keys', async () => {
524
695
  const program = createProgram('', '');
@@ -793,6 +964,42 @@ describe('browser network command', () => {
793
964
  });
794
965
  });
795
966
  });
967
+ describe('browser console command', () => {
968
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
969
+ beforeEach(() => {
970
+ process.exitCode = undefined;
971
+ consoleLogSpy.mockClear();
972
+ mockBrowserConnect.mockClear();
973
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
974
+ const now = Date.now();
975
+ browserState.page = {
976
+ setActivePage: vi.fn(),
977
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
978
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
979
+ consoleMessages: vi.fn().mockResolvedValue([
980
+ { type: 'error', text: 'boom', timestamp: now },
981
+ { type: 'log', text: 'ok', timestamp: now },
982
+ { type: 'warning', text: 'old warning', timestamp: now - 180_000 },
983
+ ]),
984
+ };
985
+ });
986
+ function lastJsonLog() {
987
+ const calls = consoleLogSpy.mock.calls;
988
+ if (calls.length === 0)
989
+ throw new Error('Expected at least one console.log call');
990
+ const last = calls[calls.length - 1][0];
991
+ if (typeof last !== 'string')
992
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
993
+ return JSON.parse(last);
994
+ }
995
+ it('filters console messages by level and time window', async () => {
996
+ const program = createProgram('', '');
997
+ await program.parseAsync(['node', 'opencli', 'browser', 'console', '--level', 'error', '--since', '120s']);
998
+ const out = lastJsonLog();
999
+ expect(out.count).toBe(1);
1000
+ expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
1001
+ });
1002
+ });
796
1003
  describe('browser get html command', () => {
797
1004
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
798
1005
  function lastLogArg() {
@@ -16,7 +16,6 @@ import { formatRegistryHelpText } from './serialization.js';
16
16
  import { render as renderOutput } from './output.js';
17
17
  import { executeCommand, prepareCommandArgs } from './execution.js';
18
18
  import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
19
- import { isDiagnosticEnabled } from './diagnostic.js';
20
19
  /**
21
20
  * Register a single CliCommand as a Commander subcommand.
22
21
  */
@@ -48,6 +47,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
48
47
  }
49
48
  subCmd
50
49
  .option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
50
+ .option('--trace <mode>', 'Trace capture: off, on, retain-on-failure', 'off')
51
51
  .option('-v, --verbose', 'Debug output', false);
52
52
  subCmd.addHelpText('after', formatRegistryHelpText(cmd));
53
53
  subCmd.action(async (...actionArgs) => {
@@ -71,6 +71,18 @@ export function registerCommandToProgram(siteCmd, cmd) {
71
71
  if (v !== undefined)
72
72
  rawKwargs[arg.name] = v;
73
73
  }
74
+ const optionSources = {};
75
+ for (const arg of cmd.args) {
76
+ if (arg.positional)
77
+ continue;
78
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
79
+ const source = subCmd.getOptionValueSource(camelName) ?? subCmd.getOptionValueSource(arg.name);
80
+ if (source === 'cli')
81
+ optionSources[arg.name] = source;
82
+ }
83
+ if (Object.keys(optionSources).length > 0) {
84
+ rawKwargs.__opencliOptionSources = optionSources;
85
+ }
74
86
  const kwargs = prepareCommandArgs(cmd, rawKwargs);
75
87
  const verbose = optionsRecord.verbose === true;
76
88
  let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
@@ -82,7 +94,12 @@ export function registerCommandToProgram(siteCmd, cmd) {
82
94
  const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
83
95
  log.warn(`Deprecated: ${message}${replacement}`);
84
96
  }
85
- const result = await executeCommand(cmd, kwargs, verbose, { prepared: true });
97
+ const globals = typeof subCmd.optsWithGlobals === 'function' ? subCmd.optsWithGlobals() : {};
98
+ const result = await executeCommand(cmd, kwargs, verbose, {
99
+ prepared: true,
100
+ ...(typeof globals.profile === 'string' && globals.profile.trim() ? { profile: globals.profile.trim() } : {}),
101
+ ...(typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
102
+ });
86
103
  if (result === null || result === undefined) {
87
104
  return;
88
105
  }
@@ -104,7 +121,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
104
121
  });
105
122
  }
106
123
  catch (err) {
107
- renderError(err, fullName(cmd), optionsRecord.verbose === true);
124
+ renderError(err, fullName(cmd), optionsRecord.verbose === true, optionsRecord.trace);
108
125
  process.exitCode = resolveExitCode(err);
109
126
  }
110
127
  });
@@ -116,13 +133,16 @@ function resolveExitCode(err) {
116
133
  return EXIT_CODES.GENERIC_ERROR;
117
134
  }
118
135
  // ── Error rendering ─────────────────────────────────────────────────────────
119
- /** Emit AutoFix hint for repairable adapter errors (skipped if already in diagnostic mode). */
120
- function emitAutoFixHint(envelope, cmdName) {
121
- if (isDiagnosticEnabled())
136
+ /** Emit AutoFix hint for repairable adapter errors (skipped if trace already exported). */
137
+ function emitAutoFixHint(envelope, cmdName, traceMode) {
138
+ if (traceMode === 'on' || traceMode === 'retain-on-failure')
122
139
  return envelope;
123
- return envelope + `# AutoFix: re-run with OPENCLI_DIAGNOSTIC=1 for repair context\n# OPENCLI_DIAGNOSTIC=1 ${cmdName}\n`;
140
+ const runnable = cmdName.replace('/', ' ');
141
+ return envelope
142
+ + `# AutoFix: re-run with --trace=retain-on-failure for trace artifact\n`
143
+ + `# opencli ${runnable} --trace retain-on-failure\n`;
124
144
  }
125
- function renderError(err, cmdName, verbose) {
145
+ function renderError(err, cmdName, verbose, traceMode) {
126
146
  const envelope = toEnvelope(err);
127
147
  // In verbose mode, include stack trace for debugging
128
148
  if (verbose && err instanceof Error && err.stack) {
@@ -132,7 +152,7 @@ function renderError(err, cmdName, verbose) {
132
152
  // Append AutoFix hint for repairable errors
133
153
  const code = envelope.error.code;
134
154
  if (code === 'SELECTOR' || code === 'EMPTY_RESULT' || code === 'ADAPTER_LOAD' || code === 'UNKNOWN') {
135
- output = emitAutoFixHint(output, cmdName);
155
+ output = emitAutoFixHint(output, cmdName, traceMode);
136
156
  }
137
157
  process.stderr.write(output);
138
158
  }
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { Command } from 'commander';
3
- import { EmptyResultError, SelectorError } from './errors.js';
3
+ import { attachTraceReceipt, EmptyResultError, selectorError } from './errors.js';
4
4
  const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
5
5
  mockExecuteCommand: vi.fn(),
6
6
  mockRenderOutput: vi.fn(),
@@ -56,6 +56,24 @@ describe('commanderAdapter arg passing', () => {
56
56
  expect(kwargs.pdf).toBe('./paper.pdf');
57
57
  expect(kwargs['prepare-only']).toBe(true);
58
58
  });
59
+ it('passes option value sources through for adapters that need explicit-vs-default semantics', async () => {
60
+ const program = new Command();
61
+ const siteCmd = program.command('paperreview');
62
+ registerCommandToProgram(siteCmd, cmd);
63
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
64
+ expect(mockExecuteCommand).toHaveBeenCalled();
65
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
66
+ expect(kwargs.__opencliOptionSources).toMatchObject({
67
+ 'prepare-only': 'cli',
68
+ });
69
+ });
70
+ it('passes explicit trace mode to executeCommand', async () => {
71
+ const program = new Command();
72
+ const siteCmd = program.command('paperreview');
73
+ registerCommandToProgram(siteCmd, cmd);
74
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--trace', 'retain-on-failure']);
75
+ expect(mockExecuteCommand).toHaveBeenCalledWith(expect.objectContaining({ site: 'paperreview', name: 'submit' }), expect.objectContaining({ pdf: './paper.pdf' }), false, { prepared: true, trace: 'retain-on-failure' });
76
+ });
59
77
  it('rejects invalid bool values before calling executeCommand', async () => {
60
78
  const program = new Command();
61
79
  const siteCmd = program.command('paperreview');
@@ -258,6 +276,9 @@ describe('commanderAdapter error envelope output', () => {
258
276
  expect(output).toContain('ok: false');
259
277
  expect(output).toContain('code: EMPTY_RESULT');
260
278
  expect(output).toContain('xsec_token');
279
+ expect(output).toContain('--trace=retain-on-failure');
280
+ expect(output).toContain('opencli xiaohongshu note --trace retain-on-failure');
281
+ expect(output).not.toContain('OPENCLI_DIAGNOSTIC');
261
282
  stderrSpy.mockRestore();
262
283
  });
263
284
  it('outputs YAML error envelope for selector errors', async () => {
@@ -265,12 +286,67 @@ describe('commanderAdapter error envelope output', () => {
265
286
  const siteCmd = program.command('xiaohongshu');
266
287
  registerCommandToProgram(siteCmd, cmd);
267
288
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
268
- mockExecuteCommand.mockRejectedValueOnce(new SelectorError('.note-title', 'The note title selector no longer matches the current page.'));
289
+ mockExecuteCommand.mockRejectedValueOnce(selectorError('.note-title', 'The note title selector no longer matches the current page.'));
269
290
  await program.parseAsync(['node', 'opencli', 'xiaohongshu', 'note', '69ca3927000000001a020fd5']);
270
291
  const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
271
292
  expect(output).toContain('ok: false');
272
293
  expect(output).toContain('code: SELECTOR');
273
294
  expect(output).toContain('selector no longer matches');
295
+ expect(output).toContain('--trace=retain-on-failure');
296
+ stderrSpy.mockRestore();
297
+ });
298
+ it('does not add an AutoFix rerun hint when trace is already enabled', async () => {
299
+ const program = new Command();
300
+ const siteCmd = program.command('xiaohongshu');
301
+ registerCommandToProgram(siteCmd, cmd);
302
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
303
+ mockExecuteCommand.mockRejectedValueOnce(selectorError('.note-title'));
304
+ await program.parseAsync([
305
+ 'node',
306
+ 'opencli',
307
+ 'xiaohongshu',
308
+ 'note',
309
+ '69ca3927000000001a020fd5',
310
+ '--trace',
311
+ 'retain-on-failure',
312
+ ]);
313
+ const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
314
+ expect(output).toContain('code: SELECTOR');
315
+ expect(output).not.toContain('AutoFix: re-run');
316
+ stderrSpy.mockRestore();
317
+ });
318
+ it('includes trace metadata from the error envelope when execution attached it', async () => {
319
+ const program = new Command();
320
+ const siteCmd = program.command('xiaohongshu');
321
+ registerCommandToProgram(siteCmd, cmd);
322
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
323
+ const err = selectorError('.note-title');
324
+ attachTraceReceipt(err, {
325
+ schemaVersion: 1,
326
+ opencliVersion: '1.7.8',
327
+ traceId: 'trace-1',
328
+ traceDir: '/tmp/opencli/profiles/default/traces/trace-1',
329
+ summaryPath: '/tmp/opencli/profiles/default/traces/trace-1/summary.md',
330
+ receiptPath: '/tmp/opencli/profiles/default/traces/trace-1/receipt.json',
331
+ status: 'failure',
332
+ createdAt: '2026-05-03T00:00:00.000Z',
333
+ error: { code: 'SELECTOR', message: 'Could not find element: .note-title' },
334
+ });
335
+ mockExecuteCommand.mockRejectedValueOnce(err);
336
+ await program.parseAsync([
337
+ 'node',
338
+ 'opencli',
339
+ 'xiaohongshu',
340
+ 'note',
341
+ '69ca3927000000001a020fd5',
342
+ '--trace',
343
+ 'retain-on-failure',
344
+ ]);
345
+ const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
346
+ expect(output).toContain('trace:');
347
+ expect(output).toContain('dir: /tmp/opencli/profiles/default/traces/trace-1');
348
+ expect(output).toContain('summaryPath: /tmp/opencli/profiles/default/traces/trace-1/summary.md');
349
+ expect(output).toContain('receiptPath: /tmp/opencli/profiles/default/traces/trace-1/receipt.json');
274
350
  stderrSpy.mockRestore();
275
351
  });
276
352
  });
@@ -21,6 +21,12 @@ export async function daemonStatus() {
21
21
  console.log(`Daemon: ${styleText('green', 'running')} (PID ${status.pid})`);
22
22
  console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
23
23
  console.log(`Extension: ${extensionLabel}`);
24
+ if (status.profiles && status.profiles.length > 0) {
25
+ console.log(`Profiles: ${status.profiles.map((profile) => {
26
+ const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : '';
27
+ return `${profile.contextId}${version}`;
28
+ }).join(', ')}`);
29
+ }
24
30
  console.log(`Memory: ${status.memoryMB} MB`);
25
31
  console.log(`Port: ${status.port}`);
26
32
  }
@@ -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', () => {