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,253 +0,0 @@
1
- import { DateTime, Effect, Option, Schema } from 'effect'
2
- import {
3
- CLI_AUTHORIZATION_CLIENT_ID,
4
- PendingInitCorrupt,
5
- type PendingInitKey,
6
- PendingInitRecord,
7
- } from '../Auth/Domain.js'
8
- import { PendingInitStore } from '../Auth/PendingInit.js'
9
- import { authMethodState } from '../Authentication.js'
10
- import { ConfigManager, ConfigParseError, CredentialStoreInsecure, NotAuthenticated } from '../Config.js'
11
- import { CLI_VERSION } from '../Version.js'
12
- import { CLAUDE_POLICY_READ_ONLY } from './ClaudeProfile.js'
13
- import { InitHarness, InitScope } from './Domain.js'
14
- import type { HarnessInspection } from './HarnessAdapter.js'
15
- import { HarnessRegistry } from './HarnessRegistry.js'
16
-
17
- const VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u
18
- const BoundedVersion = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(64), Schema.pattern(VERSION_PATTERN))
19
-
20
- export class StatusRequest extends Schema.Class<StatusRequest>('StatusRequest')({
21
- harness: InitHarness,
22
- scope: InitScope,
23
- }) {}
24
-
25
- const StatusAuth = Schema.Struct({
26
- // `healthy` means a call would authenticate right now. An expired access
27
- // token with a refresh token on disk reports `refresh_required`, because the
28
- // renewal has not happened and can fail.
29
- state: Schema.Literal('absent', 'unusable', 'refresh_required', 'healthy'),
30
- healthy: Schema.Boolean,
31
- })
32
-
33
- const PendingAuthorization = Schema.Union(
34
- Schema.Struct({ state: Schema.Literal('none'), count: Schema.Literal(0) }),
35
- Schema.Struct({
36
- state: Schema.Literal('approval_pending'),
37
- count: Schema.Literal(1),
38
- approvalExpiresAt: Schema.DateTimeUtc,
39
- }),
40
- )
41
-
42
- const StatusInstallation = Schema.Struct({
43
- configured: Schema.Boolean,
44
- integrationVersion: Schema.optional(BoundedVersion),
45
- components: Schema.Struct({
46
- webfetchHook: Schema.Boolean,
47
- runtimeSkill: Schema.Boolean,
48
- mcp: Schema.Boolean,
49
- }),
50
- runtimeState: Schema.Literal('unverified', 'reload_required', 'blocked'),
51
- reloadState: Schema.Literal('not_required', 'required'),
52
- consentState: Schema.Literal('not_required', 'may_be_required'),
53
- policyState: Schema.Literal('unverified', 'blocked'),
54
- policyCodes: Schema.Array(Schema.Literal('managed_policy_blocked', 'target_read_only')).pipe(Schema.maxItems(1)),
55
- })
56
-
57
- export class StatusResult extends Schema.Class<StatusResult>('StatusResult')({
58
- schemaVersion: Schema.Literal(1),
59
- status: Schema.Literal('ok'),
60
- harness: InitHarness,
61
- scope: InitScope,
62
- cli: Schema.Struct({ version: BoundedVersion }),
63
- auth: StatusAuth,
64
- pendingAuthorization: PendingAuthorization,
65
- installation: StatusInstallation,
66
- }) {}
67
-
68
- export const StatusServiceErrorCode = Schema.Literal(
69
- 'credential_config_malformed',
70
- 'credential_storage_insecure',
71
- 'credential_health_failed',
72
- 'pending_state_malformed',
73
- 'pending_state_unavailable',
74
- 'harness_inspection_failed',
75
- 'invalid_status_result',
76
- )
77
-
78
- export class StatusServiceError extends Schema.TaggedError<StatusServiceError>()('StatusServiceError', {
79
- code: StatusServiceErrorCode,
80
- recoverable: Schema.Boolean,
81
- }) {
82
- get message() {
83
- switch (this.code) {
84
- case 'credential_config_malformed':
85
- return 'Stored credential configuration is malformed.'
86
- case 'credential_storage_insecure':
87
- return 'Credential storage is not secure enough to inspect.'
88
- case 'credential_health_failed':
89
- return 'Stored credentials could not be inspected safely.'
90
- case 'pending_state_malformed':
91
- return 'Pending authorization state is malformed.'
92
- case 'pending_state_unavailable':
93
- return 'Pending authorization state could not be inspected safely.'
94
- case 'harness_inspection_failed':
95
- return 'The harness configuration could not be inspected safely.'
96
- case 'invalid_status_result':
97
- return 'Local diagnostics returned an invalid result.'
98
- }
99
- }
100
- }
101
-
102
- function statusError(code: typeof StatusServiceErrorCode.Type, recoverable: boolean): StatusServiceError {
103
- return new StatusServiceError({ code, recoverable })
104
- }
105
-
106
- function mapCredentialError(error: unknown): StatusServiceError {
107
- if (error instanceof ConfigParseError) return statusError('credential_config_malformed', false)
108
- if (error instanceof CredentialStoreInsecure) return statusError('credential_storage_insecure', false)
109
- return statusError('credential_health_failed', false)
110
- }
111
-
112
- function mapPendingError(error: unknown): StatusServiceError {
113
- if (error instanceof CredentialStoreInsecure) return statusError('credential_storage_insecure', false)
114
- return error instanceof PendingInitCorrupt
115
- ? statusError('pending_state_malformed', true)
116
- : statusError('pending_state_unavailable', true)
117
- }
118
-
119
- function authStatus(configManager: ConfigManager) {
120
- return Effect.gen(function* () {
121
- const loaded = yield* Effect.either(configManager.loadConfig())
122
- if (loaded._tag === 'Left') {
123
- if (loaded.left instanceof NotAuthenticated) return { state: 'absent' as const, healthy: false }
124
- return yield* mapCredentialError(loaded.left)
125
- }
126
- const now = yield* DateTime.now
127
- const state = authMethodState(loaded.right.authMethod, now)
128
- return { state, healthy: state === 'healthy' }
129
- })
130
- }
131
-
132
- function pendingStatus(record: Option.Option<PendingInitRecord>) {
133
- return Effect.gen(function* () {
134
- if (Option.isNone(record)) return { state: 'none' as const, count: 0 as const }
135
- const now = DateTime.toEpochMillis(yield* DateTime.now)
136
- if (DateTime.toEpochMillis(record.value.approvalExpiresAt) <= now) {
137
- return { state: 'none' as const, count: 0 as const }
138
- }
139
- return {
140
- state: 'approval_pending' as const,
141
- count: 1 as const,
142
- approvalExpiresAt: DateTime.formatIso(record.value.approvalExpiresAt),
143
- }
144
- })
145
- }
146
-
147
- function validateInspection(
148
- harness: typeof InitHarness.Type,
149
- inspection: HarnessInspection,
150
- ): Effect.Effect<HarnessInspection, StatusServiceError> {
151
- const componentSet = new Set(inspection.components)
152
- // Only the harnesses with a web-fetch hook install three components; cursor
153
- // and codex are complete with two. Demanding three of them reported a
154
- // correctly configured machine as an invalid result.
155
- const expectsHook = harness === 'claude-code' || harness === 'opencode'
156
- const allComponents =
157
- componentSet.has('runtime-skill') &&
158
- componentSet.has('mcp') &&
159
- (expectsHook ? componentSet.has('webfetch-hook') && componentSet.size === 3 : componentSet.size === 2)
160
- const versionValid =
161
- inspection.integrationVersion === undefined ||
162
- (inspection.integrationVersion.length <= 64 && VERSION_PATTERN.test(inspection.integrationVersion))
163
- const configuredValid = inspection.configured === allComponents
164
- const runtimeValid = inspection.runtimeState !== 'observed'
165
- return componentSet.size === inspection.components.length && versionValid && configuredValid && runtimeValid
166
- ? Effect.succeed(inspection)
167
- : statusError('invalid_status_result', false)
168
- }
169
-
170
- function installationStatus(inspection: HarnessInspection, scope: typeof InitScope.Type) {
171
- const components = new Set(inspection.components)
172
- const policyBlocked = inspection.policyBlock !== undefined || inspection.runtimeState === 'blocked'
173
- const policyCodes =
174
- inspection.policyBlock === CLAUDE_POLICY_READ_ONLY
175
- ? (['target_read_only'] as const)
176
- : policyBlocked
177
- ? (['managed_policy_blocked'] as const)
178
- : []
179
- const reloadRequired = !policyBlocked && inspection.runtimeState === 'reload_required'
180
- return {
181
- configured: inspection.configured,
182
- ...(inspection.integrationVersion === undefined ? {} : { integrationVersion: inspection.integrationVersion }),
183
- components: {
184
- webfetchHook: components.has('webfetch-hook'),
185
- runtimeSkill: components.has('runtime-skill'),
186
- mcp: components.has('mcp'),
187
- },
188
- runtimeState: policyBlocked
189
- ? ('blocked' as const)
190
- : reloadRequired
191
- ? ('reload_required' as const)
192
- : ('unverified' as const),
193
- reloadState: reloadRequired ? ('required' as const) : ('not_required' as const),
194
- consentState: scope === 'project' ? ('may_be_required' as const) : ('not_required' as const),
195
- policyState: policyBlocked ? ('blocked' as const) : ('unverified' as const),
196
- policyCodes,
197
- }
198
- }
199
-
200
- export class StatusService extends Effect.Service<StatusService>()('@expandai/cli/Init/StatusService', {
201
- dependencies: [ConfigManager.Default, PendingInitStore.Default, HarnessRegistry.Default],
202
- effect: Effect.gen(function* () {
203
- const configManager = yield* ConfigManager
204
- const pendingStore = yield* PendingInitStore
205
- const registry = yield* HarnessRegistry
206
-
207
- const run = Effect.fn('StatusService.run')(function* (request: StatusRequest) {
208
- yield* Effect.annotateCurrentSpan({ harness: request.harness, scope: request.scope })
209
- return yield* Effect.gen(function* () {
210
- yield* configManager
211
- .inspectSecureStorage()
212
- .pipe(Effect.withTracerEnabled(false), Effect.mapError(mapCredentialError))
213
- const auth = yield* authStatus(configManager).pipe(Effect.withTracerEnabled(false))
214
- const apiUrl = yield* configManager
215
- .getApiUrl()
216
- .pipe(Effect.withTracerEnabled(false), Effect.mapError(mapCredentialError))
217
- const canonicalScopeRoot = yield* pendingStore
218
- .canonicalScopeRoot(request.scope)
219
- .pipe(Effect.withTracerEnabled(false), Effect.mapError(mapPendingError))
220
- const key: PendingInitKey = {
221
- apiOrigin: apiUrl.origin,
222
- clientId: CLI_AUTHORIZATION_CLIENT_ID,
223
- harness: 'agent-harnesses',
224
- scope: request.scope,
225
- canonicalScopeRoot,
226
- }
227
- const pending = yield* pendingStore
228
- .peek(key)
229
- .pipe(Effect.withTracerEnabled(false), Effect.mapError(mapPendingError))
230
- const inspection = yield* registry.inspect(request.harness, request.scope).pipe(
231
- Effect.withTracerEnabled(false),
232
- Effect.mapError(() => statusError('harness_inspection_failed', true)),
233
- Effect.flatMap((inspection) => validateInspection(request.harness, inspection)),
234
- )
235
- const candidate = {
236
- schemaVersion: 1 as const,
237
- status: 'ok' as const,
238
- harness: request.harness,
239
- scope: request.scope,
240
- cli: { version: CLI_VERSION },
241
- auth,
242
- pendingAuthorization: yield* pendingStatus(pending),
243
- installation: installationStatus(inspection, request.scope),
244
- }
245
- return yield* Schema.decodeUnknown(StatusResult)(candidate).pipe(
246
- Effect.mapError(() => statusError('invalid_status_result', false)),
247
- )
248
- })
249
- })
250
-
251
- return { run } as const
252
- }),
253
- }) {}
@@ -1,286 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Array, Config, Console, Effect, Match, Schema } from 'effect'
3
- import { CLI_VERSION } from '../../Version.js'
4
- import { type CliCommand, ConfigIo, cliShellCommand, DIRECT_CLI } from '../Domain.js'
5
- import type { InstallScope } from '../Mcp.js'
6
-
7
- /** Claude Code WebFetch hook invocation persisted in harness settings. */
8
- export const HOOK_INVOCATION = 'expandai hook claude-code'
9
- export const HOOK_COMMAND = HOOK_INVOCATION
10
- const HOOK_ARGS = ['hook', 'claude-code'] as const
11
-
12
- /**
13
- * The hook line for a resolved CLI. Defaults to the bare `expandai` command;
14
- * when the CLI is not on PATH — an `npx expandai init` with no global install —
15
- * `CliInvocation` resolves to the npx form and the written hook stays runnable.
16
- */
17
- export function hookCommandFor(cli: CliCommand = DIRECT_CLI): string {
18
- return cliShellCommand(cli, HOOK_ARGS)
19
- }
20
- export const WEBFETCH_MATCHER = 'WebFetch'
21
-
22
- export const HookCommandEntry = Schema.Struct(
23
- {
24
- type: Schema.Literal('command'),
25
- command: Schema.String,
26
- expandaiProfileVersion: Schema.optional(Schema.String),
27
- /** Whether this install added the `WebFetch` deny rule — see `makeExpandaiMatcher`. */
28
- expandaiManagedDeny: Schema.optional(Schema.Boolean),
29
- },
30
- Schema.Record({ key: Schema.String, value: Schema.Unknown }),
31
- )
32
-
33
- export const HookMatcher = Schema.Struct(
34
- {
35
- matcher: Schema.optional(Schema.String),
36
- hooks: Schema.Array(HookCommandEntry),
37
- },
38
- Schema.Record({ key: Schema.String, value: Schema.Unknown }),
39
- )
40
-
41
- const HooksSection = Schema.Struct(
42
- { PreToolUse: Schema.optional(Schema.Array(HookMatcher)) },
43
- Schema.Record({ key: Schema.String, value: Schema.Unknown }),
44
- )
45
-
46
- const PermissionsSection = Schema.Struct(
47
- { deny: Schema.optional(Schema.Array(Schema.String)) },
48
- Schema.Record({ key: Schema.String, value: Schema.Unknown }),
49
- )
50
-
51
- export const SettingsSchema = Schema.parseJson(
52
- Schema.Struct(
53
- {
54
- hooks: Schema.optionalWith(HooksSection, { default: () => ({}) as typeof HooksSection.Type }),
55
- permissions: Schema.optional(PermissionsSection),
56
- },
57
- Schema.Record({ key: Schema.String, value: Schema.Unknown }),
58
- ),
59
- { space: 2 },
60
- )
61
-
62
- export interface ClaudeHookPlan {
63
- readonly changed: boolean
64
- readonly settings: typeof SettingsSchema.Type
65
- }
66
-
67
- export function isExpandaiHookCommand(command: string): boolean {
68
- const value = command.trim()
69
- const invocation = String.raw`expandai(?:\.exe)?\s+hook\s+(?:claude-code|claude)`
70
- const direct = new RegExp(String.raw`^(?:npx\s+-y\s+)?${invocation}$`, 'u')
71
- const absolute = new RegExp(
72
- String.raw`^(?:"(?:[A-Za-z]:\\|/)[^"]*[\\/]expandai(?:\.exe)?"|'(?:[A-Za-z]:\\|/)[^']*[\\/]expandai(?:\.exe)?'|(?:[A-Za-z]:\\|/)[^\s]*[\\/]expandai(?:\.exe)?)\s+hook\s+(?:claude-code|claude)$`,
73
- 'u',
74
- )
75
- const shellFallback =
76
- /^if\s+command\s+-v\s+expandai\s+>\/dev\/null\s+2>&1;\s*then\s+expandai\s+hook\s+(?:claude-code|claude);\s*else\s+npx\s+-y\s+expandai\s+hook\s+(?:claude-code|claude);\s*fi$/u
77
- return direct.test(value) || absolute.test(value) || shellFallback.test(value)
78
- }
79
-
80
- /**
81
- * `expandaiManagedDeny` records whether *we* added `WebFetch` to
82
- * `permissions.deny`, so uninstall can tell our entry from an identical one the
83
- * user set first. A deny entry is a bare string with nowhere to carry that, and
84
- * "our hook is present" is not the same claim: installing over a settings file
85
- * that already denied `WebFetch` leaves the hook without us having written the
86
- * rule, and removing it then revokes a control its owner chose.
87
- */
88
- export function makeExpandaiMatcher(
89
- profileVersion: string,
90
- cli: CliCommand = DIRECT_CLI,
91
- managedDeny = true,
92
- ): typeof HookMatcher.Type {
93
- return {
94
- matcher: WEBFETCH_MATCHER,
95
- hooks: [
96
- {
97
- type: 'command',
98
- command: hookCommandFor(cli),
99
- expandaiProfileVersion: profileVersion,
100
- expandaiManagedDeny: managedDeny,
101
- },
102
- ],
103
- }
104
- }
105
-
106
- /** True only when our own hook entry says we wrote the deny rule. */
107
- export function expandaiOwnsWebFetchDeny(settings: typeof SettingsSchema.Type): boolean {
108
- return (settings.hooks.PreToolUse ?? []).some((entry) =>
109
- entry.hooks.some((hook) => isExpandaiHookCommand(hook.command) && hook.expandaiManagedDeny === true),
110
- )
111
- }
112
-
113
- function webFetchAlreadyDenied(settings: typeof SettingsSchema.Type): boolean {
114
- return (settings.permissions?.deny ?? []).includes(WEBFETCH_MATCHER)
115
- }
116
-
117
- export const expandaiMatcher = makeExpandaiMatcher(CLI_VERSION)
118
-
119
- function stripExpandaiHooks(entries: ReadonlyArray<typeof HookMatcher.Type>): Array<typeof HookMatcher.Type> {
120
- return Array.flatMap(entries, (entry) => {
121
- const hooks = Array.filter(entry.hooks, (hook) => !isExpandaiHookCommand(hook.command))
122
- return hooks.length === 0 && hooks.length !== entry.hooks.length ? [] : [{ ...entry, hooks }]
123
- })
124
- }
125
-
126
- /**
127
- * Denying `WebFetch` removes it from the toolset rather than blocking it per call,
128
- * so the agent reaches for Expand's tool instead of being refused. That difference
129
- * is the whole point: a hook can only answer a WebFetch call with `deny`, which
130
- * Claude Code renders to the user as `Error:` on every single fetch.
131
- *
132
- * The hook stays installed as the fallback. With the deny rule in force WebFetch is
133
- * never called, so the hook never fires; if the rule is ever absent or overridden,
134
- * the hook still intercepts — with the red line, but correctly.
135
- */
136
- export function planClaudeWebFetchDeny(settings: typeof SettingsSchema.Type): ClaudeHookPlan {
137
- const deny = settings.permissions?.deny ?? []
138
- if (deny.includes(WEBFETCH_MATCHER)) return { changed: false, settings }
139
- return {
140
- changed: true,
141
- settings: {
142
- ...settings,
143
- permissions: { ...(settings.permissions ?? {}), deny: [...deny, WEBFETCH_MATCHER] },
144
- },
145
- }
146
- }
147
-
148
- /**
149
- * Drops the `WebFetch` entry; anything else the user denied is left alone.
150
- *
151
- * A deny entry is a bare string with nowhere to record who wrote it, so this
152
- * cannot tell our rule from an identical one the user added themselves. The
153
- * caller supplies that evidence — see `uninstall`.
154
- */
155
- export function planClaudeWebFetchDenyRemoval(settings: typeof SettingsSchema.Type): ClaudeHookPlan {
156
- const deny = settings.permissions?.deny
157
- if (deny === undefined || !deny.includes(WEBFETCH_MATCHER)) return { changed: false, settings }
158
- const remaining = deny.filter((entry) => entry !== WEBFETCH_MATCHER)
159
- const permissions = { ...settings.permissions, deny: remaining }
160
- return { changed: true, settings: { ...settings, permissions } }
161
- }
162
-
163
- export function planClaudeHook(
164
- settings: typeof SettingsSchema.Type,
165
- profileVersion: string = CLI_VERSION,
166
- cli: CliCommand = DIRECT_CLI,
167
- ): ClaudeHookPlan {
168
- const preToolUse = settings.hooks.PreToolUse ?? []
169
- const owned = preToolUse.flatMap((entry) =>
170
- entry.hooks.filter((hook) => isExpandaiHookCommand(hook.command)).map((hook) => ({ entry, hook })),
171
- )
172
- // The marker records history — did *this* install add the rule — so once it is
173
- // written it must be preserved, not recomputed. Recomputing it from present
174
- // state makes the plan disagree with what it just wrote: after we add the deny
175
- // rule, `webFetchAlreadyDenied` is true, so a second plan would decide the rule
176
- // was never ours and rewrite the hook forever.
177
- const existingMarker = owned[0]?.hook.expandaiManagedDeny
178
- const managedDeny = existingMarker ?? !webFetchAlreadyDenied(settings)
179
- const current =
180
- owned.length === 1 &&
181
- owned[0]?.entry.matcher === WEBFETCH_MATCHER &&
182
- owned[0]?.hook.command === hookCommandFor(cli) &&
183
- owned[0]?.hook.expandaiProfileVersion === profileVersion &&
184
- owned[0]?.hook.expandaiManagedDeny === managedDeny
185
- if (current) return { changed: false, settings }
186
-
187
- return {
188
- changed: true,
189
- settings: {
190
- ...settings,
191
- hooks: {
192
- ...settings.hooks,
193
- PreToolUse: [...stripExpandaiHooks(preToolUse), makeExpandaiMatcher(profileVersion, cli, managedDeny)],
194
- },
195
- },
196
- }
197
- }
198
-
199
- export function planClaudeHookRemoval(settings: typeof SettingsSchema.Type): ClaudeHookPlan {
200
- const preToolUse = settings.hooks.PreToolUse ?? []
201
- const changed = preToolUse.some((entry) => entry.hooks.some((hook) => isExpandaiHookCommand(hook.command)))
202
- return changed
203
- ? {
204
- changed,
205
- settings: {
206
- ...settings,
207
- hooks: { ...settings.hooks, PreToolUse: stripExpandaiHooks(preToolUse) },
208
- },
209
- }
210
- : { changed, settings }
211
- }
212
-
213
- export class ClaudeHook extends Effect.Service<ClaudeHook>()('@expandai/cli/Install/Claude/Hook', {
214
- dependencies: [ConfigIo.Default],
215
- effect: Effect.gen(function* () {
216
- const io = yield* ConfigIo
217
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
218
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
219
-
220
- const pathFor = Effect.fn(function* (scope: InstallScope) {
221
- return yield* Match.value(scope).pipe(
222
- Match.when('global', () => Effect.succeed(`${home}/.claude/settings.json`)),
223
- Match.when('project', () => Effect.succeed(`${cwd}/.claude/settings.json`)),
224
- Match.exhaustive,
225
- )
226
- })
227
-
228
- const install = Effect.fn('ClaudeHook.install')(function* (scope: InstallScope) {
229
- const target = yield* pathFor(scope)
230
- const settings = yield* io.read(target, SettingsSchema)
231
- // Both halves, exactly as `init` writes them. This used to install the hook
232
- // alone, which left the deny rule to `init` only — and made `uninstall`'s
233
- // ownership rule false: it treats our hook as evidence that we wrote the
234
- // deny entry, so a rule the user added after a bare `hook install` was
235
- // deleted as if it were ours.
236
- const hookPlan = planClaudeHook(settings)
237
- const plan = planClaudeWebFetchDeny(hookPlan.settings)
238
- if (!hookPlan.changed && !plan.changed) {
239
- yield* Console.log(`expandai WebFetch hook already installed at ${target}`)
240
- return
241
- }
242
- yield* io.write(target, plan.settings, SettingsSchema)
243
- yield* Console.log(`Installed expandai WebFetch hook and deny rule at ${target}`)
244
- })
245
-
246
- const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (scope: InstallScope) {
247
- const target = yield* pathFor(scope)
248
- if (!(yield* io.exists(target))) return
249
- const settings = yield* io.read(target, SettingsSchema)
250
- // Uninstall has to undo both halves, or `WebFetch` stays denied with nothing
251
- // left to serve the fetch — worse than never having installed us.
252
- //
253
- // But only the halves we wrote. `expandaiManagedDeny` on our hook records
254
- // whether this install added the deny rule; without it, the rule is either
255
- // the user's own or from an install predating the marker, and those are
256
- // indistinguishable. Guessing "ours" revokes a control its owner chose;
257
- // guessing "theirs" leaves `WebFetch` denied with nothing serving fetches.
258
- // So leave it and say so — `uninstall` also fans out over both scopes, and
259
- // would otherwise reach into a global settings file a project-scoped
260
- // install never touched.
261
- const hookPlan = planClaudeHookRemoval(settings)
262
- if (!hookPlan.changed) return
263
- // Read the marker off the hook before it is stripped.
264
- const owned = expandaiOwnsWebFetchDeny(settings)
265
- const denyPlan = owned ? planClaudeWebFetchDenyRemoval(hookPlan.settings) : hookPlan
266
- const strandedDeny = !owned && (settings.permissions?.deny ?? []).includes(WEBFETCH_MATCHER)
267
- yield* io.write(target, denyPlan.settings, SettingsSchema)
268
- // `denyPlan` is the hook plan itself when we do not own the rule, so its
269
- // `changed` flag reports the hook removal — not a deny removal that never
270
- // happened.
271
- const removedDeny = owned && denyPlan.changed
272
- yield* Console.log(
273
- removedDeny
274
- ? `Removed expandai WebFetch hook and deny rule from ${target}`
275
- : `Removed expandai WebFetch hook from ${target}`,
276
- )
277
- if (strandedDeny) {
278
- yield* Console.log(
279
- `Left "WebFetch" in permissions.deny at ${target} — expandai could not confirm it added that rule. Remove it by hand if you want Claude Code's built-in WebFetch back.`,
280
- )
281
- }
282
- })
283
-
284
- return { install, uninstall, pathFor } as const
285
- }),
286
- }) {}
@@ -1,129 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
- import {
4
- type CliCommand,
5
- ConfigIo,
6
- DIRECT_CLI,
7
- McpParseError,
8
- McpServersConfigSchema,
9
- McpWriteError,
10
- SERVER_KEY,
11
- } from '../Domain.js'
12
- import type { InstallScope } from '../Mcp.js'
13
-
14
- /** Claude Code MCP descriptor stored under the Expand-owned server key. */
15
- export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
16
- type: Schema.Literal('stdio'),
17
- command: Schema.NonEmptyTrimmedString,
18
- args: Schema.Array(Schema.String),
19
- env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
20
- }) {}
21
-
22
- export interface ClaudeMcpPlan {
23
- readonly changed: boolean
24
- readonly config: typeof McpServersConfigSchema.Type
25
- }
26
-
27
- const MCP_ARGS = ['mcp', 'run'] as const
28
-
29
- /** Claude execs the descriptor directly, so `command`/`args` stay separate. */
30
- export function claudeMcpEntry(cli: CliCommand = DIRECT_CLI): Entry {
31
- return new Entry({
32
- type: 'stdio',
33
- command: cli.command,
34
- args: [...cli.args, ...MCP_ARGS],
35
- env: { EXPANDAI_HARNESS: 'claude-code' },
36
- })
37
- }
38
-
39
- export const CLAUDE_MCP_ENTRY = claudeMcpEntry()
40
-
41
- function isUnknownRecord(value: unknown): value is Record<string, unknown> {
42
- return typeof value === 'object' && value !== null && !Array.isArray(value)
43
- }
44
-
45
- function isCurrentEntry(value: unknown, expected: Entry): boolean {
46
- if (!isUnknownRecord(value)) return false
47
- const args = value.args
48
- const env = value.env
49
- return (
50
- value.type === expected.type &&
51
- value.command === expected.command &&
52
- Array.isArray(args) &&
53
- args.length === expected.args.length &&
54
- args.every((arg, index) => arg === expected.args[index]) &&
55
- isUnknownRecord(env) &&
56
- env.EXPANDAI_HARNESS === 'claude-code'
57
- )
58
- }
59
-
60
- export function planClaudeMcp(config: typeof McpServersConfigSchema.Type, cli: CliCommand = DIRECT_CLI): ClaudeMcpPlan {
61
- const entry = claudeMcpEntry(cli)
62
- if (isCurrentEntry(config.mcpServers[SERVER_KEY], entry)) return { changed: false, config }
63
- return {
64
- changed: true,
65
- config: {
66
- ...config,
67
- mcpServers: { ...config.mcpServers, [SERVER_KEY]: entry },
68
- },
69
- }
70
- }
71
-
72
- export function planClaudeMcpRemoval(config: typeof McpServersConfigSchema.Type): ClaudeMcpPlan {
73
- if (!(SERVER_KEY in config.mcpServers)) return { changed: false, config }
74
- return {
75
- changed: true,
76
- config: { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) },
77
- }
78
- }
79
-
80
- export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Install/Claude/Mcp', {
81
- dependencies: [ConfigIo.Default],
82
- effect: Effect.gen(function* () {
83
- const io = yield* ConfigIo
84
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
85
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
86
-
87
- const pathFor = Effect.fn(function* (scope: InstallScope) {
88
- return yield* Match.value(scope).pipe(
89
- Match.when('global', () => Effect.succeed(`${home}/.claude.json`)),
90
- Match.when('project', () => Effect.succeed(`${cwd}/.mcp.json`)),
91
- Match.exhaustive,
92
- )
93
- })
94
-
95
- const readConfig = Effect.fn(function* (file: string) {
96
- return yield* io
97
- .read(file, McpServersConfigSchema)
98
- .pipe(Effect.mapError((error) => new McpParseError({ path: file, error })))
99
- })
100
-
101
- const writeConfig = Effect.fn(function* (file: string, value: unknown) {
102
- yield* io
103
- .write(file, value, McpServersConfigSchema)
104
- .pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
105
- })
106
-
107
- const install = Effect.fn(function* (scope: InstallScope) {
108
- const file = yield* pathFor(scope)
109
- const plan = planClaudeMcp(yield* readConfig(file))
110
- if (plan.changed) yield* writeConfig(file, plan.config)
111
- yield* Console.log(`Installed expandai MCP server into ${file}`)
112
- })
113
-
114
- const uninstall = Effect.fn(function* (scope: InstallScope) {
115
- const file = yield* pathFor(scope)
116
- const plan = planClaudeMcpRemoval(yield* readConfig(file))
117
- if (!plan.changed) return
118
- yield* writeConfig(file, plan.config)
119
- yield* Console.log(`Removed expandai MCP server from ${file}`)
120
- })
121
-
122
- const isInstalled = Effect.fn(function* (scope: InstallScope) {
123
- const file = yield* pathFor(scope)
124
- return SERVER_KEY in (yield* readConfig(file)).mcpServers
125
- })
126
-
127
- return { install, uninstall, isInstalled, pathFor } as const
128
- }),
129
- }) {}