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,489 @@
1
+ import { Command, Options, Prompt } from '@effect/cli'
2
+ import { Terminal } from '@effect/platform'
3
+ import { Array, Console, Effect, Either, Option, Schema } from 'effect'
4
+ import { Authentication } from '../Authentication.js'
5
+ import {
6
+ HARNESS_SELECTION_MESSAGE,
7
+ InitDetectionResult,
8
+ InitErrorEnvelope,
9
+ InitHarness,
10
+ type InitHarnesses,
11
+ InitRequest,
12
+ InitResult,
13
+ InitScope,
14
+ NO_HARNESS_DETECTED_MESSAGE,
15
+ } from '../Init/Domain.js'
16
+ import { HarnessDetector } from '../Init/HarnessDetector.js'
17
+ import { InitService, InitServiceError } from '../Init/Service.js'
18
+
19
+ const harness = Options.text('harness').pipe(
20
+ Options.optional,
21
+ Options.withDescription('Agent harness running this setup, used as a detection hint'),
22
+ )
23
+
24
+ const harnesses = Options.text('harnesses').pipe(
25
+ Options.optional,
26
+ Options.withDescription('Comma-separated harnesses to configure. Omit to configure every detected harness.'),
27
+ )
28
+
29
+ const detect = Options.boolean('detect').pipe(
30
+ Options.withDescription('Report the harnesses setup would configure, and change nothing'),
31
+ )
32
+
33
+ const scope = Options.text('scope').pipe(Options.optional, Options.withDescription('Configuration scope'))
34
+
35
+ const setupToken = Options.text('setup-token').pipe(
36
+ Options.optional,
37
+ Options.withDescription('Short-lived, single-use setup token from the Expand dashboard'),
38
+ )
39
+
40
+ const json = Options.boolean('json').pipe(Options.withDescription('Emit one machine-readable JSON result'))
41
+
42
+ const config = { harness, harnesses, detect, scope, setupToken, json }
43
+
44
+ const InitErrorJson = Schema.parseJson(InitErrorEnvelope)
45
+ const InitResultJson = Schema.parseJson(InitResult)
46
+ const InitDetectionJson = Schema.parseJson(InitDetectionResult)
47
+
48
+ const INVALID_ARGUMENTS_MESSAGE =
49
+ 'Agent init requires --scope project|global and --json, or --detect --json to only report detected harnesses. Optional: --harnesses claude-code,cursor,opencode,codex to configure an explicit subset, --harness <name> as a current-harness hint, and --setup-token <token> from a dashboard setup prompt. After the user approves in the browser, run the same command again — there is no code to pass back.'
50
+
51
+ export const SUPPORTED_HARNESS_LIST = 'claude-code, cursor, opencode, codex'
52
+
53
+ /**
54
+ * Says that the *harness itself* is unsupported, rather than that the flag was
55
+ * malformed. An agent told only "invalid arguments" alongside a list of four
56
+ * valid names will pick one of them and misreport the user's environment.
57
+ */
58
+ export const UNSUPPORTED_HARNESS_MESSAGE = `Expand does not support that agent harness. Supported harnesses are ${SUPPORTED_HARNESS_LIST}. Do not substitute a different harness: tell the user their agent is not supported yet.`
59
+
60
+ const INVALID_SETUP_TOKEN_MESSAGE =
61
+ 'That setup token is malformed. Copy it exactly as the dashboard issued it, or create a new one; do not retype or truncate it.'
62
+
63
+ const INVALID_HARNESS_SELECTION_MESSAGE = `--harnesses takes a comma-separated list drawn from ${SUPPORTED_HARNESS_LIST}, for example --harnesses claude-code,cursor. Every entry must be one of those names.`
64
+
65
+ /** Mirrors the `SetupToken` schema so a typo is reported as a bad token, not bad flags. */
66
+ const SETUP_TOKEN_PATTERN = /^[A-Za-z0-9._~-]{20,256}$/u
67
+
68
+ /** Distinct codes so an agent can fix the specific argument instead of guessing. */
69
+ function argumentErrorJson(code: string, message: string): string {
70
+ return Schema.encodeSync(InitErrorJson)(
71
+ new InitErrorEnvelope({ schemaVersion: 1, status: 'error', code, message, recoverable: true }),
72
+ )
73
+ }
74
+
75
+ function invalidArgumentsJson(): string {
76
+ return argumentErrorJson('invalid_arguments', INVALID_ARGUMENTS_MESSAGE)
77
+ }
78
+
79
+ function errorJson(code: string, message: string, recoverable: boolean): string {
80
+ return Schema.encodeSync(InitErrorJson)(
81
+ new InitErrorEnvelope({ schemaVersion: 1, status: 'error', code, message, recoverable }),
82
+ )
83
+ }
84
+
85
+ function emitError(code: string, message: string, recoverable: boolean, exitCode: number) {
86
+ return Console.error(errorJson(code, message, recoverable)).pipe(
87
+ Effect.zipRight(Effect.sync(() => void (process.exitCode = exitCode))),
88
+ )
89
+ }
90
+
91
+ export interface InitCommandOptions {
92
+ readonly detect: boolean
93
+ readonly harness: Option.Option<string>
94
+ readonly harnesses: Option.Option<string>
95
+ readonly json: boolean
96
+ readonly scope: Option.Option<string>
97
+ readonly setupToken: Option.Option<string>
98
+ }
99
+
100
+ /**
101
+ * Decodes `--harnesses claude-code,cursor`. Returns `undefined` for any
102
+ * unknown or empty entry: silently dropping one would configure less than the
103
+ * user asked for, which is the failure mode this flag exists to prevent.
104
+ */
105
+ export function parseHarnessSelection(value: string): InitHarnesses | undefined {
106
+ const entries = value.split(',').map((entry) => entry.trim())
107
+ const decoded: InitHarness[] = []
108
+ for (const entry of entries) {
109
+ const harness = Schema.decodeUnknownOption(InitHarness)(entry)
110
+ if (Option.isNone(harness)) return undefined
111
+ if (!decoded.includes(harness.value)) decoded.push(harness.value)
112
+ }
113
+ return Array.isNonEmptyArray(decoded) ? decoded : undefined
114
+ }
115
+
116
+ export interface OnboardingExecutionModeInput {
117
+ readonly argv: readonly string[]
118
+ readonly environment: Readonly<Record<string, string | undefined>>
119
+ readonly stdinIsTty: boolean
120
+ readonly stdoutIsTty: boolean
121
+ }
122
+
123
+ export interface InitExecutionModeInput extends OnboardingExecutionModeInput {}
124
+
125
+ function enabledEnvironmentFlag(value: string | undefined): boolean {
126
+ return value !== undefined && value !== '' && value !== '0' && value !== 'false'
127
+ }
128
+
129
+ /** Selects a safe onboarding surface before either CLI tree parses the command. */
130
+ export function resolveOnboardingExecutionMode(
131
+ input: OnboardingExecutionModeInput,
132
+ command: 'init' | 'status',
133
+ ): 'human' | 'machine' | undefined {
134
+ if (input.argv[2] !== command) return undefined
135
+ const args = input.argv.slice(3)
136
+ const agentSignal =
137
+ input.environment.EXPANDAI_CLI_CONTEXT === 'agent' ||
138
+ enabledEnvironmentFlag(input.environment.CLAUDECODE) ||
139
+ input.environment.CURSOR_TRACE_ID !== undefined ||
140
+ input.environment.CODEX_RUN_ID !== undefined
141
+ const machineSafe = !input.stdinIsTty || !input.stdoutIsTty || args.includes('--json') || agentSignal
142
+ return machineSafe ? 'machine' : 'human'
143
+ }
144
+
145
+ /** Selects the safe init surface before either CLI tree parses the command. */
146
+ export function resolveInitExecutionMode(input: InitExecutionModeInput): 'human' | 'machine' | undefined {
147
+ return resolveOnboardingExecutionMode(input, 'init')
148
+ }
149
+
150
+ function parseMachineArguments(args: readonly string[]): InitCommandOptions | undefined {
151
+ function parse(index: number, parsed: InitCommandOptions): InitCommandOptions | undefined {
152
+ if (index >= args.length) return parsed
153
+ const token = args[index]
154
+ if (token === '--json' && !parsed.json) {
155
+ return parse(index + 1, { ...parsed, json: true })
156
+ }
157
+ if (token === '--detect' && !parsed.detect) {
158
+ return parse(index + 1, { ...parsed, detect: true })
159
+ }
160
+ if (token !== '--setup-token' && token !== '--harness' && token !== '--harnesses' && token !== '--scope')
161
+ return undefined
162
+ const value = args[index + 1]
163
+ if (value === undefined || value.startsWith('-')) return undefined
164
+ if (token === '--setup-token' && Option.isNone(parsed.setupToken)) {
165
+ return parse(index + 2, { ...parsed, setupToken: Option.some(value) })
166
+ }
167
+ if (token === '--harness' && Option.isNone(parsed.harness)) {
168
+ return parse(index + 2, { ...parsed, harness: Option.some(value) })
169
+ }
170
+ if (token === '--harnesses' && Option.isNone(parsed.harnesses)) {
171
+ return parse(index + 2, { ...parsed, harnesses: Option.some(value) })
172
+ }
173
+ if (token === '--scope' && Option.isNone(parsed.scope)) {
174
+ return parse(index + 2, { ...parsed, scope: Option.some(value) })
175
+ }
176
+ return undefined
177
+ }
178
+
179
+ return parse(0, {
180
+ detect: false,
181
+ harness: Option.none(),
182
+ harnesses: Option.none(),
183
+ json: false,
184
+ scope: Option.none(),
185
+ setupToken: Option.none(),
186
+ })
187
+ }
188
+
189
+ /** Returns a safe JSON error before Effect CLI can render ordinary parser help. */
190
+ export function preflightAgentInitArgv(argv: readonly string[]): string | undefined {
191
+ if (argv[2] !== 'init') return undefined
192
+ const args = argv.slice(3)
193
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) return undefined
194
+ const parsed = parseMachineArguments(args)
195
+ if (parsed === undefined) return invalidArgumentsJson()
196
+ if (!parsed.json) return invalidArgumentsJson()
197
+ if (Option.isSome(parsed.harness) && Option.isNone(Schema.decodeUnknownOption(InitHarness)(parsed.harness.value))) {
198
+ return argumentErrorJson('unsupported_harness', UNSUPPORTED_HARNESS_MESSAGE)
199
+ }
200
+ if (Option.isSome(parsed.harnesses) && parseHarnessSelection(parsed.harnesses.value) === undefined) {
201
+ return argumentErrorJson('invalid_harness_selection', INVALID_HARNESS_SELECTION_MESSAGE)
202
+ }
203
+ // Detection reports and exits, so it needs no scope and no credentials.
204
+ if (parsed.detect) return undefined
205
+ if (!Option.contains(parsed.scope, 'project') && !Option.contains(parsed.scope, 'global')) {
206
+ return invalidArgumentsJson()
207
+ }
208
+ if (Option.isSome(parsed.setupToken) && !SETUP_TOKEN_PATTERN.test(parsed.setupToken.value)) {
209
+ return argumentErrorJson('invalid_setup_token', INVALID_SETUP_TOKEN_MESSAGE)
210
+ }
211
+ return undefined
212
+ }
213
+
214
+ function renderHumanResult(result: typeof InitResult.Type): string {
215
+ if (result.status === 'authorization_required') {
216
+ // The approval page asks the user to type this code and compare it against
217
+ // their terminal, so the terminal has to print it.
218
+ return `Expand authorization required\n${result.userMessage}\nOpen: ${result.authorization.url.toString()}\nEnter code: ${result.authorization.userCode}`
219
+ }
220
+ const configured = result.installations.map((installation) => installation.harness).join(', ')
221
+ const failures = result.failures ?? []
222
+ // A run that configured some harnesses and not others must not print
223
+ // "complete" — the heading is the part a user skims.
224
+ const heading = failures.length > 0 ? 'Expand setup incomplete' : 'Expand setup complete'
225
+ const failed = failures.map((failure) => `${failure.harness} (${failure.code})`).join(', ')
226
+ return [
227
+ heading,
228
+ result.userMessage,
229
+ `Configured: ${configured}`,
230
+ ...(failures.length > 0 ? [`Failed: ${failed}`] : []),
231
+ ].join('\n')
232
+ }
233
+
234
+ const runInitService = Effect.fnUntraced(function* (request: InitRequest, jsonOutput: boolean) {
235
+ const initService = yield* InitService
236
+ const result = yield* Effect.either(initService.run(request))
237
+ if (Either.isLeft(result)) {
238
+ const error = result.left
239
+ if (error instanceof InitServiceError) {
240
+ yield* emitError(error.code, error.message, error.recoverable, 1)
241
+ return
242
+ }
243
+ yield* emitError('init_failed', 'Expand init could not be completed safely.', true, 1)
244
+ return
245
+ }
246
+ if (!jsonOutput) {
247
+ yield* Console.log(renderHumanResult(result.right))
248
+ return
249
+ }
250
+ const encoded = yield* Schema.encode(InitResultJson)(result.right).pipe(
251
+ Effect.catchAll(() => emitError('invalid_result', 'Expand init returned an invalid result.', false, 1)),
252
+ )
253
+ if (typeof encoded === 'string') yield* Console.log(encoded)
254
+ })
255
+
256
+ /** Emits what setup would configure without authorizing or writing anything. */
257
+ const runAgentDetect = Effect.fnUntraced(function* (hint: Option.Option<InitHarness>) {
258
+ const detector = yield* HarnessDetector
259
+ const detected = yield* detector.detect({ ...(Option.isSome(hint) ? { hint: hint.value } : {}) })
260
+ const current = yield* detector.detectCurrent()
261
+ const encoded = yield* Schema.encode(InitDetectionJson)(
262
+ new InitDetectionResult({
263
+ schemaVersion: 1,
264
+ status: 'detected',
265
+ harnesses: detected,
266
+ ...(Option.isSome(current) ? { current: current.value } : {}),
267
+ userMessage: detected.length === 0 ? NO_HARNESS_DETECTED_MESSAGE : HARNESS_SELECTION_MESSAGE,
268
+ }),
269
+ ).pipe(Effect.catchAll(() => emitError('invalid_result', 'Expand init returned an invalid result.', false, 1)))
270
+ if (typeof encoded === 'string') yield* Console.log(encoded)
271
+ })
272
+
273
+ const runAgentInit = Effect.fnUntraced(function* ({
274
+ detect: detectOnly,
275
+ harness,
276
+ harnesses: harnessSelection,
277
+ json,
278
+ scope,
279
+ setupToken,
280
+ }: InitCommandOptions) {
281
+ if (!json) {
282
+ yield* emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2)
283
+ return
284
+ }
285
+ const harnessHint = Option.flatMap(harness, (value) => Schema.decodeUnknownOption(InitHarness)(value))
286
+ if (Option.isSome(harness) && Option.isNone(harnessHint)) {
287
+ yield* emitError('unsupported_harness', UNSUPPORTED_HARNESS_MESSAGE, true, 2)
288
+ return
289
+ }
290
+ if (detectOnly) {
291
+ yield* runAgentDetect(harnessHint)
292
+ return
293
+ }
294
+ if (!Option.contains(scope, 'project') && !Option.contains(scope, 'global')) {
295
+ yield* emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2)
296
+ return
297
+ }
298
+ const selected = Option.map(harnessSelection, parseHarnessSelection)
299
+ if (Option.isSome(selected) && selected.value === undefined) {
300
+ yield* emitError('invalid_harness_selection', INVALID_HARNESS_SELECTION_MESSAGE, true, 2)
301
+ return
302
+ }
303
+ const detector = yield* HarnessDetector
304
+ // An explicit selection is the user's answer to "which harnesses?" — it is
305
+ // authoritative, and detection must not widen it.
306
+ const harnesses =
307
+ Option.isSome(selected) && selected.value !== undefined
308
+ ? selected.value
309
+ : yield* detector.detect({ ...(Option.isSome(harnessHint) ? { hint: harnessHint.value } : {}) })
310
+ if (harnesses.length === 0) {
311
+ yield* emitError(
312
+ 'no_harness_detected',
313
+ 'No supported agent harness was detected. Pass --harness with the agent currently running setup.',
314
+ true,
315
+ 2,
316
+ )
317
+ return
318
+ }
319
+
320
+ const request = yield* Schema.decodeUnknown(InitRequest)({
321
+ harnesses,
322
+ scope: Option.getOrUndefined(scope),
323
+ ...(Option.isSome(setupToken) ? { setupToken: setupToken.value } : {}),
324
+ }).pipe(
325
+ Effect.catchAll(() =>
326
+ emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2).pipe(Effect.as(undefined)),
327
+ ),
328
+ )
329
+ if (request === undefined) return
330
+
331
+ yield* runInitService(request, true)
332
+ })
333
+
334
+ export class InitTerminalInteraction extends Effect.Service<InitTerminalInteraction>()(
335
+ '@expandai/cli/Commands/InitTerminalInteraction',
336
+ {
337
+ effect: Effect.gen(function* () {
338
+ const terminal = yield* Terminal.Terminal
339
+ const chooseScope = Effect.fnUntraced(function* () {
340
+ return yield* Prompt.select({
341
+ message: 'Where should Expand be available?',
342
+ // Global leads: most people want Expand in every project, and it is
343
+ // the scope the hosted setup guide asks agents to offer first.
344
+ choices: [
345
+ { title: 'All projects for this user', value: 'global' as const },
346
+ { title: 'This project', value: 'project' as const },
347
+ ],
348
+ }).pipe(Effect.provideService(Terminal.Terminal, terminal))
349
+ })
350
+ /**
351
+ * Setup writes to every harness the user confirms here and to no other.
352
+ * Detected harnesses start selected so the common "configure everything"
353
+ * case is one keystroke, but the list is always shown before anything
354
+ * on disk changes.
355
+ */
356
+ const chooseHarnesses = Effect.fnUntraced(function* (detected: ReadonlyArray<InitHarness>) {
357
+ return yield* Prompt.multiSelect({
358
+ message: 'Which harnesses should Expand configure?',
359
+ choices: detected.map((harness) => ({ title: harness, value: harness, selected: true })),
360
+ }).pipe(Effect.provideService(Terminal.Terminal, terminal))
361
+ })
362
+ return { chooseScope, chooseHarnesses } as const
363
+ }),
364
+ },
365
+ ) {}
366
+
367
+ /**
368
+ * Human counterpart to `runAgentDetect`: the same detection, rendered for a
369
+ * terminal instead of as JSON, and writing nothing either way.
370
+ */
371
+ const runHumanDetect = Effect.fnUntraced(function* (hint: Option.Option<InitHarness>) {
372
+ const detector = yield* HarnessDetector
373
+ const detected = yield* detector.detect({ ...(Option.isSome(hint) ? { hint: hint.value } : {}) })
374
+ const current = yield* detector.detectCurrent()
375
+ if (detected.length === 0) {
376
+ yield* Console.log('No supported agent harness was detected. Nothing was changed.')
377
+ return
378
+ }
379
+ yield* Console.log(
380
+ [
381
+ 'Detected harnesses. Nothing was changed.',
382
+ ...detected.map(
383
+ (harness) => ` ${harness}${Option.isSome(current) && current.value === harness ? ' (this one)' : ''}`,
384
+ ),
385
+ '',
386
+ 'Run `expandai init` to configure them.',
387
+ ].join('\n'),
388
+ )
389
+ })
390
+
391
+ export const runHumanInit = Effect.fnUntraced(function* (options: InitCommandOptions) {
392
+ if (options.json) {
393
+ yield* runAgentInit(options)
394
+ return
395
+ }
396
+ const decodedHarness = Option.flatMap(options.harness, (value) => Schema.decodeUnknownOption(InitHarness)(value))
397
+ if (Option.isSome(options.harness) && Option.isNone(decodedHarness)) {
398
+ yield* emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2)
399
+ return
400
+ }
401
+ // `--detect` documents itself as changing nothing, and the guide tells agents
402
+ // the same. Only `runAgentInit` ever read the flag, and humans do not pass
403
+ // `--json`, so `expandai init --detect` in a terminal performed a real install.
404
+ // Answered before the scope prompt, because asking is itself a side effect —
405
+ // and `--harnesses`/`--scope` are irrelevant to detection.
406
+ if (options.detect) {
407
+ yield* runHumanDetect(decodedHarness)
408
+ return
409
+ }
410
+ const interaction = yield* InitTerminalInteraction
411
+ const scopeInput = Option.isSome(options.scope) ? options.scope.value : yield* interaction.chooseScope()
412
+ const decodedScope = Schema.decodeUnknownOption(InitScope)(scopeInput)
413
+ if (Option.isNone(decodedScope)) {
414
+ yield* emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2)
415
+ return
416
+ }
417
+ const selected = Option.map(options.harnesses, parseHarnessSelection)
418
+ if (Option.isSome(selected) && selected.value === undefined) {
419
+ yield* emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2)
420
+ return
421
+ }
422
+ // An explicit `--harnesses` is the answer, not a hint, so detection is skipped
423
+ // entirely — it exists only to build the question. Running it first rejected
424
+ // `--harnesses codex` on a machine with no Codex config, which is exactly when
425
+ // someone passes the flag. The machine path already treats it as authoritative.
426
+ const explicit = Option.isSome(selected) ? selected.value : undefined
427
+ const harnesses =
428
+ explicit ??
429
+ (yield* Effect.gen(function* () {
430
+ const detector = yield* HarnessDetector
431
+ const detected = yield* detector.detect({
432
+ ...(Option.isSome(decodedHarness) ? { hint: decodedHarness.value } : {}),
433
+ })
434
+ return detected.length === 0 ? undefined : yield* interaction.chooseHarnesses(detected)
435
+ }))
436
+ if (harnesses === undefined) {
437
+ yield* emitError(
438
+ 'no_harness_detected',
439
+ 'No supported agent harness was detected. Re-run with --harness for the agent you want to configure.',
440
+ true,
441
+ 2,
442
+ )
443
+ return
444
+ }
445
+ if (!Array.isNonEmptyReadonlyArray(harnesses)) {
446
+ yield* Console.log('No harnesses selected. Nothing was changed.')
447
+ return
448
+ }
449
+ const request = yield* Schema.decodeUnknown(InitRequest)({
450
+ harnesses,
451
+ scope: decodedScope.value,
452
+ ...(Option.isSome(options.setupToken) ? { setupToken: options.setupToken.value } : {}),
453
+ }).pipe(
454
+ Effect.catchAll(() =>
455
+ emitError('invalid_arguments', INVALID_ARGUMENTS_MESSAGE, true, 2).pipe(Effect.as(undefined)),
456
+ ),
457
+ )
458
+ if (request === undefined) return
459
+
460
+ if (Option.isNone(options.setupToken)) {
461
+ const authentication = yield* Authentication
462
+ const credentialHealth = yield* Effect.either(authentication.hasLocallyUsableCredentials())
463
+ if (Either.isLeft(credentialHealth)) {
464
+ yield* emitError('credential_health_failed', 'Stored credentials could not be validated safely.', false, 1)
465
+ return
466
+ }
467
+ if (!credentialHealth.right) {
468
+ const login = yield* Effect.either(authentication.loginWithBrowser())
469
+ if (Either.isLeft(login)) {
470
+ yield* emitError('authorization_failed', 'Browser authorization could not be completed.', true, 1)
471
+ return
472
+ }
473
+ }
474
+ }
475
+
476
+ yield* runInitService(request, options.json)
477
+ })
478
+
479
+ const initDescription = 'Authorize Expand and configure the complete supported harness profile'
480
+
481
+ export const HumanInitCommand = Command.make('init', config).pipe(
482
+ Command.withDescription(initDescription),
483
+ Command.withHandler(runHumanInit),
484
+ )
485
+
486
+ export const AgentInitCommand = Command.make('init', config).pipe(
487
+ Command.withDescription(initDescription),
488
+ Command.withHandler(runAgentInit),
489
+ )
@@ -1,7 +1,9 @@
1
1
  import { Args, Command, Options } from '@effect/cli'
2
+ import { resolvePlaygroundHost } from '@expandai/sdk/Playground'
2
3
  import { Console, Effect, Layer, Match, Schema } from 'effect'
3
4
  import { ApiClient } from '../ApiClient.js'
4
5
  import { ConfigManager } from '../Config.js'
6
+ import { CitedSnapshotSearchResult, enrichSnapshotSearchResult } from '../Mcp/tools/Citations.js'
5
7
  import { renderSnippets } from '../Snippets.js'
6
8
 
7
9
  export class SnapshotSnippetsUnavailable extends Schema.TaggedError<SnapshotSnippetsUnavailable>()(
@@ -72,15 +74,18 @@ export const SearchCommand: Command.Command<'search', never, unknown, SearchComm
72
74
  search: { query, maxResults, minScore },
73
75
  include: { markdown: true, json: true },
74
76
  })
77
+ const cited = enrichSnapshotSearchResult(result, host, 'cli-recovery')
75
78
 
76
79
  const output = yield* Match.value(format).pipe(
77
80
  Match.when('text', () => {
78
- const snippets = result.search.snippets
81
+ const snippets = cited.search.snippets
79
82
  return snippets.length === 0
80
83
  ? Effect.fail(new SnapshotSnippetsUnavailable({ snapshotId, query }))
81
- : Effect.succeed(renderSnippets(snapshotId, host, snippets))
84
+ : Effect.succeed(
85
+ `[invocation: cli-recovery]\n${renderSnippets(snapshotId, resolvePlaygroundHost(result.playground, host), snippets)}`,
86
+ )
82
87
  }),
83
- Match.when('json', () => Schema.encode(Schema.parseJson(Schema.Unknown, { space: 2 }))(result)),
88
+ Match.when('json', () => Schema.encode(Schema.parseJson(CitedSnapshotSearchResult, { space: 2 }))(cited)),
84
89
  Match.exhaustive,
85
90
  )
86
91