@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,124 @@
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
+ import chalk from 'chalk';
8
+ import { DEFAULT_DAEMON_PORT } from '../constants.js';
9
+ const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
10
+ const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
11
+ async function fetchStatus() {
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), 2000);
14
+ try {
15
+ const res = await fetch(`${DAEMON_URL}/status`, {
16
+ headers: { 'X-OpenCLI': '1' },
17
+ signal: controller.signal,
18
+ });
19
+ if (!res.ok)
20
+ return null;
21
+ return await res.json();
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ finally {
27
+ clearTimeout(timer);
28
+ }
29
+ }
30
+ async function requestShutdown() {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), 5000);
33
+ try {
34
+ const res = await fetch(`${DAEMON_URL}/shutdown`, {
35
+ method: 'POST',
36
+ headers: { 'X-OpenCLI': '1' },
37
+ signal: controller.signal,
38
+ });
39
+ return res.ok;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ finally {
45
+ clearTimeout(timer);
46
+ }
47
+ }
48
+ function formatUptime(seconds) {
49
+ const h = Math.floor(seconds / 3600);
50
+ const m = Math.floor((seconds % 3600) / 60);
51
+ if (h > 0)
52
+ return `${h}h ${m}m`;
53
+ if (m > 0)
54
+ return `${m}m`;
55
+ return `${Math.floor(seconds)}s`;
56
+ }
57
+ function formatTimeSince(timestampMs) {
58
+ const seconds = (Date.now() - timestampMs) / 1000;
59
+ if (seconds < 60)
60
+ return `${Math.floor(seconds)}s ago`;
61
+ const m = Math.floor(seconds / 60);
62
+ if (m < 60)
63
+ return `${m} min ago`;
64
+ const h = Math.floor(m / 60);
65
+ return `${h}h ${m % 60}m ago`;
66
+ }
67
+ export async function daemonStatus() {
68
+ const status = await fetchStatus();
69
+ if (!status) {
70
+ console.log(`Daemon: ${chalk.dim('not running')}`);
71
+ return;
72
+ }
73
+ console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
74
+ console.log(`Uptime: ${formatUptime(status.uptime)}`);
75
+ console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
76
+ console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
77
+ console.log(`Memory: ${status.memoryMB} MB`);
78
+ console.log(`Port: ${status.port}`);
79
+ }
80
+ export async function daemonStop() {
81
+ const status = await fetchStatus();
82
+ if (!status) {
83
+ console.log(chalk.dim('Daemon is not running.'));
84
+ return;
85
+ }
86
+ const ok = await requestShutdown();
87
+ if (ok) {
88
+ console.log(chalk.green('Daemon stopped.'));
89
+ }
90
+ else {
91
+ console.error(chalk.red('Failed to stop daemon.'));
92
+ process.exitCode = 1;
93
+ }
94
+ }
95
+ export async function daemonRestart() {
96
+ const status = await fetchStatus();
97
+ if (status) {
98
+ const ok = await requestShutdown();
99
+ if (!ok) {
100
+ console.error(chalk.red('Failed to stop daemon.'));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ // Wait for daemon to actually exit (poll until unreachable)
105
+ const deadline = Date.now() + 5000;
106
+ while (Date.now() < deadline) {
107
+ await new Promise(r => setTimeout(r, 200));
108
+ if (!(await fetchStatus()))
109
+ break;
110
+ }
111
+ }
112
+ // Import BrowserBridge to spawn a new daemon
113
+ const { BrowserBridge } = await import('../browser/mcp.js');
114
+ const bridge = new BrowserBridge();
115
+ try {
116
+ console.log('Starting daemon...');
117
+ await bridge.connect({ timeout: 10 });
118
+ console.log(chalk.green('Daemon restarted.'));
119
+ }
120
+ catch (err) {
121
+ console.error(chalk.red(`Failed to restart daemon: ${err instanceof Error ? err.message : err}`));
122
+ process.exitCode = 1;
123
+ }
124
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ vi.mock('chalk', () => ({
3
+ default: {
4
+ green: (s) => s,
5
+ yellow: (s) => s,
6
+ red: (s) => s,
7
+ dim: (s) => s,
8
+ },
9
+ }));
10
+ const mockConnect = vi.fn();
11
+ vi.mock('../browser/mcp.js', () => ({
12
+ BrowserBridge: class {
13
+ connect = mockConnect;
14
+ },
15
+ }));
16
+ import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
17
+ describe('daemon commands', () => {
18
+ let logSpy;
19
+ let errorSpy;
20
+ beforeEach(() => {
21
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
22
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
23
+ });
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ mockConnect.mockReset();
27
+ });
28
+ describe('daemonStatus', () => {
29
+ it('shows "not running" when daemon is unreachable', async () => {
30
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
31
+ await daemonStatus();
32
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
33
+ });
34
+ it('shows "not running" when daemon returns non-ok response', async () => {
35
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
36
+ await daemonStatus();
37
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
38
+ });
39
+ it('shows daemon info when running', async () => {
40
+ const status = {
41
+ ok: true,
42
+ pid: 12345,
43
+ uptime: 3661,
44
+ extensionConnected: true,
45
+ pending: 0,
46
+ lastCliRequestTime: Date.now() - 30_000,
47
+ memoryMB: 64,
48
+ port: 19825,
49
+ };
50
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
51
+ ok: true,
52
+ json: () => Promise.resolve(status),
53
+ }));
54
+ await daemonStatus();
55
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
56
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
57
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
58
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
59
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
60
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
61
+ });
62
+ it('shows disconnected when extension is not connected', async () => {
63
+ const status = {
64
+ ok: true,
65
+ pid: 99,
66
+ uptime: 120,
67
+ extensionConnected: false,
68
+ pending: 0,
69
+ lastCliRequestTime: Date.now() - 5000,
70
+ memoryMB: 32,
71
+ port: 19825,
72
+ };
73
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
74
+ ok: true,
75
+ json: () => Promise.resolve(status),
76
+ }));
77
+ await daemonStatus();
78
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
79
+ });
80
+ });
81
+ describe('daemonStop', () => {
82
+ it('reports "not running" when daemon is unreachable', async () => {
83
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
84
+ await daemonStop();
85
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
86
+ });
87
+ it('sends shutdown and reports success', async () => {
88
+ const statusResponse = {
89
+ ok: true,
90
+ json: () => Promise.resolve({
91
+ ok: true,
92
+ pid: 12345,
93
+ uptime: 100,
94
+ extensionConnected: true,
95
+ pending: 0,
96
+ lastCliRequestTime: Date.now(),
97
+ memoryMB: 50,
98
+ port: 19825,
99
+ }),
100
+ };
101
+ const shutdownResponse = { ok: true };
102
+ const mockFetch = vi.fn()
103
+ .mockResolvedValueOnce(statusResponse)
104
+ .mockResolvedValueOnce(shutdownResponse);
105
+ vi.stubGlobal('fetch', mockFetch);
106
+ await daemonStop();
107
+ // Verify shutdown was called with POST
108
+ expect(mockFetch).toHaveBeenCalledTimes(2);
109
+ const shutdownCall = mockFetch.mock.calls[1];
110
+ expect(shutdownCall[0]).toContain('/shutdown');
111
+ expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
112
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
113
+ });
114
+ it('reports failure when shutdown request fails', async () => {
115
+ const statusResponse = {
116
+ ok: true,
117
+ json: () => Promise.resolve({
118
+ ok: true,
119
+ pid: 12345,
120
+ uptime: 100,
121
+ extensionConnected: true,
122
+ pending: 0,
123
+ lastCliRequestTime: Date.now(),
124
+ memoryMB: 50,
125
+ port: 19825,
126
+ }),
127
+ };
128
+ const shutdownResponse = { ok: false };
129
+ const mockFetch = vi.fn()
130
+ .mockResolvedValueOnce(statusResponse)
131
+ .mockResolvedValueOnce(shutdownResponse);
132
+ vi.stubGlobal('fetch', mockFetch);
133
+ await daemonStop();
134
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
135
+ });
136
+ });
137
+ describe('daemonRestart', () => {
138
+ const statusData = {
139
+ ok: true,
140
+ pid: 12345,
141
+ uptime: 100,
142
+ extensionConnected: true,
143
+ pending: 0,
144
+ lastCliRequestTime: Date.now(),
145
+ memoryMB: 50,
146
+ port: 19825,
147
+ };
148
+ it('starts daemon directly when not running', async () => {
149
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
150
+ mockConnect.mockResolvedValue(undefined);
151
+ await daemonRestart();
152
+ expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
153
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
154
+ });
155
+ it('stops then starts when daemon is running', async () => {
156
+ const mockFetch = vi.fn()
157
+ // First call: fetchStatus in daemonRestart — daemon is running
158
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
159
+ // Second call: requestShutdown — success
160
+ .mockResolvedValueOnce({ ok: true })
161
+ // Subsequent calls: polling fetchStatus until unreachable
162
+ .mockRejectedValue(new Error('ECONNREFUSED'));
163
+ vi.stubGlobal('fetch', mockFetch);
164
+ mockConnect.mockResolvedValue(undefined);
165
+ await daemonRestart();
166
+ // Verify shutdown was called
167
+ const shutdownCall = mockFetch.mock.calls[1];
168
+ expect(shutdownCall[0]).toContain('/shutdown');
169
+ expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
170
+ expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
171
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
172
+ });
173
+ it('aborts when shutdown fails', async () => {
174
+ const mockFetch = vi.fn()
175
+ // fetchStatus — daemon is running
176
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
177
+ // requestShutdown — failure
178
+ .mockResolvedValueOnce({ ok: false });
179
+ vi.stubGlobal('fetch', mockFetch);
180
+ await daemonRestart();
181
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
182
+ expect(mockConnect).not.toHaveBeenCalled();
183
+ });
184
+ });
185
+ });
@@ -50,9 +50,11 @@ export function getCompletions(words, cursor) {
50
50
  for (const [, cmd] of getRegistry()) {
51
51
  if (cmd.site === site) {
52
52
  subcommands.push(cmd.name);
53
+ if (cmd.aliases?.length)
54
+ subcommands.push(...cmd.aliases);
53
55
  }
54
56
  }
55
- return subcommands.sort();
57
+ return [...new Set(subcommands)].sort();
56
58
  }
57
59
  // cursor >= 3 → no further completion
58
60
  return [];
@@ -3,6 +3,8 @@
3
3
  */
4
4
  /** Default daemon port for HTTP/WebSocket communication with browser extension */
5
5
  export declare const DEFAULT_DAEMON_PORT = 19825;
6
+ /** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
7
+ export declare const DEFAULT_DAEMON_IDLE_TIMEOUT: number;
6
8
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
7
9
  export declare const VOLATILE_PARAMS: Set<string>;
8
10
  /** Search-related query parameter names */
package/dist/constants.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  /** Default daemon port for HTTP/WebSocket communication with browser extension */
5
5
  export const DEFAULT_DAEMON_PORT = 19825;
6
+ /** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
7
+ export const DEFAULT_DAEMON_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
6
8
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
7
9
  export const VOLATILE_PARAMS = new Set([
8
10
  'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
package/dist/daemon.d.ts CHANGED
@@ -15,7 +15,7 @@
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
  export {};
package/dist/daemon.js CHANGED
@@ -15,20 +15,20 @@
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
  import { createServer } from 'node:http';
22
22
  import { WebSocketServer, WebSocket } from 'ws';
23
- import { DEFAULT_DAEMON_PORT } from './constants.js';
23
+ import { DEFAULT_DAEMON_PORT, DEFAULT_DAEMON_IDLE_TIMEOUT } from './constants.js';
24
24
  import { EXIT_CODES } from './errors.js';
25
+ import { IdleManager } from './idle-manager.js';
25
26
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
26
- const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
27
+ const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_DAEMON_IDLE_TIMEOUT);
27
28
  // ─── State ───────────────────────────────────────────────────────────
28
29
  let extensionWs = null;
29
30
  let extensionVersion = null;
30
31
  const pending = new Map();
31
- let idleTimer = null;
32
32
  const LOG_BUFFER_SIZE = 200;
33
33
  const logBuffer = [];
34
34
  function pushLog(entry) {
@@ -37,14 +37,10 @@ function pushLog(entry) {
37
37
  logBuffer.shift();
38
38
  }
39
39
  // ─── Idle auto-exit ──────────────────────────────────────────────────
40
- function resetIdleTimer() {
41
- if (idleTimer)
42
- clearTimeout(idleTimer);
43
- idleTimer = setTimeout(() => {
44
- console.error('[daemon] Idle timeout, shutting down');
45
- process.exit(EXIT_CODES.SUCCESS);
46
- }, IDLE_TIMEOUT);
47
- }
40
+ const idleManager = new IdleManager(IDLE_TIMEOUT, () => {
41
+ console.error('[daemon] Idle timeout (no CLI requests + no Extension), shutting down');
42
+ process.exit(EXIT_CODES.SUCCESS);
43
+ });
48
44
  // ─── HTTP Server ─────────────────────────────────────────────────────
49
45
  const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
50
46
  function readBody(req) {
@@ -113,11 +109,18 @@ async function handleRequest(req, res) {
113
109
  return;
114
110
  }
115
111
  if (req.method === 'GET' && pathname === '/status') {
112
+ const uptime = process.uptime();
113
+ const mem = process.memoryUsage();
116
114
  jsonResponse(res, 200, {
117
115
  ok: true,
116
+ pid: process.pid,
117
+ uptime,
118
118
  extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
119
119
  extensionVersion,
120
120
  pending: pending.size,
121
+ lastCliRequestTime: idleManager.lastCliRequestTime,
122
+ memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
123
+ port: PORT,
121
124
  });
122
125
  return;
123
126
  }
@@ -135,8 +138,13 @@ async function handleRequest(req, res) {
135
138
  jsonResponse(res, 200, { ok: true });
136
139
  return;
137
140
  }
141
+ if (req.method === 'POST' && pathname === '/shutdown') {
142
+ jsonResponse(res, 200, { ok: true, message: 'Shutting down' });
143
+ setTimeout(() => shutdown(), 100);
144
+ return;
145
+ }
138
146
  if (req.method === 'POST' && url === '/command') {
139
- resetIdleTimer();
147
+ idleManager.onCliRequest();
140
148
  try {
141
149
  const body = JSON.parse(await readBody(req));
142
150
  if (!body.id) {
@@ -188,6 +196,7 @@ wss.on('connection', (ws) => {
188
196
  console.error('[daemon] Extension connected');
189
197
  extensionWs = ws;
190
198
  extensionVersion = null; // cleared until hello message arrives
199
+ idleManager.setExtensionConnected(true);
191
200
  // ── Heartbeat: ping every 15s, close if 2 pongs missed ──
192
201
  let missedPongs = 0;
193
202
  const heartbeatInterval = setInterval(() => {
@@ -240,6 +249,7 @@ wss.on('connection', (ws) => {
240
249
  if (extensionWs === ws) {
241
250
  extensionWs = null;
242
251
  extensionVersion = null;
252
+ idleManager.setExtensionConnected(false);
243
253
  // Reject all pending requests since the extension is gone
244
254
  for (const [id, p] of pending) {
245
255
  clearTimeout(p.timer);
@@ -253,6 +263,7 @@ wss.on('connection', (ws) => {
253
263
  if (extensionWs === ws) {
254
264
  extensionWs = null;
255
265
  extensionVersion = null;
266
+ idleManager.setExtensionConnected(false);
256
267
  // Reject pending requests in case 'close' does not follow this 'error'
257
268
  for (const [, p] of pending) {
258
269
  clearTimeout(p.timer);
@@ -265,7 +276,7 @@ wss.on('connection', (ws) => {
265
276
  // ─── Start ───────────────────────────────────────────────────────────
266
277
  httpServer.listen(PORT, '127.0.0.1', () => {
267
278
  console.error(`[daemon] Listening on http://127.0.0.1:${PORT}`);
268
- resetIdleTimer();
279
+ idleManager.onCliRequest();
269
280
  });
270
281
  httpServer.on('error', (err) => {
271
282
  if (err.code === 'EADDRINUSE') {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { IdleManager } from './idle-manager.js';
3
+ describe('IdleManager', () => {
4
+ beforeEach(() => {
5
+ vi.useFakeTimers();
6
+ });
7
+ afterEach(() => {
8
+ vi.useRealTimers();
9
+ });
10
+ it('does not start timer when extension is connected', () => {
11
+ const exit = vi.fn();
12
+ const mgr = new IdleManager(300_000, exit);
13
+ mgr.setExtensionConnected(true);
14
+ mgr.onCliRequest();
15
+ vi.advanceTimersByTime(300_000 + 1000);
16
+ expect(exit).not.toHaveBeenCalled();
17
+ });
18
+ it('starts timer when extension disconnects and CLI is idle', () => {
19
+ const exit = vi.fn();
20
+ const mgr = new IdleManager(300_000, exit);
21
+ mgr.onCliRequest();
22
+ mgr.setExtensionConnected(true);
23
+ mgr.setExtensionConnected(false);
24
+ expect(exit).not.toHaveBeenCalled();
25
+ vi.advanceTimersByTime(300_000 + 1000);
26
+ expect(exit).toHaveBeenCalledTimes(1);
27
+ });
28
+ it('exits immediately on extension disconnect if CLI has been idle past timeout', () => {
29
+ const exit = vi.fn();
30
+ const mgr = new IdleManager(300_000, exit);
31
+ mgr.onCliRequest();
32
+ mgr.setExtensionConnected(true); // connect before timeout elapses
33
+ vi.advanceTimersByTime(400_000); // CLI idle time exceeds timeout, but extension is connected so no exit
34
+ expect(exit).not.toHaveBeenCalled();
35
+ mgr.setExtensionConnected(false); // disconnect → should exit immediately since CLI idle > timeout
36
+ expect(exit).toHaveBeenCalledTimes(1);
37
+ });
38
+ it('resets timer on new CLI request', () => {
39
+ const exit = vi.fn();
40
+ const mgr = new IdleManager(300_000, exit);
41
+ mgr.onCliRequest();
42
+ vi.advanceTimersByTime(200_000);
43
+ mgr.onCliRequest();
44
+ vi.advanceTimersByTime(200_000);
45
+ expect(exit).not.toHaveBeenCalled();
46
+ vi.advanceTimersByTime(100_001);
47
+ expect(exit).toHaveBeenCalledTimes(1);
48
+ });
49
+ it('does not exit when timeout is 0 (disabled)', () => {
50
+ const exit = vi.fn();
51
+ const mgr = new IdleManager(0, exit);
52
+ mgr.onCliRequest();
53
+ vi.advanceTimersByTime(24 * 60 * 60 * 1000);
54
+ expect(exit).not.toHaveBeenCalled();
55
+ });
56
+ it('clears timer when extension connects', () => {
57
+ const exit = vi.fn();
58
+ const mgr = new IdleManager(300_000, exit);
59
+ mgr.onCliRequest();
60
+ vi.advanceTimersByTime(200_000);
61
+ mgr.setExtensionConnected(true);
62
+ vi.advanceTimersByTime(200_000);
63
+ expect(exit).not.toHaveBeenCalled();
64
+ });
65
+ });
@@ -7,8 +7,17 @@
7
7
  * TS modules are loaded lazily only when their command is executed.
8
8
  * 2. FALLBACK (filesystem scan): Traditional runtime discovery for development.
9
9
  */
10
+ /** User runtime directory: ~/.opencli */
11
+ export declare const USER_OPENCLI_DIR: string;
12
+ /** User CLIs directory: ~/.opencli/clis */
13
+ export declare const USER_CLIS_DIR: string;
10
14
  /** Plugins directory: ~/.opencli/plugins/ */
11
15
  export declare const PLUGINS_DIR: string;
16
+ /**
17
+ * Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
18
+ * importing ../../registry(.js) and ../../errors(.js).
19
+ */
20
+ export declare function ensureUserCliCompatShims(baseDir?: string): Promise<void>;
12
21
  /**
13
22
  * Discover and register CLI commands.
14
23
  * Uses pre-compiled manifest when available for instant startup.
package/dist/discovery.js CHANGED
@@ -10,13 +10,17 @@
10
10
  import * as fs from 'node:fs';
11
11
  import * as os from 'node:os';
12
12
  import * as path from 'node:path';
13
- import { pathToFileURL } from 'node:url';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
  import yaml from 'js-yaml';
15
15
  import { Strategy, registerCommand } from './registry.js';
16
16
  import { getErrorMessage } from './errors.js';
17
17
  import { log } from './logger.js';
18
+ /** User runtime directory: ~/.opencli */
19
+ export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
20
+ /** User CLIs directory: ~/.opencli/clis */
21
+ export const USER_CLIS_DIR = path.join(USER_OPENCLI_DIR, 'clis');
18
22
  /** Plugins directory: ~/.opencli/plugins/ */
19
- export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
23
+ export const PLUGINS_DIR = path.join(USER_OPENCLI_DIR, 'plugins');
20
24
  /** Matches files that register commands via cli() or lifecycle hooks */
21
25
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
22
26
  import { parseYamlArgs } from './yaml-schema.js';
@@ -27,6 +31,42 @@ function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
27
31
  return Strategy[key] ?? fallback;
28
32
  }
29
33
  import { isRecord } from './utils.js';
34
+ function resolveHostRuntimeModulePath(moduleName) {
35
+ const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
36
+ for (const ext of ['.js', '.ts']) {
37
+ const candidate = path.join(runtimeDir, `${moduleName}${ext}`);
38
+ if (fs.existsSync(candidate))
39
+ return candidate;
40
+ }
41
+ return path.join(runtimeDir, `${moduleName}.js`);
42
+ }
43
+ async function writeCompatShimIfNeeded(filePath, content) {
44
+ try {
45
+ const existing = await fs.promises.readFile(filePath, 'utf-8');
46
+ if (existing === content)
47
+ return;
48
+ }
49
+ catch {
50
+ // Fall through to write missing shim
51
+ }
52
+ await fs.promises.writeFile(filePath, content, 'utf-8');
53
+ }
54
+ /**
55
+ * Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
56
+ * importing ../../registry(.js) and ../../errors(.js).
57
+ */
58
+ export async function ensureUserCliCompatShims(baseDir = USER_OPENCLI_DIR) {
59
+ await fs.promises.mkdir(baseDir, { recursive: true });
60
+ const registryUrl = pathToFileURL(resolveHostRuntimeModulePath('registry-api')).href;
61
+ const errorsUrl = pathToFileURL(resolveHostRuntimeModulePath('errors')).href;
62
+ await Promise.all([
63
+ writeCompatShimIfNeeded(path.join(baseDir, 'registry'), `export * from '${registryUrl}';\n`),
64
+ writeCompatShimIfNeeded(path.join(baseDir, 'registry.js'), `export * from '${registryUrl}';\n`),
65
+ writeCompatShimIfNeeded(path.join(baseDir, 'errors'), `export * from '${errorsUrl}';\n`),
66
+ writeCompatShimIfNeeded(path.join(baseDir, 'errors.js'), `export * from '${errorsUrl}';\n`),
67
+ writeCompatShimIfNeeded(path.join(baseDir, 'package.json'), `${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`),
68
+ ]);
69
+ }
30
70
  /**
31
71
  * Discover and register CLI commands.
32
72
  * Uses pre-compiled manifest when available for instant startup.
@@ -63,6 +103,7 @@ async function loadFromManifest(manifestPath, clisDir) {
63
103
  const cmd = {
64
104
  site: entry.site,
65
105
  name: entry.name,
106
+ aliases: entry.aliases,
66
107
  description: entry.description ?? '',
67
108
  domain: entry.domain,
68
109
  strategy,
@@ -86,6 +127,7 @@ async function loadFromManifest(manifestPath, clisDir) {
86
127
  const cmd = {
87
128
  site: entry.site,
88
129
  name: entry.name,
130
+ aliases: entry.aliases,
89
131
  description: entry.description ?? '',
90
132
  domain: entry.domain,
91
133
  strategy,
@@ -160,6 +202,9 @@ async function registerYamlCli(filePath, defaultSite) {
160
202
  const cmd = {
161
203
  site,
162
204
  name,
205
+ aliases: isRecord(cliDef) && Array.isArray(cliDef.aliases)
206
+ ? cliDef.aliases.filter((value) => typeof value === 'string')
207
+ : undefined,
163
208
  description: cliDef.description ?? '',
164
209
  domain: cliDef.domain,
165
210
  strategy,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Electron app registry — maps site names to launch metadata.
3
+ *
4
+ * Builtin apps are defined here. User-defined apps are loaded
5
+ * from ~/.opencli/apps.yaml (additive only, does not override builtins).
6
+ */
7
+ export interface ElectronAppEntry {
8
+ /** CDP debug port (unique per app) */
9
+ port: number;
10
+ /** macOS process name for detection via pgrep */
11
+ processName: string;
12
+ /** macOS bundle ID for path discovery */
13
+ bundleId?: string;
14
+ /** Human-readable name for prompts */
15
+ displayName?: string;
16
+ /** Additional launch args beyond --remote-debugging-port */
17
+ extraArgs?: string[];
18
+ }
19
+ export declare const builtinApps: Record<string, ElectronAppEntry>;
20
+ /** Merge builtin + user-defined apps. User entries are additive only. */
21
+ export declare function loadApps(userApps?: Record<string, Omit<ElectronAppEntry, 'displayName'> & {
22
+ displayName?: string;
23
+ }>): Record<string, ElectronAppEntry>;
24
+ export declare function getElectronApp(site: string): ElectronAppEntry | undefined;
25
+ export declare function isElectronApp(site: string): boolean;
26
+ /** Get all registered apps (builtin + user-defined). */
27
+ export declare function getAllElectronApps(): Record<string, ElectronAppEntry>;
28
+ /** Reset loaded apps (for testing). */
29
+ export declare function _resetRegistry(): void;