@jackwener/opencli 1.5.6 → 1.5.7

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 (334) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/idle-manager.d.ts +19 -0
  176. package/dist/idle-manager.js +54 -0
  177. package/dist/launcher.d.ts +36 -0
  178. package/dist/launcher.js +152 -0
  179. package/dist/launcher.test.d.ts +1 -0
  180. package/dist/launcher.test.js +57 -0
  181. package/dist/main.js +3 -3
  182. package/dist/registry.d.ts +1 -0
  183. package/dist/registry.js +31 -3
  184. package/dist/registry.test.js +13 -0
  185. package/dist/runtime.d.ts +5 -3
  186. package/dist/runtime.js +12 -5
  187. package/dist/serialization.d.ts +1 -0
  188. package/dist/serialization.js +3 -0
  189. package/dist/serialization.test.js +17 -1
  190. package/dist/tui.d.ts +7 -0
  191. package/dist/tui.js +52 -0
  192. package/dist/tui.test.d.ts +1 -0
  193. package/dist/tui.test.js +19 -0
  194. package/dist/weixin-download.test.js +14 -0
  195. package/docs/.vitepress/config.mts +1 -0
  196. package/docs/adapters/browser/notebooklm.md +69 -0
  197. package/docs/adapters/browser/xiaohongshu.md +19 -10
  198. package/docs/adapters/index.md +67 -66
  199. package/docs/guide/browser-bridge.md +12 -0
  200. package/docs/guide/troubleshooting.md +9 -4
  201. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  202. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  203. package/docs/zh/guide/browser-bridge.md +12 -0
  204. package/extension/dist/background.js +794 -513
  205. package/extension/src/background.test.ts +202 -2
  206. package/extension/src/background.ts +174 -10
  207. package/extension/src/cdp.ts +12 -0
  208. package/extension/src/protocol.ts +7 -5
  209. package/package.json +1 -1
  210. package/src/browser/cdp.ts +24 -17
  211. package/src/browser/daemon-client.ts +7 -1
  212. package/src/browser/dom-helpers.test.ts +15 -1
  213. package/src/browser/dom-helpers.ts +1 -0
  214. package/src/browser/mcp.ts +18 -13
  215. package/src/browser/page.test.ts +58 -0
  216. package/src/browser/page.ts +18 -2
  217. package/src/browser/stealth.test.ts +153 -0
  218. package/src/browser/stealth.ts +198 -0
  219. package/src/browser.test.ts +1 -1
  220. package/src/build-manifest.test.ts +2 -0
  221. package/src/build-manifest.ts +6 -1
  222. package/src/cli.ts +21 -3
  223. package/src/clis/antigravity/SKILL.md +3 -12
  224. package/src/clis/antigravity/serve.ts +5 -10
  225. package/src/clis/bilibili/subtitle.test.ts +60 -0
  226. package/src/clis/bilibili/subtitle.ts +4 -0
  227. package/src/clis/chatwise/ask.ts +0 -2
  228. package/src/clis/chatwise/export.ts +0 -2
  229. package/src/clis/chatwise/history.ts +0 -2
  230. package/src/clis/chatwise/model.ts +0 -2
  231. package/src/clis/chatwise/new.ts +1 -2
  232. package/src/clis/chatwise/read.ts +0 -2
  233. package/src/clis/chatwise/screenshot.ts +1 -2
  234. package/src/clis/chatwise/send.ts +0 -2
  235. package/src/clis/chatwise/status.ts +1 -2
  236. package/src/clis/ctrip/search.test.ts +73 -0
  237. package/src/clis/ctrip/search.ts +97 -47
  238. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  239. package/src/clis/douyin/_shared/sts2.ts +11 -3
  240. package/src/clis/douyin/activities.test.ts +41 -1
  241. package/src/clis/douyin/activities.ts +12 -3
  242. package/src/clis/douyin/collections.test.ts +35 -2
  243. package/src/clis/douyin/collections.ts +1 -1
  244. package/src/clis/douyin/draft.test.ts +444 -2
  245. package/src/clis/douyin/draft.ts +382 -218
  246. package/src/clis/douyin/hashtag.test.ts +42 -2
  247. package/src/clis/douyin/hashtag.ts +11 -3
  248. package/src/clis/douyin/profile.test.ts +43 -1
  249. package/src/clis/douyin/profile.ts +9 -2
  250. package/src/clis/douyin/videos.test.ts +52 -2
  251. package/src/clis/douyin/videos.ts +49 -15
  252. package/src/clis/facebook/search.test.ts +70 -0
  253. package/src/clis/facebook/search.yaml +4 -3
  254. package/src/clis/instagram/download.test.ts +159 -0
  255. package/src/clis/instagram/download.ts +286 -0
  256. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  257. package/src/clis/notebooklm/bind-current.ts +36 -0
  258. package/src/clis/notebooklm/binding.test.ts +53 -0
  259. package/src/clis/notebooklm/compat.test.ts +19 -0
  260. package/src/clis/notebooklm/current.ts +38 -0
  261. package/src/clis/notebooklm/get.ts +53 -0
  262. package/src/clis/notebooklm/history.test.ts +70 -0
  263. package/src/clis/notebooklm/history.ts +36 -0
  264. package/src/clis/notebooklm/list.ts +40 -0
  265. package/src/clis/notebooklm/note-list.test.ts +64 -0
  266. package/src/clis/notebooklm/note-list.ts +42 -0
  267. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  268. package/src/clis/notebooklm/notes-get.ts +67 -0
  269. package/src/clis/notebooklm/rpc.test.ts +126 -0
  270. package/src/clis/notebooklm/rpc.ts +286 -0
  271. package/src/clis/notebooklm/shared.ts +98 -0
  272. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  273. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  274. package/src/clis/notebooklm/source-get.test.ts +100 -0
  275. package/src/clis/notebooklm/source-get.ts +60 -0
  276. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  277. package/src/clis/notebooklm/source-guide.ts +69 -0
  278. package/src/clis/notebooklm/source-list.ts +45 -0
  279. package/src/clis/notebooklm/status.ts +34 -0
  280. package/src/clis/notebooklm/summary.test.ts +94 -0
  281. package/src/clis/notebooklm/summary.ts +45 -0
  282. package/src/clis/notebooklm/utils.test.ts +446 -0
  283. package/src/clis/notebooklm/utils.ts +893 -0
  284. package/src/clis/substack/utils.test.ts +54 -0
  285. package/src/clis/substack/utils.ts +10 -2
  286. package/src/clis/v2ex/hot.yaml +4 -1
  287. package/src/clis/v2ex/latest.yaml +4 -1
  288. package/src/clis/v2ex/topic.yaml +6 -1
  289. package/src/clis/weixin/download.ts +95 -6
  290. package/src/clis/weread/book.ts +142 -2
  291. package/src/clis/weread/commands.test.ts +314 -154
  292. package/src/clis/weread/utils.ts +33 -4
  293. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  294. package/src/clis/xiaohongshu/comments.ts +76 -17
  295. package/src/clis/xiaohongshu/download.test.ts +96 -0
  296. package/src/clis/xiaohongshu/download.ts +83 -22
  297. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  298. package/src/clis/xiaohongshu/note.test.ts +164 -0
  299. package/src/clis/xiaohongshu/note.ts +86 -0
  300. package/src/clis/xiaohongshu/search.test.ts +11 -4
  301. package/src/clis/xiaohongshu/search.ts +13 -0
  302. package/src/clis/youtube/search.ts +57 -17
  303. package/src/clis/zhihu/question.test.ts +71 -0
  304. package/src/clis/zhihu/question.ts +27 -15
  305. package/src/commanderAdapter.test.ts +30 -0
  306. package/src/commanderAdapter.ts +7 -0
  307. package/src/commands/daemon.test.ts +238 -0
  308. package/src/commands/daemon.ts +135 -0
  309. package/src/completion.ts +2 -1
  310. package/src/constants.ts +3 -0
  311. package/src/daemon.test.ts +88 -0
  312. package/src/daemon.ts +26 -14
  313. package/src/discovery.ts +52 -2
  314. package/src/electron-apps.test.ts +50 -0
  315. package/src/electron-apps.ts +89 -0
  316. package/src/engine.test.ts +45 -9
  317. package/src/execution.ts +24 -19
  318. package/src/idle-manager.ts +60 -0
  319. package/src/launcher.test.ts +67 -0
  320. package/src/launcher.ts +185 -0
  321. package/src/main.ts +3 -2
  322. package/src/registry.test.ts +15 -0
  323. package/src/registry.ts +32 -3
  324. package/src/runtime.ts +13 -7
  325. package/src/serialization.test.ts +19 -1
  326. package/src/serialization.ts +2 -0
  327. package/src/tui.test.ts +23 -0
  328. package/src/tui.ts +65 -0
  329. package/src/weixin-download.test.ts +27 -0
  330. package/tests/e2e/browser-public-extended.test.ts +6 -2
  331. package/chatwise-opencli.ps1 +0 -82
  332. package/dist/clis/chatwise/shared.d.ts +0 -2
  333. package/dist/clis/chatwise/shared.js +0 -6
  334. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,238 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ vi.mock('chalk', () => ({
4
+ default: {
5
+ green: (s: string) => s,
6
+ yellow: (s: string) => s,
7
+ red: (s: string) => s,
8
+ dim: (s: string) => s,
9
+ },
10
+ }));
11
+
12
+ const mockConnect = vi.fn();
13
+ vi.mock('../browser/mcp.js', () => ({
14
+ BrowserBridge: class {
15
+ connect = mockConnect;
16
+ },
17
+ }));
18
+
19
+ import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
20
+
21
+ describe('daemon commands', () => {
22
+ let logSpy: ReturnType<typeof vi.spyOn>;
23
+ let errorSpy: ReturnType<typeof vi.spyOn>;
24
+
25
+ beforeEach(() => {
26
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
27
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ mockConnect.mockReset();
33
+ });
34
+
35
+ describe('daemonStatus', () => {
36
+ it('shows "not running" when daemon is unreachable', async () => {
37
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
38
+
39
+ await daemonStatus();
40
+
41
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
42
+ });
43
+
44
+ it('shows "not running" when daemon returns non-ok response', async () => {
45
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
46
+
47
+ await daemonStatus();
48
+
49
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
50
+ });
51
+
52
+ it('shows daemon info when running', async () => {
53
+ const status = {
54
+ ok: true,
55
+ pid: 12345,
56
+ uptime: 3661,
57
+ extensionConnected: true,
58
+ pending: 0,
59
+ lastCliRequestTime: Date.now() - 30_000,
60
+ memoryMB: 64,
61
+ port: 19825,
62
+ };
63
+
64
+ vi.stubGlobal(
65
+ 'fetch',
66
+ vi.fn().mockResolvedValue({
67
+ ok: true,
68
+ json: () => Promise.resolve(status),
69
+ }),
70
+ );
71
+
72
+ await daemonStatus();
73
+
74
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
75
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
76
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
77
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
78
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
79
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
80
+ });
81
+
82
+ it('shows disconnected when extension is not connected', async () => {
83
+ const status = {
84
+ ok: true,
85
+ pid: 99,
86
+ uptime: 120,
87
+ extensionConnected: false,
88
+ pending: 0,
89
+ lastCliRequestTime: Date.now() - 5000,
90
+ memoryMB: 32,
91
+ port: 19825,
92
+ };
93
+
94
+ vi.stubGlobal(
95
+ 'fetch',
96
+ vi.fn().mockResolvedValue({
97
+ ok: true,
98
+ json: () => Promise.resolve(status),
99
+ }),
100
+ );
101
+
102
+ await daemonStatus();
103
+
104
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
105
+ });
106
+ });
107
+
108
+ describe('daemonStop', () => {
109
+ it('reports "not running" when daemon is unreachable', async () => {
110
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
111
+
112
+ await daemonStop();
113
+
114
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
115
+ });
116
+
117
+ it('sends shutdown and reports success', async () => {
118
+ const statusResponse = {
119
+ ok: true,
120
+ json: () =>
121
+ Promise.resolve({
122
+ ok: true,
123
+ pid: 12345,
124
+ uptime: 100,
125
+ extensionConnected: true,
126
+ pending: 0,
127
+ lastCliRequestTime: Date.now(),
128
+ memoryMB: 50,
129
+ port: 19825,
130
+ }),
131
+ };
132
+ const shutdownResponse = { ok: true };
133
+
134
+ const mockFetch = vi.fn()
135
+ .mockResolvedValueOnce(statusResponse)
136
+ .mockResolvedValueOnce(shutdownResponse);
137
+ vi.stubGlobal('fetch', mockFetch);
138
+
139
+ await daemonStop();
140
+
141
+ // Verify shutdown was called with POST
142
+ expect(mockFetch).toHaveBeenCalledTimes(2);
143
+ const shutdownCall = mockFetch.mock.calls[1];
144
+ expect(shutdownCall[0]).toContain('/shutdown');
145
+ expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
146
+
147
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
148
+ });
149
+
150
+ it('reports failure when shutdown request fails', async () => {
151
+ const statusResponse = {
152
+ ok: true,
153
+ json: () =>
154
+ Promise.resolve({
155
+ ok: true,
156
+ pid: 12345,
157
+ uptime: 100,
158
+ extensionConnected: true,
159
+ pending: 0,
160
+ lastCliRequestTime: Date.now(),
161
+ memoryMB: 50,
162
+ port: 19825,
163
+ }),
164
+ };
165
+ const shutdownResponse = { ok: false };
166
+
167
+ const mockFetch = vi.fn()
168
+ .mockResolvedValueOnce(statusResponse)
169
+ .mockResolvedValueOnce(shutdownResponse);
170
+ vi.stubGlobal('fetch', mockFetch);
171
+
172
+ await daemonStop();
173
+
174
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
175
+ });
176
+ });
177
+
178
+ describe('daemonRestart', () => {
179
+ const statusData = {
180
+ ok: true,
181
+ pid: 12345,
182
+ uptime: 100,
183
+ extensionConnected: true,
184
+ pending: 0,
185
+ lastCliRequestTime: Date.now(),
186
+ memoryMB: 50,
187
+ port: 19825,
188
+ };
189
+
190
+ it('starts daemon directly when not running', async () => {
191
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
192
+ mockConnect.mockResolvedValue(undefined);
193
+
194
+ await daemonRestart();
195
+
196
+ expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
197
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
198
+ });
199
+
200
+ it('stops then starts when daemon is running', async () => {
201
+ const mockFetch = vi.fn()
202
+ // First call: fetchStatus in daemonRestart — daemon is running
203
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
204
+ // Second call: requestShutdown — success
205
+ .mockResolvedValueOnce({ ok: true })
206
+ // Subsequent calls: polling fetchStatus until unreachable
207
+ .mockRejectedValue(new Error('ECONNREFUSED'));
208
+
209
+ vi.stubGlobal('fetch', mockFetch);
210
+ mockConnect.mockResolvedValue(undefined);
211
+
212
+ await daemonRestart();
213
+
214
+ // Verify shutdown was called
215
+ const shutdownCall = mockFetch.mock.calls[1];
216
+ expect(shutdownCall[0]).toContain('/shutdown');
217
+ expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
218
+
219
+ expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
220
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
221
+ });
222
+
223
+ it('aborts when shutdown fails', async () => {
224
+ const mockFetch = vi.fn()
225
+ // fetchStatus — daemon is running
226
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
227
+ // requestShutdown — failure
228
+ .mockResolvedValueOnce({ ok: false });
229
+
230
+ vi.stubGlobal('fetch', mockFetch);
231
+
232
+ await daemonRestart();
233
+
234
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
235
+ expect(mockConnect).not.toHaveBeenCalled();
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * CLI commands for daemon lifecycle management:
3
+ * opencli daemon status — show daemon state
4
+ * opencli daemon stop — graceful shutdown
5
+ * opencli daemon restart — stop + respawn
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { DEFAULT_DAEMON_PORT } from '../constants.js';
10
+
11
+ const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
12
+ const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
13
+
14
+ interface DaemonStatus {
15
+ ok: boolean;
16
+ pid: number;
17
+ uptime: number;
18
+ extensionConnected: boolean;
19
+ pending: number;
20
+ lastCliRequestTime: number;
21
+ memoryMB: number;
22
+ port: number;
23
+ }
24
+
25
+ async function fetchStatus(): Promise<DaemonStatus | null> {
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), 2000);
28
+ try {
29
+ const res = await fetch(`${DAEMON_URL}/status`, {
30
+ headers: { 'X-OpenCLI': '1' },
31
+ signal: controller.signal,
32
+ });
33
+ if (!res.ok) return null;
34
+ return await res.json() as DaemonStatus;
35
+ } catch {
36
+ return null;
37
+ } finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+
42
+ async function requestShutdown(): Promise<boolean> {
43
+ const controller = new AbortController();
44
+ const timer = setTimeout(() => controller.abort(), 5000);
45
+ try {
46
+ const res = await fetch(`${DAEMON_URL}/shutdown`, {
47
+ method: 'POST',
48
+ headers: { 'X-OpenCLI': '1' },
49
+ signal: controller.signal,
50
+ });
51
+ return res.ok;
52
+ } catch {
53
+ return false;
54
+ } finally {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+
59
+ function formatUptime(seconds: number): string {
60
+ const h = Math.floor(seconds / 3600);
61
+ const m = Math.floor((seconds % 3600) / 60);
62
+ if (h > 0) return `${h}h ${m}m`;
63
+ if (m > 0) return `${m}m`;
64
+ return `${Math.floor(seconds)}s`;
65
+ }
66
+
67
+ function formatTimeSince(timestampMs: number): string {
68
+ const seconds = (Date.now() - timestampMs) / 1000;
69
+ if (seconds < 60) return `${Math.floor(seconds)}s ago`;
70
+ const m = Math.floor(seconds / 60);
71
+ if (m < 60) return `${m} min ago`;
72
+ const h = Math.floor(m / 60);
73
+ return `${h}h ${m % 60}m ago`;
74
+ }
75
+
76
+ export async function daemonStatus(): Promise<void> {
77
+ const status = await fetchStatus();
78
+ if (!status) {
79
+ console.log(`Daemon: ${chalk.dim('not running')}`);
80
+ return;
81
+ }
82
+
83
+ console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
84
+ console.log(`Uptime: ${formatUptime(status.uptime)}`);
85
+ console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
86
+ console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
87
+ console.log(`Memory: ${status.memoryMB} MB`);
88
+ console.log(`Port: ${status.port}`);
89
+ }
90
+
91
+ export async function daemonStop(): Promise<void> {
92
+ const status = await fetchStatus();
93
+ if (!status) {
94
+ console.log(chalk.dim('Daemon is not running.'));
95
+ return;
96
+ }
97
+
98
+ const ok = await requestShutdown();
99
+ if (ok) {
100
+ console.log(chalk.green('Daemon stopped.'));
101
+ } else {
102
+ console.error(chalk.red('Failed to stop daemon.'));
103
+ process.exitCode = 1;
104
+ }
105
+ }
106
+
107
+ export async function daemonRestart(): Promise<void> {
108
+ const status = await fetchStatus();
109
+ if (status) {
110
+ const ok = await requestShutdown();
111
+ if (!ok) {
112
+ console.error(chalk.red('Failed to stop daemon.'));
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ // Wait for daemon to actually exit (poll until unreachable)
117
+ const deadline = Date.now() + 5000;
118
+ while (Date.now() < deadline) {
119
+ await new Promise(r => setTimeout(r, 200));
120
+ if (!(await fetchStatus())) break;
121
+ }
122
+ }
123
+
124
+ // Import BrowserBridge to spawn a new daemon
125
+ const { BrowserBridge } = await import('../browser/mcp.js');
126
+ const bridge = new BrowserBridge();
127
+ try {
128
+ console.log('Starting daemon...');
129
+ await bridge.connect({ timeout: 10 });
130
+ console.log(chalk.green('Daemon restarted.'));
131
+ } catch (err) {
132
+ console.error(chalk.red(`Failed to restart daemon: ${err instanceof Error ? err.message : err}`));
133
+ process.exitCode = 1;
134
+ }
135
+ }
package/src/completion.ts CHANGED
@@ -57,9 +57,10 @@ export function getCompletions(words: string[], cursor: number): string[] {
57
57
  for (const [, cmd] of getRegistry()) {
58
58
  if (cmd.site === site) {
59
59
  subcommands.push(cmd.name);
60
+ if (cmd.aliases?.length) subcommands.push(...cmd.aliases);
60
61
  }
61
62
  }
62
- return subcommands.sort();
63
+ return [...new Set(subcommands)].sort();
63
64
  }
64
65
 
65
66
  // cursor >= 3 → no further completion
package/src/constants.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  /** Default daemon port for HTTP/WebSocket communication with browser extension */
6
6
  export const DEFAULT_DAEMON_PORT = 19825;
7
7
 
8
+ /** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
9
+ export const DEFAULT_DAEMON_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
10
+
8
11
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
9
12
  export const VOLATILE_PARAMS = new Set([
10
13
  'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { IdleManager } from './idle-manager.js';
3
+
4
+ describe('IdleManager', () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ });
8
+
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+
13
+ it('does not start timer when extension is connected', () => {
14
+ const exit = vi.fn();
15
+ const mgr = new IdleManager(300_000, exit);
16
+
17
+ mgr.setExtensionConnected(true);
18
+ mgr.onCliRequest();
19
+
20
+ vi.advanceTimersByTime(300_000 + 1000);
21
+ expect(exit).not.toHaveBeenCalled();
22
+ });
23
+
24
+ it('starts timer when extension disconnects and CLI is idle', () => {
25
+ const exit = vi.fn();
26
+ const mgr = new IdleManager(300_000, exit);
27
+
28
+ mgr.onCliRequest();
29
+ mgr.setExtensionConnected(true);
30
+ mgr.setExtensionConnected(false);
31
+
32
+ expect(exit).not.toHaveBeenCalled();
33
+
34
+ vi.advanceTimersByTime(300_000 + 1000);
35
+ expect(exit).toHaveBeenCalledTimes(1);
36
+ });
37
+
38
+ it('exits immediately on extension disconnect if CLI has been idle past timeout', () => {
39
+ const exit = vi.fn();
40
+ const mgr = new IdleManager(300_000, exit);
41
+
42
+ mgr.onCliRequest();
43
+ mgr.setExtensionConnected(true); // connect before timeout elapses
44
+ vi.advanceTimersByTime(400_000); // CLI idle time exceeds timeout, but extension is connected so no exit
45
+
46
+ expect(exit).not.toHaveBeenCalled();
47
+
48
+ mgr.setExtensionConnected(false); // disconnect → should exit immediately since CLI idle > timeout
49
+
50
+ expect(exit).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ it('resets timer on new CLI request', () => {
54
+ const exit = vi.fn();
55
+ const mgr = new IdleManager(300_000, exit);
56
+
57
+ mgr.onCliRequest();
58
+ vi.advanceTimersByTime(200_000);
59
+ mgr.onCliRequest();
60
+
61
+ vi.advanceTimersByTime(200_000);
62
+ expect(exit).not.toHaveBeenCalled();
63
+
64
+ vi.advanceTimersByTime(100_001);
65
+ expect(exit).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('does not exit when timeout is 0 (disabled)', () => {
69
+ const exit = vi.fn();
70
+ const mgr = new IdleManager(0, exit);
71
+
72
+ mgr.onCliRequest();
73
+ vi.advanceTimersByTime(24 * 60 * 60 * 1000);
74
+ expect(exit).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('clears timer when extension connects', () => {
78
+ const exit = vi.fn();
79
+ const mgr = new IdleManager(300_000, exit);
80
+
81
+ mgr.onCliRequest();
82
+ vi.advanceTimersByTime(200_000);
83
+
84
+ mgr.setExtensionConnected(true);
85
+ vi.advanceTimersByTime(200_000);
86
+ expect(exit).not.toHaveBeenCalled();
87
+ });
88
+ });
package/src/daemon.ts CHANGED
@@ -15,17 +15,18 @@
15
15
  *
16
16
  * Lifecycle:
17
17
  * - Auto-spawned by opencli on first browser command
18
- * - Auto-exits after 5 minutes of idle
18
+ * - Auto-exits after idle timeout (default 4h, configurable via OPENCLI_DAEMON_TIMEOUT)
19
19
  * - Listens on localhost:19825
20
20
  */
21
21
 
22
22
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
23
23
  import { WebSocketServer, WebSocket, type RawData } from 'ws';
24
- import { DEFAULT_DAEMON_PORT } from './constants.js';
24
+ import { DEFAULT_DAEMON_PORT, DEFAULT_DAEMON_IDLE_TIMEOUT } from './constants.js';
25
25
  import { EXIT_CODES } from './errors.js';
26
+ import { IdleManager } from './idle-manager.js';
26
27
 
27
28
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
28
- const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
29
+ const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_DAEMON_IDLE_TIMEOUT);
29
30
 
30
31
  // ─── State ───────────────────────────────────────────────────────────
31
32
 
@@ -36,8 +37,6 @@ const pending = new Map<string, {
36
37
  reject: (error: Error) => void;
37
38
  timer: ReturnType<typeof setTimeout>;
38
39
  }>();
39
- let idleTimer: ReturnType<typeof setTimeout> | null = null;
40
-
41
40
  // Extension log ring buffer
42
41
  interface LogEntry { level: string; msg: string; ts: number; }
43
42
  const LOG_BUFFER_SIZE = 200;
@@ -50,13 +49,10 @@ function pushLog(entry: LogEntry): void {
50
49
 
51
50
  // ─── Idle auto-exit ──────────────────────────────────────────────────
52
51
 
53
- function resetIdleTimer(): void {
54
- if (idleTimer) clearTimeout(idleTimer);
55
- idleTimer = setTimeout(() => {
56
- console.error('[daemon] Idle timeout, shutting down');
57
- process.exit(EXIT_CODES.SUCCESS);
58
- }, IDLE_TIMEOUT);
59
- }
52
+ const idleManager = new IdleManager(IDLE_TIMEOUT, () => {
53
+ console.error('[daemon] Idle timeout (no CLI requests + no Extension), shutting down');
54
+ process.exit(EXIT_CODES.SUCCESS);
55
+ });
60
56
 
61
57
  // ─── HTTP Server ─────────────────────────────────────────────────────
62
58
 
@@ -128,11 +124,18 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
128
124
  }
129
125
 
130
126
  if (req.method === 'GET' && pathname === '/status') {
127
+ const uptime = process.uptime();
128
+ const mem = process.memoryUsage();
131
129
  jsonResponse(res, 200, {
132
130
  ok: true,
131
+ pid: process.pid,
132
+ uptime,
133
133
  extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
134
134
  extensionVersion,
135
135
  pending: pending.size,
136
+ lastCliRequestTime: idleManager.lastCliRequestTime,
137
+ memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
138
+ port: PORT,
136
139
  });
137
140
  return;
138
141
  }
@@ -153,8 +156,14 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
153
156
  return;
154
157
  }
155
158
 
159
+ if (req.method === 'POST' && pathname === '/shutdown') {
160
+ jsonResponse(res, 200, { ok: true, message: 'Shutting down' });
161
+ setTimeout(() => shutdown(), 100);
162
+ return;
163
+ }
164
+
156
165
  if (req.method === 'POST' && url === '/command') {
157
- resetIdleTimer();
166
+ idleManager.onCliRequest();
158
167
  try {
159
168
  const body = JSON.parse(await readBody(req));
160
169
  if (!body.id) {
@@ -212,6 +221,7 @@ wss.on('connection', (ws: WebSocket) => {
212
221
  console.error('[daemon] Extension connected');
213
222
  extensionWs = ws;
214
223
  extensionVersion = null; // cleared until hello message arrives
224
+ idleManager.setExtensionConnected(true);
215
225
 
216
226
  // ── Heartbeat: ping every 15s, close if 2 pongs missed ──
217
227
  let missedPongs = 0;
@@ -270,6 +280,7 @@ wss.on('connection', (ws: WebSocket) => {
270
280
  if (extensionWs === ws) {
271
281
  extensionWs = null;
272
282
  extensionVersion = null;
283
+ idleManager.setExtensionConnected(false);
273
284
  // Reject all pending requests since the extension is gone
274
285
  for (const [id, p] of pending) {
275
286
  clearTimeout(p.timer);
@@ -284,6 +295,7 @@ wss.on('connection', (ws: WebSocket) => {
284
295
  if (extensionWs === ws) {
285
296
  extensionWs = null;
286
297
  extensionVersion = null;
298
+ idleManager.setExtensionConnected(false);
287
299
  // Reject pending requests in case 'close' does not follow this 'error'
288
300
  for (const [, p] of pending) {
289
301
  clearTimeout(p.timer);
@@ -298,7 +310,7 @@ wss.on('connection', (ws: WebSocket) => {
298
310
 
299
311
  httpServer.listen(PORT, '127.0.0.1', () => {
300
312
  console.error(`[daemon] Listening on http://127.0.0.1:${PORT}`);
301
- resetIdleTimer();
313
+ idleManager.onCliRequest();
302
314
  });
303
315
 
304
316
  httpServer.on('error', (err: NodeJS.ErrnoException) => {
package/src/discovery.ts CHANGED
@@ -11,15 +11,19 @@
11
11
  import * as fs from 'node:fs';
12
12
  import * as os from 'node:os';
13
13
  import * as path from 'node:path';
14
- import { pathToFileURL } from 'node:url';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
  import yaml from 'js-yaml';
16
16
  import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerCommand } from './registry.js';
17
17
  import { getErrorMessage } from './errors.js';
18
18
  import { log } from './logger.js';
19
19
  import type { ManifestEntry } from './build-manifest.js';
20
20
 
21
+ /** User runtime directory: ~/.opencli */
22
+ export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
23
+ /** User CLIs directory: ~/.opencli/clis */
24
+ export const USER_CLIS_DIR = path.join(USER_OPENCLI_DIR, 'clis');
21
25
  /** Plugins directory: ~/.opencli/plugins/ */
22
- export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
26
+ export const PLUGINS_DIR = path.join(USER_OPENCLI_DIR, 'plugins');
23
27
  /** Matches files that register commands via cli() or lifecycle hooks */
24
28
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
25
29
 
@@ -33,6 +37,47 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
33
37
 
34
38
  import { isRecord } from './utils.js';
35
39
 
40
+ function resolveHostRuntimeModulePath(moduleName: string): string {
41
+ const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
42
+ for (const ext of ['.js', '.ts']) {
43
+ const candidate = path.join(runtimeDir, `${moduleName}${ext}`);
44
+ if (fs.existsSync(candidate)) return candidate;
45
+ }
46
+ return path.join(runtimeDir, `${moduleName}.js`);
47
+ }
48
+
49
+ async function writeCompatShimIfNeeded(filePath: string, content: string): Promise<void> {
50
+ try {
51
+ const existing = await fs.promises.readFile(filePath, 'utf-8');
52
+ if (existing === content) return;
53
+ } catch {
54
+ // Fall through to write missing shim
55
+ }
56
+ await fs.promises.writeFile(filePath, content, 'utf-8');
57
+ }
58
+
59
+ /**
60
+ * Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
61
+ * importing ../../registry(.js) and ../../errors(.js).
62
+ */
63
+ export async function ensureUserCliCompatShims(baseDir: string = USER_OPENCLI_DIR): Promise<void> {
64
+ await fs.promises.mkdir(baseDir, { recursive: true });
65
+
66
+ const registryUrl = pathToFileURL(resolveHostRuntimeModulePath('registry-api')).href;
67
+ const errorsUrl = pathToFileURL(resolveHostRuntimeModulePath('errors')).href;
68
+
69
+ await Promise.all([
70
+ writeCompatShimIfNeeded(path.join(baseDir, 'registry'), `export * from '${registryUrl}';\n`),
71
+ writeCompatShimIfNeeded(path.join(baseDir, 'registry.js'), `export * from '${registryUrl}';\n`),
72
+ writeCompatShimIfNeeded(path.join(baseDir, 'errors'), `export * from '${errorsUrl}';\n`),
73
+ writeCompatShimIfNeeded(path.join(baseDir, 'errors.js'), `export * from '${errorsUrl}';\n`),
74
+ writeCompatShimIfNeeded(
75
+ path.join(baseDir, 'package.json'),
76
+ `${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`,
77
+ ),
78
+ ]);
79
+ }
80
+
36
81
  /**
37
82
  * Discover and register CLI commands.
38
83
  * Uses pre-compiled manifest when available for instant startup.
@@ -68,6 +113,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
68
113
  const cmd: CliCommand = {
69
114
  site: entry.site,
70
115
  name: entry.name,
116
+ aliases: entry.aliases,
71
117
  description: entry.description ?? '',
72
118
  domain: entry.domain,
73
119
  strategy,
@@ -90,6 +136,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
90
136
  const cmd: InternalCliCommand = {
91
137
  site: entry.site,
92
138
  name: entry.name,
139
+ aliases: entry.aliases,
93
140
  description: entry.description ?? '',
94
141
  domain: entry.domain,
95
142
  strategy,
@@ -163,6 +210,9 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
163
210
  const cmd: CliCommand = {
164
211
  site,
165
212
  name,
213
+ aliases: isRecord(cliDef) && Array.isArray((cliDef as Record<string, unknown>).aliases)
214
+ ? ((cliDef as Record<string, unknown>).aliases as unknown[]).filter((value): value is string => typeof value === 'string')
215
+ : undefined,
166
216
  description: cliDef.description ?? '',
167
217
  domain: cliDef.domain,
168
218
  strategy,