@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
@@ -23,15 +23,32 @@ describe('doctor report rendering', () => {
23
23
  });
24
24
  it('renders OK-style report when daemon and extension connected', () => {
25
25
  const text = strip(renderBrowserDoctorReport({
26
+ cliVersion: '1.7.9',
26
27
  daemonRunning: true,
28
+ daemonVersion: '1.7.9',
27
29
  extensionConnected: true,
28
30
  extensionVersion: '1.6.8',
29
31
  issues: [],
30
32
  }));
31
33
  expect(text).toContain('[OK] Daemon: running on port 19825');
34
+ expect(text).toContain('(v1.7.9)');
32
35
  expect(text).toContain('[OK] Extension: connected (v1.6.8)');
33
36
  expect(text).toContain('Everything looks good!');
34
37
  });
38
+ it('renders a warning when daemon version is stale', () => {
39
+ const text = strip(renderBrowserDoctorReport({
40
+ cliVersion: '1.7.9',
41
+ daemonRunning: true,
42
+ daemonVersion: '1.7.6',
43
+ daemonStale: true,
44
+ extensionConnected: true,
45
+ extensionVersion: '1.0.3',
46
+ issues: ['Stale daemon detected: daemon v1.7.6 != CLI v1.7.9.\n Run: opencli daemon restart'],
47
+ }));
48
+ expect(text).toContain('[WARN] Daemon: running on port 19825 (v1.7.6, stale; CLI v1.7.9)');
49
+ expect(text).toContain('Run: opencli daemon restart');
50
+ expect(text).not.toContain('Everything looks good!');
51
+ });
35
52
  it('renders MISSING when daemon not running', () => {
36
53
  const text = strip(renderBrowserDoctorReport({
37
54
  daemonRunning: false,
@@ -78,6 +95,64 @@ describe('doctor report rendering', () => {
78
95
  }));
79
96
  expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
80
97
  });
98
+ it('renders sessions with tab leases and no idle timer', () => {
99
+ const text = strip(renderBrowserDoctorReport({
100
+ daemonRunning: true,
101
+ extensionConnected: true,
102
+ issues: [],
103
+ sessions: [
104
+ {
105
+ workspace: 'bound:default',
106
+ windowId: 2,
107
+ preferredTabId: 42,
108
+ ownership: 'borrowed',
109
+ surface: 'borrowed-user-tab',
110
+ tabCount: 1,
111
+ idleMsRemaining: null,
112
+ },
113
+ ],
114
+ }));
115
+ expect(text).toContain('bound:default → tab 42, mode=borrowed, surface=borrowed-user-tab, tabs=1, idle=none');
116
+ });
117
+ it('renders connected profiles and groups sessions by profile', () => {
118
+ const text = strip(renderBrowserDoctorReport({
119
+ daemonRunning: true,
120
+ extensionConnected: false,
121
+ profiles: [
122
+ { contextId: 'work', extensionConnected: true, extensionVersion: '1.2.3', pending: 0 },
123
+ { contextId: 'personal', extensionConnected: true, extensionVersion: '1.2.3', pending: 0 },
124
+ ],
125
+ issues: [],
126
+ sessions: [
127
+ {
128
+ contextId: 'work',
129
+ workspace: 'bound:default',
130
+ windowId: 2,
131
+ preferredTabId: 42,
132
+ ownership: 'borrowed',
133
+ surface: 'borrowed-user-tab',
134
+ tabCount: 1,
135
+ idleMsRemaining: null,
136
+ },
137
+ {
138
+ contextId: 'personal',
139
+ workspace: 'site:foo',
140
+ windowId: 1,
141
+ preferredTabId: 10,
142
+ ownership: 'owned',
143
+ surface: 'dedicated-container',
144
+ tabCount: 1,
145
+ idleMsRemaining: 1000,
146
+ },
147
+ ],
148
+ }));
149
+ expect(text).toContain('Profiles:');
150
+ expect(text).toContain('work: connected v1.2.3');
151
+ expect(text).toContain('[profile: work]');
152
+ expect(text).toContain('[profile: personal]');
153
+ expect(text).toContain('bound:default → tab 42');
154
+ expect(text).toContain('site:foo → tab 10');
155
+ });
81
156
  it('renders unstable extension state when live connectivity and status disagree', () => {
82
157
  const text = strip(renderBrowserDoctorReport({
83
158
  daemonRunning: true,
@@ -142,16 +217,19 @@ describe('doctor report rendering', () => {
142
217
  });
143
218
  it('uses the fast default timeout for live connectivity checks', async () => {
144
219
  let timeoutSeen;
220
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
145
221
  mockConnect.mockImplementationOnce(async (opts) => {
146
222
  timeoutSeen = opts?.timeout;
147
223
  return {
148
224
  evaluate: vi.fn().mockResolvedValue(2),
225
+ closeWindow,
149
226
  };
150
227
  });
151
228
  mockClose.mockResolvedValueOnce(undefined);
152
229
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
153
230
  await runBrowserDoctor({ live: true });
154
231
  expect(timeoutSeen).toBe(8);
232
+ expect(closeWindow).toHaveBeenCalledTimes(1);
155
233
  });
156
234
  it('skips auto-start in no-live mode when daemon is already running', async () => {
157
235
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
@@ -177,4 +255,43 @@ describe('doctor report rendering', () => {
177
255
  expect.stringContaining('did not report a version'),
178
256
  ]));
179
257
  });
258
+ it('reports an issue when daemon version differs from CLI version', async () => {
259
+ const status = {
260
+ state: 'ready',
261
+ status: {
262
+ daemonVersion: '1.7.6',
263
+ extensionConnected: true,
264
+ extensionVersion: '1.0.3',
265
+ },
266
+ };
267
+ mockGetDaemonHealth
268
+ .mockResolvedValueOnce(status)
269
+ .mockResolvedValueOnce(status);
270
+ const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' });
271
+ expect(report.daemonStale).toBe(true);
272
+ expect(report.issues).toEqual(expect.arrayContaining([
273
+ expect.stringContaining('Stale daemon detected: daemon v1.7.6 != CLI v1.7.9'),
274
+ ]));
275
+ });
276
+ it('reports profile-required when multiple profiles are connected without a selection', async () => {
277
+ const status = {
278
+ state: 'profile-required',
279
+ status: {
280
+ extensionConnected: false,
281
+ profileRequired: true,
282
+ profiles: [
283
+ { contextId: 'work', extensionConnected: true, pending: 0 },
284
+ { contextId: 'personal', extensionConnected: true, pending: 0 },
285
+ ],
286
+ },
287
+ };
288
+ mockGetDaemonHealth
289
+ .mockResolvedValueOnce(status)
290
+ .mockResolvedValueOnce(status);
291
+ const report = await runBrowserDoctor({ live: false });
292
+ expect(report.profiles).toHaveLength(2);
293
+ expect(report.issues).toEqual(expect.arrayContaining([
294
+ expect.stringContaining('Multiple Chrome profiles are connected'),
295
+ ]));
296
+ });
180
297
  });
@@ -137,6 +137,7 @@ describe('discoverPlugins', () => {
137
137
  const symlinkTargetDir = path.join(os.tmpdir(), '__test-plugin-symlink-target__');
138
138
  const symlinkPluginDir = path.join(PLUGINS_DIR, '__test-plugin-symlink__');
139
139
  const brokenSymlinkDir = path.join(PLUGINS_DIR, '__test-plugin-broken__');
140
+ const dirSymlinkType = process.platform === 'win32' ? 'junction' : 'dir';
140
141
  afterEach(async () => {
141
142
  try {
142
143
  await fs.promises.rm(testPluginDir, { recursive: true });
@@ -183,14 +184,14 @@ description: Test plugin greeting via symlink
183
184
  strategy: public
184
185
  browser: false
185
186
  `);
186
- await fs.promises.symlink(symlinkTargetDir, symlinkPluginDir, 'dir');
187
+ await fs.promises.symlink(symlinkTargetDir, symlinkPluginDir, dirSymlinkType);
187
188
  await discoverPlugins();
188
189
  const cmd = getRegistry().get('__test-plugin-symlink__/hello');
189
190
  expect(cmd).toBeUndefined();
190
191
  });
191
192
  it('skips broken plugin symlinks without throwing', async () => {
192
193
  await fs.promises.mkdir(PLUGINS_DIR, { recursive: true });
193
- await fs.promises.symlink(path.join(os.tmpdir(), '__missing-plugin-target__'), brokenSymlinkDir, 'dir');
194
+ await fs.promises.symlink(path.join(os.tmpdir(), '__missing-plugin-target__'), brokenSymlinkDir, dirSymlinkType);
194
195
  await expect(discoverPlugins()).resolves.not.toThrow();
195
196
  expect(getRegistry().get('__test-plugin-broken__/hello')).toBeUndefined();
196
197
  });
@@ -210,7 +211,7 @@ describe('executeCommand', () => {
210
211
  args: [
211
212
  { name: 'note-id', required: true, help: 'Note ID' },
212
213
  ],
213
- func: async (_page, kwargs) => [{ noteId: kwargs['note-id'] }],
214
+ func: async (kwargs) => [{ noteId: kwargs['note-id'] }],
214
215
  });
215
216
  const result = await executeCommand(cmd, { 'note-id': 'abc123' });
216
217
  expect(result).toEqual([{ noteId: 'abc123' }]);
@@ -222,7 +223,7 @@ describe('executeCommand', () => {
222
223
  description: 'test command with func',
223
224
  browser: false,
224
225
  strategy: Strategy.PUBLIC,
225
- func: async (_page, kwargs) => {
226
+ func: async (kwargs) => {
226
227
  return [{ title: kwargs.query ?? 'default' }];
227
228
  },
228
229
  });
@@ -260,7 +261,7 @@ describe('executeCommand', () => {
260
261
  name: 'debug-test',
261
262
  description: 'debug test',
262
263
  browser: false,
263
- func: async (_page, _kwargs, debug) => {
264
+ func: async (_kwargs, debug) => {
264
265
  receivedDebug = debug ?? false;
265
266
  return [];
266
267
  },
@@ -13,12 +13,13 @@
13
13
  * 1 Generic / unexpected error
14
14
  * 2 Argument / usage error (ArgumentError)
15
15
  * 66 No input / empty result (EmptyResultError)
16
- * 69 Service unavailable (BrowserConnectError, AdapterLoadError)
16
+ * 69 Service unavailable (BrowserConnectError, adapter load failures)
17
17
  * 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
18
18
  * 77 Permission denied / auth needed (AuthRequiredError)
19
19
  * 78 Configuration error (ConfigError)
20
20
  * 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
21
21
  */
22
+ import type { ObservationTraceReceipt } from './observation/events.js';
22
23
  export declare const EXIT_CODES: {
23
24
  readonly SUCCESS: 0;
24
25
  readonly GENERIC_ERROR: 1;
@@ -40,14 +41,13 @@ export declare class CliError extends Error {
40
41
  readonly exitCode: ExitCode;
41
42
  constructor(code: string, message: string, hint?: string, exitCode?: ExitCode);
42
43
  }
43
- export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
44
+ export declare function attachTraceReceipt(err: unknown, receipt: ObservationTraceReceipt): void;
45
+ export declare function getTraceReceipt(err: unknown): ObservationTraceReceipt | undefined;
46
+ export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'profile-required' | 'profile-disconnected' | 'command-failed' | 'unknown';
44
47
  export declare class BrowserConnectError extends CliError {
45
48
  readonly kind: BrowserConnectKind;
46
49
  constructor(message: string, hint?: string, kind?: BrowserConnectKind);
47
50
  }
48
- export declare class AdapterLoadError extends CliError {
49
- constructor(message: string, hint?: string);
50
- }
51
51
  export declare class CommandExecutionError extends CliError {
52
52
  constructor(message: string, hint?: string);
53
53
  }
@@ -67,9 +67,8 @@ export declare class ArgumentError extends CliError {
67
67
  export declare class EmptyResultError extends CliError {
68
68
  constructor(command: string, hint?: string);
69
69
  }
70
- export declare class SelectorError extends CliError {
71
- constructor(selector: string, hint?: string);
72
- }
70
+ export declare function adapterLoadError(message: string, hint?: string): CliError;
71
+ export declare function selectorError(selector: string, hint?: string): CliError;
73
72
  export declare class PluginError extends CliError {
74
73
  constructor(message: string, hint?: string);
75
74
  }
@@ -84,6 +83,13 @@ export interface ErrorEnvelope {
84
83
  stack?: string;
85
84
  cause?: string;
86
85
  };
86
+ trace?: {
87
+ traceId: string;
88
+ dir: string;
89
+ summaryPath: string;
90
+ receiptPath: string;
91
+ status: ObservationTraceReceipt['status'];
92
+ };
87
93
  }
88
94
  /** Extract a human-readable message from an unknown caught value. */
89
95
  export declare function getErrorMessage(error: unknown): string;
@@ -1,24 +1,3 @@
1
- /**
2
- * Unified error types for opencli.
3
- *
4
- * All errors thrown by the framework should extend CliError so that
5
- * the top-level handler in commanderAdapter.ts can render consistent,
6
- * helpful output with emoji-coded severity and actionable hints.
7
- *
8
- * ## Exit codes
9
- *
10
- * opencli follows Unix conventions (sysexits.h) for process exit codes:
11
- *
12
- * 0 Success
13
- * 1 Generic / unexpected error
14
- * 2 Argument / usage error (ArgumentError)
15
- * 66 No input / empty result (EmptyResultError)
16
- * 69 Service unavailable (BrowserConnectError, AdapterLoadError)
17
- * 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
18
- * 77 Permission denied / auth needed (AuthRequiredError)
19
- * 78 Configuration error (ConfigError)
20
- * 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
21
- */
22
1
  // ── Exit code table ──────────────────────────────────────────────────────────
23
2
  export const EXIT_CODES = {
24
3
  SUCCESS: 0,
@@ -47,6 +26,27 @@ export class CliError extends Error {
47
26
  this.exitCode = exitCode;
48
27
  }
49
28
  }
29
+ const TRACE_RECEIPT_SYMBOL = Symbol.for('opencli.traceReceipt');
30
+ export function attachTraceReceipt(err, receipt) {
31
+ if (!err || (typeof err !== 'object' && typeof err !== 'function'))
32
+ return;
33
+ try {
34
+ Object.defineProperty(err, TRACE_RECEIPT_SYMBOL, {
35
+ value: receipt,
36
+ enumerable: false,
37
+ configurable: true,
38
+ });
39
+ }
40
+ catch {
41
+ // Non-extensible thrown objects are rare; trace export should never mask the
42
+ // original adapter error just because metadata attachment failed.
43
+ }
44
+ }
45
+ export function getTraceReceipt(err) {
46
+ if (!err || (typeof err !== 'object' && typeof err !== 'function'))
47
+ return undefined;
48
+ return err[TRACE_RECEIPT_SYMBOL];
49
+ }
50
50
  export class BrowserConnectError extends CliError {
51
51
  kind;
52
52
  constructor(message, hint, kind = 'unknown') {
@@ -54,11 +54,6 @@ export class BrowserConnectError extends CliError {
54
54
  this.kind = kind;
55
55
  }
56
56
  }
57
- export class AdapterLoadError extends CliError {
58
- constructor(message, hint) {
59
- super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
60
- }
61
- }
62
57
  export class CommandExecutionError extends CliError {
63
58
  constructor(message, hint) {
64
59
  super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
@@ -91,10 +86,11 @@ export class EmptyResultError extends CliError {
91
86
  super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in', EXIT_CODES.EMPTY_RESULT);
92
87
  }
93
88
  }
94
- export class SelectorError extends CliError {
95
- constructor(selector, hint) {
96
- super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.', EXIT_CODES.GENERIC_ERROR);
97
- }
89
+ export function adapterLoadError(message, hint) {
90
+ return new CliError('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
91
+ }
92
+ export function selectorError(selector, hint) {
93
+ return new CliError('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.', EXIT_CODES.GENERIC_ERROR);
98
94
  }
99
95
  export class PluginError extends CliError {
100
96
  constructor(message, hint) {
@@ -121,6 +117,14 @@ function serializeCause(cause, depth = 0) {
121
117
  /** Build an ErrorEnvelope from any caught value. */
122
118
  export function toEnvelope(err) {
123
119
  const cause = err instanceof Error && err.cause ? serializeCause(err.cause) : undefined;
120
+ const traceReceipt = getTraceReceipt(err);
121
+ const trace = traceReceipt ? {
122
+ traceId: traceReceipt.traceId,
123
+ dir: traceReceipt.traceDir,
124
+ summaryPath: traceReceipt.summaryPath,
125
+ receiptPath: traceReceipt.receiptPath,
126
+ status: traceReceipt.status,
127
+ } : undefined;
124
128
  if (err instanceof CliError) {
125
129
  return {
126
130
  ok: false,
@@ -131,6 +135,7 @@ export function toEnvelope(err) {
131
135
  exitCode: err.exitCode,
132
136
  ...(cause ? { cause } : {}),
133
137
  },
138
+ ...(trace ? { trace } : {}),
134
139
  };
135
140
  }
136
141
  const msg = getErrorMessage(err);
@@ -142,5 +147,6 @@ export function toEnvelope(err) {
142
147
  exitCode: EXIT_CODES.GENERIC_ERROR,
143
148
  ...(cause ? { cause } : {}),
144
149
  },
150
+ ...(trace ? { trace } : {}),
145
151
  };
146
152
  }
@@ -1,17 +1,17 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { CliError, BrowserConnectError, AdapterLoadError, CommandExecutionError, ConfigError, AuthRequiredError, TimeoutError, ArgumentError, EmptyResultError, SelectorError, toEnvelope, } from './errors.js';
2
+ import { CliError, BrowserConnectError, adapterLoadError, CommandExecutionError, ConfigError, AuthRequiredError, TimeoutError, ArgumentError, EmptyResultError, selectorError, toEnvelope, } from './errors.js';
3
3
  describe('Error type hierarchy', () => {
4
4
  it('all error types extend CliError', () => {
5
5
  const errors = [
6
6
  new BrowserConnectError('test'),
7
- new AdapterLoadError('test'),
7
+ adapterLoadError('test'),
8
8
  new CommandExecutionError('test'),
9
9
  new ConfigError('test'),
10
10
  new AuthRequiredError('example.com'),
11
11
  new TimeoutError('test', 30),
12
12
  new ArgumentError('test'),
13
13
  new EmptyResultError('test/cmd'),
14
- new SelectorError('.btn'),
14
+ selectorError('.btn'),
15
15
  ];
16
16
  for (const err of errors) {
17
17
  expect(err).toBeInstanceOf(CliError);
@@ -46,8 +46,8 @@ describe('Error type hierarchy', () => {
46
46
  expect(err.message).toBe('hackernews/top returned no data');
47
47
  expect(err.hint).toBeTruthy();
48
48
  });
49
- it('SelectorError has default hint about page changes', () => {
50
- const err = new SelectorError('.submit-btn');
49
+ it('selectorError has default hint about page changes', () => {
50
+ const err = selectorError('.submit-btn');
51
51
  expect(err.code).toBe('SELECTOR');
52
52
  expect(err.message).toContain('.submit-btn');
53
53
  expect(err.hint).toContain('report');
@@ -10,8 +10,12 @@
10
10
  * 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
11
11
  */
12
12
  import { type CliCommand, type Arg, type CommandArgs } from './registry.js';
13
+ import { type ObservationExportResult } from './observation/index.js';
13
14
  export declare function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): CommandArgs;
14
15
  export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs, debug?: boolean, opts?: {
15
16
  prepared?: boolean;
17
+ profile?: string;
18
+ trace?: string;
19
+ onTraceExport?: (trace: ObservationExportResult) => void;
16
20
  }): Promise<unknown>;
17
21
  export declare function prepareCommandArgs(cmd: CliCommand, rawKwargs: CommandArgs): CommandArgs;