workos 0.17.1 → 0.19.0

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 (366) hide show
  1. package/README.md +11 -149
  2. package/dist/bin.js +155 -23
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.config.d.ts +0 -2
  5. package/dist/cli.config.js +3 -3
  6. package/dist/cli.config.js.map +1 -1
  7. package/dist/commands/api/index.js +18 -10
  8. package/dist/commands/api/index.js.map +1 -1
  9. package/dist/commands/api/interactive.js +11 -11
  10. package/dist/commands/api/interactive.js.map +1 -1
  11. package/dist/commands/claim.js +28 -16
  12. package/dist/commands/claim.js.map +1 -1
  13. package/dist/commands/connection.js +3 -3
  14. package/dist/commands/connection.js.map +1 -1
  15. package/dist/commands/debug.d.ts +11 -0
  16. package/dist/commands/debug.js +30 -14
  17. package/dist/commands/debug.js.map +1 -1
  18. package/dist/commands/dev.d.ts +7 -8
  19. package/dist/commands/dev.js +14 -14
  20. package/dist/commands/dev.js.map +1 -1
  21. package/dist/commands/directory.js +3 -3
  22. package/dist/commands/directory.js.map +1 -1
  23. package/dist/commands/doctor.js +2 -2
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/emulate.d.ts +1 -0
  26. package/dist/commands/emulate.js +3 -2
  27. package/dist/commands/emulate.js.map +1 -1
  28. package/dist/commands/env.d.ts +10 -0
  29. package/dist/commands/env.js +94 -18
  30. package/dist/commands/env.js.map +1 -1
  31. package/dist/commands/install.js +9 -10
  32. package/dist/commands/install.js.map +1 -1
  33. package/dist/commands/login.d.ts +25 -15
  34. package/dist/commands/login.js +126 -71
  35. package/dist/commands/login.js.map +1 -1
  36. package/dist/commands/logout.js +4 -4
  37. package/dist/commands/logout.js.map +1 -1
  38. package/dist/commands/mcp.d.ts +14 -0
  39. package/dist/commands/mcp.js +99 -0
  40. package/dist/commands/mcp.js.map +1 -0
  41. package/dist/commands/migrations.js +2 -1
  42. package/dist/commands/migrations.js.map +1 -1
  43. package/dist/commands/seed.js +5 -4
  44. package/dist/commands/seed.js.map +1 -1
  45. package/dist/commands/setup.d.ts +50 -0
  46. package/dist/commands/setup.js +237 -0
  47. package/dist/commands/setup.js.map +1 -0
  48. package/dist/commands/vault-run.js +3 -2
  49. package/dist/commands/vault-run.js.map +1 -1
  50. package/dist/commands/verify-login.d.ts +25 -0
  51. package/dist/commands/verify-login.js +158 -0
  52. package/dist/commands/verify-login.js.map +1 -0
  53. package/dist/doctor/checks/ai-analysis.js +2 -1
  54. package/dist/doctor/checks/ai-analysis.js.map +1 -1
  55. package/dist/doctor/checks/mcp.d.ts +15 -0
  56. package/dist/doctor/checks/mcp.js +34 -0
  57. package/dist/doctor/checks/mcp.js.map +1 -0
  58. package/dist/doctor/index.js +7 -1
  59. package/dist/doctor/index.js.map +1 -1
  60. package/dist/doctor/issues.js +18 -1
  61. package/dist/doctor/issues.js.map +1 -1
  62. package/dist/doctor/output.js +18 -3
  63. package/dist/doctor/output.js.map +1 -1
  64. package/dist/doctor/types.d.ts +17 -0
  65. package/dist/doctor/types.js.map +1 -1
  66. package/dist/integrations/dotnet/index.js +1 -1
  67. package/dist/integrations/dotnet/index.js.map +1 -1
  68. package/dist/integrations/elixir/index.js +1 -1
  69. package/dist/integrations/elixir/index.js.map +1 -1
  70. package/dist/integrations/go/index.js +1 -1
  71. package/dist/integrations/go/index.js.map +1 -1
  72. package/dist/integrations/nextjs/index.js +8 -5
  73. package/dist/integrations/nextjs/index.js.map +1 -1
  74. package/dist/integrations/nextjs/utils.d.ts +1 -1
  75. package/dist/integrations/nextjs/utils.js +21 -6
  76. package/dist/integrations/nextjs/utils.js.map +1 -1
  77. package/dist/integrations/react-router/index.js +5 -5
  78. package/dist/integrations/react-router/index.js.map +1 -1
  79. package/dist/integrations/react-router/utils.js +28 -12
  80. package/dist/integrations/react-router/utils.js.map +1 -1
  81. package/dist/integrations/ruby/index.js +1 -1
  82. package/dist/integrations/ruby/index.js.map +1 -1
  83. package/dist/lib/adapters/cli-adapter.d.ts +33 -4
  84. package/dist/lib/adapters/cli-adapter.js +251 -135
  85. package/dist/lib/adapters/cli-adapter.js.map +1 -1
  86. package/dist/lib/adapters/dashboard-adapter.js +3 -3
  87. package/dist/lib/adapters/dashboard-adapter.js.map +1 -1
  88. package/dist/lib/adapters/headless-adapter.d.ts +7 -0
  89. package/dist/lib/adapters/headless-adapter.js +42 -5
  90. package/dist/lib/adapters/headless-adapter.js.map +1 -1
  91. package/dist/lib/agent-interface.js +19 -5
  92. package/dist/lib/agent-interface.js.map +1 -1
  93. package/dist/lib/agent-runner.js +1 -1
  94. package/dist/lib/agent-runner.js.map +1 -1
  95. package/dist/lib/ai-content.js +2 -1
  96. package/dist/lib/ai-content.js.map +1 -1
  97. package/dist/lib/api-key.js +2 -1
  98. package/dist/lib/api-key.js.map +1 -1
  99. package/dist/lib/completion-data.d.ts +33 -0
  100. package/dist/lib/completion-data.js +33 -0
  101. package/dist/lib/completion-data.js.map +1 -0
  102. package/dist/lib/config-store.d.ts +13 -0
  103. package/dist/lib/config-store.js +24 -0
  104. package/dist/lib/config-store.js.map +1 -1
  105. package/dist/lib/config.d.ts +1 -1
  106. package/dist/lib/config.js +1 -1
  107. package/dist/lib/config.js.map +1 -1
  108. package/dist/lib/constants.d.ts +17 -0
  109. package/dist/lib/constants.js +17 -0
  110. package/dist/lib/constants.js.map +1 -1
  111. package/dist/lib/events.d.ts +34 -1
  112. package/dist/lib/events.js.map +1 -1
  113. package/dist/lib/framework-config.d.ts +6 -0
  114. package/dist/lib/framework-config.js.map +1 -1
  115. package/dist/lib/installer-core.d.ts +34 -21
  116. package/dist/lib/installer-core.js +24 -5
  117. package/dist/lib/installer-core.js.map +1 -1
  118. package/dist/lib/installer-core.types.d.ts +3 -1
  119. package/dist/lib/installer-core.types.js.map +1 -1
  120. package/dist/lib/mcp-clients.d.ts +45 -0
  121. package/dist/lib/mcp-clients.js +297 -0
  122. package/dist/lib/mcp-clients.js.map +1 -0
  123. package/dist/lib/port-detection.js +19 -13
  124. package/dist/lib/port-detection.js.map +1 -1
  125. package/dist/lib/preferences.d.ts +42 -0
  126. package/dist/lib/preferences.js +34 -5
  127. package/dist/lib/preferences.js.map +1 -1
  128. package/dist/lib/run-with-core.js +42 -4
  129. package/dist/lib/run-with-core.js.map +1 -1
  130. package/dist/lib/settings.d.ts +5 -14
  131. package/dist/lib/settings.js +5 -16
  132. package/dist/lib/settings.js.map +1 -1
  133. package/dist/lib/startup-notice-gate.d.ts +21 -0
  134. package/dist/lib/startup-notice-gate.js +29 -0
  135. package/dist/lib/startup-notice-gate.js.map +1 -0
  136. package/dist/lib/telemetry-notice.d.ts +5 -5
  137. package/dist/lib/telemetry-notice.js +10 -8
  138. package/dist/lib/telemetry-notice.js.map +1 -1
  139. package/dist/lib/unclaimed-env-provision.js +15 -12
  140. package/dist/lib/unclaimed-env-provision.js.map +1 -1
  141. package/dist/lib/unclaimed-warning.js +7 -3
  142. package/dist/lib/unclaimed-warning.js.map +1 -1
  143. package/dist/lib/validation/security-checks.js +2 -1
  144. package/dist/lib/validation/security-checks.js.map +1 -1
  145. package/dist/lib/validation/validator.js +30 -3
  146. package/dist/lib/validation/validator.js.map +1 -1
  147. package/dist/lib/workos-management.js +26 -19
  148. package/dist/lib/workos-management.js.map +1 -1
  149. package/dist/run.d.ts +1 -0
  150. package/dist/run.js +5 -1
  151. package/dist/run.js.map +1 -1
  152. package/dist/steps/add-or-update-environment-variables.js +10 -10
  153. package/dist/steps/add-or-update-environment-variables.js.map +1 -1
  154. package/dist/steps/run-prettier.js +3 -3
  155. package/dist/steps/run-prettier.js.map +1 -1
  156. package/dist/steps/upload-environment-variables/index.js +15 -3
  157. package/dist/steps/upload-environment-variables/index.js.map +1 -1
  158. package/dist/steps/upload-environment-variables/providers/vercel.js +2 -2
  159. package/dist/steps/upload-environment-variables/providers/vercel.js.map +1 -1
  160. package/dist/utils/analytics.d.ts +1 -0
  161. package/dist/utils/analytics.js +5 -1
  162. package/dist/utils/analytics.js.map +1 -1
  163. package/dist/utils/box.d.ts +6 -30
  164. package/dist/utils/box.js +8 -95
  165. package/dist/utils/box.js.map +1 -1
  166. package/dist/utils/cli-symbols.d.ts +14 -2
  167. package/dist/utils/cli-symbols.js +12 -0
  168. package/dist/utils/cli-symbols.js.map +1 -1
  169. package/dist/utils/debug.js +3 -3
  170. package/dist/utils/debug.js.map +1 -1
  171. package/dist/utils/environment.js +1 -1
  172. package/dist/utils/environment.js.map +1 -1
  173. package/dist/utils/help-json.js +153 -4
  174. package/dist/utils/help-json.js.map +1 -1
  175. package/dist/utils/interaction-mode.d.ts +1 -1
  176. package/dist/utils/interaction-mode.js +0 -3
  177. package/dist/utils/interaction-mode.js.map +1 -1
  178. package/dist/utils/output.d.ts +5 -3
  179. package/dist/utils/output.js +5 -5
  180. package/dist/utils/output.js.map +1 -1
  181. package/dist/utils/package-manager.js +1 -1
  182. package/dist/utils/package-manager.js.map +1 -1
  183. package/dist/utils/recovery-hints.d.ts +5 -0
  184. package/dist/utils/recovery-hints.js +12 -0
  185. package/dist/utils/recovery-hints.js.map +1 -1
  186. package/dist/utils/summary-box.d.ts +8 -1
  187. package/dist/utils/summary-box.js +67 -4
  188. package/dist/utils/summary-box.js.map +1 -1
  189. package/dist/utils/telemetry-types.d.ts +1 -1
  190. package/dist/utils/telemetry-types.js.map +1 -1
  191. package/dist/utils/types.d.ts +10 -0
  192. package/dist/utils/types.js.map +1 -1
  193. package/dist/utils/{clack-utils.d.ts → ui-utils.d.ts} +1 -1
  194. package/dist/utils/{clack-utils.js → ui-utils.js} +64 -37
  195. package/dist/utils/ui-utils.js.map +1 -0
  196. package/dist/utils/ui.d.ts +148 -0
  197. package/dist/utils/ui.js +368 -0
  198. package/dist/utils/ui.js.map +1 -0
  199. package/dist/utils/urls.d.ts +9 -3
  200. package/dist/utils/urls.js +10 -4
  201. package/dist/utils/urls.js.map +1 -1
  202. package/package.json +13 -20
  203. package/dist/check-coverage.ts +0 -237
  204. package/dist/emulate/core/id.d.ts +0 -48
  205. package/dist/emulate/core/id.js +0 -73
  206. package/dist/emulate/core/id.js.map +0 -1
  207. package/dist/emulate/core/index.d.ts +0 -8
  208. package/dist/emulate/core/index.js +0 -8
  209. package/dist/emulate/core/index.js.map +0 -1
  210. package/dist/emulate/core/jwt.d.ts +0 -28
  211. package/dist/emulate/core/jwt.js +0 -78
  212. package/dist/emulate/core/jwt.js.map +0 -1
  213. package/dist/emulate/core/middleware/auth.d.ts +0 -15
  214. package/dist/emulate/core/middleware/auth.js +0 -17
  215. package/dist/emulate/core/middleware/auth.js.map +0 -1
  216. package/dist/emulate/core/middleware/error-handler.d.ts +0 -22
  217. package/dist/emulate/core/middleware/error-handler.js +0 -72
  218. package/dist/emulate/core/middleware/error-handler.js.map +0 -1
  219. package/dist/emulate/core/pagination.d.ts +0 -27
  220. package/dist/emulate/core/pagination.js +0 -43
  221. package/dist/emulate/core/pagination.js.map +0 -1
  222. package/dist/emulate/core/plugin.d.ts +0 -15
  223. package/dist/emulate/core/plugin.js +0 -2
  224. package/dist/emulate/core/plugin.js.map +0 -1
  225. package/dist/emulate/core/server.d.ts +0 -17
  226. package/dist/emulate/core/server.js +0 -90
  227. package/dist/emulate/core/server.js.map +0 -1
  228. package/dist/emulate/core/store.d.ts +0 -44
  229. package/dist/emulate/core/store.js +0 -169
  230. package/dist/emulate/core/store.js.map +0 -1
  231. package/dist/emulate/index.d.ts +0 -25
  232. package/dist/emulate/index.js +0 -47
  233. package/dist/emulate/index.js.map +0 -1
  234. package/dist/emulate/workos/constants.d.ts +0 -56
  235. package/dist/emulate/workos/constants.js +0 -56
  236. package/dist/emulate/workos/constants.js.map +0 -1
  237. package/dist/emulate/workos/entities.d.ts +0 -360
  238. package/dist/emulate/workos/entities.js +0 -2
  239. package/dist/emulate/workos/entities.js.map +0 -1
  240. package/dist/emulate/workos/event-bus.d.ts +0 -17
  241. package/dist/emulate/workos/event-bus.js +0 -70
  242. package/dist/emulate/workos/event-bus.js.map +0 -1
  243. package/dist/emulate/workos/helpers.d.ts +0 -72
  244. package/dist/emulate/workos/helpers.js +0 -211
  245. package/dist/emulate/workos/helpers.js.map +0 -1
  246. package/dist/emulate/workos/index.d.ts +0 -93
  247. package/dist/emulate/workos/index.js +0 -326
  248. package/dist/emulate/workos/index.js.map +0 -1
  249. package/dist/emulate/workos/role-helpers.d.ts +0 -21
  250. package/dist/emulate/workos/role-helpers.js +0 -130
  251. package/dist/emulate/workos/role-helpers.js.map +0 -1
  252. package/dist/emulate/workos/routes/api-keys.d.ts +0 -2
  253. package/dist/emulate/workos/routes/api-keys.js +0 -32
  254. package/dist/emulate/workos/routes/api-keys.js.map +0 -1
  255. package/dist/emulate/workos/routes/audit-logs.d.ts +0 -2
  256. package/dist/emulate/workos/routes/audit-logs.js +0 -104
  257. package/dist/emulate/workos/routes/audit-logs.js.map +0 -1
  258. package/dist/emulate/workos/routes/auth-challenges.d.ts +0 -2
  259. package/dist/emulate/workos/routes/auth-challenges.js +0 -51
  260. package/dist/emulate/workos/routes/auth-challenges.js.map +0 -1
  261. package/dist/emulate/workos/routes/auth-factors.d.ts +0 -2
  262. package/dist/emulate/workos/routes/auth-factors.js +0 -51
  263. package/dist/emulate/workos/routes/auth-factors.js.map +0 -1
  264. package/dist/emulate/workos/routes/auth.d.ts +0 -2
  265. package/dist/emulate/workos/routes/auth.js +0 -350
  266. package/dist/emulate/workos/routes/auth.js.map +0 -1
  267. package/dist/emulate/workos/routes/authorization-checks.d.ts +0 -10
  268. package/dist/emulate/workos/routes/authorization-checks.js +0 -123
  269. package/dist/emulate/workos/routes/authorization-checks.js.map +0 -1
  270. package/dist/emulate/workos/routes/authorization-org-roles.d.ts +0 -2
  271. package/dist/emulate/workos/routes/authorization-org-roles.js +0 -64
  272. package/dist/emulate/workos/routes/authorization-org-roles.js.map +0 -1
  273. package/dist/emulate/workos/routes/authorization-permissions.d.ts +0 -2
  274. package/dist/emulate/workos/routes/authorization-permissions.js +0 -67
  275. package/dist/emulate/workos/routes/authorization-permissions.js.map +0 -1
  276. package/dist/emulate/workos/routes/authorization-resources.d.ts +0 -2
  277. package/dist/emulate/workos/routes/authorization-resources.js +0 -117
  278. package/dist/emulate/workos/routes/authorization-resources.js.map +0 -1
  279. package/dist/emulate/workos/routes/authorization-roles.d.ts +0 -2
  280. package/dist/emulate/workos/routes/authorization-roles.js +0 -13
  281. package/dist/emulate/workos/routes/authorization-roles.js.map +0 -1
  282. package/dist/emulate/workos/routes/config.d.ts +0 -2
  283. package/dist/emulate/workos/routes/config.js +0 -57
  284. package/dist/emulate/workos/routes/config.js.map +0 -1
  285. package/dist/emulate/workos/routes/connect.d.ts +0 -2
  286. package/dist/emulate/workos/routes/connect.js +0 -65
  287. package/dist/emulate/workos/routes/connect.js.map +0 -1
  288. package/dist/emulate/workos/routes/connections.d.ts +0 -2
  289. package/dist/emulate/workos/routes/connections.js +0 -73
  290. package/dist/emulate/workos/routes/connections.js.map +0 -1
  291. package/dist/emulate/workos/routes/data-integrations.d.ts +0 -2
  292. package/dist/emulate/workos/routes/data-integrations.js +0 -55
  293. package/dist/emulate/workos/routes/data-integrations.js.map +0 -1
  294. package/dist/emulate/workos/routes/directories.d.ts +0 -2
  295. package/dist/emulate/workos/routes/directories.js +0 -90
  296. package/dist/emulate/workos/routes/directories.js.map +0 -1
  297. package/dist/emulate/workos/routes/email-verification.d.ts +0 -2
  298. package/dist/emulate/workos/routes/email-verification.js +0 -49
  299. package/dist/emulate/workos/routes/email-verification.js.map +0 -1
  300. package/dist/emulate/workos/routes/events.d.ts +0 -2
  301. package/dist/emulate/workos/routes/events.js +0 -18
  302. package/dist/emulate/workos/routes/events.js.map +0 -1
  303. package/dist/emulate/workos/routes/feature-flags.d.ts +0 -2
  304. package/dist/emulate/workos/routes/feature-flags.js +0 -103
  305. package/dist/emulate/workos/routes/feature-flags.js.map +0 -1
  306. package/dist/emulate/workos/routes/invitations.d.ts +0 -2
  307. package/dist/emulate/workos/routes/invitations.js +0 -122
  308. package/dist/emulate/workos/routes/invitations.js.map +0 -1
  309. package/dist/emulate/workos/routes/legacy-mfa.d.ts +0 -2
  310. package/dist/emulate/workos/routes/legacy-mfa.js +0 -75
  311. package/dist/emulate/workos/routes/legacy-mfa.js.map +0 -1
  312. package/dist/emulate/workos/routes/magic-auth.d.ts +0 -2
  313. package/dist/emulate/workos/routes/magic-auth.js +0 -32
  314. package/dist/emulate/workos/routes/magic-auth.js.map +0 -1
  315. package/dist/emulate/workos/routes/memberships.d.ts +0 -2
  316. package/dist/emulate/workos/routes/memberships.js +0 -114
  317. package/dist/emulate/workos/routes/memberships.js.map +0 -1
  318. package/dist/emulate/workos/routes/organization-domains.d.ts +0 -2
  319. package/dist/emulate/workos/routes/organization-domains.js +0 -58
  320. package/dist/emulate/workos/routes/organization-domains.js.map +0 -1
  321. package/dist/emulate/workos/routes/organizations.d.ts +0 -2
  322. package/dist/emulate/workos/routes/organizations.js +0 -131
  323. package/dist/emulate/workos/routes/organizations.js.map +0 -1
  324. package/dist/emulate/workos/routes/password-reset.d.ts +0 -2
  325. package/dist/emulate/workos/routes/password-reset.js +0 -61
  326. package/dist/emulate/workos/routes/password-reset.js.map +0 -1
  327. package/dist/emulate/workos/routes/pipes.d.ts +0 -2
  328. package/dist/emulate/workos/routes/pipes.js +0 -82
  329. package/dist/emulate/workos/routes/pipes.js.map +0 -1
  330. package/dist/emulate/workos/routes/portal.d.ts +0 -2
  331. package/dist/emulate/workos/routes/portal.js +0 -18
  332. package/dist/emulate/workos/routes/portal.js.map +0 -1
  333. package/dist/emulate/workos/routes/radar.d.ts +0 -2
  334. package/dist/emulate/workos/routes/radar.js +0 -41
  335. package/dist/emulate/workos/routes/radar.js.map +0 -1
  336. package/dist/emulate/workos/routes/sessions.d.ts +0 -2
  337. package/dist/emulate/workos/routes/sessions.js +0 -51
  338. package/dist/emulate/workos/routes/sessions.js.map +0 -1
  339. package/dist/emulate/workos/routes/sso.d.ts +0 -2
  340. package/dist/emulate/workos/routes/sso.js +0 -161
  341. package/dist/emulate/workos/routes/sso.js.map +0 -1
  342. package/dist/emulate/workos/routes/user-features.d.ts +0 -2
  343. package/dist/emulate/workos/routes/user-features.js +0 -50
  344. package/dist/emulate/workos/routes/user-features.js.map +0 -1
  345. package/dist/emulate/workos/routes/users.d.ts +0 -2
  346. package/dist/emulate/workos/routes/users.js +0 -129
  347. package/dist/emulate/workos/routes/users.js.map +0 -1
  348. package/dist/emulate/workos/routes/webhook-endpoints.d.ts +0 -2
  349. package/dist/emulate/workos/routes/webhook-endpoints.js +0 -67
  350. package/dist/emulate/workos/routes/webhook-endpoints.js.map +0 -1
  351. package/dist/emulate/workos/routes/widgets.d.ts +0 -2
  352. package/dist/emulate/workos/routes/widgets.js +0 -27
  353. package/dist/emulate/workos/routes/widgets.js.map +0 -1
  354. package/dist/emulate/workos/store.d.ts +0 -48
  355. package/dist/emulate/workos/store.js +0 -102
  356. package/dist/emulate/workos/store.js.map +0 -1
  357. package/dist/emulate/workos/webhook-signer.d.ts +0 -1
  358. package/dist/emulate/workos/webhook-signer.js +0 -8
  359. package/dist/emulate/workos/webhook-signer.js.map +0 -1
  360. package/dist/gen-routes-lib.spec.ts +0 -659
  361. package/dist/gen-routes-lib.ts +0 -647
  362. package/dist/gen-routes.ts +0 -96
  363. package/dist/utils/clack-utils.js.map +0 -1
  364. package/dist/utils/clack.d.ts +0 -5
  365. package/dist/utils/clack.js +0 -34
  366. package/dist/utils/clack.js.map +0 -1
@@ -0,0 +1,297 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { access, mkdir, readFile, writeFile } from 'fs/promises';
4
+ import * as jsonc from 'jsonc-parser';
5
+ import { execFileNoThrow } from '../utils/exec-file.js';
6
+ import { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';
7
+ /**
8
+ * Client-writer library for the WorkOS MCP server.
9
+ *
10
+ * One `McpClientTarget` interface, three implementations. Claude Code and Codex
11
+ * are configured by shelling out to their own CLIs (robust to their config
12
+ * format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly
13
+ * with `jsonc-parser` (hand-edited files may contain comments / trailing
14
+ * commas). Every operation degrades to a reported outcome — a missing binary,
15
+ * an old client without HTTP-transport support, or an unparseable config is a
16
+ * `failed`/`skipped`/`not-installed` result, never a thrown error.
17
+ *
18
+ * The library owns detection and per-client config writing; callers (the `mcp`
19
+ * command today; install-flow / doctor in Phase 2) stay thin. Nothing here may
20
+ * assume it runs inside the `mcp` command.
21
+ */
22
+ /** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */
23
+ const EXEC_TIMEOUT_MS = 10_000;
24
+ /** jsonc parse options: tolerate the comments / trailing commas real users
25
+ * hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely
26
+ * malformed JSON is flagged. */
27
+ const JSONC_PARSE_OPTIONS = {
28
+ allowTrailingComma: true,
29
+ allowEmptyContent: true,
30
+ };
31
+ const JSONC_MODIFY_OPTIONS = {
32
+ formattingOptions: { tabSize: 2, insertSpaces: true },
33
+ };
34
+ /**
35
+ * Human phrasing for each outcome (JSON mode emits the raw `outcome` value).
36
+ * Lives with the `McpOutcome` type so every consumer (`mcp` command, `setup`)
37
+ * stays in lockstep when an outcome is added.
38
+ */
39
+ export const MCP_OUTCOME_LABELS = {
40
+ installed: 'installed',
41
+ 'already-installed': 'already installed',
42
+ removed: 'removed',
43
+ 'not-installed': 'not installed',
44
+ skipped: 'skipped',
45
+ failed: 'failed',
46
+ };
47
+ /** Stable list of known agent keys, for `--agent` validation by callers. */
48
+ export const MCP_AGENT_KEYS = ['claude-code', 'codex', 'cursor'];
49
+ /** Async existence check — `access` rejects with ENOENT when the path is missing. */
50
+ async function pathExists(p) {
51
+ try {
52
+ await access(p);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** Trim a shell-out message to a single-line excerpt for the `error` field. */
60
+ function excerpt(raw) {
61
+ const text = raw.trim();
62
+ if (!text)
63
+ return 'Unknown error';
64
+ const firstLine = text.split('\n').find((l) => l.trim().length > 0) ?? text;
65
+ return firstLine.trim().slice(0, 300);
66
+ }
67
+ /**
68
+ * Does a CLI `mcp list` output contain our server?
69
+ *
70
+ * Matches the first whitespace-delimited token of any line (with an optional
71
+ * trailing colon stripped), which covers both list shapes we support:
72
+ * - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`
73
+ * - Codex table: `workos https://mcp.workos.com/mcp ...`
74
+ *
75
+ * First-token matching avoids the substring collision a naive
76
+ * `output.includes('workos')` would hit on an unrelated `workos-docs` entry.
77
+ */
78
+ function listHasServer(stdout, name) {
79
+ return stdout.split('\n').some((line) => {
80
+ const firstToken = line.trim().split(/\s+/)[0] ?? '';
81
+ return firstToken.replace(/:$/, '') === name;
82
+ });
83
+ }
84
+ /**
85
+ * Shared shell-out client for Claude Code and Codex. The two differ only in
86
+ * binary name, config dir, and the `add` arg shape; every outcome-mapping rule
87
+ * is identical, so they share one implementation.
88
+ */
89
+ function createCliClient(config) {
90
+ const { key, displayName, binary, configDir, addArgs, removeArgs } = config;
91
+ const result = (outcome, error) => ({
92
+ agent: key,
93
+ displayName,
94
+ outcome,
95
+ ...(error ? { error } : {}),
96
+ });
97
+ async function checkInstalled() {
98
+ const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });
99
+ if (res.status !== 0)
100
+ return false;
101
+ return listHasServer(res.stdout, MCP_SERVER_NAME);
102
+ }
103
+ return {
104
+ key,
105
+ displayName,
106
+ async isAvailable() {
107
+ // Two layers: the config dir can exist while the binary isn't on PATH,
108
+ // and vice versa — require both before we try to shell out.
109
+ if (!(await pathExists(join(homedir(), configDir))))
110
+ return false;
111
+ const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });
112
+ return res.status === 0;
113
+ },
114
+ isInstalled: checkInstalled,
115
+ async add() {
116
+ const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });
117
+ if (res.status === 0)
118
+ return result('installed');
119
+ // Idempotent: an "already exists" collision is a success, not a failure.
120
+ const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
121
+ if (combined.includes('already exists') || combined.includes('already configured')) {
122
+ return result('already-installed');
123
+ }
124
+ // A non-zero exit doesn't always mean the write failed: Codex persists the
125
+ // server config and THEN starts an OAuth flow that blocks (no browser /
126
+ // callback available here) until our timeout kills it with a non-zero
127
+ // status. Confirm against the actual config before declaring failure —
128
+ // more robust than matching each client's success wording.
129
+ if (await checkInstalled())
130
+ return result('installed');
131
+ // Otherwise a real failure — an old client lacking `--transport http` /
132
+ // `--url`, a bad invocation, etc. Reported, never thrown.
133
+ return result('failed', excerpt(res.stderr || res.stdout));
134
+ },
135
+ async remove() {
136
+ const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });
137
+ const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
138
+ // Claude exits 1 and Codex exits 0 when the server is absent; both print
139
+ // "No MCP server named ...". Treat either as an idempotent no-op.
140
+ if (combined.includes('no mcp server'))
141
+ return result('not-installed');
142
+ if (res.status === 0)
143
+ return result('removed');
144
+ return result('failed', excerpt(res.stderr || res.stdout));
145
+ },
146
+ };
147
+ }
148
+ function createClaudeCodeClient() {
149
+ return createCliClient({
150
+ key: 'claude-code',
151
+ displayName: 'Claude Code',
152
+ binary: 'claude',
153
+ configDir: '.claude',
154
+ // User scope: the management MCP is account-level, not per-repo — project
155
+ // scope would nag teammates via checked-in config.
156
+ addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],
157
+ removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],
158
+ });
159
+ }
160
+ function createCodexClient() {
161
+ return createCliClient({
162
+ key: 'codex',
163
+ displayName: 'Codex',
164
+ binary: 'codex',
165
+ configDir: '.codex',
166
+ addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],
167
+ removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],
168
+ });
169
+ }
170
+ /**
171
+ * Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with
172
+ * `jsonc-parser`, preserving existing servers, comments, and formatting.
173
+ */
174
+ function createCursorClient() {
175
+ const key = 'cursor';
176
+ const displayName = 'Cursor';
177
+ const cursorDir = () => join(homedir(), '.cursor');
178
+ const configPath = () => join(cursorDir(), 'mcp.json');
179
+ const result = (outcome, error) => ({
180
+ agent: key,
181
+ displayName,
182
+ outcome,
183
+ ...(error ? { error } : {}),
184
+ });
185
+ /** Read + parse the config. `parseError` is true only for genuinely malformed
186
+ * JSON (not comments / trailing commas / empty). */
187
+ async function readConfig() {
188
+ const path = configPath();
189
+ if (!(await pathExists(path))) {
190
+ return { content: '', parsed: undefined, parseError: false, exists: false };
191
+ }
192
+ const content = await readFile(path, 'utf8');
193
+ const errors = [];
194
+ const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS);
195
+ return { content, parsed, parseError: errors.length > 0, exists: true };
196
+ }
197
+ function hasWorkosServer(parsed) {
198
+ const servers = parsed?.mcpServers;
199
+ return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in servers);
200
+ }
201
+ return {
202
+ key,
203
+ displayName,
204
+ async isAvailable() {
205
+ return pathExists(cursorDir());
206
+ },
207
+ async isInstalled() {
208
+ try {
209
+ const { parsed, parseError } = await readConfig();
210
+ if (parseError)
211
+ return false;
212
+ return hasWorkosServer(parsed);
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ },
218
+ async add() {
219
+ const path = configPath();
220
+ try {
221
+ const { content, parseError, exists } = await readConfig();
222
+ // Never overwrite a file we couldn't parse — the user must fix it first.
223
+ if (exists && parseError) {
224
+ return result('failed', `Could not parse ${path}; fix it manually and retry.`);
225
+ }
226
+ await mkdir(cursorDir(), { recursive: true });
227
+ // Converge to our URL — an existing `workos` key with a different URL is
228
+ // overwritten (reported as `installed`).
229
+ const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], { url: MCP_SERVER_URL }, JSONC_MODIFY_OPTIONS);
230
+ await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
231
+ return result('installed');
232
+ }
233
+ catch (error) {
234
+ return result('failed', error instanceof Error ? error.message : 'Unknown error');
235
+ }
236
+ },
237
+ async remove() {
238
+ const path = configPath();
239
+ try {
240
+ const { content, parsed, parseError, exists } = await readConfig();
241
+ if (!exists)
242
+ return result('not-installed');
243
+ if (parseError) {
244
+ return result('failed', `Could not parse ${path}; fix it manually and retry.`);
245
+ }
246
+ if (!hasWorkosServer(parsed))
247
+ return result('not-installed');
248
+ const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);
249
+ await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
250
+ return result('removed');
251
+ }
252
+ catch (error) {
253
+ return result('failed', error instanceof Error ? error.message : 'Unknown error');
254
+ }
255
+ },
256
+ };
257
+ }
258
+ /** The three client targets, in a stable order. */
259
+ export function createMcpClients() {
260
+ return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];
261
+ }
262
+ /**
263
+ * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,
264
+ * or null when the file/entry is absent, unreadable, or unparseable.
265
+ *
266
+ * Cursor is the only client whose config we read directly (the CLI clients don't
267
+ * expose per-entry URLs), so this powers doctor's URL-drift ("misconfigured")
268
+ * check without a second jsonc reader duplicating the config schema. Read-only
269
+ * and never throws — a problem reading just yields null.
270
+ */
271
+ export async function getCursorConfiguredUrl() {
272
+ try {
273
+ const path = join(homedir(), '.cursor', 'mcp.json');
274
+ if (!(await pathExists(path)))
275
+ return null;
276
+ const content = await readFile(path, 'utf8');
277
+ const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS);
278
+ const servers = parsed?.mcpServers;
279
+ const entry = servers?.[MCP_SERVER_NAME];
280
+ return typeof entry?.url === 'string' ? entry.url : null;
281
+ }
282
+ catch {
283
+ return null;
284
+ }
285
+ }
286
+ /**
287
+ * Return only the clients available on this machine, optionally narrowed to
288
+ * `agentFilter` keys. Availability is probed in parallel. An unknown filter key
289
+ * simply matches nothing here — callers validate keys and surface the error.
290
+ */
291
+ export async function detectMcpClients(agentFilter) {
292
+ const clients = createMcpClients();
293
+ const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;
294
+ const availability = await Promise.all(filtered.map((c) => c.isAvailable()));
295
+ return filtered.filter((_, i) => availability[i]);
296
+ }
297
+ //# sourceMappingURL=mcp-clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-clients.js","sourceRoot":"","sources":["../../src/lib/mcp-clients.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir } from 'os';\nimport { join } from 'path';\nimport { access, mkdir, readFile, writeFile } from 'fs/promises';\nimport * as jsonc from 'jsonc-parser';\nimport { execFileNoThrow } from '../utils/exec-file.js';\nimport { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';\n\n/**\n * Client-writer library for the WorkOS MCP server.\n *\n * One `McpClientTarget` interface, three implementations. Claude Code and Codex\n * are configured by shelling out to their own CLIs (robust to their config\n * format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly\n * with `jsonc-parser` (hand-edited files may contain comments / trailing\n * commas). Every operation degrades to a reported outcome — a missing binary,\n * an old client without HTTP-transport support, or an unparseable config is a\n * `failed`/`skipped`/`not-installed` result, never a thrown error.\n *\n * The library owns detection and per-client config writing; callers (the `mcp`\n * command today; install-flow / doctor in Phase 2) stay thin. Nothing here may\n * assume it runs inside the `mcp` command.\n */\n\n/** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */\nconst EXEC_TIMEOUT_MS = 10_000;\n\n/** jsonc parse options: tolerate the comments / trailing commas real users\n * hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely\n * malformed JSON is flagged. */\nconst JSONC_PARSE_OPTIONS: jsonc.ParseOptions = {\n allowTrailingComma: true,\n allowEmptyContent: true,\n};\n\nconst JSONC_MODIFY_OPTIONS: jsonc.ModificationOptions = {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n};\n\nexport type McpAgentKey = 'claude-code' | 'codex' | 'cursor';\n\nexport type McpOutcome = 'installed' | 'already-installed' | 'removed' | 'not-installed' | 'skipped' | 'failed';\n\n/**\n * Human phrasing for each outcome (JSON mode emits the raw `outcome` value).\n * Lives with the `McpOutcome` type so every consumer (`mcp` command, `setup`)\n * stays in lockstep when an outcome is added.\n */\nexport const MCP_OUTCOME_LABELS: Record<McpOutcome, string> = {\n installed: 'installed',\n 'already-installed': 'already installed',\n removed: 'removed',\n 'not-installed': 'not installed',\n skipped: 'skipped',\n failed: 'failed',\n};\n\nexport interface McpClientResult {\n agent: McpAgentKey;\n displayName: string;\n outcome: McpOutcome;\n /** stderr/message excerpt when `outcome === 'failed'`. */\n error?: string;\n}\n\nexport interface McpClientTarget {\n key: McpAgentKey;\n displayName: string;\n /** Agent is usable on this machine (config dir present AND, for CLI clients, binary runnable). */\n isAvailable(): Promise<boolean>;\n /** The WorkOS server is present in this client's config. */\n isInstalled(): Promise<boolean>;\n add(): Promise<McpClientResult>;\n remove(): Promise<McpClientResult>;\n}\n\n/** Stable list of known agent keys, for `--agent` validation by callers. */\nexport const MCP_AGENT_KEYS: McpAgentKey[] = ['claude-code', 'codex', 'cursor'];\n\n/** Async existence check — `access` rejects with ENOENT when the path is missing. */\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Trim a shell-out message to a single-line excerpt for the `error` field. */\nfunction excerpt(raw: string): string {\n const text = raw.trim();\n if (!text) return 'Unknown error';\n const firstLine = text.split('\\n').find((l) => l.trim().length > 0) ?? text;\n return firstLine.trim().slice(0, 300);\n}\n\n/**\n * Does a CLI `mcp list` output contain our server?\n *\n * Matches the first whitespace-delimited token of any line (with an optional\n * trailing colon stripped), which covers both list shapes we support:\n * - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`\n * - Codex table: `workos https://mcp.workos.com/mcp ...`\n *\n * First-token matching avoids the substring collision a naive\n * `output.includes('workos')` would hit on an unrelated `workos-docs` entry.\n */\nfunction listHasServer(stdout: string, name: string): boolean {\n return stdout.split('\\n').some((line) => {\n const firstToken = line.trim().split(/\\s+/)[0] ?? '';\n return firstToken.replace(/:$/, '') === name;\n });\n}\n\n/**\n * Shared shell-out client for Claude Code and Codex. The two differ only in\n * binary name, config dir, and the `add` arg shape; every outcome-mapping rule\n * is identical, so they share one implementation.\n */\nfunction createCliClient(config: {\n key: McpAgentKey;\n displayName: string;\n binary: string;\n configDir: string;\n addArgs: string[];\n removeArgs: string[];\n}): McpClientTarget {\n const { key, displayName, binary, configDir, addArgs, removeArgs } = config;\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n async function checkInstalled(): Promise<boolean> {\n const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });\n if (res.status !== 0) return false;\n return listHasServer(res.stdout, MCP_SERVER_NAME);\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n // Two layers: the config dir can exist while the binary isn't on PATH,\n // and vice versa — require both before we try to shell out.\n if (!(await pathExists(join(homedir(), configDir)))) return false;\n const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });\n return res.status === 0;\n },\n isInstalled: checkInstalled,\n async add() {\n const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });\n if (res.status === 0) return result('installed');\n // Idempotent: an \"already exists\" collision is a success, not a failure.\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n if (combined.includes('already exists') || combined.includes('already configured')) {\n return result('already-installed');\n }\n // A non-zero exit doesn't always mean the write failed: Codex persists the\n // server config and THEN starts an OAuth flow that blocks (no browser /\n // callback available here) until our timeout kills it with a non-zero\n // status. Confirm against the actual config before declaring failure —\n // more robust than matching each client's success wording.\n if (await checkInstalled()) return result('installed');\n // Otherwise a real failure — an old client lacking `--transport http` /\n // `--url`, a bad invocation, etc. Reported, never thrown.\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n async remove() {\n const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n // Claude exits 1 and Codex exits 0 when the server is absent; both print\n // \"No MCP server named ...\". Treat either as an idempotent no-op.\n if (combined.includes('no mcp server')) return result('not-installed');\n if (res.status === 0) return result('removed');\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n };\n}\n\nfunction createClaudeCodeClient(): McpClientTarget {\n return createCliClient({\n key: 'claude-code',\n displayName: 'Claude Code',\n binary: 'claude',\n configDir: '.claude',\n // User scope: the management MCP is account-level, not per-repo — project\n // scope would nag teammates via checked-in config.\n addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],\n });\n}\n\nfunction createCodexClient(): McpClientTarget {\n return createCliClient({\n key: 'codex',\n displayName: 'Codex',\n binary: 'codex',\n configDir: '.codex',\n addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],\n });\n}\n\n/**\n * Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with\n * `jsonc-parser`, preserving existing servers, comments, and formatting.\n */\nfunction createCursorClient(): McpClientTarget {\n const key: McpAgentKey = 'cursor';\n const displayName = 'Cursor';\n const cursorDir = () => join(homedir(), '.cursor');\n const configPath = () => join(cursorDir(), 'mcp.json');\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n /** Read + parse the config. `parseError` is true only for genuinely malformed\n * JSON (not comments / trailing commas / empty). */\n async function readConfig(): Promise<{\n content: string;\n parsed: Record<string, unknown> | undefined;\n parseError: boolean;\n exists: boolean;\n }> {\n const path = configPath();\n if (!(await pathExists(path))) {\n return { content: '', parsed: undefined, parseError: false, exists: false };\n }\n const content = await readFile(path, 'utf8');\n const errors: jsonc.ParseError[] = [];\n const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n return { content, parsed, parseError: errors.length > 0, exists: true };\n }\n\n function hasWorkosServer(parsed: Record<string, unknown> | undefined): boolean {\n const servers = parsed?.mcpServers;\n return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in (servers as object));\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n return pathExists(cursorDir());\n },\n async isInstalled() {\n try {\n const { parsed, parseError } = await readConfig();\n if (parseError) return false;\n return hasWorkosServer(parsed);\n } catch {\n return false;\n }\n },\n async add() {\n const path = configPath();\n try {\n const { content, parseError, exists } = await readConfig();\n // Never overwrite a file we couldn't parse — the user must fix it first.\n if (exists && parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n await mkdir(cursorDir(), { recursive: true });\n // Converge to our URL — an existing `workos` key with a different URL is\n // overwritten (reported as `installed`).\n const edits = jsonc.modify(\n content,\n ['mcpServers', MCP_SERVER_NAME],\n { url: MCP_SERVER_URL },\n JSONC_MODIFY_OPTIONS,\n );\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('installed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n async remove() {\n const path = configPath();\n try {\n const { content, parsed, parseError, exists } = await readConfig();\n if (!exists) return result('not-installed');\n if (parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n if (!hasWorkosServer(parsed)) return result('not-installed');\n const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('removed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n };\n}\n\n/** The three client targets, in a stable order. */\nexport function createMcpClients(): McpClientTarget[] {\n return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];\n}\n\n/**\n * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,\n * or null when the file/entry is absent, unreadable, or unparseable.\n *\n * Cursor is the only client whose config we read directly (the CLI clients don't\n * expose per-entry URLs), so this powers doctor's URL-drift (\"misconfigured\")\n * check without a second jsonc reader duplicating the config schema. Read-only\n * and never throws — a problem reading just yields null.\n */\nexport async function getCursorConfiguredUrl(): Promise<string | null> {\n try {\n const path = join(homedir(), '.cursor', 'mcp.json');\n if (!(await pathExists(path))) return null;\n const content = await readFile(path, 'utf8');\n const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n const servers = parsed?.mcpServers as Record<string, unknown> | undefined;\n const entry = servers?.[MCP_SERVER_NAME] as { url?: unknown } | undefined;\n return typeof entry?.url === 'string' ? entry.url : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Return only the clients available on this machine, optionally narrowed to\n * `agentFilter` keys. Availability is probed in parallel. An unknown filter key\n * simply matches nothing here — callers validate keys and surface the error.\n */\nexport async function detectMcpClients(agentFilter?: string[]): Promise<McpClientTarget[]> {\n const clients = createMcpClients();\n const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;\n const availability = await Promise.all(filtered.map((c) => c.isAvailable()));\n return filtered.filter((_, i) => availability[i]);\n}\n"]}
@@ -45,6 +45,20 @@ function parseViteConfigPort(configPath) {
45
45
  }
46
46
  return null;
47
47
  }
48
+ /**
49
+ * Try vite.config.{ts,js,mjs} in installDir for a server.port value.
50
+ * NOTE: parseViteConfigPort uses a greedy first-match /port\s*:\s*.../ regex,
51
+ * so an unrelated `port:` (e.g. hmr.port) appearing before server.port wins.
52
+ * Pre-existing behavior for all Vite frameworks — not a regression here.
53
+ */
54
+ function parseViteConfigPortFromDir(installDir) {
55
+ for (const p of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
56
+ const port = parseViteConfigPort(join(installDir, p));
57
+ if (port)
58
+ return port;
59
+ }
60
+ return null;
61
+ }
48
62
  /**
49
63
  * Parse port from Next.js package.json scripts.
50
64
  * Next.js uses: "dev": "next dev -p 4000" or --port 4000
@@ -202,24 +216,16 @@ export function detectPort(integration, installDir) {
202
216
  detectedPort = parseNextConfigPort(installDir);
203
217
  break;
204
218
  case 'tanstack-start':
205
- detectedPort = parseTanStackPort(installDir);
219
+ // Modern @tanstack/react-start is Vite-based (port in vite.config.ts);
220
+ // legacy Vinxi projects use app.config.ts. Try Vite first.
221
+ detectedPort = parseViteConfigPortFromDir(installDir) ?? parseTanStackPort(installDir);
206
222
  break;
207
223
  case 'react':
208
224
  case 'react-router':
209
- case 'vanilla-js': {
225
+ case 'vanilla-js':
210
226
  // Vite-based frameworks
211
- const viteConfigs = [
212
- join(installDir, 'vite.config.ts'),
213
- join(installDir, 'vite.config.js'),
214
- join(installDir, 'vite.config.mjs'),
215
- ];
216
- for (const configPath of viteConfigs) {
217
- detectedPort = parseViteConfigPort(configPath);
218
- if (detectedPort)
219
- break;
220
- }
227
+ detectedPort = parseViteConfigPortFromDir(installDir);
221
228
  break;
222
- }
223
229
  case 'dotnet':
224
230
  detectedPort = parseDotnetPort(installDir);
225
231
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"port-detection.js","sourceRoot":"","sources":["../../src/lib/port-detection.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport { join } from 'node:path';\nimport type { Integration } from './constants.js';\nimport { getConfig } from './settings.js';\n\nconst settings = getConfig();\n\nconst INTEGRATION_TO_SETTINGS_KEY: Record<string, string> = {\n nextjs: 'nextjs',\n react: 'react',\n 'tanstack-start': 'tanstackStart',\n 'react-router': 'reactRouter',\n 'vanilla-js': 'vanillaJs',\n python: 'python',\n ruby: 'ruby',\n php: 'php',\n 'php-laravel': 'phpLaravel',\n go: 'go',\n dotnet: 'dotnet',\n elixir: 'elixir',\n kotlin: 'kotlin',\n};\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_CALLBACK_PATH = '/auth/callback';\n\nfunction getDefaultPort(integration: Integration): number {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.port ?? DEFAULT_PORT;\n}\n\nexport function getCallbackPath(integration: Integration): string {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.callbackPath ?? DEFAULT_CALLBACK_PATH;\n}\n\n/**\n * Parse port from Vite config file.\n * Looks for server.port in vite.config.{ts,js,mjs}\n */\nfunction parseViteConfigPort(configPath: string): number | null {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match: port: 3000 or port: \"3000\" or port: '3000'\n const portMatch = content.match(/port\\s*:\\s*['\"]?(\\d+)['\"]?/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // File doesn't exist or can't be read\n }\n return null;\n}\n\n/**\n * Parse port from Next.js package.json scripts.\n * Next.js uses: \"dev\": \"next dev -p 4000\" or --port 4000\n */\nfunction parseNextConfigPort(installDir: string): number | null {\n try {\n const packageJsonPath = join(installDir, 'package.json');\n const content = fs.readFileSync(packageJsonPath, 'utf-8');\n const packageJson = JSON.parse(content);\n\n const devScript = packageJson.scripts?.dev || '';\n // Match: -p 4000, --port 4000, --port=4000\n const portMatch = devScript.match(/-p\\s+(\\d+)|--port[=\\s]+(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1] || portMatch[2], 10);\n }\n } catch {\n // Can't read package.json\n }\n return null;\n}\n\n/**\n * Parse port from TanStack Start app.config.ts.\n * Uses Vinxi: server: { port: N }\n */\nfunction parseTanStackPort(installDir: string): number | null {\n const configPaths = [join(installDir, 'app.config.ts'), join(installDir, 'app.config.js')];\n\n for (const configPath of configPaths) {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match server config with port\n const portMatch = content.match(/server\\s*:\\s*\\{[^}]*port\\s*:\\s*(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // Config file doesn't exist\n }\n }\n return null;\n}\n\n/**\n * Parse port from .NET Properties/launchSettings.json.\n * VS/Rider scaffold: profiles[*].applicationUrl = \"http://localhost:5000;https://localhost:5001\"\n */\nfunction parseDotnetPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'Properties', 'launchSettings.json');\n const content = fs.readFileSync(configPath, 'utf-8');\n const parsed = JSON.parse(content) as { profiles?: Record<string, { applicationUrl?: string }> };\n for (const profile of Object.values(parsed.profiles ?? {})) {\n const match = profile.applicationUrl?.match(/http:\\/\\/[^:/]+:(\\d+)/);\n if (match) return parseInt(match[1], 10);\n }\n } catch {\n // File doesn't exist or can't parse\n }\n return null;\n}\n\n/**\n * Parse port from Phoenix config/dev.exs or config/runtime.exs.\n * Looks for `port: NNNN` — typically inside `http: [...]` but a bare regex is fine.\n */\nfunction parseElixirPort(installDir: string): number | null {\n for (const relPath of ['config/dev.exs', 'config/runtime.exs']) {\n try {\n const content = fs.readFileSync(join(installDir, relPath), 'utf-8');\n const match = content.match(/port:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Spring Boot application.properties or application.yml.\n * Both the default `src/main/resources/` location and a top-level file are checked.\n */\nfunction parseKotlinPort(installDir: string): number | null {\n const propsPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.properties'),\n join(installDir, 'application.properties'),\n ];\n for (const propsPath of propsPaths) {\n try {\n const content = fs.readFileSync(propsPath, 'utf-8');\n const match = content.match(/^server\\.port\\s*=\\s*(\\d+)/m);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n\n const ymlPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.yml'),\n join(installDir, 'src', 'main', 'resources', 'application.yaml'),\n join(installDir, 'application.yml'),\n join(installDir, 'application.yaml'),\n ];\n for (const ymlPath of ymlPaths) {\n try {\n const content = fs.readFileSync(ymlPath, 'utf-8');\n // `server:\\n port: 8080` — shallow YAML parse via regex\n const match = content.match(/server\\s*:\\s*\\n[^\\S\\n]+port\\s*:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Rails config/puma.rb.\n * Common forms:\n * port ENV.fetch(\"PORT\") { 3000 }\n * port ENV.fetch(\"PORT\", 3000)\n * port 3000\n */\nfunction parseRubyPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'config', 'puma.rb');\n const content = fs.readFileSync(configPath, 'utf-8');\n const blockFetch = content.match(/port\\s+ENV\\.fetch\\([^)]*\\)\\s*\\{\\s*(\\d+)\\s*\\}/);\n if (blockFetch) return parseInt(blockFetch[1], 10);\n const argFetch = content.match(/port\\s+ENV\\.fetch\\([^,]+,\\s*(\\d+)\\)/);\n if (argFetch) return parseInt(argFetch[1], 10);\n const literal = content.match(/^\\s*port\\s+(\\d+)/m);\n if (literal) return parseInt(literal[1], 10);\n } catch {\n // skip\n }\n return null;\n}\n\n/**\n * Detect the dev server port for a framework.\n * Checks config files first, falls back to framework default.\n */\nexport function detectPort(integration: Integration, installDir: string): number {\n let detectedPort: number | null = null;\n\n switch (integration) {\n case 'nextjs':\n detectedPort = parseNextConfigPort(installDir);\n break;\n\n case 'tanstack-start':\n detectedPort = parseTanStackPort(installDir);\n break;\n\n case 'react':\n case 'react-router':\n case 'vanilla-js': {\n // Vite-based frameworks\n const viteConfigs = [\n join(installDir, 'vite.config.ts'),\n join(installDir, 'vite.config.js'),\n join(installDir, 'vite.config.mjs'),\n ];\n for (const configPath of viteConfigs) {\n detectedPort = parseViteConfigPort(configPath);\n if (detectedPort) break;\n }\n break;\n }\n\n case 'dotnet':\n detectedPort = parseDotnetPort(installDir);\n break;\n\n case 'elixir':\n detectedPort = parseElixirPort(installDir);\n break;\n\n case 'kotlin':\n detectedPort = parseKotlinPort(installDir);\n break;\n\n case 'ruby':\n detectedPort = parseRubyPort(installDir);\n break;\n }\n\n return detectedPort ?? getDefaultPort(integration);\n}\n"]}
1
+ {"version":3,"file":"port-detection.js","sourceRoot":"","sources":["../../src/lib/port-detection.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport { join } from 'node:path';\nimport type { Integration } from './constants.js';\nimport { getConfig } from './settings.js';\n\nconst settings = getConfig();\n\nconst INTEGRATION_TO_SETTINGS_KEY: Record<string, string> = {\n nextjs: 'nextjs',\n react: 'react',\n 'tanstack-start': 'tanstackStart',\n 'react-router': 'reactRouter',\n 'vanilla-js': 'vanillaJs',\n python: 'python',\n ruby: 'ruby',\n php: 'php',\n 'php-laravel': 'phpLaravel',\n go: 'go',\n dotnet: 'dotnet',\n elixir: 'elixir',\n kotlin: 'kotlin',\n};\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_CALLBACK_PATH = '/auth/callback';\n\nfunction getDefaultPort(integration: Integration): number {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.port ?? DEFAULT_PORT;\n}\n\nexport function getCallbackPath(integration: Integration): string {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.callbackPath ?? DEFAULT_CALLBACK_PATH;\n}\n\n/**\n * Parse port from Vite config file.\n * Looks for server.port in vite.config.{ts,js,mjs}\n */\nfunction parseViteConfigPort(configPath: string): number | null {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match: port: 3000 or port: \"3000\" or port: '3000'\n const portMatch = content.match(/port\\s*:\\s*['\"]?(\\d+)['\"]?/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // File doesn't exist or can't be read\n }\n return null;\n}\n\n/**\n * Try vite.config.{ts,js,mjs} in installDir for a server.port value.\n * NOTE: parseViteConfigPort uses a greedy first-match /port\\s*:\\s*.../ regex,\n * so an unrelated `port:` (e.g. hmr.port) appearing before server.port wins.\n * Pre-existing behavior for all Vite frameworks — not a regression here.\n */\nfunction parseViteConfigPortFromDir(installDir: string): number | null {\n for (const p of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {\n const port = parseViteConfigPort(join(installDir, p));\n if (port) return port;\n }\n return null;\n}\n\n/**\n * Parse port from Next.js package.json scripts.\n * Next.js uses: \"dev\": \"next dev -p 4000\" or --port 4000\n */\nfunction parseNextConfigPort(installDir: string): number | null {\n try {\n const packageJsonPath = join(installDir, 'package.json');\n const content = fs.readFileSync(packageJsonPath, 'utf-8');\n const packageJson = JSON.parse(content);\n\n const devScript = packageJson.scripts?.dev || '';\n // Match: -p 4000, --port 4000, --port=4000\n const portMatch = devScript.match(/-p\\s+(\\d+)|--port[=\\s]+(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1] || portMatch[2], 10);\n }\n } catch {\n // Can't read package.json\n }\n return null;\n}\n\n/**\n * Parse port from TanStack Start app.config.ts.\n * Uses Vinxi: server: { port: N }\n */\nfunction parseTanStackPort(installDir: string): number | null {\n const configPaths = [join(installDir, 'app.config.ts'), join(installDir, 'app.config.js')];\n\n for (const configPath of configPaths) {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match server config with port\n const portMatch = content.match(/server\\s*:\\s*\\{[^}]*port\\s*:\\s*(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // Config file doesn't exist\n }\n }\n return null;\n}\n\n/**\n * Parse port from .NET Properties/launchSettings.json.\n * VS/Rider scaffold: profiles[*].applicationUrl = \"http://localhost:5000;https://localhost:5001\"\n */\nfunction parseDotnetPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'Properties', 'launchSettings.json');\n const content = fs.readFileSync(configPath, 'utf-8');\n const parsed = JSON.parse(content) as { profiles?: Record<string, { applicationUrl?: string }> };\n for (const profile of Object.values(parsed.profiles ?? {})) {\n const match = profile.applicationUrl?.match(/http:\\/\\/[^:/]+:(\\d+)/);\n if (match) return parseInt(match[1], 10);\n }\n } catch {\n // File doesn't exist or can't parse\n }\n return null;\n}\n\n/**\n * Parse port from Phoenix config/dev.exs or config/runtime.exs.\n * Looks for `port: NNNN` — typically inside `http: [...]` but a bare regex is fine.\n */\nfunction parseElixirPort(installDir: string): number | null {\n for (const relPath of ['config/dev.exs', 'config/runtime.exs']) {\n try {\n const content = fs.readFileSync(join(installDir, relPath), 'utf-8');\n const match = content.match(/port:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Spring Boot application.properties or application.yml.\n * Both the default `src/main/resources/` location and a top-level file are checked.\n */\nfunction parseKotlinPort(installDir: string): number | null {\n const propsPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.properties'),\n join(installDir, 'application.properties'),\n ];\n for (const propsPath of propsPaths) {\n try {\n const content = fs.readFileSync(propsPath, 'utf-8');\n const match = content.match(/^server\\.port\\s*=\\s*(\\d+)/m);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n\n const ymlPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.yml'),\n join(installDir, 'src', 'main', 'resources', 'application.yaml'),\n join(installDir, 'application.yml'),\n join(installDir, 'application.yaml'),\n ];\n for (const ymlPath of ymlPaths) {\n try {\n const content = fs.readFileSync(ymlPath, 'utf-8');\n // `server:\\n port: 8080` — shallow YAML parse via regex\n const match = content.match(/server\\s*:\\s*\\n[^\\S\\n]+port\\s*:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Rails config/puma.rb.\n * Common forms:\n * port ENV.fetch(\"PORT\") { 3000 }\n * port ENV.fetch(\"PORT\", 3000)\n * port 3000\n */\nfunction parseRubyPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'config', 'puma.rb');\n const content = fs.readFileSync(configPath, 'utf-8');\n const blockFetch = content.match(/port\\s+ENV\\.fetch\\([^)]*\\)\\s*\\{\\s*(\\d+)\\s*\\}/);\n if (blockFetch) return parseInt(blockFetch[1], 10);\n const argFetch = content.match(/port\\s+ENV\\.fetch\\([^,]+,\\s*(\\d+)\\)/);\n if (argFetch) return parseInt(argFetch[1], 10);\n const literal = content.match(/^\\s*port\\s+(\\d+)/m);\n if (literal) return parseInt(literal[1], 10);\n } catch {\n // skip\n }\n return null;\n}\n\n/**\n * Detect the dev server port for a framework.\n * Checks config files first, falls back to framework default.\n */\nexport function detectPort(integration: Integration, installDir: string): number {\n let detectedPort: number | null = null;\n\n switch (integration) {\n case 'nextjs':\n detectedPort = parseNextConfigPort(installDir);\n break;\n\n case 'tanstack-start':\n // Modern @tanstack/react-start is Vite-based (port in vite.config.ts);\n // legacy Vinxi projects use app.config.ts. Try Vite first.\n detectedPort = parseViteConfigPortFromDir(installDir) ?? parseTanStackPort(installDir);\n break;\n\n case 'react':\n case 'react-router':\n case 'vanilla-js':\n // Vite-based frameworks\n detectedPort = parseViteConfigPortFromDir(installDir);\n break;\n\n case 'dotnet':\n detectedPort = parseDotnetPort(installDir);\n break;\n\n case 'elixir':\n detectedPort = parseElixirPort(installDir);\n break;\n\n case 'kotlin':\n detectedPort = parseKotlinPort(installDir);\n break;\n\n case 'ruby':\n detectedPort = parseRubyPort(installDir);\n break;\n }\n\n return detectedPort ?? getDefaultPort(integration);\n}\n"]}
@@ -18,6 +18,34 @@ export interface CliPreferences {
18
18
  /** ISO timestamp the first-run notice was shown — written in Phase 2 only. */
19
19
  noticeShownAt?: string;
20
20
  };
21
+ /**
22
+ * Legacy MCP-install onboarding state. The MCP offer folded into `setup` (see
23
+ * commands/setup.ts); these fields are retained for back-compat so a user who
24
+ * declined the old standalone MCP offer is still treated as setup-declined
25
+ * (see isSetupDeclined). Kept in this plain-JSON store alongside telemetry.
26
+ */
27
+ mcp?: {
28
+ /**
29
+ * true => the user explicitly declined an automatic MCP install offer. The
30
+ * decline is absolute: no automatic surface (prompt or banner) asks again;
31
+ * `workos mcp install` stays available manually.
32
+ */
33
+ promptDeclined?: boolean;
34
+ /** ISO timestamp the one-time MCP banner was shown. */
35
+ bannerShownAt?: string;
36
+ };
37
+ /**
38
+ * Consolidated agent-setup onboarding state (skills + MCP). Owned by
39
+ * commands/setup.ts. `declined` is absolute for the automatic surfaces
40
+ * (post-login / post-install); `workos setup` stays available manually.
41
+ * Legacy `mcp.promptDeclined` is treated as an implicit setup-decline on read
42
+ * (see isSetupDeclined) so users who already declined MCP are never re-asked.
43
+ */
44
+ setup?: {
45
+ declined?: boolean;
46
+ /** ISO timestamp the user completed a setup run. */
47
+ completedAt?: string;
48
+ };
21
49
  }
22
50
  /** Effective source of the resolved telemetry-enabled decision. */
23
51
  export type TelemetrySource = 'env' | 'preference' | 'default';
@@ -56,6 +84,20 @@ export declare function isNoticeShown(): boolean;
56
84
  * telemetry-notice.ts swallows it so a read-only FS never blocks a command.
57
85
  */
58
86
  export declare function markNoticeShown(): void;
87
+ /**
88
+ * Whether an automatic setup offer should be suppressed. Absolute once the user
89
+ * declines. Treats the legacy `mcp.promptDeclined` flag as an implicit decline
90
+ * so anyone who declined the old MCP offer is never re-prompted by the new flow.
91
+ */
92
+ export declare function isSetupDeclined(): boolean;
93
+ /** Whether the user has completed a setup run (ever). */
94
+ export declare function isSetupCompleted(): boolean;
95
+ /** Persist an explicit setup decline. Throws on write failure (see savePreferences). */
96
+ export declare function recordSetupDeclined(): void;
97
+ /** Persist a completed setup run, stamping the current time. */
98
+ export declare function recordSetupCompleted(): void;
99
+ /** Clear the setup decline (new + legacy) so automatic offers resume. For `workos setup --reset`. */
100
+ export declare function clearSetupDecline(): void;
59
101
  /**
60
102
  * Tri-state env override for telemetry.
61
103
  *
@@ -97,11 +97,15 @@ export function savePreferences(next) {
97
97
  catch {
98
98
  // No existing file (or unreadable) — start from empty.
99
99
  }
100
- const merged = {
101
- ...current,
102
- ...next,
103
- ...(current.telemetry || next.telemetry ? { telemetry: { ...current.telemetry, ...next.telemetry } } : {}),
104
- };
100
+ // Shallow-merge each known nested group (one level) so writing one field
101
+ // (e.g. setup.completedAt) never clobbers a sibling written at a different
102
+ // time (e.g. setup.declined). A new nested group is one entry in this list.
103
+ const merged = { ...current, ...next };
104
+ for (const key of ['telemetry', 'mcp', 'setup']) {
105
+ if (current[key] || next[key]) {
106
+ Object.assign(merged, { [key]: { ...current[key], ...next[key] } });
107
+ }
108
+ }
105
109
  fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
106
110
  fs.writeFileSync(filePath, JSON.stringify(merged), { encoding: 'utf8', mode: 0o600 });
107
111
  cached = merged;
@@ -127,6 +131,31 @@ export function isNoticeShown() {
127
131
  export function markNoticeShown() {
128
132
  savePreferences({ telemetry: { noticeShownAt: new Date().toISOString() } });
129
133
  }
134
+ /**
135
+ * Whether an automatic setup offer should be suppressed. Absolute once the user
136
+ * declines. Treats the legacy `mcp.promptDeclined` flag as an implicit decline
137
+ * so anyone who declined the old MCP offer is never re-prompted by the new flow.
138
+ */
139
+ export function isSetupDeclined() {
140
+ const prefs = getPreferences();
141
+ return prefs.setup?.declined === true || prefs.mcp?.promptDeclined === true;
142
+ }
143
+ /** Whether the user has completed a setup run (ever). */
144
+ export function isSetupCompleted() {
145
+ return !!getPreferences().setup?.completedAt;
146
+ }
147
+ /** Persist an explicit setup decline. Throws on write failure (see savePreferences). */
148
+ export function recordSetupDeclined() {
149
+ savePreferences({ setup: { declined: true } });
150
+ }
151
+ /** Persist a completed setup run, stamping the current time. */
152
+ export function recordSetupCompleted() {
153
+ savePreferences({ setup: { completedAt: new Date().toISOString() } });
154
+ }
155
+ /** Clear the setup decline (new + legacy) so automatic offers resume. For `workos setup --reset`. */
156
+ export function clearSetupDecline() {
157
+ savePreferences({ setup: { declined: false }, mcp: { promptDeclined: false } });
158
+ }
130
159
  /**
131
160
  * Tri-state env override for telemetry.
132
161
  *