@creeperhost/modlens-mcp 1.6.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 (382) hide show
  1. package/README.md +938 -0
  2. package/dist/access-flags.d.ts +58 -0
  3. package/dist/access-flags.d.ts.map +1 -0
  4. package/dist/access-flags.js +79 -0
  5. package/dist/access-flags.js.map +1 -0
  6. package/dist/access-flags.test.d.ts +2 -0
  7. package/dist/access-flags.test.d.ts.map +1 -0
  8. package/dist/access-flags.test.js +189 -0
  9. package/dist/access-flags.test.js.map +1 -0
  10. package/dist/batch-ingest.d.ts +2 -0
  11. package/dist/batch-ingest.d.ts.map +1 -0
  12. package/dist/batch-ingest.js +54 -0
  13. package/dist/batch-ingest.js.map +1 -0
  14. package/dist/batch-resolve-mixins.d.ts +2 -0
  15. package/dist/batch-resolve-mixins.d.ts.map +1 -0
  16. package/dist/batch-resolve-mixins.js +36 -0
  17. package/dist/batch-resolve-mixins.js.map +1 -0
  18. package/dist/cache.d.ts +16 -0
  19. package/dist/cache.d.ts.map +1 -0
  20. package/dist/cache.js +36 -0
  21. package/dist/cache.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.d.ts.map +1 -0
  24. package/dist/cli.js +1318 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/curseforge.d.ts +53 -0
  27. package/dist/curseforge.d.ts.map +1 -0
  28. package/dist/curseforge.js +104 -0
  29. package/dist/curseforge.js.map +1 -0
  30. package/dist/db-backend.d.ts +17 -0
  31. package/dist/db-backend.d.ts.map +1 -0
  32. package/dist/db-backend.js +40 -0
  33. package/dist/db-backend.js.map +1 -0
  34. package/dist/db.d.ts +11 -0
  35. package/dist/db.d.ts.map +1 -0
  36. package/dist/db.js +55 -0
  37. package/dist/db.js.map +1 -0
  38. package/dist/embed-queue.d.ts +12 -0
  39. package/dist/embed-queue.d.ts.map +1 -0
  40. package/dist/embed-queue.js +103 -0
  41. package/dist/embed-queue.js.map +1 -0
  42. package/dist/embeddings.d.ts +21 -0
  43. package/dist/embeddings.d.ts.map +1 -0
  44. package/dist/embeddings.js +55 -0
  45. package/dist/embeddings.js.map +1 -0
  46. package/dist/env-path.d.ts +19 -0
  47. package/dist/env-path.d.ts.map +1 -0
  48. package/dist/env-path.js +40 -0
  49. package/dist/env-path.js.map +1 -0
  50. package/dist/fetch-utils.d.ts +24 -0
  51. package/dist/fetch-utils.d.ts.map +1 -0
  52. package/dist/fetch-utils.js +73 -0
  53. package/dist/fetch-utils.js.map +1 -0
  54. package/dist/fetch-utils.test.d.ts +2 -0
  55. package/dist/fetch-utils.test.d.ts.map +1 -0
  56. package/dist/fetch-utils.test.js +133 -0
  57. package/dist/fetch-utils.test.js.map +1 -0
  58. package/dist/generated/sqlite/browser.d.ts +55 -0
  59. package/dist/generated/sqlite/browser.d.ts.map +1 -0
  60. package/dist/generated/sqlite/browser.js +18 -0
  61. package/dist/generated/sqlite/browser.js.map +1 -0
  62. package/dist/generated/sqlite/client.d.ts +72 -0
  63. package/dist/generated/sqlite/client.d.ts.map +1 -0
  64. package/dist/generated/sqlite/client.js +33 -0
  65. package/dist/generated/sqlite/client.js.map +1 -0
  66. package/dist/generated/sqlite/commonInputTypes.d.ts +387 -0
  67. package/dist/generated/sqlite/commonInputTypes.d.ts.map +1 -0
  68. package/dist/generated/sqlite/commonInputTypes.js +11 -0
  69. package/dist/generated/sqlite/commonInputTypes.js.map +1 -0
  70. package/dist/generated/sqlite/enums.d.ts +2 -0
  71. package/dist/generated/sqlite/enums.d.ts.map +1 -0
  72. package/dist/generated/sqlite/enums.js +11 -0
  73. package/dist/generated/sqlite/enums.js.map +1 -0
  74. package/dist/generated/sqlite/internal/class.d.ts +226 -0
  75. package/dist/generated/sqlite/internal/class.d.ts.map +1 -0
  76. package/dist/generated/sqlite/internal/class.js +84 -0
  77. package/dist/generated/sqlite/internal/class.js.map +1 -0
  78. package/dist/generated/sqlite/internal/prismaNamespace.d.ts +1344 -0
  79. package/dist/generated/sqlite/internal/prismaNamespace.d.ts.map +1 -0
  80. package/dist/generated/sqlite/internal/prismaNamespace.js +220 -0
  81. package/dist/generated/sqlite/internal/prismaNamespace.js.map +1 -0
  82. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +204 -0
  83. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts.map +1 -0
  84. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js +191 -0
  85. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js.map +1 -0
  86. package/dist/generated/sqlite/models/DocEntry.d.ts +1223 -0
  87. package/dist/generated/sqlite/models/DocEntry.d.ts.map +1 -0
  88. package/dist/generated/sqlite/models/DocEntry.js +2 -0
  89. package/dist/generated/sqlite/models/DocEntry.js.map +1 -0
  90. package/dist/generated/sqlite/models/McSourceFile.d.ts +1289 -0
  91. package/dist/generated/sqlite/models/McSourceFile.d.ts.map +1 -0
  92. package/dist/generated/sqlite/models/McSourceFile.js +2 -0
  93. package/dist/generated/sqlite/models/McSourceFile.js.map +1 -0
  94. package/dist/generated/sqlite/models/McVersion.d.ts +1315 -0
  95. package/dist/generated/sqlite/models/McVersion.d.ts.map +1 -0
  96. package/dist/generated/sqlite/models/McVersion.js +2 -0
  97. package/dist/generated/sqlite/models/McVersion.js.map +1 -0
  98. package/dist/generated/sqlite/models/McVersionDiff.d.ts +1061 -0
  99. package/dist/generated/sqlite/models/McVersionDiff.d.ts.map +1 -0
  100. package/dist/generated/sqlite/models/McVersionDiff.js +2 -0
  101. package/dist/generated/sqlite/models/McVersionDiff.js.map +1 -0
  102. package/dist/generated/sqlite/models/Mod.d.ts +2396 -0
  103. package/dist/generated/sqlite/models/Mod.d.ts.map +1 -0
  104. package/dist/generated/sqlite/models/Mod.js +2 -0
  105. package/dist/generated/sqlite/models/Mod.js.map +1 -0
  106. package/dist/generated/sqlite/models/ModClass.d.ts +1254 -0
  107. package/dist/generated/sqlite/models/ModClass.d.ts.map +1 -0
  108. package/dist/generated/sqlite/models/ModClass.js +2 -0
  109. package/dist/generated/sqlite/models/ModClass.js.map +1 -0
  110. package/dist/generated/sqlite/models/ModSourceFile.d.ts +1283 -0
  111. package/dist/generated/sqlite/models/ModSourceFile.d.ts.map +1 -0
  112. package/dist/generated/sqlite/models/ModSourceFile.js +2 -0
  113. package/dist/generated/sqlite/models/ModSourceFile.js.map +1 -0
  114. package/dist/generated/sqlite/models/ModTag.d.ts +1278 -0
  115. package/dist/generated/sqlite/models/ModTag.d.ts.map +1 -0
  116. package/dist/generated/sqlite/models/ModTag.js +2 -0
  117. package/dist/generated/sqlite/models/ModTag.js.map +1 -0
  118. package/dist/generated/sqlite/models/ModVersionDiff.d.ts +1073 -0
  119. package/dist/generated/sqlite/models/ModVersionDiff.d.ts.map +1 -0
  120. package/dist/generated/sqlite/models/ModVersionDiff.js +2 -0
  121. package/dist/generated/sqlite/models/ModVersionDiff.js.map +1 -0
  122. package/dist/generated/sqlite/models/Primer.d.ts +1319 -0
  123. package/dist/generated/sqlite/models/Primer.d.ts.map +1 -0
  124. package/dist/generated/sqlite/models/Primer.js +2 -0
  125. package/dist/generated/sqlite/models/Primer.js.map +1 -0
  126. package/dist/generated/sqlite/models.d.ts +12 -0
  127. package/dist/generated/sqlite/models.d.ts.map +1 -0
  128. package/dist/generated/sqlite/models.js +2 -0
  129. package/dist/generated/sqlite/models.js.map +1 -0
  130. package/dist/jar.d.ts +7 -0
  131. package/dist/jar.d.ts.map +1 -0
  132. package/dist/jar.js +52 -0
  133. package/dist/jar.js.map +1 -0
  134. package/dist/java-tools.d.ts +46 -0
  135. package/dist/java-tools.d.ts.map +1 -0
  136. package/dist/java-tools.js +263 -0
  137. package/dist/java-tools.js.map +1 -0
  138. package/dist/launcher.d.ts +3 -0
  139. package/dist/launcher.d.ts.map +1 -0
  140. package/dist/launcher.js +105 -0
  141. package/dist/launcher.js.map +1 -0
  142. package/dist/mappings.d.ts +95 -0
  143. package/dist/mappings.d.ts.map +1 -0
  144. package/dist/mappings.js +1094 -0
  145. package/dist/mappings.js.map +1 -0
  146. package/dist/mappings.test.d.ts +2 -0
  147. package/dist/mappings.test.d.ts.map +1 -0
  148. package/dist/mappings.test.js +161 -0
  149. package/dist/mappings.test.js.map +1 -0
  150. package/dist/minecraft.d.ts +21 -0
  151. package/dist/minecraft.d.ts.map +1 -0
  152. package/dist/minecraft.js +70 -0
  153. package/dist/minecraft.js.map +1 -0
  154. package/dist/modpacks-ch.d.ts +224 -0
  155. package/dist/modpacks-ch.d.ts.map +1 -0
  156. package/dist/modpacks-ch.js +146 -0
  157. package/dist/modpacks-ch.js.map +1 -0
  158. package/dist/modrinth.d.ts +67 -0
  159. package/dist/modrinth.d.ts.map +1 -0
  160. package/dist/modrinth.js +88 -0
  161. package/dist/modrinth.js.map +1 -0
  162. package/dist/platform-adapter.d.ts +23 -0
  163. package/dist/platform-adapter.d.ts.map +1 -0
  164. package/dist/platform-adapter.js +2 -0
  165. package/dist/platform-adapter.js.map +1 -0
  166. package/dist/platform.d.ts +47 -0
  167. package/dist/platform.d.ts.map +1 -0
  168. package/dist/platform.js +175 -0
  169. package/dist/platform.js.map +1 -0
  170. package/dist/processor.d.ts +51 -0
  171. package/dist/processor.d.ts.map +1 -0
  172. package/dist/processor.js +655 -0
  173. package/dist/processor.js.map +1 -0
  174. package/dist/processor.test.d.ts +2 -0
  175. package/dist/processor.test.d.ts.map +1 -0
  176. package/dist/processor.test.js +629 -0
  177. package/dist/processor.test.js.map +1 -0
  178. package/dist/repositories/embeddings-sqlite.d.ts +38 -0
  179. package/dist/repositories/embeddings-sqlite.d.ts.map +1 -0
  180. package/dist/repositories/embeddings-sqlite.js +208 -0
  181. package/dist/repositories/embeddings-sqlite.js.map +1 -0
  182. package/dist/repositories/embeddings.d.ts +40 -0
  183. package/dist/repositories/embeddings.d.ts.map +1 -0
  184. package/dist/repositories/embeddings.js +136 -0
  185. package/dist/repositories/embeddings.js.map +1 -0
  186. package/dist/repositories/index.d.ts +2 -0
  187. package/dist/repositories/index.d.ts.map +1 -0
  188. package/dist/repositories/index.js +15 -0
  189. package/dist/repositories/index.js.map +1 -0
  190. package/dist/repositories/mcVersion.d.ts +16 -0
  191. package/dist/repositories/mcVersion.d.ts.map +1 -0
  192. package/dist/repositories/mcVersion.js +52 -0
  193. package/dist/repositories/mcVersion.js.map +1 -0
  194. package/dist/repositories/mod.d.ts +241 -0
  195. package/dist/repositories/mod.d.ts.map +1 -0
  196. package/dist/repositories/mod.js +342 -0
  197. package/dist/repositories/mod.js.map +1 -0
  198. package/dist/repositories/packs.d.ts +44 -0
  199. package/dist/repositories/packs.d.ts.map +1 -0
  200. package/dist/repositories/packs.js +138 -0
  201. package/dist/repositories/packs.js.map +1 -0
  202. package/dist/search-adapter.d.ts +34 -0
  203. package/dist/search-adapter.d.ts.map +1 -0
  204. package/dist/search-adapter.js +195 -0
  205. package/dist/search-adapter.js.map +1 -0
  206. package/dist/search.d.ts +6 -0
  207. package/dist/search.d.ts.map +1 -0
  208. package/dist/search.js +54 -0
  209. package/dist/search.js.map +1 -0
  210. package/dist/search.test.d.ts +2 -0
  211. package/dist/search.test.d.ts.map +1 -0
  212. package/dist/search.test.js +71 -0
  213. package/dist/search.test.js.map +1 -0
  214. package/dist/security.d.ts +98 -0
  215. package/dist/security.d.ts.map +1 -0
  216. package/dist/security.js +446 -0
  217. package/dist/security.js.map +1 -0
  218. package/dist/security.test.d.ts +2 -0
  219. package/dist/security.test.d.ts.map +1 -0
  220. package/dist/security.test.js +687 -0
  221. package/dist/security.test.js.map +1 -0
  222. package/dist/server.d.ts +2 -0
  223. package/dist/server.d.ts.map +1 -0
  224. package/dist/server.js +1465 -0
  225. package/dist/server.js.map +1 -0
  226. package/dist/setup.d.ts +2 -0
  227. package/dist/setup.d.ts.map +1 -0
  228. package/dist/setup.js +1113 -0
  229. package/dist/setup.js.map +1 -0
  230. package/dist/tools/bytecode.d.ts +129 -0
  231. package/dist/tools/bytecode.d.ts.map +1 -0
  232. package/dist/tools/bytecode.js +679 -0
  233. package/dist/tools/bytecode.js.map +1 -0
  234. package/dist/tools/catalog.d.ts +54 -0
  235. package/dist/tools/catalog.d.ts.map +1 -0
  236. package/dist/tools/catalog.js +249 -0
  237. package/dist/tools/catalog.js.map +1 -0
  238. package/dist/tools/compat-check.d.ts +23 -0
  239. package/dist/tools/compat-check.d.ts.map +1 -0
  240. package/dist/tools/compat-check.js +250 -0
  241. package/dist/tools/compat-check.js.map +1 -0
  242. package/dist/tools/compat-check.test.d.ts +2 -0
  243. package/dist/tools/compat-check.test.d.ts.map +1 -0
  244. package/dist/tools/compat-check.test.js +179 -0
  245. package/dist/tools/compat-check.test.js.map +1 -0
  246. package/dist/tools/diagnostics.d.ts +10 -0
  247. package/dist/tools/diagnostics.d.ts.map +1 -0
  248. package/dist/tools/diagnostics.js +133 -0
  249. package/dist/tools/diagnostics.js.map +1 -0
  250. package/dist/tools/diagnostics.test.d.ts +2 -0
  251. package/dist/tools/diagnostics.test.d.ts.map +1 -0
  252. package/dist/tools/diagnostics.test.js +168 -0
  253. package/dist/tools/diagnostics.test.js.map +1 -0
  254. package/dist/tools/docs.d.ts +19 -0
  255. package/dist/tools/docs.d.ts.map +1 -0
  256. package/dist/tools/docs.js +230 -0
  257. package/dist/tools/docs.js.map +1 -0
  258. package/dist/tools/embed-registry.d.ts +125 -0
  259. package/dist/tools/embed-registry.d.ts.map +1 -0
  260. package/dist/tools/embed-registry.js +560 -0
  261. package/dist/tools/embed-registry.js.map +1 -0
  262. package/dist/tools/gradle.d.ts +16 -0
  263. package/dist/tools/gradle.d.ts.map +1 -0
  264. package/dist/tools/gradle.js +182 -0
  265. package/dist/tools/gradle.js.map +1 -0
  266. package/dist/tools/graphify.d.ts +144 -0
  267. package/dist/tools/graphify.d.ts.map +1 -0
  268. package/dist/tools/graphify.js +798 -0
  269. package/dist/tools/graphify.js.map +1 -0
  270. package/dist/tools/graphify.test.d.ts +2 -0
  271. package/dist/tools/graphify.test.d.ts.map +1 -0
  272. package/dist/tools/graphify.test.js +39 -0
  273. package/dist/tools/graphify.test.js.map +1 -0
  274. package/dist/tools/ingest.d.ts +85 -0
  275. package/dist/tools/ingest.d.ts.map +1 -0
  276. package/dist/tools/ingest.js +477 -0
  277. package/dist/tools/ingest.js.map +1 -0
  278. package/dist/tools/ingest.test.d.ts +2 -0
  279. package/dist/tools/ingest.test.d.ts.map +1 -0
  280. package/dist/tools/ingest.test.js +340 -0
  281. package/dist/tools/ingest.test.js.map +1 -0
  282. package/dist/tools/kubejs.d.ts +37 -0
  283. package/dist/tools/kubejs.d.ts.map +1 -0
  284. package/dist/tools/kubejs.js +245 -0
  285. package/dist/tools/kubejs.js.map +1 -0
  286. package/dist/tools/mappings.d.ts +17 -0
  287. package/dist/tools/mappings.d.ts.map +1 -0
  288. package/dist/tools/mappings.js +85 -0
  289. package/dist/tools/mappings.js.map +1 -0
  290. package/dist/tools/mc-fts.d.ts +101 -0
  291. package/dist/tools/mc-fts.d.ts.map +1 -0
  292. package/dist/tools/mc-fts.js +279 -0
  293. package/dist/tools/mc-fts.js.map +1 -0
  294. package/dist/tools/mcmeta.d.ts +41 -0
  295. package/dist/tools/mcmeta.d.ts.map +1 -0
  296. package/dist/tools/mcmeta.js +365 -0
  297. package/dist/tools/mcmeta.js.map +1 -0
  298. package/dist/tools/mixin-scan.d.ts +113 -0
  299. package/dist/tools/mixin-scan.d.ts.map +1 -0
  300. package/dist/tools/mixin-scan.js +433 -0
  301. package/dist/tools/mixin-scan.js.map +1 -0
  302. package/dist/tools/mod-data.d.ts +83 -0
  303. package/dist/tools/mod-data.d.ts.map +1 -0
  304. package/dist/tools/mod-data.js +630 -0
  305. package/dist/tools/mod-data.js.map +1 -0
  306. package/dist/tools/mod-tags.d.ts +59 -0
  307. package/dist/tools/mod-tags.d.ts.map +1 -0
  308. package/dist/tools/mod-tags.js +279 -0
  309. package/dist/tools/mod-tags.js.map +1 -0
  310. package/dist/tools/mod-tags.test.d.ts +2 -0
  311. package/dist/tools/mod-tags.test.d.ts.map +1 -0
  312. package/dist/tools/mod-tags.test.js +75 -0
  313. package/dist/tools/mod-tags.test.js.map +1 -0
  314. package/dist/tools/modpacks-ch.d.ts +257 -0
  315. package/dist/tools/modpacks-ch.d.ts.map +1 -0
  316. package/dist/tools/modpacks-ch.js +516 -0
  317. package/dist/tools/modpacks-ch.js.map +1 -0
  318. package/dist/tools/packtools.d.ts +113 -0
  319. package/dist/tools/packtools.d.ts.map +1 -0
  320. package/dist/tools/packtools.js +809 -0
  321. package/dist/tools/packtools.js.map +1 -0
  322. package/dist/tools/packtools.test.d.ts +2 -0
  323. package/dist/tools/packtools.test.d.ts.map +1 -0
  324. package/dist/tools/packtools.test.js +99 -0
  325. package/dist/tools/packtools.test.js.map +1 -0
  326. package/dist/tools/platform.d.ts +68 -0
  327. package/dist/tools/platform.d.ts.map +1 -0
  328. package/dist/tools/platform.js +452 -0
  329. package/dist/tools/platform.js.map +1 -0
  330. package/dist/tools/primers.d.ts +36 -0
  331. package/dist/tools/primers.d.ts.map +1 -0
  332. package/dist/tools/primers.js +451 -0
  333. package/dist/tools/primers.js.map +1 -0
  334. package/dist/tools/reports.d.ts +19 -0
  335. package/dist/tools/reports.d.ts.map +1 -0
  336. package/dist/tools/reports.js +573 -0
  337. package/dist/tools/reports.js.map +1 -0
  338. package/dist/tools/source.d.ts +10 -0
  339. package/dist/tools/source.d.ts.map +1 -0
  340. package/dist/tools/source.js +101 -0
  341. package/dist/tools/source.js.map +1 -0
  342. package/dist/tools/vanilla-data.d.ts +127 -0
  343. package/dist/tools/vanilla-data.d.ts.map +1 -0
  344. package/dist/tools/vanilla-data.js +824 -0
  345. package/dist/tools/vanilla-data.js.map +1 -0
  346. package/dist/tools/vanilla.d.ts +171 -0
  347. package/dist/tools/vanilla.d.ts.map +1 -0
  348. package/dist/tools/vanilla.js +612 -0
  349. package/dist/tools/vanilla.js.map +1 -0
  350. package/dist/tools/version-diff.d.ts +113 -0
  351. package/dist/tools/version-diff.d.ts.map +1 -0
  352. package/dist/tools/version-diff.js +519 -0
  353. package/dist/tools/version-diff.js.map +1 -0
  354. package/dist/tools/version-diff.test.d.ts +2 -0
  355. package/dist/tools/version-diff.test.d.ts.map +1 -0
  356. package/dist/tools/version-diff.test.js +102 -0
  357. package/dist/tools/version-diff.test.js.map +1 -0
  358. package/dist/validate.d.ts +19 -0
  359. package/dist/validate.d.ts.map +1 -0
  360. package/dist/validate.js +54 -0
  361. package/dist/validate.js.map +1 -0
  362. package/dist/validate.test.d.ts +2 -0
  363. package/dist/validate.test.d.ts.map +1 -0
  364. package/dist/validate.test.js +73 -0
  365. package/dist/validate.test.js.map +1 -0
  366. package/package.json +62 -0
  367. package/prisma/backends/schema.sqlite.prisma +215 -0
  368. package/prisma/backends/template.db +0 -0
  369. package/prisma/schema.prisma +303 -0
  370. package/scripts/backup.mjs +151 -0
  371. package/scripts/build-template-db.mjs +38 -0
  372. package/scripts/check-atm10.mjs +27 -0
  373. package/scripts/check-sync.mjs +13 -0
  374. package/scripts/create-fts-index.mjs +50 -0
  375. package/scripts/enable-pgvector.mjs +90 -0
  376. package/scripts/enable-sqlite-vec.mjs +158 -0
  377. package/scripts/migrate-backend.mjs +188 -0
  378. package/scripts/migrate-embed-provenance.mjs +80 -0
  379. package/scripts/migrate-metadata-source.mjs +62 -0
  380. package/scripts/migrate-mod-source-files.mjs +67 -0
  381. package/scripts/post-update.mjs +77 -0
  382. package/scripts/verify-atm10.mjs +63 -0
package/dist/setup.js ADDED
@@ -0,0 +1,1113 @@
1
+ /**
2
+ * modlens setup wizard — interactive TUI for first-time setup.
3
+ * Usage: npm run setup
4
+ *
5
+ * Handles:
6
+ * 1. PostgreSQL via Docker Compose
7
+ * 2. Schema migration (prisma db push)
8
+ * 3. Ollama (Docker or existing local install) + model pull
9
+ * 4. pgvector extension + embedding columns
10
+ * 5. Embedding backfill for existing docs/primers
11
+ * 6. MCP client config snippet
12
+ */
13
+ import * as p from "@clack/prompts";
14
+ import { execSync } from "child_process";
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
16
+ import { homedir, platform } from "os";
17
+ import { join, dirname } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { seedDefaultDocumentation } from "./tools/docs.js";
20
+ import { seedDefaultPrimers } from "./tools/primers.js";
21
+ import { backfillDocEmbeddings } from "./tools/docs.js";
22
+ import { backfillPrimerEmbeddings } from "./tools/primers.js";
23
+ import { disconnect } from "./db.js";
24
+ import { PKG_ROOT, MODLENS_HOME, ENV_PATH, IS_INSTALLED, ensureModlensHome } from "./env-path.js";
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const ROOT = PKG_ROOT;
27
+ const DEFAULT_GRAPH_REGISTRY_URL = "https://raw.githubusercontent.com/Mattabase/modlens-graphs/main/index.json";
28
+ const DEFAULT_EMBED_REGISTRY_URL = "https://raw.githubusercontent.com/Mattabase/modlens-embeddings/main/index.json";
29
+ // ── Helpers ───────────────────────────────────────────────────────────────────
30
+ function run(cmd, opts) {
31
+ try {
32
+ const out = execSync(cmd, {
33
+ cwd: opts?.cwd ?? ROOT,
34
+ encoding: "utf8",
35
+ stdio: opts?.silent ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
36
+ });
37
+ return { ok: true, out: out.trim() };
38
+ }
39
+ catch (e) {
40
+ const err = e;
41
+ const msg = [err.stdout, err.stderr].filter(Boolean).join("\n").toString().trim();
42
+ return { ok: false, out: msg };
43
+ }
44
+ }
45
+ function isCancel(val) {
46
+ return typeof val === "symbol";
47
+ }
48
+ function checkCancel(val) {
49
+ if (isCancel(val)) {
50
+ p.cancel("Setup cancelled.");
51
+ process.exit(0);
52
+ }
53
+ }
54
+ /** Read existing .env as key→value map */
55
+ function readEnv() {
56
+ if (!existsSync(ENV_PATH))
57
+ return {};
58
+ const map = {};
59
+ for (const line of readFileSync(ENV_PATH, "utf8").split("\n")) {
60
+ const m = line.match(/^([^#=\s][^=]*)=(.*)/);
61
+ if (m)
62
+ map[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, "");
63
+ }
64
+ return map;
65
+ }
66
+ /** Write key→value map to .env (preserves comments if file exists) */
67
+ function writeEnv(vars) {
68
+ ensureModlensHome();
69
+ const lines = [];
70
+ for (const [k, v] of Object.entries(vars)) {
71
+ lines.push(`${k}=${v}`);
72
+ }
73
+ writeFileSync(ENV_PATH, lines.join("\n") + "\n");
74
+ }
75
+ /** Wait up to `ms` for a URL to respond OK */
76
+ async function waitForHttp(url, ms = 30_000) {
77
+ const deadline = Date.now() + ms;
78
+ while (Date.now() < deadline) {
79
+ try {
80
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
81
+ if (res.ok)
82
+ return true;
83
+ }
84
+ catch { /* not up yet */ }
85
+ await new Promise(r => setTimeout(r, 1000));
86
+ }
87
+ return false;
88
+ }
89
+ /** Pull an Ollama model via the HTTP API with a progress spinner */
90
+ async function pullOllamaModel(ollamaUrl, model) {
91
+ try {
92
+ const res = await fetch(`${ollamaUrl}/api/pull`, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({ name: model }),
96
+ });
97
+ if (!res.ok)
98
+ return false;
99
+ // Drain the response stream (streaming JSON lines — last line has status:"success")
100
+ const reader = res.body?.getReader();
101
+ if (!reader)
102
+ return false;
103
+ const decoder = new TextDecoder();
104
+ let lastStatus = "";
105
+ while (true) {
106
+ const { done, value } = await reader.read();
107
+ if (done)
108
+ break;
109
+ const chunk = decoder.decode(value);
110
+ for (const line of chunk.split("\n").filter(Boolean)) {
111
+ try {
112
+ const obj = JSON.parse(line);
113
+ if (obj.status)
114
+ lastStatus = obj.status;
115
+ }
116
+ catch { /* partial line */ }
117
+ }
118
+ }
119
+ return lastStatus === "success" || lastStatus.includes("already");
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ /** Detect where VS Code writes its user-level mcp.json */
126
+ function vscodeUserMcpPath() {
127
+ const base = platform() === "win32"
128
+ ? join(process.env.APPDATA ?? homedir(), "Code", "User")
129
+ : join(homedir(), ".config", "Code", "User");
130
+ const insiders = platform() === "win32"
131
+ ? join(process.env.APPDATA ?? homedir(), "Code - Insiders", "User")
132
+ : join(homedir(), ".config", "Code - Insiders", "User");
133
+ // Prefer Insiders if it exists
134
+ for (const dir of [insiders, base]) {
135
+ if (existsSync(dir))
136
+ return join(dir, "mcp.json");
137
+ }
138
+ return null;
139
+ }
140
+ /** Detect Claude Desktop config path */
141
+ function claudeDesktopConfigPath() {
142
+ if (platform() === "win32") {
143
+ return join(process.env.APPDATA ?? homedir(), "Claude", "claude_desktop_config.json");
144
+ }
145
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
146
+ }
147
+ /** Write or merge modlens entry into a JSON MCP config file */
148
+ function writeMcpConfig(filePath, serverEntry) {
149
+ try {
150
+ let cfg = {};
151
+ if (existsSync(filePath)) {
152
+ cfg = JSON.parse(readFileSync(filePath, "utf8"));
153
+ }
154
+ const dir = dirname(filePath);
155
+ if (!existsSync(dir))
156
+ mkdirSync(dir, { recursive: true });
157
+ // VS Code uses { servers: { modlens: {...} } }
158
+ // Claude Desktop uses { mcpServers: { modlens: {...} } }
159
+ const isVSCode = filePath.toLowerCase().includes("code");
160
+ const key = isVSCode ? "servers" : "mcpServers";
161
+ const existing = (cfg[key] ?? {});
162
+ existing["modlens"] = serverEntry;
163
+ cfg[key] = existing;
164
+ writeFileSync(filePath, JSON.stringify(cfg, null, 2) + "\n");
165
+ return true;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ // ── Main ──────────────────────────────────────────────────────────────────────
172
+ // Detect whether this is a fresh install or a reconfigure run
173
+ const existingEnv = readEnv();
174
+ const isReconfigure = existsSync(ENV_PATH);
175
+ const ALL_SECTIONS = ["containers", "semantic", "schema", "pgvector", "seed", "backfill", "graphs", "automation", "mcp"];
176
+ const PROFILES = {
177
+ "full": {
178
+ name: "full",
179
+ label: "Full power — Docker Postgres + optional semantic search",
180
+ hint: "recommended",
181
+ backend: "postgres",
182
+ sections: ["containers", "semantic", "schema", "pgvector", "seed", "backfill", "graphs", "automation", "mcp"],
183
+ requiresDocker: true,
184
+ },
185
+ "zero-friction": {
186
+ name: "zero-friction",
187
+ label: "Zero-friction — PGlite (embedded Postgres, no Docker required)",
188
+ hint: "great for solo use or CI",
189
+ backend: "pglite",
190
+ sections: ["schema", "pgvector", "seed", "backfill", "graphs", "automation", "mcp"],
191
+ requiresDocker: false,
192
+ },
193
+ "lightweight": {
194
+ name: "lightweight",
195
+ label: "Lightweight — SQLite (fully embedded, no Docker, no semantic search)",
196
+ hint: "smallest footprint",
197
+ backend: "sqlite",
198
+ sections: ["schema", "seed", "automation", "mcp"],
199
+ requiresDocker: false,
200
+ },
201
+ "standard": {
202
+ name: "standard",
203
+ label: "Standard — Docker Postgres, no semantic search",
204
+ hint: "good default if Ollama is unavailable",
205
+ backend: "postgres",
206
+ sections: ["containers", "schema", "pgvector", "seed", "automation", "mcp"],
207
+ requiresDocker: true,
208
+ },
209
+ "existing": {
210
+ name: "existing",
211
+ label: "Existing Postgres server — connect to a running Postgres instance",
212
+ hint: "bring your own database",
213
+ backend: "postgres",
214
+ sections: ["schema", "pgvector", "seed", "backfill", "graphs", "automation", "mcp"],
215
+ requiresDocker: false,
216
+ },
217
+ "custom": {
218
+ name: "custom",
219
+ label: "Custom — choose individual steps",
220
+ hint: "power users",
221
+ backend: "postgres",
222
+ sections: [],
223
+ requiresDocker: false,
224
+ },
225
+ };
226
+ function detectProfileFromUrl(url) {
227
+ if (!url)
228
+ return null;
229
+ if (url.startsWith("pglite://") || url.startsWith("pglite:"))
230
+ return "zero-friction";
231
+ if (url.startsWith("file:") || url.endsWith(".db"))
232
+ return "lightweight";
233
+ return null; // postgres — could be full/standard/existing/custom
234
+ }
235
+ if (isReconfigure) {
236
+ p.intro(" modlens — reconfigure ");
237
+ const lines = [];
238
+ const currentProfileName = existingEnv.MODLENS_PROFILE ?? null;
239
+ const profileLabel = currentProfileName ? (PROFILES[currentProfileName]?.label ?? currentProfileName) : null;
240
+ if (profileLabel)
241
+ lines.push(` Profile: ${profileLabel}`);
242
+ if (existingEnv.DATABASE_URL)
243
+ lines.push(` DATABASE_URL: ${existingEnv.DATABASE_URL}`);
244
+ if (existingEnv.OLLAMA_URL)
245
+ lines.push(` OLLAMA_URL: ${existingEnv.OLLAMA_URL}`);
246
+ if (existingEnv.OLLAMA_EMBED_MODEL)
247
+ lines.push(` OLLAMA_EMBED_MODEL: ${existingEnv.OLLAMA_EMBED_MODEL}`);
248
+ if (existingEnv.CURSEFORGE_API_KEY)
249
+ lines.push(` CURSEFORGE_API_KEY: (configured)`);
250
+ if (existingEnv.MODRINTH_TOKEN)
251
+ lines.push(` MODRINTH_TOKEN: (configured)`);
252
+ if (lines.length)
253
+ p.log.message("Current config:\n" + lines.join("\n"));
254
+ }
255
+ else {
256
+ p.intro(" modlens setup wizard ");
257
+ }
258
+ let sections;
259
+ let selectedProfile = existingEnv.MODLENS_PROFILE ?? "full";
260
+ if (!isReconfigure) {
261
+ // ── Fresh install: profile picker ─────────────────────────────────────────
262
+ const profileChoice = await p.select({
263
+ message: "Choose a setup profile",
264
+ options: Object.values(PROFILES).map(pr => ({
265
+ value: pr.name,
266
+ label: pr.label,
267
+ hint: pr.hint,
268
+ })),
269
+ });
270
+ checkCancel(profileChoice);
271
+ selectedProfile = profileChoice;
272
+ if (selectedProfile === "custom") {
273
+ sections = new Set(ALL_SECTIONS);
274
+ }
275
+ else if (selectedProfile === "existing") {
276
+ const url = await p.text({
277
+ message: "Enter your Postgres connection URL",
278
+ placeholder: "postgresql://user:password@host:5432/modlens",
279
+ validate: v => ((v ?? "").startsWith("postgresql://") || (v ?? "").startsWith("postgres://"))
280
+ ? undefined : "Must be a postgresql:// URL",
281
+ });
282
+ checkCancel(url);
283
+ writeEnv({ ...readEnv(), DATABASE_URL: url });
284
+ sections = new Set(PROFILES["existing"].sections);
285
+ }
286
+ else if (selectedProfile === "zero-friction") {
287
+ const defaultDir = join(homedir(), ".modlens-data");
288
+ const dataDir = await p.text({
289
+ message: "PGlite data directory (created if it does not exist)",
290
+ initialValue: defaultDir,
291
+ });
292
+ checkCancel(dataDir);
293
+ writeEnv({ ...readEnv(), DATABASE_URL: `pglite://${dataDir}` });
294
+ sections = new Set(PROFILES["zero-friction"].sections);
295
+ }
296
+ else if (selectedProfile === "lightweight") {
297
+ const defaultFile = IS_INSTALLED
298
+ ? join(MODLENS_HOME, "data", "modlens.db")
299
+ : join(homedir(), ".modlens-data", "modlens.db");
300
+ const dbFile = await p.text({
301
+ message: "SQLite database file path (created if it does not exist)",
302
+ initialValue: defaultFile,
303
+ });
304
+ checkCancel(dbFile);
305
+ writeEnv({ ...readEnv(), DATABASE_URL: `file:${dbFile}` });
306
+ sections = new Set(PROFILES["lightweight"].sections);
307
+ }
308
+ else {
309
+ // full / standard — Postgres via Docker
310
+ sections = new Set(PROFILES[selectedProfile].sections);
311
+ }
312
+ writeEnv({ ...readEnv(), MODLENS_PROFILE: selectedProfile });
313
+ }
314
+ else {
315
+ const wizardMode = await p.select({
316
+ message: "What would you like to do?",
317
+ options: [
318
+ { value: "full", label: "Full wizard — re-run all steps", hint: "safe for upgrades, all ops are idempotent" },
319
+ { value: "pick", label: "Pick tasks — choose which steps to run" },
320
+ { value: "switch", label: "Switch profile — change backend or feature set" },
321
+ ],
322
+ });
323
+ checkCancel(wizardMode);
324
+ if (wizardMode === "full") {
325
+ sections = new Set(ALL_SECTIONS);
326
+ }
327
+ else if (wizardMode === "switch") {
328
+ // ── Switch profile ────────────────────────────────────────────────────
329
+ const currentUrl = existingEnv.DATABASE_URL ?? "";
330
+ if (currentUrl) {
331
+ const s = p.spinner();
332
+ s.start("Creating backup before switching profile");
333
+ try {
334
+ const { backup } = await import("../scripts/backup.mjs");
335
+ const result = await backup(currentUrl);
336
+ s.stop(`Backup created: ${result.file ?? result.dir}`);
337
+ }
338
+ catch (e) {
339
+ s.message(`Backup failed: ${e.message}`);
340
+ const cont = await p.confirm({ message: "Continue switching profile without backup?" });
341
+ checkCancel(cont);
342
+ if (!cont)
343
+ process.exit(1);
344
+ }
345
+ }
346
+ const newProfileChoice = await p.select({
347
+ message: "Switch to which profile?",
348
+ options: Object.values(PROFILES).map(pr => ({
349
+ value: pr.name,
350
+ label: pr.label,
351
+ hint: pr.hint,
352
+ })),
353
+ });
354
+ checkCancel(newProfileChoice);
355
+ selectedProfile = newProfileChoice;
356
+ let newUrl = "";
357
+ if (selectedProfile === "existing") {
358
+ const url = await p.text({
359
+ message: "Enter your Postgres connection URL",
360
+ placeholder: "postgresql://user:password@host:5432/modlens",
361
+ validate: v => ((v ?? "").startsWith("postgresql://") || (v ?? "").startsWith("postgres://"))
362
+ ? undefined : "Must be a postgresql:// URL",
363
+ });
364
+ checkCancel(url);
365
+ newUrl = url;
366
+ }
367
+ else if (selectedProfile === "zero-friction") {
368
+ const defaultDir = join(homedir(), ".modlens-data");
369
+ const dataDir = await p.text({
370
+ message: "PGlite data directory",
371
+ initialValue: defaultDir,
372
+ });
373
+ checkCancel(dataDir);
374
+ newUrl = `pglite://${dataDir}`;
375
+ }
376
+ else if (selectedProfile === "lightweight") {
377
+ const defaultFile = join(homedir(), ".modlens-data", "modlens.db");
378
+ const dbFile = await p.text({
379
+ message: "SQLite database file path",
380
+ initialValue: defaultFile,
381
+ });
382
+ checkCancel(dbFile);
383
+ newUrl = `file:${dbFile}`;
384
+ }
385
+ else if (selectedProfile === "full" || selectedProfile === "standard") {
386
+ newUrl = "postgresql://modlens:modlens@localhost:5433/modlens";
387
+ }
388
+ else {
389
+ newUrl = currentUrl;
390
+ }
391
+ if (currentUrl && newUrl && currentUrl !== newUrl) {
392
+ const wantMigrate = await p.confirm({
393
+ message: "Migrate existing data (mods, docs, primers) to new backend? (MC source not migrated — too large)",
394
+ initialValue: true,
395
+ });
396
+ checkCancel(wantMigrate);
397
+ if (wantMigrate) {
398
+ const s = p.spinner();
399
+ s.start("Migrating data");
400
+ try {
401
+ const { migrate } = await import("../scripts/migrate-backend.mjs");
402
+ await migrate(currentUrl, newUrl);
403
+ s.stop("Migration complete");
404
+ }
405
+ catch (e) {
406
+ s.message(`Migration failed: ${e.message}`);
407
+ p.log.warn("Re-run manually: SOURCE_DATABASE_URL=<old> DATABASE_URL=<new> node scripts/migrate-backend.mjs");
408
+ }
409
+ }
410
+ }
411
+ if (currentUrl && currentUrl !== newUrl) {
412
+ const wantCleanup = await p.confirm({
413
+ message: "Delete old backend data? (backup was saved — this frees disk space)",
414
+ initialValue: false,
415
+ });
416
+ checkCancel(wantCleanup);
417
+ if (wantCleanup) {
418
+ const { rmSync } = await import("fs");
419
+ const oldProfileKind = detectProfileFromUrl(currentUrl);
420
+ if (oldProfileKind === "zero-friction") {
421
+ const dataDir = currentUrl.replace(/^pglite:\/\//, "");
422
+ rmSync(dataDir, { recursive: true, force: true });
423
+ p.log.success(`Removed PGlite data directory: ${dataDir}`);
424
+ }
425
+ else if (oldProfileKind === "lightweight") {
426
+ const dbPath = currentUrl.replace(/^file:\/\//, "").replace(/^file:/, "");
427
+ rmSync(dbPath, { force: true });
428
+ p.log.success(`Removed SQLite database: ${dbPath}`);
429
+ }
430
+ else {
431
+ p.log.warn("Cannot auto-clean Postgres data. To remove: docker compose down -v");
432
+ }
433
+ }
434
+ }
435
+ writeEnv({ ...readEnv(), DATABASE_URL: newUrl, MODLENS_PROFILE: selectedProfile });
436
+ sections = new Set(selectedProfile === "custom" ? ALL_SECTIONS : PROFILES[selectedProfile].sections);
437
+ }
438
+ else {
439
+ const hasSemantic = !!existingEnv.OLLAMA_URL;
440
+ const selected = await p.multiselect({
441
+ message: "Select sections to run (space to toggle, enter to confirm)",
442
+ options: [
443
+ { value: "containers", label: "Start / verify Docker containers" },
444
+ { value: "semantic", label: "Semantic search config",
445
+ hint: hasSemantic ? `current: ${existingEnv.OLLAMA_URL}` : "not configured — add Ollama" },
446
+ { value: "schema", label: "Re-apply Prisma schema" },
447
+ { value: "pgvector", label: "pgvector / sqlite-vec extension + embedding columns",
448
+ hint: hasSemantic ? "already enabled" : "required for semantic search" },
449
+ { value: "seed", label: "Re-seed default docs and primers" },
450
+ { value: "backfill", label: "Re-generate embeddings (docs + primers)",
451
+ hint: hasSemantic ? "" : "requires Ollama" },
452
+ { value: "graphs", label: "Graph extraction config (Graphify)" },
453
+ { value: "automation", label: "Automation preferences (auto-embed, auto-graph, auto-cache)" },
454
+ { value: "mcp", label: "Update MCP client config" },
455
+ ],
456
+ initialValues: hasSemantic ? [] : ["semantic", "pgvector", "backfill"],
457
+ required: false,
458
+ });
459
+ checkCancel(selected);
460
+ sections = new Set(selected);
461
+ }
462
+ if (sections.size === 0) {
463
+ p.outro("Nothing selected — no changes made.");
464
+ await disconnect();
465
+ process.exit(0);
466
+ }
467
+ }
468
+ // ── Check Docker (only required for container-using profiles) ────────────────
469
+ const profileNeedsDocker = selectedProfile === "custom"
470
+ ? sections.has("containers")
471
+ : (PROFILES[selectedProfile]?.requiresDocker ?? false);
472
+ if (profileNeedsDocker) {
473
+ const s = p.spinner();
474
+ s.start("Checking Docker");
475
+ const docker = run("docker info", { silent: true });
476
+ if (!docker.ok) {
477
+ s.error("Docker not found or not running");
478
+ p.log.error("Please install Docker Desktop and make sure it is running, then re-run setup.");
479
+ process.exit(1);
480
+ }
481
+ s.stop("Docker is running");
482
+ }
483
+ // ── Semantic search / Ollama config ──────────────────────────────────────────
484
+ // Initialise from existing .env so non-semantic sections can read these
485
+ let wantSemantic = !!existingEnv.OLLAMA_URL;
486
+ let ollamaUrl = existingEnv.OLLAMA_URL ?? "http://localhost:11434";
487
+ let embedModel = existingEnv.OLLAMA_EMBED_MODEL ?? "nomic-embed-text";
488
+ let ollamaMode = "skip"; // "docker" | "local" | "remote" | "skip"
489
+ const DOCKER_OLLAMA_URL = "http://localhost:11435";
490
+ const MODEL_DIMS = {
491
+ "nomic-embed-text": "768",
492
+ "mxbai-embed-large": "1024",
493
+ };
494
+ const MODEL_CHUNK = {
495
+ "nomic-embed-text": "1500",
496
+ "mxbai-embed-large": "500",
497
+ };
498
+ if (sections.has("semantic")) {
499
+ const answer = await p.confirm({
500
+ message: "Enable semantic search? (Ollama + pgvector — optional but recommended)",
501
+ initialValue: wantSemantic,
502
+ });
503
+ checkCancel(answer);
504
+ wantSemantic = answer;
505
+ if (wantSemantic) {
506
+ const mode = await p.select({
507
+ message: "How should Ollama run?",
508
+ options: [
509
+ { value: "docker", label: "Docker (managed by this project's docker-compose — easiest)", hint: "recommended" },
510
+ { value: "local", label: "Local install (Ollama already installed on this machine)" },
511
+ { value: "remote", label: "Remote (Ollama running on another machine)" },
512
+ ],
513
+ });
514
+ checkCancel(mode);
515
+ ollamaMode = mode;
516
+ if (ollamaMode === "docker")
517
+ ollamaUrl = DOCKER_OLLAMA_URL;
518
+ if (ollamaMode === "remote") {
519
+ const existingRemote = existingEnv.OLLAMA_URL?.startsWith("http://localhost") ||
520
+ existingEnv.OLLAMA_URL?.startsWith("http://127") ? "" : (existingEnv.OLLAMA_URL ?? "");
521
+ const url = await p.text({
522
+ message: "Ollama base URL",
523
+ placeholder: "http://192.168.1.x:11434",
524
+ initialValue: existingRemote,
525
+ validate: v => (v ?? "").startsWith("http") ? undefined : "Must start with http:// or https://",
526
+ });
527
+ checkCancel(url);
528
+ ollamaUrl = url;
529
+ }
530
+ const modelChoice = await p.select({
531
+ message: "Embedding model",
532
+ options: [
533
+ { value: "nomic-embed-text", label: "nomic-embed-text (768-dim, fast, good quality)", hint: "recommended" },
534
+ { value: "mxbai-embed-large", label: "mxbai-embed-large (1024-dim, higher quality, slower)" },
535
+ { value: "custom", label: "Custom model name" },
536
+ ],
537
+ });
538
+ checkCancel(modelChoice);
539
+ if (modelChoice === "custom") {
540
+ const custom = await p.text({
541
+ message: "Model name (as shown in `ollama list`)",
542
+ initialValue: existingEnv.OLLAMA_EMBED_MODEL ?? "",
543
+ });
544
+ checkCancel(custom);
545
+ embedModel = custom;
546
+ }
547
+ else {
548
+ embedModel = modelChoice;
549
+ }
550
+ }
551
+ else {
552
+ ollamaMode = "skip";
553
+ }
554
+ }
555
+ else if (wantSemantic) {
556
+ // Semantic not reconfigured — infer ollamaMode from existing URL for container startup
557
+ ollamaMode = existingEnv.OLLAMA_URL === DOCKER_OLLAMA_URL
558
+ ? "docker" // Docker-managed modlens-ollama on port 11435
559
+ : existingEnv.OLLAMA_URL?.startsWith("http://localhost") || existingEnv.OLLAMA_URL?.startsWith("http://127")
560
+ ? "local" // local Ollama install
561
+ : "remote"; // remote Ollama — don't need to start local container
562
+ }
563
+ // ── Start Docker containers ───────────────────────────────────────────────────
564
+ if (sections.has("containers")) {
565
+ const profile = wantSemantic && ollamaMode === "docker" ? "--profile semantic" : "";
566
+ {
567
+ const s = p.spinner();
568
+ s.start("Starting Docker containers");
569
+ const up = run(`docker compose ${profile} up -d`);
570
+ if (!up.ok) {
571
+ s.error("Failed to start containers");
572
+ p.log.error(up.out);
573
+ process.exit(1);
574
+ }
575
+ s.stop("Containers started");
576
+ }
577
+ // Wait for Ollama (only when we just started or reconfigured it)
578
+ if (wantSemantic && (ollamaMode === "docker" || ollamaMode === "local")) {
579
+ const s = p.spinner();
580
+ s.start(`Waiting for Ollama at ${ollamaUrl}`);
581
+ const ready = await waitForHttp(`${ollamaUrl}/api/tags`, ollamaMode === "docker" ? 60_000 : 10_000);
582
+ if (!ready) {
583
+ s.error("Ollama did not respond in time");
584
+ if (ollamaMode === "local")
585
+ p.log.warn("Make sure Ollama is running: `ollama serve`");
586
+ const cont = await p.confirm({ message: "Continue anyway (skip model pull)?" });
587
+ checkCancel(cont);
588
+ if (!cont)
589
+ process.exit(1);
590
+ }
591
+ else {
592
+ s.stop("Ollama is ready");
593
+ const s2 = p.spinner();
594
+ s2.start(`Pulling model ${embedModel} (may take a few minutes on first run)`);
595
+ const ok = await pullOllamaModel(ollamaUrl, embedModel);
596
+ s2.stop(ok
597
+ ? `Model ${embedModel} ready`
598
+ : `Pull may not have completed — check with: docker exec modlens-ollama ollama list`);
599
+ }
600
+ }
601
+ // Wait for PostgreSQL
602
+ {
603
+ const s = p.spinner();
604
+ s.start("Waiting for PostgreSQL");
605
+ let healthy = false;
606
+ const deadline = Date.now() + 30_000;
607
+ while (Date.now() < deadline) {
608
+ const r = run("docker exec modlens-postgres pg_isready -U modlens", { silent: true });
609
+ if (r.ok) {
610
+ healthy = true;
611
+ break;
612
+ }
613
+ await new Promise(r2 => setTimeout(r2, 1000));
614
+ }
615
+ s.stop(healthy ? "PostgreSQL is ready" : "PostgreSQL health check timed out — continuing anyway");
616
+ }
617
+ }
618
+ else if (sections.has("semantic") && wantSemantic && ollamaMode === "docker") {
619
+ // Semantic config changed to Docker Ollama but containers section wasn't selected — start quietly
620
+ const s = p.spinner();
621
+ s.start("Starting containers with semantic profile");
622
+ const up = run("docker compose --profile semantic up -d");
623
+ s.stop(up.ok ? "Containers updated" : "Container update had warnings — run: docker compose --profile semantic up -d");
624
+ const s2 = p.spinner();
625
+ s2.start(`Waiting for Ollama at ${ollamaUrl}`);
626
+ const ready = await waitForHttp(`${ollamaUrl}/api/tags`, 60_000);
627
+ if (ready) {
628
+ s2.stop("Ollama is ready");
629
+ const s3 = p.spinner();
630
+ s3.start(`Pulling model ${embedModel}`);
631
+ const ok = await pullOllamaModel(ollamaUrl, embedModel);
632
+ s3.stop(ok ? `Model ${embedModel} ready` : "Pull may not have completed");
633
+ }
634
+ else {
635
+ s2.stop("Ollama did not respond — continuing");
636
+ }
637
+ }
638
+ // ── Write .env ────────────────────────────────────────────────────────────────
639
+ if (sections.has("semantic") || sections.has("containers") || !isReconfigure) {
640
+ const env = {
641
+ DATABASE_URL: existingEnv.DATABASE_URL ?? "postgresql://modlens:modlens@localhost:5433/modlens",
642
+ ...(wantSemantic ? {
643
+ OLLAMA_URL: ollamaUrl,
644
+ OLLAMA_EMBED_MODEL: embedModel,
645
+ OLLAMA_EMBED_DIM: MODEL_DIMS[embedModel] ?? existingEnv.OLLAMA_EMBED_DIM ?? "768",
646
+ OLLAMA_EMBED_CHUNK: MODEL_CHUNK[embedModel] ?? existingEnv.OLLAMA_EMBED_CHUNK ?? "500",
647
+ } : {}),
648
+ };
649
+ if (existingEnv.CURSEFORGE_API_KEY)
650
+ env.CURSEFORGE_API_KEY = existingEnv.CURSEFORGE_API_KEY;
651
+ if (existingEnv.MODRINTH_TOKEN)
652
+ env.MODRINTH_TOKEN = existingEnv.MODRINTH_TOKEN;
653
+ writeEnv(env);
654
+ p.log.success(".env written");
655
+ }
656
+ // ── Prisma schema ─────────────────────────────────────────────────────────────
657
+ if (sections.has("schema")) {
658
+ const backend = PROFILES[selectedProfile]?.backend ?? "postgres";
659
+ // On Postgres/PGlite the schema contains `embedding vector(N)` columns.
660
+ // The `vector` type requires the pgvector extension to exist BEFORE
661
+ // prisma db push — so enable the extension here, quietly.
662
+ if (backend !== "sqlite") {
663
+ const spre = p.spinner();
664
+ spre.start("Enabling pgvector extension (required before schema push)");
665
+ const rpre = run("node scripts/enable-pgvector.mjs", { silent: true });
666
+ if (rpre.ok) {
667
+ spre.stop("pgvector extension ready");
668
+ }
669
+ else {
670
+ // Non-fatal — the extension may already exist or the script may
671
+ // have partially succeeded; prisma db push will tell us if not.
672
+ spre.stop("pgvector pre-check had warnings (continuing)");
673
+ }
674
+ }
675
+ const s = p.spinner();
676
+ s.start("Applying database schema (prisma db push)");
677
+ const schemaArg = backend === "sqlite"
678
+ ? "--schema prisma/backends/schema.sqlite.prisma"
679
+ : "";
680
+ const r = run(`npx prisma db push ${schemaArg} --skip-generate`, { silent: true });
681
+ if (!r.ok) {
682
+ s.error("prisma db push failed");
683
+ p.log.error(r.out.slice(0, 500));
684
+ process.exit(1);
685
+ }
686
+ s.stop("Database schema applied");
687
+ }
688
+ // ── pgvector / sqlite-vec ─────────────────────────────────────────────────────
689
+ if (sections.has("pgvector")) {
690
+ const backend = PROFILES[selectedProfile]?.backend;
691
+ if (backend === "sqlite") {
692
+ const s = p.spinner();
693
+ s.start("Setting up FTS5 tables and sqlite-vec");
694
+ const r = run("node scripts/enable-sqlite-vec.mjs");
695
+ if (!r.ok) {
696
+ s.error("sqlite-vec setup failed");
697
+ p.log.error(r.out.slice(0, 500));
698
+ p.log.warn("You can retry manually: npm run db:vector:sqlite");
699
+ }
700
+ else {
701
+ s.stop("SQLite FTS5 and sqlite-vec ready");
702
+ }
703
+ }
704
+ else if (!wantSemantic) {
705
+ p.log.warn("pgvector skipped — semantic search is not enabled");
706
+ }
707
+ else {
708
+ const s = p.spinner();
709
+ s.start("Enabling pgvector extension and adding embedding columns");
710
+ const r = run("node scripts/enable-pgvector.mjs");
711
+ if (!r.ok) {
712
+ s.error("pgvector setup failed");
713
+ p.log.error(r.out.slice(0, 500));
714
+ p.log.warn("You can retry manually: npm run db:vector");
715
+ }
716
+ else {
717
+ s.stop("pgvector ready");
718
+ }
719
+ }
720
+ }
721
+ // ── Seed docs + primers ───────────────────────────────────────────────────────
722
+ if (sections.has("seed")) {
723
+ const wantSeed = await p.confirm({
724
+ message: isReconfigure
725
+ ? "Re-seed default docs and primers? (safe — uses upsert, will not delete custom entries)"
726
+ : "Seed default documentation and migration primers?",
727
+ initialValue: true,
728
+ });
729
+ checkCancel(wantSeed);
730
+ if (wantSeed) {
731
+ const s = p.spinner();
732
+ s.start("Seeding docs and primers");
733
+ try {
734
+ await seedDefaultDocumentation();
735
+ await seedDefaultPrimers();
736
+ s.stop("Docs and primers seeded");
737
+ }
738
+ catch {
739
+ s.stop("Seed had warnings (data may already exist — that is fine)");
740
+ }
741
+ }
742
+ }
743
+ // ── Embedding backfill ────────────────────────────────────────────────────────
744
+ if (sections.has("backfill")) {
745
+ if (!wantSemantic) {
746
+ p.log.warn("Embedding backfill skipped — semantic search is not enabled");
747
+ }
748
+ else {
749
+ const wantBackfill = await p.confirm({
750
+ message: isReconfigure
751
+ ? "Re-generate embeddings for docs and primers? (overwrites existing)"
752
+ : "Embed existing docs and primers now for semantic search?",
753
+ initialValue: true,
754
+ });
755
+ checkCancel(wantBackfill);
756
+ if (wantBackfill) {
757
+ const s = p.spinner();
758
+ s.start("Generating embeddings (docs + primers)");
759
+ try {
760
+ await backfillDocEmbeddings();
761
+ await backfillPrimerEmbeddings();
762
+ s.stop("Embeddings generated");
763
+ }
764
+ catch {
765
+ s.stop("Backfill had errors — run `node dist/cli.js backfill-embeddings` to retry");
766
+ }
767
+ }
768
+ }
769
+ }
770
+ // ── Graph extraction (Graphify) ───────────────────────────────────────────────
771
+ if (sections.has("graphs")) {
772
+ const wantGraphs = await p.confirm({
773
+ message: "Set up graph extraction + optional distribution registry URLs?",
774
+ initialValue: true,
775
+ });
776
+ checkCancel(wantGraphs);
777
+ if (wantGraphs) {
778
+ p.log.message("This step configures graph extraction. Graph/embed downloads only work after you publish registry index URLs.");
779
+ // Check if graphify CLI is installed
780
+ const graphifyCheck = run("graphify --version", { silent: true });
781
+ if (!graphifyCheck.ok) {
782
+ const installGraphify = await p.confirm({
783
+ message: "graphify CLI not found. Install it? (requires Python + uv/pip)",
784
+ initialValue: true,
785
+ });
786
+ checkCancel(installGraphify);
787
+ if (installGraphify) {
788
+ const s = p.spinner();
789
+ s.start("Installing graphify CLI (uv tool install graphifyy)");
790
+ const uvResult = run("uv tool install graphifyy", { silent: true });
791
+ if (!uvResult.ok) {
792
+ const pipResult = run("pip install graphifyy", { silent: true });
793
+ if (!pipResult.ok) {
794
+ s.stop("Could not install graphify — install manually: uv tool install graphifyy");
795
+ }
796
+ else {
797
+ s.stop("graphify installed via pip");
798
+ }
799
+ }
800
+ else {
801
+ s.stop("graphify installed via uv");
802
+ }
803
+ }
804
+ }
805
+ else {
806
+ p.log.success(`graphify CLI found: ${graphifyCheck.out.trim()}`);
807
+ }
808
+ // Backend selection
809
+ const backendChoice = await p.select({
810
+ message: "Graph extraction backend:",
811
+ options: [
812
+ { value: "auto", label: "Auto-detect — picks best available (Ollama → cloud → AST-only)" },
813
+ { value: "ollama", label: "Ollama — local, free, private" },
814
+ { value: "gemini", label: "Gemini — Google AI, fast, cheap" },
815
+ { value: "deepseek", label: "DeepSeek — affordable cloud" },
816
+ { value: "openai", label: "OpenAI — GPT models" },
817
+ { value: "claude", label: "Claude — Anthropic" },
818
+ { value: "kimi", label: "Kimi — Moonshot AI" },
819
+ { value: "custom", label: "Custom — any OpenAI-compatible endpoint" },
820
+ { value: "ast-only", label: "AST-only — no LLM, structural analysis only (free)" },
821
+ ],
822
+ initialValue: "auto",
823
+ });
824
+ checkCancel(backendChoice);
825
+ const graphEnv = {};
826
+ const back = backendChoice;
827
+ if (back !== "auto") {
828
+ graphEnv.GRAPHIFY_BACKEND = back;
829
+ }
830
+ // Prompt for API keys based on backend choice
831
+ if (back === "gemini" || back === "auto") {
832
+ const existing = existingEnv.GEMINI_API_KEY;
833
+ if (!existing) {
834
+ const key = await p.text({ message: "Gemini API key (leave blank to skip):", initialValue: "" });
835
+ checkCancel(key);
836
+ if (key)
837
+ graphEnv.GEMINI_API_KEY = key;
838
+ }
839
+ }
840
+ if (back === "deepseek") {
841
+ const key = await p.text({ message: "DeepSeek API key:", initialValue: existingEnv.DEEPSEEK_API_KEY ?? "" });
842
+ checkCancel(key);
843
+ if (key)
844
+ graphEnv.DEEPSEEK_API_KEY = key;
845
+ }
846
+ if (back === "openai") {
847
+ const key = await p.text({ message: "OpenAI API key:", initialValue: existingEnv.OPENAI_API_KEY ?? "" });
848
+ checkCancel(key);
849
+ if (key)
850
+ graphEnv.OPENAI_API_KEY = key;
851
+ }
852
+ if (back === "claude") {
853
+ const key = await p.text({ message: "Anthropic API key:", initialValue: existingEnv.ANTHROPIC_API_KEY ?? "" });
854
+ checkCancel(key);
855
+ if (key)
856
+ graphEnv.ANTHROPIC_API_KEY = key;
857
+ }
858
+ if (back === "kimi") {
859
+ const key = await p.text({ message: "Kimi/Moonshot API key:", initialValue: existingEnv.KIMI_API_KEY ?? "" });
860
+ checkCancel(key);
861
+ if (key)
862
+ graphEnv.KIMI_API_KEY = key;
863
+ }
864
+ if (back === "custom") {
865
+ const baseUrl = await p.text({ message: "Custom endpoint base URL:", initialValue: existingEnv.GRAPHIFY_CUSTOM_BASE_URL ?? "" });
866
+ checkCancel(baseUrl);
867
+ if (baseUrl)
868
+ graphEnv.GRAPHIFY_CUSTOM_BASE_URL = baseUrl;
869
+ const apiKey = await p.text({ message: "Custom endpoint API key:", initialValue: existingEnv.GRAPHIFY_CUSTOM_API_KEY ?? "" });
870
+ checkCancel(apiKey);
871
+ if (apiKey)
872
+ graphEnv.GRAPHIFY_CUSTOM_API_KEY = apiKey;
873
+ const model = await p.text({ message: "Custom endpoint model name:", initialValue: existingEnv.GRAPHIFY_CUSTOM_MODEL ?? "" });
874
+ checkCancel(model);
875
+ if (model)
876
+ graphEnv.GRAPHIFY_CUSTOM_MODEL = model;
877
+ }
878
+ // Warn if a cloud backend was chosen but no API key is configured
879
+ const cloudKeyMap = {
880
+ gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
881
+ deepseek: ["DEEPSEEK_API_KEY"],
882
+ openai: ["OPENAI_API_KEY"],
883
+ claude: ["ANTHROPIC_API_KEY"],
884
+ kimi: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
885
+ custom: ["GRAPHIFY_CUSTOM_API_KEY"],
886
+ };
887
+ const requiredKeys = cloudKeyMap[back];
888
+ if (requiredKeys) {
889
+ const hasKey = requiredKeys.some(k => graphEnv[k] || existingEnv[k]);
890
+ if (!hasKey) {
891
+ p.log.warn(`⚠ No API key set for ${back} — graph extraction will fall back to AST-only until a key is provided.`);
892
+ }
893
+ }
894
+ const currentGraphRegistry = (existingEnv.MODLENS_GRAPH_REGISTRY_URL ?? "").trim() || DEFAULT_GRAPH_REGISTRY_URL;
895
+ const currentEmbedRegistry = (existingEnv.MODLENS_EMBED_REGISTRY_URL ?? "").trim() || DEFAULT_EMBED_REGISTRY_URL;
896
+ const useDefaultRegistryUrls = await p.confirm({
897
+ message: "Use default registry URLs for graph_download/embed_download?",
898
+ initialValue: true,
899
+ });
900
+ checkCancel(useDefaultRegistryUrls);
901
+ if (useDefaultRegistryUrls) {
902
+ graphEnv.MODLENS_GRAPH_REGISTRY_URL = DEFAULT_GRAPH_REGISTRY_URL;
903
+ graphEnv.MODLENS_EMBED_REGISTRY_URL = DEFAULT_EMBED_REGISTRY_URL;
904
+ p.log.message(`Graph registry: ${DEFAULT_GRAPH_REGISTRY_URL}`);
905
+ p.log.message(`Embed registry: ${DEFAULT_EMBED_REGISTRY_URL}`);
906
+ }
907
+ else {
908
+ const graphRegistryUrl = await p.text({
909
+ message: "Graph registry index URL",
910
+ initialValue: currentGraphRegistry,
911
+ placeholder: "https://.../index.json",
912
+ });
913
+ checkCancel(graphRegistryUrl);
914
+ const embedRegistryUrl = await p.text({
915
+ message: "Embedding registry index URL",
916
+ initialValue: currentEmbedRegistry,
917
+ placeholder: "https://.../index.json",
918
+ });
919
+ checkCancel(embedRegistryUrl);
920
+ graphEnv.MODLENS_GRAPH_REGISTRY_URL = graphRegistryUrl.trim() || DEFAULT_GRAPH_REGISTRY_URL;
921
+ graphEnv.MODLENS_EMBED_REGISTRY_URL = embedRegistryUrl.trim() || DEFAULT_EMBED_REGISTRY_URL;
922
+ }
923
+ // Save graph-related env vars
924
+ if (Object.keys(graphEnv).length > 0) {
925
+ writeEnv({ ...readEnv(), ...graphEnv });
926
+ p.log.success("Graph extraction config saved");
927
+ }
928
+ }
929
+ }
930
+ // ── Automation preferences ────────────────────────────────────────────────────
931
+ if (sections.has("automation")) {
932
+ const autoEnv = {};
933
+ // Current defaults: all "1" (on) unless explicitly set to "0"
934
+ const currentAutoEmbed = existingEnv.MODLENS_AUTO_EMBED !== "0";
935
+ const currentAutoGraph = existingEnv.MODLENS_AUTO_GRAPH !== "0";
936
+ const currentAutoEmbedStartup = existingEnv.MODLENS_AUTO_EMBED !== "0";
937
+ const currentAutoCacheDiffs = existingEnv.AUTO_CACHE_MOD_DIFFS === "1";
938
+ // Batch defaults: OFF unless explicitly set to "1"
939
+ const currentBatchAutoEmbed = existingEnv.MODLENS_BATCH_AUTO_EMBED === "1";
940
+ const currentBatchAutoGraph = existingEnv.MODLENS_BATCH_AUTO_GRAPH === "1";
941
+ p.log.message("These settings control what happens automatically after tool actions.\nEach can also be overridden per-call with autoEmbed/autoGraph parameters.");
942
+ const autoChoices = await p.multiselect({
943
+ message: "Select which auto-behaviors to enable (space to toggle)",
944
+ options: [
945
+ {
946
+ value: "autoEmbed",
947
+ label: "Auto-embed on decompile",
948
+ hint: "Queue semantic embeddings when decompile completes (requires Ollama)",
949
+ },
950
+ {
951
+ value: "autoGraph",
952
+ label: "Auto-build graph on decompile / source download",
953
+ hint: "Build knowledge graph when decompile completes or source is downloaded",
954
+ },
955
+ {
956
+ value: "autoEmbedStartup",
957
+ label: "Auto-embed scan on startup",
958
+ hint: "Re-queue unfinished embeddings when the server starts",
959
+ },
960
+ {
961
+ value: "batchAutoEmbed",
962
+ label: "Auto-embed during batch_decompile",
963
+ hint: "Also queue embeddings during batch operations (slower but hands-free)",
964
+ },
965
+ {
966
+ value: "batchAutoGraph",
967
+ label: "Auto-build graph during batch_decompile",
968
+ hint: "Also build graphs during batch operations (slower but hands-free)",
969
+ },
970
+ {
971
+ value: "autoCacheDiffs",
972
+ label: "Auto-cache mod diffs",
973
+ hint: "Cache diff_detailed results to DB automatically",
974
+ },
975
+ ],
976
+ initialValues: [
977
+ ...(currentAutoEmbed ? ["autoEmbed"] : []),
978
+ ...(currentAutoGraph ? ["autoGraph"] : []),
979
+ ...(currentAutoEmbedStartup ? ["autoEmbedStartup"] : []),
980
+ ...(currentBatchAutoEmbed ? ["batchAutoEmbed"] : []),
981
+ ...(currentBatchAutoGraph ? ["batchAutoGraph"] : []),
982
+ ...(currentAutoCacheDiffs ? ["autoCacheDiffs"] : []),
983
+ ],
984
+ required: false,
985
+ });
986
+ checkCancel(autoChoices);
987
+ const choices = new Set(autoChoices);
988
+ // MODLENS_AUTO_EMBED controls both per-decompile and startup scan
989
+ autoEnv.MODLENS_AUTO_EMBED = (choices.has("autoEmbed") || choices.has("autoEmbedStartup")) ? "1" : "0";
990
+ autoEnv.MODLENS_AUTO_GRAPH = choices.has("autoGraph") ? "1" : "0";
991
+ autoEnv.MODLENS_BATCH_AUTO_EMBED = choices.has("batchAutoEmbed") ? "1" : "";
992
+ autoEnv.MODLENS_BATCH_AUTO_GRAPH = choices.has("batchAutoGraph") ? "1" : "";
993
+ autoEnv.AUTO_CACHE_MOD_DIFFS = choices.has("autoCacheDiffs") ? "1" : "";
994
+ writeEnv({ ...readEnv(), ...autoEnv });
995
+ p.log.success("Automation preferences saved");
996
+ }
997
+ // ── Primer security ───────────────────────────────────────────────────────────
998
+ if (sections.has("automation")) {
999
+ const { DEFAULT_PRIMER_HOSTS } = await import("./tools/primers.js");
1000
+ const secEnv = {};
1001
+ const currentAllowAny = existingEnv.MODLENS_PRIMER_ALLOW_ANY_HTTPS === "1";
1002
+ const currentExtraHosts = existingEnv.MODLENS_PRIMER_ALLOWED_HOSTS ?? "";
1003
+ p.log.message(`Primer URL security: when fetchContent is enabled, primers are fetched server-side.\n` +
1004
+ `Default allowed hosts: ${DEFAULT_PRIMER_HOSTS.slice(0, 5).join(", ")}… (${DEFAULT_PRIMER_HOSTS.length} total)\n` +
1005
+ `You can add extra hosts or allow any HTTPS URL.`);
1006
+ const primerMode = await p.select({
1007
+ message: "Primer URL security mode",
1008
+ options: [
1009
+ { value: "default", label: "Allowlist only (recommended)", hint: `${DEFAULT_PRIMER_HOSTS.length} known modding sites` },
1010
+ { value: "extra", label: "Allowlist + extra hosts", hint: "Add custom domains" },
1011
+ { value: "any", label: "Allow any HTTPS URL", hint: "Less secure — trusts all HTTPS sources" },
1012
+ ],
1013
+ initialValue: currentAllowAny ? "any" : currentExtraHosts ? "extra" : "default",
1014
+ });
1015
+ checkCancel(primerMode);
1016
+ if (primerMode === "any") {
1017
+ secEnv.MODLENS_PRIMER_ALLOW_ANY_HTTPS = "1";
1018
+ secEnv.MODLENS_PRIMER_ALLOWED_HOSTS = "";
1019
+ }
1020
+ else if (primerMode === "extra") {
1021
+ secEnv.MODLENS_PRIMER_ALLOW_ANY_HTTPS = "";
1022
+ const extraHosts = await p.text({
1023
+ message: "Extra allowed hostnames (comma-separated)",
1024
+ placeholder: "myblog.example.com, wiki.modloader.dev",
1025
+ initialValue: currentExtraHosts,
1026
+ });
1027
+ checkCancel(extraHosts);
1028
+ secEnv.MODLENS_PRIMER_ALLOWED_HOSTS = extraHosts.trim();
1029
+ }
1030
+ else {
1031
+ secEnv.MODLENS_PRIMER_ALLOW_ANY_HTTPS = "";
1032
+ secEnv.MODLENS_PRIMER_ALLOWED_HOSTS = "";
1033
+ }
1034
+ writeEnv({ ...readEnv(), ...secEnv });
1035
+ p.log.success("Primer security preferences saved");
1036
+ }
1037
+ // ── MCP client config ─────────────────────────────────────────────────────────
1038
+ if (sections.has("mcp")) {
1039
+ // For npx/installed mode: env is loaded from ~/.modlens/.env by the launcher,
1040
+ // so we don't need to embed vars in the MCP config — just the command.
1041
+ // For git-clone mode: keep the existing node + env approach.
1042
+ const pkgVersion = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
1043
+ const envVars = IS_INSTALLED ? {} : {
1044
+ DATABASE_URL: existingEnv.DATABASE_URL ?? "postgresql://modlens:modlens@localhost:5433/modlens",
1045
+ ...(wantSemantic ? {
1046
+ OLLAMA_URL: ollamaUrl,
1047
+ OLLAMA_EMBED_MODEL: embedModel,
1048
+ OLLAMA_EMBED_DIM: MODEL_DIMS[embedModel] ?? existingEnv.OLLAMA_EMBED_DIM ?? "768",
1049
+ OLLAMA_EMBED_CHUNK: MODEL_CHUNK[embedModel] ?? existingEnv.OLLAMA_EMBED_CHUNK ?? "500",
1050
+ } : {}),
1051
+ };
1052
+ const serverPath = join(ROOT, "dist", "server.js").replace(/\\/g, "/");
1053
+ // Pinned version so MCP client never auto-updates mid-project
1054
+ const npxArgs = ["-y", `@creeperhost/modlens-mcp@${pkgVersion}`];
1055
+ const vscodeEntry = IS_INSTALLED
1056
+ ? { type: "stdio", command: "npx", args: npxArgs }
1057
+ : { type: "stdio", command: "node", args: [serverPath], env: envVars };
1058
+ const claudeEntry = IS_INSTALLED
1059
+ ? { command: "npx", args: npxArgs }
1060
+ : { command: "node", args: [serverPath], env: envVars };
1061
+ const mcpClient = await p.select({
1062
+ message: "Configure MCP client?",
1063
+ options: [
1064
+ { value: "vscode", label: "VS Code (user-level mcp.json)" },
1065
+ { value: "claude", label: "Claude Desktop" },
1066
+ { value: "show", label: "Show config snippet — I'll add it myself" },
1067
+ { value: "skip", label: "Skip" },
1068
+ ],
1069
+ });
1070
+ checkCancel(mcpClient);
1071
+ if (mcpClient === "vscode") {
1072
+ const mcpPath = vscodeUserMcpPath();
1073
+ if (!mcpPath) {
1074
+ p.log.warn("Could not find VS Code user directory — showing snippet instead.");
1075
+ p.log.info(JSON.stringify({ servers: { modlens: vscodeEntry } }, null, 2));
1076
+ }
1077
+ else {
1078
+ const ok = writeMcpConfig(mcpPath, vscodeEntry);
1079
+ if (ok)
1080
+ p.log.success(`Written to ${mcpPath}`);
1081
+ else
1082
+ p.log.warn(`Failed to write to ${mcpPath} — add manually.`);
1083
+ }
1084
+ }
1085
+ else if (mcpClient === "claude") {
1086
+ const cfgPath = claudeDesktopConfigPath();
1087
+ if (!cfgPath) {
1088
+ p.log.warn("Could not find Claude Desktop config path — showing snippet instead.");
1089
+ p.log.info(JSON.stringify({ mcpServers: { modlens: claudeEntry } }, null, 2));
1090
+ }
1091
+ else {
1092
+ const ok = writeMcpConfig(cfgPath, claudeEntry);
1093
+ if (ok)
1094
+ p.log.success(`Written to ${cfgPath}`);
1095
+ else
1096
+ p.log.warn(`Failed to write to ${cfgPath} — add manually.`);
1097
+ }
1098
+ }
1099
+ else if (mcpClient === "show") {
1100
+ p.log.message("\n── VS Code (mcp.json) ──");
1101
+ p.log.info(JSON.stringify({ servers: { modlens: vscodeEntry } }, null, 2));
1102
+ p.log.message("\n── Claude Desktop ──");
1103
+ p.log.info(JSON.stringify({ mcpServers: { modlens: claudeEntry } }, null, 2));
1104
+ }
1105
+ }
1106
+ // ── Done ──────────────────────────────────────────────────────────────────────
1107
+ p.outro(isReconfigure
1108
+ ? "Done! Restart your MCP client if you changed the server config."
1109
+ : wantSemantic
1110
+ ? "modlens is ready with semantic search! Restart your MCP client to pick up the server."
1111
+ : "modlens is ready! Restart your MCP client to pick up the server.");
1112
+ await disconnect();
1113
+ //# sourceMappingURL=setup.js.map