expandai 0.0.2 → 0.0.3

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 (271) hide show
  1. package/README.md +42 -5
  2. package/build/dts/ApiClient.d.ts +128 -194
  3. package/build/dts/ApiClient.d.ts.map +1 -1
  4. package/build/dts/Auth/Domain.d.ts +95 -0
  5. package/build/dts/Auth/Domain.d.ts.map +1 -0
  6. package/build/dts/Auth/ManualAuthorization.d.ts +107 -0
  7. package/build/dts/Auth/ManualAuthorization.d.ts.map +1 -0
  8. package/build/dts/Auth/PendingInit.d.ts +35 -0
  9. package/build/dts/Auth/PendingInit.d.ts.map +1 -0
  10. package/build/dts/Auth/Pkce.d.ts +10 -0
  11. package/build/dts/Auth/Pkce.d.ts.map +1 -0
  12. package/build/dts/Auth/SetupGrantAuthorization.d.ts +34 -0
  13. package/build/dts/Auth/SetupGrantAuthorization.d.ts.map +1 -0
  14. package/build/dts/Authentication.d.ts +10 -9
  15. package/build/dts/Authentication.d.ts.map +1 -1
  16. package/build/dts/BrowserOAuth.d.ts +7 -7
  17. package/build/dts/BrowserOAuth.d.ts.map +1 -1
  18. package/build/dts/CommandPath.d.ts +2 -0
  19. package/build/dts/CommandPath.d.ts.map +1 -0
  20. package/build/dts/Commands/Fetch.d.ts.map +1 -1
  21. package/build/dts/Commands/Hook.d.ts +5 -5
  22. package/build/dts/Commands/Init.d.ts +55 -0
  23. package/build/dts/Commands/Init.d.ts.map +1 -0
  24. package/build/dts/Commands/Mcp.d.ts +6 -6
  25. package/build/dts/Commands/Search.d.ts.map +1 -1
  26. package/build/dts/Commands/Skill.d.ts +4 -4
  27. package/build/dts/Commands/Status.d.ts +27 -0
  28. package/build/dts/Commands/Status.d.ts.map +1 -0
  29. package/build/dts/Commands/Telemetry.d.ts +7 -0
  30. package/build/dts/Commands/Telemetry.d.ts.map +1 -0
  31. package/build/dts/Commands/Uninstall.d.ts +4 -4
  32. package/build/dts/Config.d.ts +108 -17
  33. package/build/dts/Config.d.ts.map +1 -1
  34. package/build/dts/Hook/SnapshotNote.d.ts +8 -4
  35. package/build/dts/Hook/SnapshotNote.d.ts.map +1 -1
  36. package/build/dts/Init/ClaudeAdapter.d.ts +61 -0
  37. package/build/dts/Init/ClaudeAdapter.d.ts.map +1 -0
  38. package/build/dts/Init/ClaudeProfile.d.ts +78 -0
  39. package/build/dts/Init/ClaudeProfile.d.ts.map +1 -0
  40. package/build/dts/Init/Domain.d.ts +139 -0
  41. package/build/dts/Init/Domain.d.ts.map +1 -0
  42. package/build/dts/Init/HarnessAdapter.d.ts +33 -0
  43. package/build/dts/Init/HarnessAdapter.d.ts.map +1 -0
  44. package/build/dts/Init/HarnessDetector.d.ts +19 -0
  45. package/build/dts/Init/HarnessDetector.d.ts.map +1 -0
  46. package/build/dts/Init/HarnessRegistry.d.ts +50 -0
  47. package/build/dts/Init/HarnessRegistry.d.ts.map +1 -0
  48. package/build/dts/Init/MutationTransaction.d.ts +96 -0
  49. package/build/dts/Init/MutationTransaction.d.ts.map +1 -0
  50. package/build/dts/Init/Service.d.ts +72 -0
  51. package/build/dts/Init/Service.d.ts.map +1 -0
  52. package/build/dts/Init/StandardAdapters.d.ts +42 -0
  53. package/build/dts/Init/StandardAdapters.d.ts.map +1 -0
  54. package/build/dts/Init/Status.d.ts +149 -0
  55. package/build/dts/Init/Status.d.ts.map +1 -0
  56. package/build/dts/Install/Claude/Hook.d.ts +85 -25
  57. package/build/dts/Install/Claude/Hook.d.ts.map +1 -1
  58. package/build/dts/Install/Claude/Mcp.d.ts +21 -18
  59. package/build/dts/Install/Claude/Mcp.d.ts.map +1 -1
  60. package/build/dts/Install/Claude/Skill.d.ts +14 -15
  61. package/build/dts/Install/Claude/Skill.d.ts.map +1 -1
  62. package/build/dts/Install/Codex/Mcp.d.ts +14 -8
  63. package/build/dts/Install/Codex/Mcp.d.ts.map +1 -1
  64. package/build/dts/Install/Codex/Skill.d.ts +4 -2
  65. package/build/dts/Install/Codex/Skill.d.ts.map +1 -1
  66. package/build/dts/Install/Cursor/Mcp.d.ts +13 -13
  67. package/build/dts/Install/Cursor/Mcp.d.ts.map +1 -1
  68. package/build/dts/Install/Cursor/Skill.d.ts +4 -2
  69. package/build/dts/Install/Cursor/Skill.d.ts.map +1 -1
  70. package/build/dts/Install/Domain.d.ts +35 -5
  71. package/build/dts/Install/Domain.d.ts.map +1 -1
  72. package/build/dts/Install/Hook.d.ts +4 -4
  73. package/build/dts/Install/Hook.d.ts.map +1 -1
  74. package/build/dts/Install/Mcp.d.ts +5 -5
  75. package/build/dts/Install/Mcp.d.ts.map +1 -1
  76. package/build/dts/Install/Opencode/Hook.d.ts +4 -3
  77. package/build/dts/Install/Opencode/Hook.d.ts.map +1 -1
  78. package/build/dts/Install/Opencode/Mcp.d.ts +11 -11
  79. package/build/dts/Install/Opencode/Mcp.d.ts.map +1 -1
  80. package/build/dts/Install/Opencode/Skill.d.ts +4 -2
  81. package/build/dts/Install/Opencode/Skill.d.ts.map +1 -1
  82. package/build/dts/Install/Skill.d.ts +3 -3
  83. package/build/dts/Install/Uninstall.d.ts +6 -6
  84. package/build/dts/Install/Uninstall.d.ts.map +1 -1
  85. package/build/dts/MarkdownAnsi.d.ts +29 -0
  86. package/build/dts/MarkdownAnsi.d.ts.map +1 -0
  87. package/build/dts/Mcp/ExpandDocs.d.ts +17 -5
  88. package/build/dts/Mcp/ExpandDocs.d.ts.map +1 -1
  89. package/build/dts/Mcp/Server.d.ts +1 -1
  90. package/build/dts/Mcp/Server.d.ts.map +1 -1
  91. package/build/dts/Mcp/tools/Citations.d.ts +100 -71
  92. package/build/dts/Mcp/tools/Citations.d.ts.map +1 -1
  93. package/build/dts/Mcp/tools/Fetch.d.ts +16 -15
  94. package/build/dts/Mcp/tools/Fetch.d.ts.map +1 -1
  95. package/build/dts/SecureFile.d.ts +23 -0
  96. package/build/dts/SecureFile.d.ts.map +1 -0
  97. package/build/dts/Snippets.d.ts +11 -12
  98. package/build/dts/Snippets.d.ts.map +1 -1
  99. package/build/dts/Telemetry.d.ts +30 -0
  100. package/build/dts/Telemetry.d.ts.map +1 -0
  101. package/build/dts/Version.d.ts +2 -0
  102. package/build/dts/Version.d.ts.map +1 -0
  103. package/build/dts/Xdg.d.ts +11 -0
  104. package/build/dts/Xdg.d.ts.map +1 -0
  105. package/build/esm/ApiClient.js +133 -10
  106. package/build/esm/ApiClient.js.map +1 -1
  107. package/build/esm/Auth/Domain.js +56 -0
  108. package/build/esm/Auth/Domain.js.map +1 -0
  109. package/build/esm/Auth/ManualAuthorization.js +253 -0
  110. package/build/esm/Auth/ManualAuthorization.js.map +1 -0
  111. package/build/esm/Auth/PendingInit.js +267 -0
  112. package/build/esm/Auth/PendingInit.js.map +1 -0
  113. package/build/esm/Auth/Pkce.js +23 -0
  114. package/build/esm/Auth/Pkce.js.map +1 -0
  115. package/build/esm/Auth/SetupGrantAuthorization.js +91 -0
  116. package/build/esm/Auth/SetupGrantAuthorization.js.map +1 -0
  117. package/build/esm/Authentication.js +38 -7
  118. package/build/esm/Authentication.js.map +1 -1
  119. package/build/esm/BrowserOAuth.js +7 -10
  120. package/build/esm/BrowserOAuth.js.map +1 -1
  121. package/build/esm/CommandPath.js +31 -0
  122. package/build/esm/CommandPath.js.map +1 -0
  123. package/build/esm/Commands/Fetch.js +30 -6
  124. package/build/esm/Commands/Fetch.js.map +1 -1
  125. package/build/esm/Commands/Init.js +371 -0
  126. package/build/esm/Commands/Init.js.map +1 -0
  127. package/build/esm/Commands/Search.js +6 -3
  128. package/build/esm/Commands/Search.js.map +1 -1
  129. package/build/esm/Commands/Status.js +186 -0
  130. package/build/esm/Commands/Status.js.map +1 -0
  131. package/build/esm/Commands/Telemetry.js +23 -0
  132. package/build/esm/Commands/Telemetry.js.map +1 -0
  133. package/build/esm/Config.js +502 -28
  134. package/build/esm/Config.js.map +1 -1
  135. package/build/esm/Hook/Claude.js +1 -1
  136. package/build/esm/Hook/Claude.js.map +1 -1
  137. package/build/esm/Hook/SnapshotNote.js +54 -7
  138. package/build/esm/Hook/SnapshotNote.js.map +1 -1
  139. package/build/esm/Init/ClaudeAdapter.js +137 -0
  140. package/build/esm/Init/ClaudeAdapter.js.map +1 -0
  141. package/build/esm/Init/ClaudeProfile.js +380 -0
  142. package/build/esm/Init/ClaudeProfile.js.map +1 -0
  143. package/build/esm/Init/Domain.js +118 -0
  144. package/build/esm/Init/Domain.js.map +1 -0
  145. package/build/esm/Init/HarnessAdapter.js +21 -0
  146. package/build/esm/Init/HarnessAdapter.js.map +1 -0
  147. package/build/esm/Init/HarnessDetector.js +65 -0
  148. package/build/esm/Init/HarnessDetector.js.map +1 -0
  149. package/build/esm/Init/HarnessRegistry.js +58 -0
  150. package/build/esm/Init/HarnessRegistry.js.map +1 -0
  151. package/build/esm/Init/MutationTransaction.js +494 -0
  152. package/build/esm/Init/MutationTransaction.js.map +1 -0
  153. package/build/esm/Init/Service.js +319 -0
  154. package/build/esm/Init/Service.js.map +1 -0
  155. package/build/esm/Init/StandardAdapters.js +116 -0
  156. package/build/esm/Init/StandardAdapters.js.map +1 -0
  157. package/build/esm/Init/Status.js +215 -0
  158. package/build/esm/Init/Status.js.map +1 -0
  159. package/build/esm/Install/Claude/Hook.js +182 -50
  160. package/build/esm/Install/Claude/Hook.js.map +1 -1
  161. package/build/esm/Install/Claude/Mcp.js +56 -26
  162. package/build/esm/Install/Claude/Mcp.js.map +1 -1
  163. package/build/esm/Install/Claude/Skill.js +49 -27
  164. package/build/esm/Install/Claude/Skill.js.map +1 -1
  165. package/build/esm/Install/Codex/Mcp.js +9 -4
  166. package/build/esm/Install/Codex/Mcp.js.map +1 -1
  167. package/build/esm/Install/Codex/Skill.js +4 -1
  168. package/build/esm/Install/Codex/Skill.js.map +1 -1
  169. package/build/esm/Install/Cursor/Mcp.js +7 -4
  170. package/build/esm/Install/Cursor/Mcp.js.map +1 -1
  171. package/build/esm/Install/Cursor/Skill.js +4 -1
  172. package/build/esm/Install/Cursor/Skill.js.map +1 -1
  173. package/build/esm/Install/Domain.js +60 -3
  174. package/build/esm/Install/Domain.js.map +1 -1
  175. package/build/esm/Install/Hook.js.map +1 -1
  176. package/build/esm/Install/Mcp.js +1 -1
  177. package/build/esm/Install/Mcp.js.map +1 -1
  178. package/build/esm/Install/Opencode/Hook.js +1 -1
  179. package/build/esm/Install/Opencode/Hook.js.map +1 -1
  180. package/build/esm/Install/Opencode/Mcp.js +6 -3
  181. package/build/esm/Install/Opencode/Mcp.js.map +1 -1
  182. package/build/esm/Install/Opencode/Skill.js +4 -1
  183. package/build/esm/Install/Opencode/Skill.js.map +1 -1
  184. package/build/esm/Install/Opencode/expandai-webfetch.js +27 -9
  185. package/build/esm/Install/Skill.js.map +1 -1
  186. package/build/esm/Install/Uninstall.js +2 -2
  187. package/build/esm/Install/Uninstall.js.map +1 -1
  188. package/build/esm/MarkdownAnsi.js +142 -0
  189. package/build/esm/MarkdownAnsi.js.map +1 -0
  190. package/build/esm/Mcp/ExpandDocs.js +44 -52
  191. package/build/esm/Mcp/ExpandDocs.js.map +1 -1
  192. package/build/esm/Mcp/RequestHeaders.js.map +1 -1
  193. package/build/esm/Mcp/Server.js +2 -1
  194. package/build/esm/Mcp/Server.js.map +1 -1
  195. package/build/esm/Mcp/tools/Citations.js +109 -8
  196. package/build/esm/Mcp/tools/Citations.js.map +1 -1
  197. package/build/esm/Mcp/tools/Fetch.js +36 -12
  198. package/build/esm/Mcp/tools/Fetch.js.map +1 -1
  199. package/build/esm/SecureFile.js +36 -0
  200. package/build/esm/SecureFile.js.map +1 -0
  201. package/build/esm/Snippets.js +12 -1
  202. package/build/esm/Snippets.js.map +1 -1
  203. package/build/esm/Telemetry.js +157 -0
  204. package/build/esm/Telemetry.js.map +1 -0
  205. package/build/esm/Version.js +4 -0
  206. package/build/esm/Version.js.map +1 -0
  207. package/build/esm/Xdg.js +15 -0
  208. package/build/esm/Xdg.js.map +1 -0
  209. package/build/esm/main.js +122 -14
  210. package/build/esm/main.js.map +1 -1
  211. package/build/skills/expandai/SKILL.md +26 -3
  212. package/package.json +4 -4
  213. package/src/ApiClient.ts +206 -13
  214. package/src/Auth/Domain.ts +63 -0
  215. package/src/Auth/ManualAuthorization.ts +296 -0
  216. package/src/Auth/PendingInit.ts +338 -0
  217. package/src/Auth/Pkce.ts +26 -0
  218. package/src/Auth/SetupGrantAuthorization.ts +116 -0
  219. package/src/Authentication.ts +49 -11
  220. package/src/BrowserOAuth.ts +10 -12
  221. package/src/CommandPath.ts +30 -0
  222. package/src/Commands/Fetch.ts +42 -11
  223. package/src/Commands/Init.ts +489 -0
  224. package/src/Commands/Search.ts +8 -3
  225. package/src/Commands/Status.ts +228 -0
  226. package/src/Commands/Telemetry.ts +42 -0
  227. package/src/Config.ts +695 -50
  228. package/src/Hook/Claude.ts +1 -1
  229. package/src/Hook/SnapshotNote.ts +55 -7
  230. package/src/Init/ClaudeAdapter.ts +191 -0
  231. package/src/Init/ClaudeProfile.ts +496 -0
  232. package/src/Init/Domain.ts +153 -0
  233. package/src/Init/HarnessAdapter.ts +35 -0
  234. package/src/Init/HarnessDetector.ts +77 -0
  235. package/src/Init/HarnessRegistry.ts +87 -0
  236. package/src/Init/MutationTransaction.ts +747 -0
  237. package/src/Init/Service.ts +385 -0
  238. package/src/Init/StandardAdapters.ts +141 -0
  239. package/src/Init/Status.ts +253 -0
  240. package/src/Install/Claude/Hook.ts +234 -81
  241. package/src/Install/Claude/Mcp.ts +80 -33
  242. package/src/Install/Claude/Skill.ts +70 -37
  243. package/src/Install/Codex/Mcp.ts +14 -9
  244. package/src/Install/Codex/Skill.ts +9 -5
  245. package/src/Install/Cursor/Mcp.ts +12 -9
  246. package/src/Install/Cursor/Skill.ts +9 -5
  247. package/src/Install/Domain.ts +77 -4
  248. package/src/Install/Hook.ts +3 -3
  249. package/src/Install/Mcp.ts +6 -6
  250. package/src/Install/Opencode/Hook.ts +5 -5
  251. package/src/Install/Opencode/Mcp.ts +11 -8
  252. package/src/Install/Opencode/Skill.ts +9 -5
  253. package/src/Install/Opencode/expandai-webfetch.js +27 -9
  254. package/src/Install/Skill.ts +3 -3
  255. package/src/Install/Uninstall.ts +3 -3
  256. package/src/MarkdownAnsi.ts +162 -0
  257. package/src/Mcp/ExpandDocs.ts +46 -55
  258. package/src/Mcp/Server.ts +2 -1
  259. package/src/Mcp/tools/Citations.ts +117 -8
  260. package/src/Mcp/tools/Fetch.ts +40 -14
  261. package/src/SecureFile.ts +70 -0
  262. package/src/Snippets.ts +19 -12
  263. package/src/Telemetry.ts +186 -0
  264. package/src/Version.ts +3 -0
  265. package/src/Xdg.ts +18 -0
  266. package/src/main.ts +171 -12
  267. package/build/dts/DeviceAuth.d.ts +0 -87
  268. package/build/dts/DeviceAuth.d.ts.map +0 -1
  269. package/build/esm/DeviceAuth.js +0 -127
  270. package/build/esm/DeviceAuth.js.map +0 -1
  271. package/src/DeviceAuth.ts +0 -193
@@ -1,15 +1,17 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
- import { ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
4
- import type { Scope } from '../Mcp.js'
5
-
6
- /**
7
- * Claude Code MCP descriptor — `mcpServers` shape (de facto Claude Desktop
8
- * convention; also read by Cursor and other clients).
9
- *
10
- * - global → `~/.claude.json` (Claude Code's user-scope store)
11
- * - project → `<cwd>/.mcp.json` (the agent-agnostic project file)
12
- */
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. */
13
15
  export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
14
16
  type: Schema.Literal('stdio'),
15
17
  command: Schema.NonEmptyTrimmedString,
@@ -17,6 +19,64 @@ export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
17
19
  env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
18
20
  }) {}
19
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
+
20
80
  export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Install/Claude/Mcp', {
21
81
  dependencies: [ConfigIo.Default],
22
82
  effect: Effect.gen(function* () {
@@ -24,7 +84,7 @@ export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Instal
24
84
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
25
85
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
26
86
 
27
- const pathFor = Effect.fn(function* (scope: Scope) {
87
+ const pathFor = Effect.fn(function* (scope: InstallScope) {
28
88
  return yield* Match.value(scope).pipe(
29
89
  Match.when('global', () => Effect.succeed(`${home}/.claude.json`)),
30
90
  Match.when('project', () => Effect.succeed(`${cwd}/.mcp.json`)),
@@ -44,37 +104,24 @@ export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Instal
44
104
  .pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
45
105
  })
46
106
 
47
- const install = Effect.fn(function* (scope: Scope) {
107
+ const install = Effect.fn(function* (scope: InstallScope) {
48
108
  const file = yield* pathFor(scope)
49
- const config = yield* readConfig(file)
50
- yield* writeConfig(file, {
51
- ...config,
52
- mcpServers: {
53
- ...config.mcpServers,
54
- [SERVER_KEY]: new Entry({
55
- type: 'stdio',
56
- command: 'expandai',
57
- args: ['mcp', 'run'],
58
- }),
59
- },
60
- })
109
+ const plan = planClaudeMcp(yield* readConfig(file))
110
+ if (plan.changed) yield* writeConfig(file, plan.config)
61
111
  yield* Console.log(`Installed expandai MCP server into ${file}`)
62
112
  })
63
113
 
64
- const uninstall = Effect.fn(function* (scope: Scope) {
114
+ const uninstall = Effect.fn(function* (scope: InstallScope) {
65
115
  const file = yield* pathFor(scope)
66
- const config = yield* readConfig(file)
67
- if (!(SERVER_KEY in config.mcpServers)) {
68
- return
69
- }
70
- yield* writeConfig(file, { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) })
116
+ const plan = planClaudeMcpRemoval(yield* readConfig(file))
117
+ if (!plan.changed) return
118
+ yield* writeConfig(file, plan.config)
71
119
  yield* Console.log(`Removed expandai MCP server from ${file}`)
72
120
  })
73
121
 
74
- const isInstalled = Effect.fn(function* (scope: Scope) {
122
+ const isInstalled = Effect.fn(function* (scope: InstallScope) {
75
123
  const file = yield* pathFor(scope)
76
- const config = yield* readConfig(file)
77
- return SERVER_KEY in config.mcpServers
124
+ return SERVER_KEY in (yield* readConfig(file)).mcpServers
78
125
  })
79
126
 
80
127
  return { install, uninstall, isInstalled, pathFor } as const
@@ -1,44 +1,77 @@
1
1
  import { homedir } from 'node:os'
2
+ import { FileSystem, Path } from '@effect/platform'
3
+ import { NodeFileSystem, NodePath } from '@effect/platform-node'
2
4
  import { Config, Effect, Match } from 'effect'
3
- import { SkillIo } from '../Domain.js'
4
- import type { Scope } from '../Mcp.js'
5
-
6
- /**
7
- * Claude Code skills live under `<scope>/.claude/skills/<name>/SKILL.md`.
8
- *
9
- * - global → `~/.claude/skills/expandai/SKILL.md`
10
- * - project → `<cwd>/.claude/skills/expandai/SKILL.md`
11
- *
12
- * (Globally-scoped Claude Code skills are also reachable via `npx skills`,
13
- * but file-copy gives us a uniform install path with OpenCode and works
14
- * without npm.)
15
- */
5
+ import { CLI_VERSION } from '../../Version.js'
6
+ import type { InstallScope } from '../Mcp.js'
16
7
 
17
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
+ })
18
73
 
19
74
  export class ClaudeSkill extends Effect.Service<ClaudeSkill>()('@expandai/cli/Install/Claude/Skill', {
20
- dependencies: [SkillIo.Default],
21
- effect: Effect.gen(function* () {
22
- const io = yield* SkillIo
23
- const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
24
- const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
25
-
26
- const pathFor = Effect.fn(function* (scope: Scope) {
27
- return yield* Match.value(scope).pipe(
28
- Match.when('global', () => Effect.succeed(`${home}/.claude/skills/${SKILL_NAME}`)),
29
- Match.when('project', () => Effect.succeed(`${cwd}/.claude/skills/${SKILL_NAME}`)),
30
- Match.exhaustive,
31
- )
32
- })
33
-
34
- const install = Effect.fn(function* (scope: Scope) {
35
- return yield* io.install(yield* pathFor(scope))
36
- })
37
-
38
- const uninstall = Effect.fn(function* (scope: Scope) {
39
- return yield* io.uninstall(yield* pathFor(scope))
40
- })
41
-
42
- return { install, uninstall } as const
43
- }),
75
+ dependencies: [NodeFileSystem.layer, NodePath.layer],
76
+ effect: makeClaudeSkill,
44
77
  }) {}
@@ -1,12 +1,13 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
3
  import { parse, stringify, type TomlTableWithoutBigInt } from 'smol-toml'
4
- import { ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
5
- import type { Scope } from '../Mcp.js'
4
+ import { CliInvocation, ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
5
+ import type { InstallScope } from '../Mcp.js'
6
6
 
7
7
  export class Entry extends Schema.Class<Entry>('CodexMcpEntry')({
8
8
  command: Schema.NonEmptyTrimmedString,
9
9
  args: Schema.Array(Schema.String),
10
+ env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
10
11
  enabled: Schema.Boolean,
11
12
  }) {}
12
13
 
@@ -32,13 +33,14 @@ function toStringifyValue(value: TomlTableWithoutBigInt) {
32
33
  * - project → `<cwd>/.codex/config.toml`
33
34
  */
34
35
  export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/Codex/Mcp', {
35
- dependencies: [ConfigIo.Default],
36
+ dependencies: [ConfigIo.Default, CliInvocation.Default],
36
37
  effect: Effect.gen(function* () {
37
38
  const io = yield* ConfigIo
39
+ const cli = yield* CliInvocation
38
40
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
39
41
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
40
42
 
41
- const pathFor = Effect.fn(function* (scope: Scope) {
43
+ const pathFor = Effect.fn(function* (scope: InstallScope) {
42
44
  return yield* Match.value(scope).pipe(
43
45
  Match.when('global', () => Effect.succeed(`${home}/.codex/config.toml`)),
44
46
  Match.when('project', () => Effect.succeed(`${cwd}/.codex/config.toml`)),
@@ -62,20 +64,23 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
62
64
  yield* io.writeText(file, encoded).pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
63
65
  })
64
66
 
65
- const install = Effect.fn(function* (scope: Scope) {
67
+ const install = Effect.fn(function* (scope: InstallScope) {
66
68
  const file = yield* pathFor(scope)
67
69
  const config = yield* readConfig(file)
68
70
  const servers = yield* getMcpServers(config)
69
71
 
72
+ const invocation = yield* cli.resolve()
70
73
  const entryModel = new Entry({
71
- command: 'expandai',
72
- args: ['mcp', 'run'],
74
+ command: invocation.command,
75
+ args: [...invocation.args, 'mcp', 'run'],
76
+ env: { EXPANDAI_HARNESS: 'codex' },
73
77
  enabled: true,
74
78
  })
75
79
 
76
80
  const entry: TomlTableWithoutBigInt = {
77
81
  command: entryModel.command,
78
82
  args: [...entryModel.args],
83
+ env: { ...entryModel.env },
79
84
  enabled: entryModel.enabled,
80
85
  }
81
86
 
@@ -86,7 +91,7 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
86
91
  yield* Console.log(`Installed expandai MCP server into ${file}`)
87
92
  })
88
93
 
89
- const uninstall = Effect.fn(function* (scope: Scope) {
94
+ const uninstall = Effect.fn(function* (scope: InstallScope) {
90
95
  const file = yield* pathFor(scope)
91
96
  const config = yield* readConfig(file)
92
97
  const servers = yield* getMcpServers(config)
@@ -97,7 +102,7 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
97
102
  yield* Console.log(`Removed expandai MCP server from ${file}`)
98
103
  })
99
104
 
100
- const isInstalled = Effect.fn(function* (scope: Scope) {
105
+ const isInstalled = Effect.fn(function* (scope: InstallScope) {
101
106
  const file = yield* pathFor(scope)
102
107
  const config = yield* readConfig(file)
103
108
  const servers = yield* getMcpServers(config)
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { Config, Effect, Match } from 'effect'
3
3
  import { SkillIo } from '../Domain.js'
4
- import type { Scope } from '../Mcp.js'
4
+ import type { InstallScope } from '../Mcp.js'
5
5
 
6
6
  /**
7
7
  * Codex skills use the open agent skills locations:
@@ -19,7 +19,7 @@ export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Inst
19
19
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
20
20
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
21
21
 
22
- const pathFor = Effect.fn(function* (scope: Scope) {
22
+ const pathFor = Effect.fn(function* (scope: InstallScope) {
23
23
  return yield* Match.value(scope).pipe(
24
24
  Match.when('global', () => Effect.succeed(`${home}/.agents/skills/${SKILL_NAME}`)),
25
25
  Match.when('project', () => Effect.succeed(`${cwd}/.agents/skills/${SKILL_NAME}`)),
@@ -27,14 +27,18 @@ export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Inst
27
27
  )
28
28
  })
29
29
 
30
- const install = Effect.fn(function* (scope: Scope) {
30
+ const install = Effect.fn(function* (scope: InstallScope) {
31
31
  return yield* io.install(yield* pathFor(scope))
32
32
  })
33
33
 
34
- const uninstall = Effect.fn(function* (scope: Scope) {
34
+ const uninstall = Effect.fn(function* (scope: InstallScope) {
35
35
  return yield* io.uninstall(yield* pathFor(scope))
36
36
  })
37
37
 
38
- return { install, uninstall } as const
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
39
43
  }),
40
44
  }) {}
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { Config, Console, Effect, Match, Record, Schema } from 'effect'
3
- import { ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
4
- import type { Scope } from '../Mcp.js'
3
+ import { CliInvocation, ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
4
+ import type { InstallScope } from '../Mcp.js'
5
5
 
6
6
  /**
7
7
  * Cursor MCP descriptor — same `mcpServers` shape as Claude Code, different
@@ -18,13 +18,14 @@ export class Entry extends Schema.Class<Entry>('CursorMcpEntry')({
18
18
  }) {}
19
19
 
20
20
  export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Install/Cursor/Mcp', {
21
- dependencies: [ConfigIo.Default],
21
+ dependencies: [ConfigIo.Default, CliInvocation.Default],
22
22
  effect: Effect.gen(function* () {
23
23
  const io = yield* ConfigIo
24
+ const cli = yield* CliInvocation
24
25
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
25
26
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
26
27
 
27
- const pathFor = Effect.fn(function* (scope: Scope) {
28
+ const pathFor = Effect.fn(function* (scope: InstallScope) {
28
29
  return yield* Match.value(scope).pipe(
29
30
  Match.when('global', () => Effect.succeed(`${home}/.cursor/mcp.json`)),
30
31
  Match.when('project', () => Effect.succeed(`${cwd}/.cursor/mcp.json`)),
@@ -44,24 +45,26 @@ export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Instal
44
45
  .pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
45
46
  })
46
47
 
47
- const install = Effect.fn(function* (scope: Scope) {
48
+ const install = Effect.fn(function* (scope: InstallScope) {
48
49
  const file = yield* pathFor(scope)
49
50
  const config = yield* readConfig(file)
51
+ const invocation = yield* cli.resolve()
50
52
  yield* writeConfig(file, {
51
53
  ...config,
52
54
  mcpServers: {
53
55
  ...config.mcpServers,
54
56
  [SERVER_KEY]: new Entry({
55
57
  type: 'stdio',
56
- command: 'expandai',
57
- args: ['mcp', 'run'],
58
+ command: invocation.command,
59
+ args: [...invocation.args, 'mcp', 'run'],
60
+ env: { EXPANDAI_HARNESS: 'cursor' },
58
61
  }),
59
62
  },
60
63
  })
61
64
  yield* Console.log(`Installed expandai MCP server into ${file}`)
62
65
  })
63
66
 
64
- const uninstall = Effect.fn(function* (scope: Scope) {
67
+ const uninstall = Effect.fn(function* (scope: InstallScope) {
65
68
  const file = yield* pathFor(scope)
66
69
  const config = yield* readConfig(file)
67
70
  if (!(SERVER_KEY in config.mcpServers)) {
@@ -71,7 +74,7 @@ export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Instal
71
74
  yield* Console.log(`Removed expandai MCP server from ${file}`)
72
75
  })
73
76
 
74
- const isInstalled = Effect.fn(function* (scope: Scope) {
77
+ const isInstalled = Effect.fn(function* (scope: InstallScope) {
75
78
  const file = yield* pathFor(scope)
76
79
  const config = yield* readConfig(file)
77
80
  return SERVER_KEY in config.mcpServers
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { Config, Effect, Match } from 'effect'
3
3
  import { SkillIo } from '../Domain.js'
4
- import type { Scope } from '../Mcp.js'
4
+ import type { InstallScope } from '../Mcp.js'
5
5
 
6
6
  /**
7
7
  * Cursor skills are auto-loaded from these locations (per
@@ -26,7 +26,7 @@ export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/In
26
26
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
27
27
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
28
28
 
29
- const pathFor = Effect.fn(function* (scope: Scope) {
29
+ const pathFor = Effect.fn(function* (scope: InstallScope) {
30
30
  return yield* Match.value(scope).pipe(
31
31
  Match.when('global', () => Effect.succeed(`${home}/.cursor/skills/${SKILL_NAME}`)),
32
32
  Match.when('project', () => Effect.succeed(`${cwd}/.cursor/skills/${SKILL_NAME}`)),
@@ -34,14 +34,18 @@ export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/In
34
34
  )
35
35
  })
36
36
 
37
- const install = Effect.fn(function* (scope: Scope) {
37
+ const install = Effect.fn(function* (scope: InstallScope) {
38
38
  return yield* io.install(yield* pathFor(scope))
39
39
  })
40
40
 
41
- const uninstall = Effect.fn(function* (scope: Scope) {
41
+ const uninstall = Effect.fn(function* (scope: InstallScope) {
42
42
  return yield* io.uninstall(yield* pathFor(scope))
43
43
  })
44
44
 
45
- return { install, uninstall } as const
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
46
50
  }),
47
51
  }) {}
@@ -1,6 +1,6 @@
1
1
  import { FileSystem, Path } from '@effect/platform'
2
2
  import { NodeFileSystem, NodePath } from '@effect/platform-node'
3
- import { Effect, Inspectable, Schema } from 'effect'
3
+ import { Array, Config, Effect, Inspectable, Layer, Schema, String } from 'effect'
4
4
 
5
5
  export class McpParseError extends Schema.TaggedError<McpParseError>()('McpParseError', {
6
6
  path: Schema.String,
@@ -23,8 +23,16 @@ export class McpWriteError extends Schema.TaggedError<McpWriteError>()('McpWrite
23
23
  export const Host = Schema.Literal('claude-code', 'cursor', 'opencode', 'codex')
24
24
  export type Host = typeof Host.Type
25
25
 
26
- export const Scope = Schema.Literal('project', 'global')
27
- export type Scope = typeof Scope.Type
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
28
36
 
29
37
  const McpServerMap = Schema.Record({ key: Schema.String, value: Schema.Unknown })
30
38
 
@@ -40,6 +48,56 @@ export const OpencodeConfigSchema = Schema.parseJson(
40
48
 
41
49
  export const SERVER_KEY = 'expandai'
42
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
+
43
101
  export const readConfigText = Effect.fn(function* (file: string) {
44
102
  const fs = yield* FileSystem.FileSystem
45
103
  const exists = yield* fs.exists(file)
@@ -143,6 +201,21 @@ export class SkillIo extends Effect.Service<SkillIo>()('@expandai/cli/Install/Sk
143
201
  return target
144
202
  })
145
203
 
146
- return { install, uninstall } as const
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
147
220
  }),
148
221
  }) {}
@@ -1,6 +1,6 @@
1
1
  import { Effect, Match, Schema } from 'effect'
2
2
  import { ClaudeHook } from './Claude/Hook.js'
3
- import { Mcp, type Scope } from './Mcp.js'
3
+ import { type InstallScope, Mcp } from './Mcp.js'
4
4
  import { OpencodeHook } from './Opencode/Hook.js'
5
5
 
6
6
  /**
@@ -26,7 +26,7 @@ export class HookInstaller extends Effect.Service<HookInstaller>()('@expandai/cl
26
26
  const claudeHook = yield* ClaudeHook
27
27
  const opencodeHook = yield* OpencodeHook
28
28
 
29
- const install = Effect.fn('HookInstaller.install')(function* (host: HookHost, scope: Scope) {
29
+ const install = Effect.fn('HookInstaller.install')(function* (host: HookHost, scope: InstallScope) {
30
30
  // Bundle: MCP install first, then host-specific hook config.
31
31
  yield* mcp.install(host, scope)
32
32
  yield* Match.value(host).pipe(
@@ -36,7 +36,7 @@ export class HookInstaller extends Effect.Service<HookInstaller>()('@expandai/cl
36
36
  )
37
37
  })
38
38
 
39
- const uninstall = Effect.fn('HookInstaller.uninstall')(function* (host: HookHost, scope: Scope) {
39
+ const uninstall = Effect.fn('HookInstaller.uninstall')(function* (host: HookHost, scope: InstallScope) {
40
40
  yield* Match.value(host).pipe(
41
41
  Match.when('claude-code', () => claudeHook.uninstall(scope)),
42
42
  Match.when('opencode', () => opencodeHook.uninstall(scope)),
@@ -2,10 +2,10 @@ import { Effect, Match } from 'effect'
2
2
  import { ClaudeMcp } from './Claude/Mcp.js'
3
3
  import { CodexMcp } from './Codex/Mcp.js'
4
4
  import { CursorMcp } from './Cursor/Mcp.js'
5
- import type { Host, Scope } from './Domain.js'
5
+ import type { Host, InstallScope } from './Domain.js'
6
6
  import { OpencodeMcp } from './Opencode/Mcp.js'
7
7
 
8
- export { Host, McpParseError, McpWriteError, Scope } from './Domain.js'
8
+ export { Host, InstallScope, McpParseError, McpWriteError } from './Domain.js'
9
9
 
10
10
  /**
11
11
  * MCP install dispatcher. Per-host descriptors live in `Install/<Host>/Mcp.ts`
@@ -26,7 +26,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
26
26
  const opencode = yield* OpencodeMcp
27
27
  const codex = yield* CodexMcp
28
28
 
29
- const pathFor = Effect.fn('Mcp.pathFor')(function* (host: Host, scope: Scope) {
29
+ const pathFor = Effect.fn('Mcp.pathFor')(function* (host: Host, scope: InstallScope) {
30
30
  return yield* Match.value(host).pipe(
31
31
  Match.when('claude-code', () => claude.pathFor(scope)),
32
32
  Match.when('cursor', () => cursor.pathFor(scope)),
@@ -36,7 +36,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
36
36
  )
37
37
  })
38
38
 
39
- const install = Effect.fn('Mcp.install')(function* (host: Host, scope: Scope) {
39
+ const install = Effect.fn('Mcp.install')(function* (host: Host, scope: InstallScope) {
40
40
  yield* Match.value(host).pipe(
41
41
  Match.when('claude-code', () => claude.install(scope)),
42
42
  Match.when('cursor', () => cursor.install(scope)),
@@ -46,7 +46,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
46
46
  )
47
47
  })
48
48
 
49
- const uninstall = Effect.fn('Mcp.uninstall')(function* (host: Host, scope: Scope) {
49
+ const uninstall = Effect.fn('Mcp.uninstall')(function* (host: Host, scope: InstallScope) {
50
50
  yield* Match.value(host).pipe(
51
51
  Match.when('claude-code', () => claude.uninstall(scope)),
52
52
  Match.when('cursor', () => cursor.uninstall(scope)),
@@ -56,7 +56,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
56
56
  )
57
57
  })
58
58
 
59
- const isInstalled = Effect.fn('Mcp.isInstalled')(function* (host: Host, scope: Scope) {
59
+ const isInstalled = Effect.fn('Mcp.isInstalled')(function* (host: Host, scope: InstallScope) {
60
60
  return yield* Match.value(host).pipe(
61
61
  Match.when('claude-code', () => claude.isInstalled(scope)),
62
62
  Match.when('cursor', () => cursor.isInstalled(scope)),