expandai 0.0.3 → 0.0.5

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 (320) hide show
  1. package/build/{esm/Install/Opencode → assets/opencode}/expandai-webfetch.js +22 -4
  2. package/build/dts/Mcp/RequestHeaders.d.ts +0 -1
  3. package/build/dts/Mcp/Server.d.ts +1 -2
  4. package/build/dts/index.d.ts +0 -1
  5. package/build/esm/index.js +180 -2
  6. package/build/esm/main.js +677 -200
  7. package/build/skills/expandai/SKILL.md +12 -6
  8. package/package.json +19 -11
  9. package/build/dts/ApiClient.d.ts +0 -165
  10. package/build/dts/ApiClient.d.ts.map +0 -1
  11. package/build/dts/Auth/Domain.d.ts +0 -95
  12. package/build/dts/Auth/Domain.d.ts.map +0 -1
  13. package/build/dts/Auth/ManualAuthorization.d.ts +0 -107
  14. package/build/dts/Auth/ManualAuthorization.d.ts.map +0 -1
  15. package/build/dts/Auth/PendingInit.d.ts +0 -35
  16. package/build/dts/Auth/PendingInit.d.ts.map +0 -1
  17. package/build/dts/Auth/Pkce.d.ts +0 -10
  18. package/build/dts/Auth/Pkce.d.ts.map +0 -1
  19. package/build/dts/Auth/SetupGrantAuthorization.d.ts +0 -34
  20. package/build/dts/Auth/SetupGrantAuthorization.d.ts.map +0 -1
  21. package/build/dts/Authentication.d.ts +0 -24
  22. package/build/dts/Authentication.d.ts.map +0 -1
  23. package/build/dts/BrowserOAuth.d.ts +0 -99
  24. package/build/dts/BrowserOAuth.d.ts.map +0 -1
  25. package/build/dts/CommandPath.d.ts +0 -2
  26. package/build/dts/CommandPath.d.ts.map +0 -1
  27. package/build/dts/Commands/Fetch.d.ts +0 -30
  28. package/build/dts/Commands/Fetch.d.ts.map +0 -1
  29. package/build/dts/Commands/Hook.d.ts +0 -17
  30. package/build/dts/Commands/Hook.d.ts.map +0 -1
  31. package/build/dts/Commands/Init.d.ts +0 -55
  32. package/build/dts/Commands/Init.d.ts.map +0 -1
  33. package/build/dts/Commands/Mcp.d.ts +0 -11
  34. package/build/dts/Commands/Mcp.d.ts.map +0 -1
  35. package/build/dts/Commands/McpRun.d.ts +0 -5
  36. package/build/dts/Commands/McpRun.d.ts.map +0 -1
  37. package/build/dts/Commands/Search.d.ts +0 -21
  38. package/build/dts/Commands/Search.d.ts.map +0 -1
  39. package/build/dts/Commands/Skill.d.ts +0 -12
  40. package/build/dts/Commands/Skill.d.ts.map +0 -1
  41. package/build/dts/Commands/Status.d.ts +0 -27
  42. package/build/dts/Commands/Status.d.ts.map +0 -1
  43. package/build/dts/Commands/Telemetry.d.ts +0 -7
  44. package/build/dts/Commands/Telemetry.d.ts.map +0 -1
  45. package/build/dts/Commands/Uninstall.d.ts +0 -9
  46. package/build/dts/Commands/Uninstall.d.ts.map +0 -1
  47. package/build/dts/Config.d.ts +0 -196
  48. package/build/dts/Config.d.ts.map +0 -1
  49. package/build/dts/Hook/Claude.d.ts +0 -13
  50. package/build/dts/Hook/Claude.d.ts.map +0 -1
  51. package/build/dts/Hook/Errors.d.ts +0 -20
  52. package/build/dts/Hook/Errors.d.ts.map +0 -1
  53. package/build/dts/Hook/Opencode.d.ts +0 -13
  54. package/build/dts/Hook/Opencode.d.ts.map +0 -1
  55. package/build/dts/Hook/SnapshotNote.d.ts +0 -12
  56. package/build/dts/Hook/SnapshotNote.d.ts.map +0 -1
  57. package/build/dts/Init/ClaudeAdapter.d.ts +0 -61
  58. package/build/dts/Init/ClaudeAdapter.d.ts.map +0 -1
  59. package/build/dts/Init/ClaudeProfile.d.ts +0 -78
  60. package/build/dts/Init/ClaudeProfile.d.ts.map +0 -1
  61. package/build/dts/Init/Domain.d.ts +0 -139
  62. package/build/dts/Init/Domain.d.ts.map +0 -1
  63. package/build/dts/Init/HarnessAdapter.d.ts +0 -33
  64. package/build/dts/Init/HarnessAdapter.d.ts.map +0 -1
  65. package/build/dts/Init/HarnessDetector.d.ts +0 -19
  66. package/build/dts/Init/HarnessDetector.d.ts.map +0 -1
  67. package/build/dts/Init/HarnessRegistry.d.ts +0 -50
  68. package/build/dts/Init/HarnessRegistry.d.ts.map +0 -1
  69. package/build/dts/Init/MutationTransaction.d.ts +0 -96
  70. package/build/dts/Init/MutationTransaction.d.ts.map +0 -1
  71. package/build/dts/Init/Service.d.ts +0 -72
  72. package/build/dts/Init/Service.d.ts.map +0 -1
  73. package/build/dts/Init/StandardAdapters.d.ts +0 -42
  74. package/build/dts/Init/StandardAdapters.d.ts.map +0 -1
  75. package/build/dts/Init/Status.d.ts +0 -149
  76. package/build/dts/Init/Status.d.ts.map +0 -1
  77. package/build/dts/Install/Claude/Hook.d.ts +0 -120
  78. package/build/dts/Install/Claude/Hook.d.ts.map +0 -1
  79. package/build/dts/Install/Claude/Mcp.d.ts +0 -48
  80. package/build/dts/Install/Claude/Mcp.d.ts.map +0 -1
  81. package/build/dts/Install/Claude/Skill.d.ts +0 -23
  82. package/build/dts/Install/Claude/Skill.d.ts.map +0 -1
  83. package/build/dts/Install/Codex/Mcp.d.ts +0 -45
  84. package/build/dts/Install/Codex/Mcp.d.ts.map +0 -1
  85. package/build/dts/Install/Codex/Skill.d.ts +0 -22
  86. package/build/dts/Install/Codex/Skill.d.ts.map +0 -1
  87. package/build/dts/Install/Cursor/Mcp.d.ts +0 -45
  88. package/build/dts/Install/Cursor/Mcp.d.ts.map +0 -1
  89. package/build/dts/Install/Cursor/Skill.d.ts +0 -29
  90. package/build/dts/Install/Cursor/Skill.d.ts.map +0 -1
  91. package/build/dts/Install/Domain.d.ts +0 -103
  92. package/build/dts/Install/Domain.d.ts.map +0 -1
  93. package/build/dts/Install/Hook.d.ts +0 -30
  94. package/build/dts/Install/Hook.d.ts.map +0 -1
  95. package/build/dts/Install/Mcp.d.ts +0 -28
  96. package/build/dts/Install/Mcp.d.ts.map +0 -1
  97. package/build/dts/Install/Opencode/Hook.d.ts +0 -24
  98. package/build/dts/Install/Opencode/Hook.d.ts.map +0 -1
  99. package/build/dts/Install/Opencode/Mcp.d.ts +0 -47
  100. package/build/dts/Install/Opencode/Mcp.d.ts.map +0 -1
  101. package/build/dts/Install/Opencode/Skill.d.ts +0 -26
  102. package/build/dts/Install/Opencode/Skill.d.ts.map +0 -1
  103. package/build/dts/Install/Skill.d.ts +0 -32
  104. package/build/dts/Install/Skill.d.ts.map +0 -1
  105. package/build/dts/Install/Uninstall.d.ts +0 -32
  106. package/build/dts/Install/Uninstall.d.ts.map +0 -1
  107. package/build/dts/MarkdownAnsi.d.ts +0 -29
  108. package/build/dts/MarkdownAnsi.d.ts.map +0 -1
  109. package/build/dts/Mcp/ExpandDocs.d.ts +0 -19
  110. package/build/dts/Mcp/ExpandDocs.d.ts.map +0 -1
  111. package/build/dts/Mcp/RequestHeaders.d.ts.map +0 -1
  112. package/build/dts/Mcp/Server.d.ts.map +0 -1
  113. package/build/dts/Mcp/Toolkit.d.ts +0 -3
  114. package/build/dts/Mcp/Toolkit.d.ts.map +0 -1
  115. package/build/dts/Mcp/tools/Citations.d.ts +0 -266
  116. package/build/dts/Mcp/tools/Citations.d.ts.map +0 -1
  117. package/build/dts/Mcp/tools/Fetch.d.ts +0 -168
  118. package/build/dts/Mcp/tools/Fetch.d.ts.map +0 -1
  119. package/build/dts/SecureFile.d.ts +0 -23
  120. package/build/dts/SecureFile.d.ts.map +0 -1
  121. package/build/dts/Snippets.d.ts +0 -18
  122. package/build/dts/Snippets.d.ts.map +0 -1
  123. package/build/dts/Telemetry.d.ts +0 -30
  124. package/build/dts/Telemetry.d.ts.map +0 -1
  125. package/build/dts/Version.d.ts +0 -2
  126. package/build/dts/Version.d.ts.map +0 -1
  127. package/build/dts/Xdg.d.ts +0 -11
  128. package/build/dts/Xdg.d.ts.map +0 -1
  129. package/build/dts/index.d.ts.map +0 -1
  130. package/build/dts/main.d.ts +0 -3
  131. package/build/dts/main.d.ts.map +0 -1
  132. package/build/esm/ApiClient.js +0 -305
  133. package/build/esm/ApiClient.js.map +0 -1
  134. package/build/esm/Auth/Domain.js +0 -56
  135. package/build/esm/Auth/Domain.js.map +0 -1
  136. package/build/esm/Auth/ManualAuthorization.js +0 -253
  137. package/build/esm/Auth/ManualAuthorization.js.map +0 -1
  138. package/build/esm/Auth/PendingInit.js +0 -267
  139. package/build/esm/Auth/PendingInit.js.map +0 -1
  140. package/build/esm/Auth/Pkce.js +0 -23
  141. package/build/esm/Auth/Pkce.js.map +0 -1
  142. package/build/esm/Auth/SetupGrantAuthorization.js +0 -91
  143. package/build/esm/Auth/SetupGrantAuthorization.js.map +0 -1
  144. package/build/esm/Authentication.js +0 -143
  145. package/build/esm/Authentication.js.map +0 -1
  146. package/build/esm/BrowserOAuth.js +0 -251
  147. package/build/esm/BrowserOAuth.js.map +0 -1
  148. package/build/esm/CommandPath.js +0 -31
  149. package/build/esm/CommandPath.js.map +0 -1
  150. package/build/esm/Commands/Fetch.js +0 -119
  151. package/build/esm/Commands/Fetch.js.map +0 -1
  152. package/build/esm/Commands/Hook.js +0 -20
  153. package/build/esm/Commands/Hook.js.map +0 -1
  154. package/build/esm/Commands/Init.js +0 -371
  155. package/build/esm/Commands/Init.js.map +0 -1
  156. package/build/esm/Commands/Mcp.js +0 -19
  157. package/build/esm/Commands/Mcp.js.map +0 -1
  158. package/build/esm/Commands/McpRun.js +0 -15
  159. package/build/esm/Commands/McpRun.js.map +0 -1
  160. package/build/esm/Commands/Search.js +0 -46
  161. package/build/esm/Commands/Search.js.map +0 -1
  162. package/build/esm/Commands/Skill.js +0 -17
  163. package/build/esm/Commands/Skill.js.map +0 -1
  164. package/build/esm/Commands/Status.js +0 -186
  165. package/build/esm/Commands/Status.js.map +0 -1
  166. package/build/esm/Commands/Telemetry.js +0 -23
  167. package/build/esm/Commands/Telemetry.js.map +0 -1
  168. package/build/esm/Commands/Uninstall.js +0 -28
  169. package/build/esm/Commands/Uninstall.js.map +0 -1
  170. package/build/esm/Config.js +0 -619
  171. package/build/esm/Config.js.map +0 -1
  172. package/build/esm/Hook/Claude.js +0 -74
  173. package/build/esm/Hook/Claude.js.map +0 -1
  174. package/build/esm/Hook/Errors.js +0 -18
  175. package/build/esm/Hook/Errors.js.map +0 -1
  176. package/build/esm/Hook/Opencode.js +0 -50
  177. package/build/esm/Hook/Opencode.js.map +0 -1
  178. package/build/esm/Hook/SnapshotNote.js +0 -65
  179. package/build/esm/Hook/SnapshotNote.js.map +0 -1
  180. package/build/esm/Init/ClaudeAdapter.js +0 -137
  181. package/build/esm/Init/ClaudeAdapter.js.map +0 -1
  182. package/build/esm/Init/ClaudeProfile.js +0 -380
  183. package/build/esm/Init/ClaudeProfile.js.map +0 -1
  184. package/build/esm/Init/Domain.js +0 -118
  185. package/build/esm/Init/Domain.js.map +0 -1
  186. package/build/esm/Init/HarnessAdapter.js +0 -21
  187. package/build/esm/Init/HarnessAdapter.js.map +0 -1
  188. package/build/esm/Init/HarnessDetector.js +0 -65
  189. package/build/esm/Init/HarnessDetector.js.map +0 -1
  190. package/build/esm/Init/HarnessRegistry.js +0 -58
  191. package/build/esm/Init/HarnessRegistry.js.map +0 -1
  192. package/build/esm/Init/MutationTransaction.js +0 -494
  193. package/build/esm/Init/MutationTransaction.js.map +0 -1
  194. package/build/esm/Init/Service.js +0 -319
  195. package/build/esm/Init/Service.js.map +0 -1
  196. package/build/esm/Init/StandardAdapters.js +0 -116
  197. package/build/esm/Init/StandardAdapters.js.map +0 -1
  198. package/build/esm/Init/Status.js +0 -215
  199. package/build/esm/Init/Status.js.map +0 -1
  200. package/build/esm/Install/Claude/Hook.js +0 -221
  201. package/build/esm/Install/Claude/Hook.js.map +0 -1
  202. package/build/esm/Install/Claude/Mcp.js +0 -101
  203. package/build/esm/Install/Claude/Mcp.js.map +0 -1
  204. package/build/esm/Install/Claude/Skill.js +0 -56
  205. package/build/esm/Install/Claude/Skill.js.map +0 -1
  206. package/build/esm/Install/Codex/Mcp.js +0 -97
  207. package/build/esm/Install/Codex/Mcp.js.map +0 -1
  208. package/build/esm/Install/Codex/Skill.js +0 -33
  209. package/build/esm/Install/Codex/Skill.js.map +0 -1
  210. package/build/esm/Install/Cursor/Mcp.js +0 -74
  211. package/build/esm/Install/Cursor/Mcp.js.map +0 -1
  212. package/build/esm/Install/Cursor/Skill.js +0 -40
  213. package/build/esm/Install/Cursor/Skill.js.map +0 -1
  214. package/build/esm/Install/Domain.js +0 -162
  215. package/build/esm/Install/Domain.js.map +0 -1
  216. package/build/esm/Install/Hook.js +0 -38
  217. package/build/esm/Install/Hook.js.map +0 -1
  218. package/build/esm/Install/Mcp.js +0 -40
  219. package/build/esm/Install/Mcp.js.map +0 -1
  220. package/build/esm/Install/Opencode/Hook.js +0 -44
  221. package/build/esm/Install/Opencode/Hook.js.map +0 -1
  222. package/build/esm/Install/Opencode/Mcp.js +0 -76
  223. package/build/esm/Install/Opencode/Mcp.js.map +0 -1
  224. package/build/esm/Install/Opencode/Skill.js +0 -37
  225. package/build/esm/Install/Opencode/Skill.js.map +0 -1
  226. package/build/esm/Install/Skill.js +0 -41
  227. package/build/esm/Install/Skill.js.map +0 -1
  228. package/build/esm/Install/Uninstall.js +0 -49
  229. package/build/esm/Install/Uninstall.js.map +0 -1
  230. package/build/esm/MarkdownAnsi.js +0 -142
  231. package/build/esm/MarkdownAnsi.js.map +0 -1
  232. package/build/esm/Mcp/ExpandDocs.js +0 -56
  233. package/build/esm/Mcp/ExpandDocs.js.map +0 -1
  234. package/build/esm/Mcp/RequestHeaders.js +0 -13
  235. package/build/esm/Mcp/RequestHeaders.js.map +0 -1
  236. package/build/esm/Mcp/Server.js +0 -12
  237. package/build/esm/Mcp/Server.js.map +0 -1
  238. package/build/esm/Mcp/Toolkit.js +0 -16
  239. package/build/esm/Mcp/Toolkit.js.map +0 -1
  240. package/build/esm/Mcp/tools/Citations.js +0 -193
  241. package/build/esm/Mcp/tools/Citations.js.map +0 -1
  242. package/build/esm/Mcp/tools/Fetch.js +0 -165
  243. package/build/esm/Mcp/tools/Fetch.js.map +0 -1
  244. package/build/esm/SecureFile.js +0 -36
  245. package/build/esm/SecureFile.js.map +0 -1
  246. package/build/esm/Snippets.js +0 -29
  247. package/build/esm/Snippets.js.map +0 -1
  248. package/build/esm/Telemetry.js +0 -157
  249. package/build/esm/Telemetry.js.map +0 -1
  250. package/build/esm/Version.js +0 -4
  251. package/build/esm/Version.js.map +0 -1
  252. package/build/esm/Xdg.js +0 -15
  253. package/build/esm/Xdg.js.map +0 -1
  254. package/build/esm/index.js.map +0 -1
  255. package/build/esm/main.js.map +0 -1
  256. package/src/ApiClient.ts +0 -450
  257. package/src/Auth/Domain.ts +0 -63
  258. package/src/Auth/ManualAuthorization.ts +0 -296
  259. package/src/Auth/PendingInit.ts +0 -338
  260. package/src/Auth/Pkce.ts +0 -26
  261. package/src/Auth/SetupGrantAuthorization.ts +0 -116
  262. package/src/Authentication.ts +0 -202
  263. package/src/BrowserOAuth.ts +0 -359
  264. package/src/CommandPath.ts +0 -30
  265. package/src/Commands/Fetch.ts +0 -188
  266. package/src/Commands/Hook.ts +0 -48
  267. package/src/Commands/Init.ts +0 -489
  268. package/src/Commands/Mcp.ts +0 -30
  269. package/src/Commands/McpRun.ts +0 -25
  270. package/src/Commands/Search.ts +0 -97
  271. package/src/Commands/Skill.ts +0 -30
  272. package/src/Commands/Status.ts +0 -228
  273. package/src/Commands/Telemetry.ts +0 -42
  274. package/src/Commands/Uninstall.ts +0 -38
  275. package/src/Config.ts +0 -840
  276. package/src/Hook/Claude.ts +0 -81
  277. package/src/Hook/Errors.ts +0 -19
  278. package/src/Hook/Opencode.ts +0 -58
  279. package/src/Hook/SnapshotNote.ts +0 -66
  280. package/src/Init/ClaudeAdapter.ts +0 -191
  281. package/src/Init/ClaudeProfile.ts +0 -496
  282. package/src/Init/Domain.ts +0 -153
  283. package/src/Init/HarnessAdapter.ts +0 -35
  284. package/src/Init/HarnessDetector.ts +0 -77
  285. package/src/Init/HarnessRegistry.ts +0 -87
  286. package/src/Init/MutationTransaction.ts +0 -747
  287. package/src/Init/Service.ts +0 -385
  288. package/src/Init/StandardAdapters.ts +0 -141
  289. package/src/Init/Status.ts +0 -253
  290. package/src/Install/Claude/Hook.ts +0 -286
  291. package/src/Install/Claude/Mcp.ts +0 -129
  292. package/src/Install/Claude/Skill.ts +0 -77
  293. package/src/Install/Codex/Mcp.ts +0 -114
  294. package/src/Install/Codex/Skill.ts +0 -44
  295. package/src/Install/Cursor/Mcp.ts +0 -85
  296. package/src/Install/Cursor/Skill.ts +0 -51
  297. package/src/Install/Domain.ts +0 -221
  298. package/src/Install/Hook.ts +0 -50
  299. package/src/Install/Mcp.ts +0 -71
  300. package/src/Install/Opencode/Hook.ts +0 -55
  301. package/src/Install/Opencode/Mcp.ts +0 -87
  302. package/src/Install/Opencode/Skill.ts +0 -48
  303. package/src/Install/Opencode/expandai-webfetch.d.ts +0 -23
  304. package/src/Install/Opencode/expandai-webfetch.js +0 -56
  305. package/src/Install/Skill.ts +0 -57
  306. package/src/Install/Uninstall.ts +0 -82
  307. package/src/MarkdownAnsi.ts +0 -162
  308. package/src/Mcp/ExpandDocs.ts +0 -62
  309. package/src/Mcp/RequestHeaders.ts +0 -16
  310. package/src/Mcp/Server.ts +0 -15
  311. package/src/Mcp/Toolkit.ts +0 -21
  312. package/src/Mcp/tools/Citations.ts +0 -219
  313. package/src/Mcp/tools/Fetch.ts +0 -203
  314. package/src/SecureFile.ts +0 -70
  315. package/src/Snippets.ts +0 -39
  316. package/src/Telemetry.ts +0 -186
  317. package/src/Version.ts +0 -3
  318. package/src/Xdg.ts +0 -18
  319. package/src/index.ts +0 -1
  320. package/src/main.ts +0 -305
@@ -1,77 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { FileSystem, Path } from '@effect/platform'
3
- import { NodeFileSystem, NodePath } from '@effect/platform-node'
4
- import { Config, Effect, Match } from 'effect'
5
- import { CLI_VERSION } from '../../Version.js'
6
- import type { InstallScope } from '../Mcp.js'
7
-
8
- export const SKILL_NAME = 'expandai'
9
- export const SKILL_FILE_NAME = 'SKILL.md'
10
- const PROFILE_VERSION_LINE = /^(\s*expandai-profile-version:\s*).+$/mu
11
-
12
- export interface ClaudeSkillPlan {
13
- readonly changed: boolean
14
- readonly text: string
15
- }
16
-
17
- export function renderClaudeSkill(source: string, profileVersion: string = CLI_VERSION): string {
18
- if (!PROFILE_VERSION_LINE.test(source)) {
19
- throw new Error('Bundled Expand skill is missing expandai-profile-version metadata')
20
- }
21
- return source.replace(PROFILE_VERSION_LINE, `$1${profileVersion}`)
22
- }
23
-
24
- export function planClaudeSkill(
25
- current: string | undefined,
26
- source: string,
27
- profileVersion: string = CLI_VERSION,
28
- ): ClaudeSkillPlan {
29
- const text = renderClaudeSkill(source, profileVersion)
30
- return { changed: current !== text, text }
31
- }
32
-
33
- const makeClaudeSkill = Effect.gen(function* () {
34
- const fileSystem = yield* FileSystem.FileSystem
35
- const path = yield* Path.Path
36
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
37
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
38
- const moduleFile = yield* path.fromFileUrl(new URL(import.meta.url))
39
- const sourceFile = path.resolve(path.dirname(moduleFile), '..', '..', '..', 'skills', SKILL_NAME, SKILL_FILE_NAME)
40
-
41
- const pathFor = Effect.fn(function* (scope: InstallScope) {
42
- return yield* Match.value(scope).pipe(
43
- Match.when('global', () => Effect.succeed(path.join(home, '.claude', 'skills', SKILL_NAME))),
44
- Match.when('project', () => Effect.succeed(path.join(cwd, '.claude', 'skills', SKILL_NAME))),
45
- Match.exhaustive,
46
- )
47
- })
48
-
49
- const source = Effect.fn('ClaudeSkill.source')(function* () {
50
- return yield* fileSystem.readFileString(sourceFile)
51
- })
52
-
53
- const install = Effect.fn('ClaudeSkill.install')(function* (scope: InstallScope) {
54
- const target = yield* pathFor(scope)
55
- const targetFile = path.join(target, SKILL_FILE_NAME)
56
- const current = (yield* fileSystem.exists(targetFile)) ? yield* fileSystem.readFileString(targetFile) : undefined
57
- const plan = planClaudeSkill(current, yield* source())
58
- if (plan.changed) {
59
- yield* fileSystem.makeDirectory(target, { recursive: true })
60
- yield* fileSystem.writeFileString(targetFile, plan.text)
61
- }
62
- return target
63
- })
64
-
65
- const uninstall = Effect.fn('ClaudeSkill.uninstall')(function* (scope: InstallScope) {
66
- const target = yield* pathFor(scope)
67
- if (yield* fileSystem.exists(target)) yield* fileSystem.remove(target, { recursive: true })
68
- return target
69
- })
70
-
71
- return { install, uninstall, pathFor, source } as const
72
- })
73
-
74
- export class ClaudeSkill extends Effect.Service<ClaudeSkill>()('@expandai/cli/Install/Claude/Skill', {
75
- dependencies: [NodeFileSystem.layer, NodePath.layer],
76
- effect: makeClaudeSkill,
77
- }) {}
@@ -1,114 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
- import { parse, stringify, type TomlTableWithoutBigInt } from 'smol-toml'
4
- import { CliInvocation, ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
5
- import type { InstallScope } from '../Mcp.js'
6
-
7
- export class Entry extends Schema.Class<Entry>('CodexMcpEntry')({
8
- command: Schema.NonEmptyTrimmedString,
9
- args: Schema.Array(Schema.String),
10
- env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
11
- enabled: Schema.Boolean,
12
- }) {}
13
-
14
- function isRecord(value: unknown): value is Record<string, unknown> {
15
- return typeof value === 'object' && value !== null && !Array.isArray(value)
16
- }
17
-
18
- const getMcpServers = Effect.fn(function* (config: TomlTableWithoutBigInt) {
19
- const servers = config.mcp_servers
20
- return isRecord(servers) ? servers : {}
21
- })
22
-
23
- function toStringifyValue(value: TomlTableWithoutBigInt) {
24
- // smol-toml's stringify type accepts any; keep the unsafe boundary local.
25
- return value as unknown as Record<string, unknown>
26
- }
27
-
28
- /**
29
- * Codex MCP descriptor — Codex stores MCP servers in TOML config under
30
- * `[mcp_servers.<name>]`.
31
- *
32
- * - global → `~/.codex/config.toml`
33
- * - project → `<cwd>/.codex/config.toml`
34
- */
35
- export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/Codex/Mcp', {
36
- dependencies: [ConfigIo.Default, CliInvocation.Default],
37
- effect: Effect.gen(function* () {
38
- const io = yield* ConfigIo
39
- const cli = yield* CliInvocation
40
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
41
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
42
-
43
- const pathFor = Effect.fn(function* (scope: InstallScope) {
44
- return yield* Match.value(scope).pipe(
45
- Match.when('global', () => Effect.succeed(`${home}/.codex/config.toml`)),
46
- Match.when('project', () => Effect.succeed(`${cwd}/.codex/config.toml`)),
47
- Match.exhaustive,
48
- )
49
- })
50
-
51
- const readConfig = Effect.fn(function* (file: string) {
52
- const text = yield* io.readText(file)
53
- return yield* Effect.try({
54
- try: () => parse(text, { integersAsBigInt: false }),
55
- catch: (error) => new McpParseError({ path: file, error }),
56
- })
57
- })
58
-
59
- const writeConfig = Effect.fn(function* (file: string, value: TomlTableWithoutBigInt) {
60
- const encoded = yield* Effect.try({
61
- try: () => stringify(toStringifyValue(value)),
62
- catch: (error) => new McpWriteError({ path: file, error }),
63
- })
64
- yield* io.writeText(file, encoded).pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
65
- })
66
-
67
- const install = Effect.fn(function* (scope: InstallScope) {
68
- const file = yield* pathFor(scope)
69
- const config = yield* readConfig(file)
70
- const servers = yield* getMcpServers(config)
71
-
72
- const invocation = yield* cli.resolve()
73
- const entryModel = new Entry({
74
- command: invocation.command,
75
- args: [...invocation.args, 'mcp', 'run'],
76
- env: { EXPANDAI_HARNESS: 'codex' },
77
- enabled: true,
78
- })
79
-
80
- const entry: TomlTableWithoutBigInt = {
81
- command: entryModel.command,
82
- args: [...entryModel.args],
83
- env: { ...entryModel.env },
84
- enabled: entryModel.enabled,
85
- }
86
-
87
- yield* writeConfig(file, {
88
- ...config,
89
- mcp_servers: Record.set(servers, SERVER_KEY, entry),
90
- })
91
- yield* Console.log(`Installed expandai MCP server into ${file}`)
92
- })
93
-
94
- const uninstall = Effect.fn(function* (scope: InstallScope) {
95
- const file = yield* pathFor(scope)
96
- const config = yield* readConfig(file)
97
- const servers = yield* getMcpServers(config)
98
- if (!(SERVER_KEY in servers)) {
99
- return
100
- }
101
- yield* writeConfig(file, { ...config, mcp_servers: Record.remove(servers, SERVER_KEY) })
102
- yield* Console.log(`Removed expandai MCP server from ${file}`)
103
- })
104
-
105
- const isInstalled = Effect.fn(function* (scope: InstallScope) {
106
- const file = yield* pathFor(scope)
107
- const config = yield* readConfig(file)
108
- const servers = yield* getMcpServers(config)
109
- return SERVER_KEY in servers
110
- })
111
-
112
- return { install, uninstall, isInstalled, pathFor } as const
113
- }),
114
- }) {}
@@ -1,44 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Config, Effect, Match } from 'effect'
3
- import { SkillIo } from '../Domain.js'
4
- import type { InstallScope } from '../Mcp.js'
5
-
6
- /**
7
- * Codex skills use the open agent skills locations:
8
- *
9
- * - global → `~/.agents/skills/<name>/`
10
- * - project → `<cwd>/.agents/skills/<name>/`
11
- */
12
-
13
- export const SKILL_NAME = 'expandai'
14
-
15
- export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Install/Codex/Skill', {
16
- dependencies: [SkillIo.Default],
17
- effect: Effect.gen(function* () {
18
- const io = yield* SkillIo
19
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
20
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
21
-
22
- const pathFor = Effect.fn(function* (scope: InstallScope) {
23
- return yield* Match.value(scope).pipe(
24
- Match.when('global', () => Effect.succeed(`${home}/.agents/skills/${SKILL_NAME}`)),
25
- Match.when('project', () => Effect.succeed(`${cwd}/.agents/skills/${SKILL_NAME}`)),
26
- Match.exhaustive,
27
- )
28
- })
29
-
30
- const install = Effect.fn(function* (scope: InstallScope) {
31
- return yield* io.install(yield* pathFor(scope))
32
- })
33
-
34
- const uninstall = Effect.fn(function* (scope: InstallScope) {
35
- return yield* io.uninstall(yield* pathFor(scope))
36
- })
37
-
38
- const isCurrent = Effect.fn(function* (scope: InstallScope) {
39
- return yield* io.isCurrent(yield* pathFor(scope))
40
- })
41
-
42
- return { install, uninstall, isCurrent, pathFor } as const
43
- }),
44
- }) {}
@@ -1,85 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
- import { CliInvocation, ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
4
- import type { InstallScope } from '../Mcp.js'
5
-
6
- /**
7
- * Cursor MCP descriptor — same `mcpServers` shape as Claude Code, different
8
- * paths.
9
- *
10
- * - global → `~/.cursor/mcp.json`
11
- * - project → `<cwd>/.cursor/mcp.json`
12
- */
13
- export class Entry extends Schema.Class<Entry>('CursorMcpEntry')({
14
- type: Schema.Literal('stdio'),
15
- command: Schema.NonEmptyTrimmedString,
16
- args: Schema.Array(Schema.String),
17
- env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
18
- }) {}
19
-
20
- export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Install/Cursor/Mcp', {
21
- dependencies: [ConfigIo.Default, CliInvocation.Default],
22
- effect: Effect.gen(function* () {
23
- const io = yield* ConfigIo
24
- const cli = yield* CliInvocation
25
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
26
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
27
-
28
- const pathFor = Effect.fn(function* (scope: InstallScope) {
29
- return yield* Match.value(scope).pipe(
30
- Match.when('global', () => Effect.succeed(`${home}/.cursor/mcp.json`)),
31
- Match.when('project', () => Effect.succeed(`${cwd}/.cursor/mcp.json`)),
32
- Match.exhaustive,
33
- )
34
- })
35
-
36
- const readConfig = Effect.fn(function* (file: string) {
37
- return yield* io
38
- .read(file, McpServersConfigSchema)
39
- .pipe(Effect.mapError((error) => new McpParseError({ path: file, error })))
40
- })
41
-
42
- const writeConfig = Effect.fn(function* (file: string, value: unknown) {
43
- yield* io
44
- .write(file, value, McpServersConfigSchema)
45
- .pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
46
- })
47
-
48
- const install = Effect.fn(function* (scope: InstallScope) {
49
- const file = yield* pathFor(scope)
50
- const config = yield* readConfig(file)
51
- const invocation = yield* cli.resolve()
52
- yield* writeConfig(file, {
53
- ...config,
54
- mcpServers: {
55
- ...config.mcpServers,
56
- [SERVER_KEY]: new Entry({
57
- type: 'stdio',
58
- command: invocation.command,
59
- args: [...invocation.args, 'mcp', 'run'],
60
- env: { EXPANDAI_HARNESS: 'cursor' },
61
- }),
62
- },
63
- })
64
- yield* Console.log(`Installed expandai MCP server into ${file}`)
65
- })
66
-
67
- const uninstall = Effect.fn(function* (scope: InstallScope) {
68
- const file = yield* pathFor(scope)
69
- const config = yield* readConfig(file)
70
- if (!(SERVER_KEY in config.mcpServers)) {
71
- return
72
- }
73
- yield* writeConfig(file, { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) })
74
- yield* Console.log(`Removed expandai MCP server from ${file}`)
75
- })
76
-
77
- const isInstalled = Effect.fn(function* (scope: InstallScope) {
78
- const file = yield* pathFor(scope)
79
- const config = yield* readConfig(file)
80
- return SERVER_KEY in config.mcpServers
81
- })
82
-
83
- return { install, uninstall, isInstalled, pathFor } as const
84
- }),
85
- }) {}
@@ -1,51 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Config, Effect, Match } from 'effect'
3
- import { SkillIo } from '../Domain.js'
4
- import type { InstallScope } from '../Mcp.js'
5
-
6
- /**
7
- * Cursor skills are auto-loaded from these locations (per
8
- * cursor.com/docs/skills):
9
- *
10
- * - global → `~/.cursor/skills/<name>/`
11
- * - project → `<cwd>/.cursor/skills/<name>/`
12
- *
13
- * Cursor also compat-loads `.claude/skills/`, `.codex/skills/`, `.agents/skills/`
14
- * at both project and user scope, but we ship to Cursor's native paths.
15
- *
16
- * Format is the same SKILL.md + YAML frontmatter open standard as Claude Code
17
- * and OpenCode, so the bundled `skills/expandai/` directory copies in unchanged.
18
- */
19
-
20
- export const SKILL_NAME = 'expandai'
21
-
22
- export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/Install/Cursor/Skill', {
23
- dependencies: [SkillIo.Default],
24
- effect: Effect.gen(function* () {
25
- const io = yield* SkillIo
26
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
27
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
28
-
29
- const pathFor = Effect.fn(function* (scope: InstallScope) {
30
- return yield* Match.value(scope).pipe(
31
- Match.when('global', () => Effect.succeed(`${home}/.cursor/skills/${SKILL_NAME}`)),
32
- Match.when('project', () => Effect.succeed(`${cwd}/.cursor/skills/${SKILL_NAME}`)),
33
- Match.exhaustive,
34
- )
35
- })
36
-
37
- const install = Effect.fn(function* (scope: InstallScope) {
38
- return yield* io.install(yield* pathFor(scope))
39
- })
40
-
41
- const uninstall = Effect.fn(function* (scope: InstallScope) {
42
- return yield* io.uninstall(yield* pathFor(scope))
43
- })
44
-
45
- const isCurrent = Effect.fn(function* (scope: InstallScope) {
46
- return yield* io.isCurrent(yield* pathFor(scope))
47
- })
48
-
49
- return { install, uninstall, isCurrent, pathFor } as const
50
- }),
51
- }) {}
@@ -1,221 +0,0 @@
1
- import { FileSystem, Path } from '@effect/platform'
2
- import { NodeFileSystem, NodePath } from '@effect/platform-node'
3
- import { Array, Config, Effect, Inspectable, Layer, Schema, String } from 'effect'
4
-
5
- export class McpParseError extends Schema.TaggedError<McpParseError>()('McpParseError', {
6
- path: Schema.String,
7
- error: Schema.Defect,
8
- }) {
9
- get message() {
10
- return `Failed to parse MCP config at ${this.path}: ${Inspectable.format(this.error)}`
11
- }
12
- }
13
-
14
- export class McpWriteError extends Schema.TaggedError<McpWriteError>()('McpWriteError', {
15
- path: Schema.String,
16
- error: Schema.Defect,
17
- }) {
18
- get message() {
19
- return `Failed to write MCP config at ${this.path}: ${Inspectable.format(this.error)}`
20
- }
21
- }
22
-
23
- export const Host = Schema.Literal('claude-code', 'cursor', 'opencode', 'codex')
24
- export type Host = typeof Host.Type
25
-
26
- /**
27
- * The agent harness invoking the CLI — every install target plus 'unknown'
28
- * for uninstrumented callers. Installers bake `EXPANDAI_HARNESS=<host>` into
29
- * MCP/hook configs; telemetry reads it back.
30
- */
31
- export const Harness = Schema.Literal(...Host.literals, 'unknown')
32
- export type Harness = typeof Harness.Type
33
-
34
- export const InstallScope = Schema.Literal('project', 'global')
35
- export type InstallScope = typeof InstallScope.Type
36
-
37
- const McpServerMap = Schema.Record({ key: Schema.String, value: Schema.Unknown })
38
-
39
- export const McpServersConfigSchema = Schema.parseJson(
40
- Schema.Struct({ mcpServers: Schema.optionalWith(McpServerMap, { default: () => ({}) }) }, McpServerMap),
41
- { space: 2 },
42
- )
43
-
44
- export const OpencodeConfigSchema = Schema.parseJson(
45
- Schema.Struct({ mcp: Schema.optionalWith(McpServerMap, { default: () => ({}) }) }, McpServerMap),
46
- { space: 2 },
47
- )
48
-
49
- export const SERVER_KEY = 'expandai'
50
-
51
- /** How a host config should launch the CLI: `command` plus leading args. */
52
- export interface CliCommand {
53
- readonly command: string
54
- readonly args: ReadonlyArray<string>
55
- }
56
-
57
- export const DIRECT_CLI: CliCommand = { command: 'expandai', args: [] }
58
- const NPX_CLI: CliCommand = { command: 'npx', args: ['-y', 'expandai'] }
59
-
60
- /**
61
- * Renders a `CliCommand` as the single shell string Claude Code stores for hook
62
- * entries. MCP descriptors keep `command`/`args` separate because they exec
63
- * directly; hooks are run through a shell, so they need one flat line.
64
- */
65
- export function cliShellCommand(cli: CliCommand, args: ReadonlyArray<string>): string {
66
- return [cli.command, ...cli.args, ...args].join(' ')
67
- }
68
-
69
- /**
70
- * Resolves how installed host configs should invoke the expandai CLI. MCP
71
- * entries exec their `command` directly (no shell), so the fallback decision
72
- * happens at install time: prefer the `expandai` binary when it is on PATH,
73
- * otherwise write an `npx -y expandai` invocation so installs performed via
74
- * `npx expandai …` keep working without a global CLI install.
75
- */
76
- const makeCliInvocation = Effect.gen(function* () {
77
- const fs = yield* FileSystem.FileSystem
78
- const path = yield* Path.Path
79
- const searchPath = yield* Config.string('PATH').pipe(Config.withDefault(''))
80
-
81
- const resolve = Effect.fn('CliInvocation.resolve')(function* () {
82
- // ':' is the POSIX PATH delimiter — the install flow already assumes
83
- // POSIX conventions (HOME-based config paths, shell hooks).
84
- const onPath = yield* Effect.exists(Array.filter(searchPath.split(':'), String.isNonEmpty), (dir) =>
85
- fs.exists(path.join(dir, 'expandai')).pipe(Effect.orElseSucceed(() => false)),
86
- )
87
- return onPath ? DIRECT_CLI : NPX_CLI
88
- })
89
-
90
- return { resolve } as const
91
- })
92
-
93
- export class CliInvocation extends Effect.Service<CliInvocation>()('@expandai/cli/Install/CliInvocation', {
94
- dependencies: [NodeFileSystem.layer, NodePath.layer],
95
- effect: makeCliInvocation,
96
- }) {
97
- /** Dependency-open layer so fixtures can supply their own file system and PATH. */
98
- static readonly layer = Layer.effect(CliInvocation, makeCliInvocation.pipe(Effect.map(CliInvocation.make)))
99
- }
100
-
101
- export const readConfigText = Effect.fn(function* (file: string) {
102
- const fs = yield* FileSystem.FileSystem
103
- const exists = yield* fs.exists(file)
104
- if (!exists) {
105
- return '{}'
106
- }
107
- return yield* fs.readFileString(file)
108
- })
109
-
110
- export const writeConfigText = Effect.fn(function* (file: string, text: string) {
111
- const fs = yield* FileSystem.FileSystem
112
- const path = yield* Path.Path
113
- yield* fs.makeDirectory(path.dirname(file), { recursive: true })
114
- yield* fs.writeFileString(file, `${text}\n`)
115
- })
116
-
117
- /**
118
- * Captures `FileSystem` + `Path` once and exposes string-encoded JSON read/
119
- * write methods that don't re-yield them on every call. Use this from inside
120
- * an `Effect.Service` gen block so the install/uninstall methods don't have
121
- * to re-thread `provideService` calls.
122
- */
123
- export class ConfigIo extends Effect.Service<ConfigIo>()('@expandai/cli/Install/ConfigIo', {
124
- dependencies: [NodeFileSystem.layer, NodePath.layer],
125
- effect: Effect.gen(function* () {
126
- const fs = yield* FileSystem.FileSystem
127
- const path = yield* Path.Path
128
-
129
- const read = Effect.fn('ConfigIo.read')(function* <A>(file: string, schema: Schema.Schema<A, string>) {
130
- const text = yield* readConfigText(file).pipe(Effect.provideService(FileSystem.FileSystem, fs))
131
- return yield* Schema.decode(schema)(text)
132
- })
133
-
134
- const write = Effect.fn('ConfigIo.write')(function* <A>(
135
- file: string,
136
- value: unknown,
137
- schema: Schema.Schema<A, string>,
138
- ) {
139
- const encoded = yield* Schema.encodeUnknown(schema)(value)
140
- yield* writeConfigText(file, encoded).pipe(
141
- Effect.provideService(FileSystem.FileSystem, fs),
142
- Effect.provideService(Path.Path, path),
143
- )
144
- })
145
-
146
- const exists = Effect.fn('ConfigIo.exists')(function* (file: string) {
147
- return yield* fs.exists(file)
148
- })
149
-
150
- // Raw text I/O for formats `ConfigIo.read`/`write` can't handle through a
151
- // `Schema.parseJson(...)` schema (e.g. Codex's TOML). Missing file reads
152
- // as an empty string — callers attach their own parser-specific default.
153
- const readText = Effect.fn('ConfigIo.readText')(function* (file: string) {
154
- const present = yield* fs.exists(file)
155
- return present ? yield* fs.readFileString(file) : ''
156
- })
157
-
158
- const writeText = Effect.fn('ConfigIo.writeText')(function* (file: string, text: string) {
159
- yield* writeConfigText(file, text).pipe(
160
- Effect.provideService(FileSystem.FileSystem, fs),
161
- Effect.provideService(Path.Path, path),
162
- )
163
- })
164
-
165
- return { read, write, exists, readText, writeText } as const
166
- }),
167
- }) {}
168
-
169
- /**
170
- * Captures `FileSystem` + `Path` once and exposes skill directory copy/remove
171
- * operations. The bundled `SKILL.md` lives at `<cli>/skills/expandai/` (or
172
- * `<build>/skills/expandai/` in production); this service computes that path
173
- * from its own `import.meta.url`, so per-host skill descriptors only need to
174
- * provide `pathFor` and call `io.install(target)` / `io.uninstall(target)`.
175
- */
176
- export class SkillIo extends Effect.Service<SkillIo>()('@expandai/cli/Install/SkillIo', {
177
- dependencies: [NodeFileSystem.layer, NodePath.layer],
178
- effect: Effect.gen(function* () {
179
- const fs = yield* FileSystem.FileSystem
180
- const path = yield* Path.Path
181
-
182
- const sourceDir = path.resolve(
183
- path.dirname(yield* path.fromFileUrl(new URL(import.meta.url))),
184
- '..',
185
- '..',
186
- 'skills',
187
- 'expandai',
188
- )
189
-
190
- const install = Effect.fn('SkillIo.install')(function* (target: string) {
191
- yield* fs.makeDirectory(path.dirname(target), { recursive: true })
192
- yield* fs.copy(sourceDir, target, { overwrite: true })
193
- return target
194
- })
195
-
196
- const uninstall = Effect.fn('SkillIo.uninstall')(function* (target: string) {
197
- const exists = yield* fs.exists(target)
198
- if (exists) {
199
- yield* fs.remove(target, { recursive: true })
200
- }
201
- return target
202
- })
203
-
204
- /**
205
- * Whether the installed skill matches the bundled one. Presence alone is not
206
- * enough: an upgraded CLI ships new skill text, and a harness whose install is
207
- * gated on "the file exists" would keep serving the old instructions forever.
208
- */
209
- const isCurrent = Effect.fn('SkillIo.isCurrent')(function* (target: string) {
210
- const targetFile = path.join(target, 'SKILL.md')
211
- if (!(yield* fs.exists(targetFile))) return false
212
- const [installed, bundled] = yield* Effect.all([
213
- fs.readFileString(targetFile),
214
- fs.readFileString(path.join(sourceDir, 'SKILL.md')),
215
- ])
216
- return installed === bundled
217
- })
218
-
219
- return { install, uninstall, isCurrent } as const
220
- }),
221
- }) {}
@@ -1,50 +0,0 @@
1
- import { Effect, Match, Schema } from 'effect'
2
- import { ClaudeHook } from './Claude/Hook.js'
3
- import { type InstallScope, Mcp } from './Mcp.js'
4
- import { OpencodeHook } from './Opencode/Hook.js'
5
-
6
- /**
7
- * Hook install dispatcher. Bundles MCP install + per-host hook config:
8
- *
9
- * - claude-code: writes the matcher into `~/.claude/settings.json` (global)
10
- * or `<cwd>/.claude/settings.json` (project), then registers expandai as
11
- * an MCP server at the matching scope.
12
- * - opencode: writes the webfetch plugin file into `.opencode/plugins/`
13
- * (project) or `~/.config/opencode/plugins/` (global), plus registers
14
- * expandai as an MCP server.
15
- *
16
- * Cursor has no hook system per opencode.ai/docs and Cursor's docs, so it's
17
- * absent from this Host union (compare with `Mcp.Host` which includes Cursor).
18
- */
19
- export const HookHost = Schema.Literal('claude-code', 'opencode')
20
- export type HookHost = typeof HookHost.Type
21
-
22
- export class HookInstaller extends Effect.Service<HookInstaller>()('@expandai/cli/Install/Hook', {
23
- dependencies: [Mcp.Default, ClaudeHook.Default, OpencodeHook.Default],
24
- effect: Effect.gen(function* () {
25
- const mcp = yield* Mcp
26
- const claudeHook = yield* ClaudeHook
27
- const opencodeHook = yield* OpencodeHook
28
-
29
- const install = Effect.fn('HookInstaller.install')(function* (host: HookHost, scope: InstallScope) {
30
- // Bundle: MCP install first, then host-specific hook config.
31
- yield* mcp.install(host, scope)
32
- yield* Match.value(host).pipe(
33
- Match.when('claude-code', () => claudeHook.install(scope)),
34
- Match.when('opencode', () => opencodeHook.install(scope)),
35
- Match.exhaustive,
36
- )
37
- })
38
-
39
- const uninstall = Effect.fn('HookInstaller.uninstall')(function* (host: HookHost, scope: InstallScope) {
40
- yield* Match.value(host).pipe(
41
- Match.when('claude-code', () => claudeHook.uninstall(scope)),
42
- Match.when('opencode', () => opencodeHook.uninstall(scope)),
43
- Match.exhaustive,
44
- )
45
- yield* mcp.uninstall(host, scope)
46
- })
47
-
48
- return { install, uninstall } as const
49
- }),
50
- }) {}