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
@@ -0,0 +1,496 @@
1
+ import { homedir } from 'node:os'
2
+ import { FileSystem, Path } from '@effect/platform'
3
+ import { NodeFileSystem, NodePath } from '@effect/platform-node'
4
+ import { Array, Config, Effect, Either, Layer, Option, Schema } from 'effect'
5
+ import {
6
+ hookCommandFor,
7
+ isExpandaiHookCommand,
8
+ planClaudeHook,
9
+ planClaudeWebFetchDeny,
10
+ SettingsSchema,
11
+ WEBFETCH_MATCHER,
12
+ } from '../Install/Claude/Hook.js'
13
+ import { planClaudeMcp } from '../Install/Claude/Mcp.js'
14
+ import { planClaudeSkill, SKILL_FILE_NAME, SKILL_NAME } from '../Install/Claude/Skill.js'
15
+ import { type CliCommand, CliInvocation, McpServersConfigSchema, SERVER_KEY } from '../Install/Domain.js'
16
+ import { CLI_VERSION } from '../Version.js'
17
+ import type { InitComponent, InitScope } from './Domain.js'
18
+ import type { MutationExpectedState, MutationOperation } from './MutationTransaction.js'
19
+
20
+ export const CLAUDE_PROFILE_VERSION = CLI_VERSION
21
+
22
+ export const CLAUDE_POLICY_MANAGED_HOOKS = 'managed_hooks_only'
23
+ export const CLAUDE_POLICY_MANAGED_MCP = 'managed_mcp_only'
24
+ export const CLAUDE_POLICY_MCP_DENIED = 'managed_mcp_denied'
25
+ export const CLAUDE_POLICY_MCP_NOT_ALLOWED = 'managed_mcp_not_allowed'
26
+ export const CLAUDE_POLICY_READ_ONLY = 'target_read_only'
27
+
28
+ interface ClaudeTargetLocations {
29
+ readonly settingsFile: string
30
+ readonly skillDirectory: string
31
+ readonly skillFile: string
32
+ readonly mcpFile: string
33
+ readonly managedSettingsFiles: string[]
34
+ readonly managedSettingsDirectories: string[]
35
+ }
36
+
37
+ export interface ClaudeManagedSettingsLocationSet {
38
+ readonly files: string[]
39
+ readonly directories: string[]
40
+ }
41
+
42
+ interface ClaudeRegistrationInspection {
43
+ readonly current: boolean
44
+ readonly installed: boolean
45
+ readonly legacyVariants: string[]
46
+ readonly version?: string
47
+ }
48
+
49
+ export interface ClaudeProfileInspection {
50
+ readonly scope: InitScope
51
+ readonly targets: ClaudeTargetLocations
52
+ readonly hook: ClaudeRegistrationInspection
53
+ readonly skill: ClaudeRegistrationInspection
54
+ readonly mcp: ClaudeRegistrationInspection
55
+ readonly components: InitComponent[]
56
+ readonly configured: boolean
57
+ readonly profileVersion: string
58
+ readonly componentVersions: {
59
+ readonly hook?: string
60
+ readonly skill?: string
61
+ readonly mcp?: string
62
+ }
63
+ readonly policyBlocks: string[]
64
+ readonly remotePolicyMayApply: true
65
+ readonly reloadMayBeRequired: boolean
66
+ readonly consentMayBeRequired: boolean
67
+ }
68
+
69
+ export interface ClaudeProfileMutationPlan {
70
+ readonly changed: boolean
71
+ readonly inspection: ClaudeProfileInspection
72
+ readonly resultInspection: ClaudeProfileInspection
73
+ readonly operations: MutationOperation[]
74
+ }
75
+
76
+ interface LoadedClaudeProfile {
77
+ readonly targets: ClaudeTargetLocations
78
+ readonly settings: typeof SettingsSchema.Type
79
+ readonly settingsExpected: MutationExpectedState
80
+ readonly settingsText?: string
81
+ readonly skillExpected: MutationExpectedState
82
+ readonly skillText?: string
83
+ readonly bundledSkillText: string
84
+ readonly mcp: typeof McpServersConfigSchema.Type
85
+ readonly mcpExpected: MutationExpectedState
86
+ readonly mcpText?: string
87
+ readonly managedSettings: ReadonlyArray<Record<string, unknown>>
88
+ readonly readOnlyTargets: string[]
89
+ }
90
+
91
+ const JsonRecord = Schema.parseJson(Schema.Record({ key: Schema.String, value: Schema.Unknown }))
92
+ const PROFILE_VERSION_PATTERN = /^\s*expandai-profile-version:\s*([^\s]+)\s*$/mu
93
+ const textEncoder = new TextEncoder()
94
+ const textDecoder = new TextDecoder()
95
+ const MODE_MAX = 0o7777
96
+
97
+ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
98
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
99
+ }
100
+
101
+ function defaultManagedSettingsLocations(): ClaudeManagedSettingsLocationSet {
102
+ if (process.platform === 'darwin') {
103
+ const root = '/Library/Application Support/ClaudeCode'
104
+ return {
105
+ files: [`${root}/managed-settings.json`, `${root}/managed-mcp.json`],
106
+ directories: [`${root}/managed-settings.d`],
107
+ }
108
+ }
109
+ if (process.platform === 'win32') {
110
+ const root = 'C:\\Program Files\\ClaudeCode'
111
+ return {
112
+ files: [`${root}\\managed-settings.json`, `${root}\\managed-mcp.json`],
113
+ directories: [`${root}\\managed-settings.d`],
114
+ }
115
+ }
116
+ return {
117
+ files: ['/etc/claude-code/managed-settings.json', '/etc/claude-code/managed-mcp.json'],
118
+ directories: ['/etc/claude-code/managed-settings.d'],
119
+ }
120
+ }
121
+
122
+ /** Discoverable file-based Claude managed-policy locations for the current platform. */
123
+ export class ClaudeManagedSettingsLocations extends Effect.Service<ClaudeManagedSettingsLocations>()(
124
+ '@expandai/cli/Init/ClaudeManagedSettingsLocations',
125
+ {
126
+ sync: defaultManagedSettingsLocations,
127
+ },
128
+ ) {
129
+ static testLayer(locations: ClaudeManagedSettingsLocationSet) {
130
+ return Layer.succeed(ClaudeManagedSettingsLocations, ClaudeManagedSettingsLocations.make(locations))
131
+ }
132
+ }
133
+
134
+ function listContainsExpandai(value: unknown): boolean {
135
+ if (!Array.isArray(value)) return false
136
+ return value.some((entry) => {
137
+ if (entry === SERVER_KEY) return true
138
+ if (!isUnknownRecord(entry)) return false
139
+ return entry.name === SERVER_KEY || entry.serverName === SERVER_KEY
140
+ })
141
+ }
142
+
143
+ function managedPolicyBlocks(settings: ReadonlyArray<Record<string, unknown>>): string[] {
144
+ const blocks = new Set<string>()
145
+ const managedMcpOnly = settings.some((value) => value.allowManagedMcpServersOnly === true)
146
+ const allowedMcpConfigured = settings.some((value) => Array.isArray(value.allowedMcpServers))
147
+ const expandaiAllowed = settings.some((value) => listContainsExpandai(value.allowedMcpServers))
148
+ for (const value of settings) {
149
+ if (value.allowManagedHooksOnly === true) blocks.add(CLAUDE_POLICY_MANAGED_HOOKS)
150
+ if (listContainsExpandai(value.deniedMcpServers)) blocks.add(CLAUDE_POLICY_MCP_DENIED)
151
+ }
152
+ if (managedMcpOnly && !expandaiAllowed) blocks.add(CLAUDE_POLICY_MANAGED_MCP)
153
+ if (allowedMcpConfigured && !expandaiAllowed) blocks.add(CLAUDE_POLICY_MCP_NOT_ALLOWED)
154
+ return [...blocks].sort()
155
+ }
156
+
157
+ function skillVersion(text: string | undefined): string | undefined {
158
+ if (text === undefined) return undefined
159
+ return PROFILE_VERSION_PATTERN.exec(text)?.[1]
160
+ }
161
+
162
+ function mcpLegacyVariant(value: unknown): string[] {
163
+ if (!isUnknownRecord(value)) return value === undefined ? [] : ['owned-entry']
164
+ const command = value.command
165
+ const args = value.args
166
+ if (command === 'npx' && Array.isArray(args) && args[0] === '-y') return ['npx-y']
167
+ if (typeof command === 'string' && command !== 'expandai' && /(?:^|[\\/])expandai(?:\.exe)?$/u.test(command)) {
168
+ return ['absolute-path']
169
+ }
170
+ return ['owned-entry']
171
+ }
172
+
173
+ function hookLegacyVariant(command: string): string {
174
+ if (/^npx\s+-y\s+/u.test(command.trim())) return 'npx-y'
175
+ if (/\shook\s+claude\s*$/u.test(command)) return 'old-command-spelling'
176
+ if (/^(?:"|'|\/|[A-Za-z]:\\)/u.test(command.trim())) return 'absolute-path'
177
+ if (/^if\s+command\s+-v\s+expandai/u.test(command.trim())) return 'npx-shell-fallback'
178
+ return 'unversioned'
179
+ }
180
+
181
+ function makeInspection(scope: InitScope, loaded: LoadedClaudeProfile, cli: CliCommand): ClaudeProfileInspection {
182
+ const hookPlan = planClaudeHook(loaded.settings, CLAUDE_PROFILE_VERSION, cli)
183
+ const denyPlan = planClaudeWebFetchDeny(hookPlan.settings)
184
+ const hookEntries = (loaded.settings.hooks.PreToolUse ?? []).flatMap((entry) =>
185
+ entry.hooks.filter((hook) => isExpandaiHookCommand(hook.command)).map((hook) => ({ entry, hook })),
186
+ )
187
+ const hookVersion = hookEntries.length === 1 ? hookEntries[0]?.hook.expandaiProfileVersion : undefined
188
+ const hookCurrent = !hookPlan.changed && !denyPlan.changed
189
+ const hookLegacy = hookCurrent
190
+ ? []
191
+ : hookEntries.map(({ entry, hook }) =>
192
+ entry.matcher === WEBFETCH_MATCHER && hook.command === hookCommandFor(cli)
193
+ ? 'unversioned'
194
+ : hookLegacyVariant(hook.command),
195
+ )
196
+
197
+ const plannedSkill = planClaudeSkill(loaded.skillText, loaded.bundledSkillText, CLAUDE_PROFILE_VERSION)
198
+ const currentSkillVersion = skillVersion(loaded.skillText)
199
+ const skillCurrent = loaded.skillText !== undefined && !plannedSkill.changed
200
+
201
+ const mcpValue = loaded.mcp.mcpServers[SERVER_KEY]
202
+ const mcpCurrent = !planClaudeMcp(loaded.mcp, cli).changed
203
+ const components: InitComponent[] = []
204
+ if (hookCurrent) components.push('webfetch-hook')
205
+ if (skillCurrent) components.push('runtime-skill')
206
+ if (mcpCurrent) components.push('mcp')
207
+
208
+ const policyBlocks = managedPolicyBlocks(loaded.managedSettings)
209
+ if (loaded.readOnlyTargets.length > 0) policyBlocks.push(CLAUDE_POLICY_READ_ONLY)
210
+
211
+ return {
212
+ scope,
213
+ targets: loaded.targets,
214
+ hook: {
215
+ current: hookCurrent,
216
+ installed: hookEntries.length > 0,
217
+ legacyVariants: hookLegacy,
218
+ ...(hookVersion === undefined ? {} : { version: hookVersion }),
219
+ },
220
+ skill: {
221
+ current: skillCurrent,
222
+ installed: loaded.skillText !== undefined,
223
+ legacyVariants: loaded.skillText === undefined || skillCurrent ? [] : ['outdated-runtime-skill'],
224
+ ...(currentSkillVersion === undefined ? {} : { version: currentSkillVersion }),
225
+ },
226
+ mcp: {
227
+ current: mcpCurrent,
228
+ installed: mcpValue !== undefined,
229
+ legacyVariants: mcpCurrent ? [] : mcpLegacyVariant(mcpValue),
230
+ },
231
+ components,
232
+ configured: components.length === 3,
233
+ profileVersion: CLAUDE_PROFILE_VERSION,
234
+ componentVersions: {
235
+ ...(hookVersion === undefined ? {} : { hook: hookVersion }),
236
+ ...(currentSkillVersion === undefined ? {} : { skill: currentSkillVersion }),
237
+ ...(mcpCurrent ? { mcp: CLAUDE_PROFILE_VERSION } : {}),
238
+ },
239
+ policyBlocks: [...new Set(policyBlocks)].sort(),
240
+ remotePolicyMayApply: true,
241
+ reloadMayBeRequired: !hookCurrent || !mcpCurrent,
242
+ consentMayBeRequired: scope === 'project',
243
+ }
244
+ }
245
+
246
+ const makeClaudeProfile = Effect.gen(function* () {
247
+ const fileSystem = yield* FileSystem.FileSystem
248
+ const path = yield* Path.Path
249
+ const managedSettingsLocations = yield* ClaudeManagedSettingsLocations
250
+ // Resolved once per service: whether `expandai` is on PATH decides what the
251
+ // written hook and MCP entries invoke, and it cannot change mid-install.
252
+ const cli = yield* (yield* CliInvocation).resolve()
253
+ const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
254
+ const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
255
+ const moduleFile = yield* path.fromFileUrl(new URL(import.meta.url))
256
+ const bundledSkillFile = path.resolve(path.dirname(moduleFile), '..', '..', 'skills', SKILL_NAME, SKILL_FILE_NAME)
257
+
258
+ const readOptional = Effect.fnUntraced(function* (file: string) {
259
+ return (yield* fileSystem.exists(file)) ? yield* fileSystem.readFileString(file) : undefined
260
+ })
261
+
262
+ const readPlannedFile = Effect.fnUntraced(function* (file: string) {
263
+ if (!(yield* fileSystem.exists(file))) {
264
+ return { expected: { exists: false } as const }
265
+ }
266
+ const info = yield* fileSystem.stat(file)
267
+ const bytes = yield* fileSystem.readFile(file)
268
+ return {
269
+ expected: {
270
+ exists: true,
271
+ type: 'File',
272
+ bytes,
273
+ mode: info.mode & MODE_MAX,
274
+ } as const,
275
+ text: textDecoder.decode(bytes),
276
+ }
277
+ })
278
+
279
+ const expectedDirectory = Effect.fnUntraced(function* (directory: string) {
280
+ if (!(yield* fileSystem.exists(directory))) return { exists: false } as const
281
+ const info = yield* fileSystem.stat(directory)
282
+ if (info.type !== 'Directory') throw new Error('Planned directory target is not a directory')
283
+ return {
284
+ exists: true,
285
+ type: 'Directory',
286
+ mode: info.mode & MODE_MAX,
287
+ } as const
288
+ })
289
+
290
+ const writableAncestor = Effect.fnUntraced(function* (target: string) {
291
+ function ancestors(candidate: string): string[] {
292
+ const parent = path.dirname(candidate)
293
+ return parent === candidate ? [candidate] : [candidate, ...ancestors(parent)]
294
+ }
295
+ const candidates = ancestors(target)
296
+ const states = yield* Effect.forEach(candidates, (candidate) =>
297
+ fileSystem.exists(candidate).pipe(Effect.map((exists) => ({ candidate, exists }))),
298
+ )
299
+ const candidate = states.find((state) => state.exists)?.candidate ?? candidates[candidates.length - 1]
300
+ if (candidate === undefined) return false
301
+ const info = yield* fileSystem.stat(candidate)
302
+ return (info.mode & 0o222) !== 0
303
+ })
304
+
305
+ const mutationRequirementsWritable = Effect.fnUntraced(function* (operation: MutationOperation) {
306
+ if (
307
+ operation.type === 'write' &&
308
+ operation.expected?.exists === true &&
309
+ operation.expected.type === 'File' &&
310
+ !(yield* Effect.either(fileSystem.access(operation.path, { writable: true }))).pipe(Either.isRight)
311
+ ) {
312
+ return false
313
+ }
314
+ if (operation.type === 'mkdir' && operation.expected?.exists === true) return true
315
+
316
+ const parent = path.dirname(operation.path)
317
+ function ancestors(candidate: string): string[] {
318
+ const next = path.dirname(candidate)
319
+ return next === candidate ? [candidate] : [candidate, ...ancestors(next)]
320
+ }
321
+ const candidates = ancestors(parent)
322
+ const states = yield* Effect.forEach(candidates, (candidate) =>
323
+ fileSystem.exists(candidate).pipe(Effect.map((exists) => ({ candidate, exists }))),
324
+ )
325
+ const nearest = states.find((state) => state.exists)?.candidate
326
+ if (nearest === undefined) return false
327
+ const existing = states.filter((state) => state.exists)
328
+ const traversable = yield* Effect.forEach(existing, ({ candidate }) =>
329
+ fileSystem.stat(candidate).pipe(
330
+ Effect.map((info) => info.type === 'Directory' && (process.platform === 'win32' || (info.mode & 0o111) !== 0)),
331
+ Effect.orElseSucceed(() => false),
332
+ ),
333
+ )
334
+ if (!traversable.every(Boolean)) return false
335
+ // FileSystem.access performs the platform's effective writable check.
336
+ // Windows has no separate directory-traverse/ACL primitive here, so the
337
+ // transaction remains the final fail-closed enforcement for that facet.
338
+ return (yield* Effect.either(fileSystem.access(nearest, { writable: true }))).pipe(Either.isRight)
339
+ })
340
+
341
+ const load = Effect.fn('ClaudeProfile.load')(function* (scope: InitScope) {
342
+ const root = scope === 'global' ? home : cwd
343
+ const skillDirectory = path.join(root, '.claude', 'skills', SKILL_NAME)
344
+ const managed = managedSettingsLocations
345
+ const dropInFiles = yield* Effect.flatMap(
346
+ Effect.forEach(managed.directories, (directory) =>
347
+ fileSystem.exists(directory).pipe(
348
+ Effect.flatMap((exists) => (exists ? fileSystem.readDirectory(directory) : Effect.succeed([]))),
349
+ Effect.map((entries) =>
350
+ entries
351
+ .filter((entry) => !entry.startsWith('.') && entry.endsWith('.json'))
352
+ .sort()
353
+ .map((entry) => path.join(directory, entry)),
354
+ ),
355
+ ),
356
+ ),
357
+ (files) => Effect.succeed(files.flat()),
358
+ )
359
+ const targets: ClaudeTargetLocations = {
360
+ settingsFile: path.join(root, '.claude', 'settings.json'),
361
+ skillDirectory,
362
+ skillFile: path.join(skillDirectory, SKILL_FILE_NAME),
363
+ mcpFile: scope === 'global' ? path.join(home, '.claude.json') : path.join(cwd, '.mcp.json'),
364
+ managedSettingsFiles: [...managed.files, ...dropInFiles],
365
+ managedSettingsDirectories: managed.directories,
366
+ }
367
+
368
+ const settingsFile = yield* readPlannedFile(targets.settingsFile)
369
+ const skillFile = yield* readPlannedFile(targets.skillFile)
370
+ const mcpFile = yield* readPlannedFile(targets.mcpFile)
371
+ const settingsText = settingsFile.text
372
+ const skillText = skillFile.text
373
+ const mcpText = mcpFile.text
374
+ const bundledSkillText = yield* fileSystem.readFileString(bundledSkillFile)
375
+ const settings = yield* Schema.decode(SettingsSchema)(settingsText ?? '{}')
376
+ const mcp = yield* Schema.decode(McpServersConfigSchema)(mcpText ?? '{}')
377
+ const managedSettings = yield* Effect.forEach(targets.managedSettingsFiles, (file) =>
378
+ readOptional(file).pipe(
379
+ Effect.flatMap((text) =>
380
+ text === undefined
381
+ ? Effect.succeed(Option.none())
382
+ : Schema.decode(JsonRecord)(text).pipe(Effect.map(Option.some)),
383
+ ),
384
+ ),
385
+ )
386
+ const readOnlyTargets = yield* Effect.filter([targets.settingsFile, targets.skillFile, targets.mcpFile], (target) =>
387
+ writableAncestor(target).pipe(Effect.map((writable) => !writable)),
388
+ )
389
+
390
+ return {
391
+ targets,
392
+ settings,
393
+ settingsExpected: settingsFile.expected,
394
+ ...(settingsText === undefined ? {} : { settingsText }),
395
+ skillExpected: skillFile.expected,
396
+ ...(skillText === undefined ? {} : { skillText }),
397
+ bundledSkillText,
398
+ mcp,
399
+ mcpExpected: mcpFile.expected,
400
+ ...(mcpText === undefined ? {} : { mcpText }),
401
+ managedSettings: Array.getSomes(managedSettings),
402
+ readOnlyTargets,
403
+ } satisfies LoadedClaudeProfile
404
+ })
405
+
406
+ const inspect = Effect.fn('ClaudeProfile.inspect')(function* (scope: InitScope) {
407
+ return makeInspection(scope, yield* load(scope), cli)
408
+ })
409
+
410
+ const plan = Effect.fn('ClaudeProfile.plan')(function* (scope: InitScope) {
411
+ const loaded = yield* load(scope)
412
+ const hookPlan = planClaudeHook(loaded.settings, CLAUDE_PROFILE_VERSION, cli)
413
+ // One settings file, one write: the deny rule is layered onto the hook plan's
414
+ // output so both land together or neither does.
415
+ const denyPlan = planClaudeWebFetchDeny(hookPlan.settings)
416
+ const settingsChanged = hookPlan.changed || denyPlan.changed
417
+ const skillPlan = planClaudeSkill(loaded.skillText, loaded.bundledSkillText, CLAUDE_PROFILE_VERSION)
418
+ const mcpPlan = planClaudeMcp(loaded.mcp, cli)
419
+ const settingsText = `${yield* Schema.encode(SettingsSchema)(denyPlan.settings)}\n`
420
+ const mcpText = `${yield* Schema.encode(McpServersConfigSchema)(mcpPlan.config)}\n`
421
+ const settingsDirectory = path.dirname(loaded.targets.settingsFile)
422
+ const skillsDirectory = path.dirname(loaded.targets.skillDirectory)
423
+ const operations: MutationOperation[] = []
424
+ if (settingsChanged || skillPlan.changed) {
425
+ operations.push({
426
+ type: 'mkdir',
427
+ path: settingsDirectory,
428
+ expected: yield* expectedDirectory(settingsDirectory),
429
+ })
430
+ }
431
+ if (settingsChanged) {
432
+ operations.push({
433
+ type: 'write',
434
+ path: loaded.targets.settingsFile,
435
+ bytes: textEncoder.encode(settingsText),
436
+ expected: loaded.settingsExpected,
437
+ })
438
+ }
439
+ if (skillPlan.changed) {
440
+ operations.push({
441
+ type: 'mkdir',
442
+ path: skillsDirectory,
443
+ expected: yield* expectedDirectory(skillsDirectory),
444
+ })
445
+ operations.push({
446
+ type: 'mkdir',
447
+ path: loaded.targets.skillDirectory,
448
+ expected: yield* expectedDirectory(loaded.targets.skillDirectory),
449
+ })
450
+ operations.push({
451
+ type: 'write',
452
+ path: loaded.targets.skillFile,
453
+ bytes: textEncoder.encode(skillPlan.text),
454
+ expected: loaded.skillExpected,
455
+ })
456
+ }
457
+ if (mcpPlan.changed) {
458
+ operations.push({
459
+ type: 'write',
460
+ path: loaded.targets.mcpFile,
461
+ bytes: textEncoder.encode(mcpText),
462
+ expected: loaded.mcpExpected,
463
+ })
464
+ }
465
+ const requirements = yield* Effect.forEach(operations, mutationRequirementsWritable)
466
+ const inspectedLoaded = requirements.every(Boolean)
467
+ ? loaded
468
+ : { ...loaded, readOnlyTargets: [...loaded.readOnlyTargets, 'mutation-requirement'] }
469
+ const inspection = makeInspection(scope, inspectedLoaded, cli)
470
+ const resultInspection = makeInspection(
471
+ scope,
472
+ {
473
+ ...inspectedLoaded,
474
+ settings: denyPlan.settings,
475
+ settingsText,
476
+ skillText: skillPlan.text,
477
+ mcp: mcpPlan.config,
478
+ mcpText,
479
+ },
480
+ cli,
481
+ )
482
+ return {
483
+ changed: settingsChanged || skillPlan.changed || mcpPlan.changed,
484
+ inspection,
485
+ resultInspection,
486
+ operations,
487
+ } satisfies ClaudeProfileMutationPlan
488
+ })
489
+
490
+ return { inspect, plan } as const
491
+ })
492
+
493
+ export class ClaudeProfile extends Effect.Service<ClaudeProfile>()('@expandai/cli/Init/ClaudeProfile', {
494
+ dependencies: [NodeFileSystem.layer, NodePath.layer, ClaudeManagedSettingsLocations.Default, CliInvocation.Default],
495
+ effect: makeClaudeProfile,
496
+ }) {}
@@ -0,0 +1,153 @@
1
+ import { Schema } from 'effect'
2
+
3
+ // Relayed verbatim by the agent, and printed as-is in the terminal, so it has to
4
+ // carry the whole browser round-trip on its own — the guide no longer restates it.
5
+ export const AUTHORIZATION_REQUIRED_MESSAGE =
6
+ 'Open the approval page in your browser, type in the code shown here, and approve. Only enter this code on the Expand approval page your terminal named — never a code someone sent you, and never on a page from a link you did not expect. When you have approved, say so, and the same command will pick the authorization up.'
7
+
8
+ export const AUTHORIZATION_CONSUMED_MESSAGE =
9
+ 'Authorization complete. That setup code has been consumed and cannot be reused.'
10
+
11
+ export const ALREADY_AUTHENTICATED_MESSAGE = 'Expand is already authorized. Agent setup is complete.'
12
+
13
+ /**
14
+ * Some harnesses were configured and some were not. This has to be its own
15
+ * message: the agent relays `userMessage` verbatim, so reporting a partial run
16
+ * with "Agent setup is complete" tells the user something untrue about a
17
+ * machine they are about to rely on.
18
+ */
19
+ export const PARTIAL_CONFIGURATION_MESSAGE =
20
+ 'Expand is authorized, but not every harness could be configured. The harnesses that failed are listed under `failures`; the ones under `installations` are ready to use.'
21
+
22
+ export const HARNESS_SELECTION_MESSAGE =
23
+ 'Show these harnesses to the user and ask which to configure. Pass the chosen ones to `expandai init --harnesses`. Harnesses that are not chosen are left untouched.'
24
+
25
+ /** There is nothing to choose from, so the agent must not present an empty list. */
26
+ export const NO_HARNESS_DETECTED_MESSAGE =
27
+ 'No supported agent harness was found on this machine. Do not ask the user to choose from an empty list. Tell them Expand supports claude-code, cursor, opencode, and codex, and that setup cannot continue until one of them is installed.'
28
+
29
+ export const InitHarness = Schema.Literal('claude-code', 'cursor', 'opencode', 'codex')
30
+ export type InitHarness = typeof InitHarness.Type
31
+
32
+ /**
33
+ * Answer to `init --detect`: what setup *would* touch, before it touches
34
+ * anything. Separate from `InitResult` because detection never authorizes,
35
+ * never writes, and can legitimately find nothing.
36
+ */
37
+ export class InitDetectionResult extends Schema.Class<InitDetectionResult>('InitDetectionResult')({
38
+ schemaVersion: Schema.Literal(1),
39
+ status: Schema.Literal('detected'),
40
+ harnesses: Schema.Array(InitHarness),
41
+ current: Schema.optional(InitHarness),
42
+ userMessage: Schema.Literal(HARNESS_SELECTION_MESSAGE, NO_HARNESS_DETECTED_MESSAGE),
43
+ }) {}
44
+
45
+ export const InitHarnesses = Schema.NonEmptyArray(InitHarness)
46
+ export type InitHarnesses = typeof InitHarnesses.Type
47
+
48
+ export const InitScope = Schema.Literal('project', 'global')
49
+ export type InitScope = typeof InitScope.Type
50
+
51
+ export const InitComponent = Schema.Literal('webfetch-hook', 'runtime-skill', 'mcp')
52
+ export type InitComponent = typeof InitComponent.Type
53
+
54
+ export const InitRuntimeState = Schema.Literal('unverified', 'observed', 'reload_required', 'blocked')
55
+ export type InitRuntimeState = typeof InitRuntimeState.Type
56
+
57
+ export const InitStatus = Schema.Literal('authorization_required', 'completed')
58
+ export type InitStatus = typeof InitStatus.Type
59
+
60
+ // A dashboard setup token is a pre-approved RFC 8628 device code minted by
61
+ // better-auth: opaque, bounded, single-use.
62
+ const SetupToken = Schema.String.pipe(Schema.minLength(20), Schema.maxLength(256))
63
+
64
+ const SafeSetupToken = Schema.transform(SetupToken, Schema.Redacted(Schema.String), {
65
+ strict: true,
66
+ decode: (setupToken) => setupToken,
67
+ // Long enough to satisfy the base schema on re-encode; still obviously not a token.
68
+ encode: () => '<redacted-redacted-setup-token>',
69
+ })
70
+
71
+ export class InitRequest extends Schema.Class<InitRequest>('InitRequest')({
72
+ harnesses: InitHarnesses,
73
+ scope: InitScope,
74
+ setupToken: Schema.optional(SafeSetupToken),
75
+ }) {}
76
+
77
+ const InitAuthorizationRequiredResult = Schema.Struct({
78
+ schemaVersion: Schema.Literal(1),
79
+ status: Schema.Literal('authorization_required'),
80
+ harnesses: InitHarnesses,
81
+ scope: InitScope,
82
+ userMessage: Schema.Literal(AUTHORIZATION_REQUIRED_MESSAGE),
83
+ authorization: Schema.Struct({
84
+ url: Schema.URL,
85
+ // Typed by the human into the approval page. Deliberately not embedded in
86
+ // the URL: entering it is the one moment a phished user can notice the
87
+ // request is not theirs.
88
+ userCode: Schema.String,
89
+ approvalExpiresAt: Schema.DateTimeUtc,
90
+ }),
91
+ })
92
+
93
+ /** Whether the harness has actually loaded what we wrote, and what to do if not. */
94
+ const InitVerification = Schema.Struct({
95
+ runtimeState: InitRuntimeState,
96
+ nextAction: Schema.String,
97
+ })
98
+
99
+ /** One harness that setup wrote to, and what it now reports about itself. */
100
+ const InitInstallation = Schema.Struct({
101
+ harness: InitHarness,
102
+ configured: Schema.Boolean,
103
+ changed: Schema.Boolean,
104
+ components: Schema.Array(InitComponent),
105
+ version: Schema.String,
106
+ verification: InitVerification,
107
+ })
108
+
109
+ /**
110
+ * A harness that was attempted and did not land. Named separately because the
111
+ * agent is asked to tell the user *which* of their agents is not set up, and it
112
+ * has no other way to know: `harnesses` lists what was requested and
113
+ * `installations` what worked, but the difference alone says neither why nor
114
+ * whether retrying helps.
115
+ */
116
+ const InitFailure = Schema.Struct({
117
+ harness: InitHarness,
118
+ code: Schema.String,
119
+ recoverable: Schema.Boolean,
120
+ })
121
+
122
+ const InitCompletedResult = Schema.Struct({
123
+ schemaVersion: Schema.Literal(1),
124
+ status: Schema.Literal('completed'),
125
+ harnesses: InitHarnesses,
126
+ scope: InitScope,
127
+ userMessage: Schema.Literal(
128
+ AUTHORIZATION_CONSUMED_MESSAGE,
129
+ ALREADY_AUTHENTICATED_MESSAGE,
130
+ PARTIAL_CONFIGURATION_MESSAGE,
131
+ ),
132
+ auth: Schema.Struct({
133
+ healthy: Schema.Boolean,
134
+ }),
135
+ // Which project `scope: 'project'` actually meant. It resolves to the working
136
+ // directory, which for an agent may be anywhere — a repo, a home directory,
137
+ // a temp folder — and nothing else in this result says where config landed.
138
+ projectPath: Schema.optional(Schema.String),
139
+ installations: Schema.NonEmptyArray(InitInstallation),
140
+ // Absent when every selected harness succeeded.
141
+ failures: Schema.optional(Schema.Array(InitFailure)),
142
+ })
143
+
144
+ export const InitResult = Schema.Union(InitAuthorizationRequiredResult, InitCompletedResult)
145
+ export type InitResult = typeof InitResult.Type
146
+
147
+ export class InitErrorEnvelope extends Schema.Class<InitErrorEnvelope>('InitErrorEnvelope')({
148
+ schemaVersion: Schema.Literal(1),
149
+ status: Schema.Literal('error'),
150
+ code: Schema.String,
151
+ message: Schema.String,
152
+ recoverable: Schema.Boolean,
153
+ }) {}
@@ -0,0 +1,35 @@
1
+ import { Inspectable, Schema } from 'effect'
2
+ import { type InitComponent, InitHarness, type InitRuntimeState, InitScope } from './Domain.js'
3
+
4
+ export interface HarnessInspection {
5
+ readonly configured: boolean
6
+ readonly changed?: boolean
7
+ readonly components: InitComponent[]
8
+ readonly integrationVersion?: string
9
+ readonly policyBlock?: string
10
+ readonly nextAction: string
11
+ readonly runtimeState?: InitRuntimeState
12
+ }
13
+
14
+ export class HarnessInspectionError extends Schema.TaggedError<HarnessInspectionError>()('HarnessInspectionError', {
15
+ harness: InitHarness,
16
+ scope: InitScope,
17
+ error: Schema.Defect,
18
+ }) {
19
+ get message() {
20
+ return `Failed to inspect ${this.harness} (${this.scope}): ${Inspectable.format(this.error)}`
21
+ }
22
+ }
23
+
24
+ export class HarnessConfigurationError extends Schema.TaggedError<HarnessConfigurationError>()(
25
+ 'HarnessConfigurationError',
26
+ {
27
+ harness: InitHarness,
28
+ scope: InitScope,
29
+ error: Schema.Defect,
30
+ },
31
+ ) {
32
+ get message() {
33
+ return `Failed to configure ${this.harness} (${this.scope}): ${Inspectable.format(this.error)}`
34
+ }
35
+ }