@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
@@ -0,0 +1,798 @@
1
+ import { spawn, execFile } from "child_process";
2
+ import { join } from "path";
3
+ import { writeFile, readFile, mkdir, unlink } from "fs/promises";
4
+ import { createHash } from "crypto";
5
+ import { paths, exists } from "../cache.js";
6
+ import { findModById, updateMod } from "../repositories/mod.js";
7
+ import { validateDbId } from "../validate.js";
8
+ const graphSentinelDone = (dir) => join(dir, ".graph.done");
9
+ const graphSentinelErr = (dir) => join(dir, ".graph.error");
10
+ const graphRunning = (dir) => join(dir, ".graph.running");
11
+ const enrichCursorFile = (graphDir) => join(graphDir, ".graph.enrich-cursor");
12
+ let graphifyCli; // undefined = not checked yet
13
+ // ── Backend detection ─────────────────────────────────────────────────────────
14
+ /** Per-1M-token pricing for cloud backends */
15
+ const BACKEND_PRICING = {
16
+ gemini: { input: 0.50, output: 3.00 },
17
+ openai: { input: 0.40, output: 1.60 },
18
+ claude: { input: 3.00, output: 15.00 },
19
+ deepseek: { input: 0.14, output: 0.28 },
20
+ kimi: { input: 0.74, output: 4.66 },
21
+ ollama: { input: 0, output: 0 },
22
+ custom: { input: 0, output: 0 },
23
+ };
24
+ const BACKEND_DEFAULT_MODELS = {
25
+ gemini: "gemini-3-flash-preview",
26
+ openai: "gpt-4.1-mini",
27
+ claude: "claude-sonnet-4-6",
28
+ deepseek: "deepseek-v4-flash",
29
+ kimi: "kimi-k2.6",
30
+ ollama: "qwen2.5-coder:7b",
31
+ };
32
+ /** Model preference order for Ollama generation model detection */
33
+ const OLLAMA_MODEL_PREFERENCE = [
34
+ "qwen2.5-coder:14b",
35
+ "qwen2.5-coder:7b",
36
+ "qwen3:8b",
37
+ "qwen2.5-coder:3b",
38
+ ];
39
+ /** Map backend name → env var(s) that must hold an API key */
40
+ const BACKEND_KEY_ENVS = {
41
+ gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
42
+ deepseek: ["DEEPSEEK_API_KEY"],
43
+ openai: ["OPENAI_API_KEY"],
44
+ claude: ["ANTHROPIC_API_KEY"],
45
+ kimi: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
46
+ custom: ["GRAPHIFY_CUSTOM_API_KEY"],
47
+ };
48
+ /** Returns true if at least one of the required API key env vars is set for this backend. */
49
+ export function hasBackendKey(backend) {
50
+ const envKeys = BACKEND_KEY_ENVS[backend];
51
+ if (!envKeys)
52
+ return true; // ollama, ast-only, unknown — no key needed
53
+ return envKeys.some(k => !!process.env[k]);
54
+ }
55
+ let cachedOllamaModel;
56
+ let cachedOllamaModelAt = 0;
57
+ const OLLAMA_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
58
+ function cacheOllamaResult(v) { cachedOllamaModel = v; cachedOllamaModelAt = Date.now(); return v; }
59
+ /**
60
+ * Detect the best available Ollama generation model.
61
+ * Returns null if no suitable model found or Ollama unreachable.
62
+ * Results are cached for 5 minutes.
63
+ */
64
+ export async function detectOllamaModel() {
65
+ if (cachedOllamaModel !== undefined && Date.now() - cachedOllamaModelAt < OLLAMA_CACHE_TTL_MS)
66
+ return cachedOllamaModel;
67
+ const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
68
+ try {
69
+ const res = await fetch(`${ollamaUrl}/api/tags`, {
70
+ signal: AbortSignal.timeout(3000),
71
+ });
72
+ if (!res.ok) {
73
+ cacheOllamaResult(null);
74
+ return null;
75
+ }
76
+ const data = await res.json();
77
+ const models = data.models ?? [];
78
+ const modelNames = models.map(m => m.name);
79
+ // Check preference order first
80
+ for (const pref of OLLAMA_MODEL_PREFERENCE) {
81
+ const match = modelNames.find(n => n === pref || n.startsWith(pref));
82
+ if (match) {
83
+ const m = models.find(mod => mod.name === match);
84
+ return cacheOllamaResult({
85
+ model: match,
86
+ contextLength: 32768,
87
+ params: m.details?.parameter_size ?? "unknown",
88
+ });
89
+ }
90
+ }
91
+ // Fallback: any model with "coder" in name
92
+ const coderModel = models.find(m => m.name.toLowerCase().includes("coder"));
93
+ if (coderModel) {
94
+ return cacheOllamaResult({
95
+ model: coderModel.name,
96
+ contextLength: 32768,
97
+ params: coderModel.details?.parameter_size ?? "unknown",
98
+ });
99
+ }
100
+ // Fallback: any model >= 7B (by name heuristic — look for :7b, :8b, :14b etc.)
101
+ const largeModel = models.find(m => {
102
+ const sizeMatch = m.name.match(/:(\d+)b/i);
103
+ return sizeMatch && parseInt(sizeMatch[1]) >= 7;
104
+ });
105
+ if (largeModel) {
106
+ return cacheOllamaResult({
107
+ model: largeModel.name,
108
+ contextLength: 32768,
109
+ params: largeModel.details?.parameter_size ?? "unknown",
110
+ });
111
+ }
112
+ cacheOllamaResult(null);
113
+ return null;
114
+ }
115
+ catch {
116
+ cacheOllamaResult(null);
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * Detect the best available backend for graphify semantic extraction.
122
+ * Checks explicit config, cloud API keys, then Ollama.
123
+ */
124
+ export async function detectBackend() {
125
+ const explicit = process.env.GRAPHIFY_BACKEND;
126
+ const explicitModel = process.env.GRAPHIFY_MODEL;
127
+ // Explicit backend set
128
+ if (explicit && explicit !== "ast-only") {
129
+ if (explicit === "custom") {
130
+ const baseUrl = process.env.GRAPHIFY_CUSTOM_BASE_URL;
131
+ const apiKey = process.env.GRAPHIFY_CUSTOM_API_KEY;
132
+ if (!baseUrl || !apiKey)
133
+ return null;
134
+ return {
135
+ backend: "custom",
136
+ model: explicitModel ?? process.env.GRAPHIFY_CUSTOM_MODEL ?? "gpt-4.1-mini",
137
+ pricing: { input: 0, output: 0 },
138
+ };
139
+ }
140
+ // Verify cloud backends have their API key set
141
+ if (!hasBackendKey(explicit))
142
+ return null;
143
+ return {
144
+ backend: explicit,
145
+ model: explicitModel ?? BACKEND_DEFAULT_MODELS[explicit] ?? "unknown",
146
+ pricing: BACKEND_PRICING[explicit] ?? { input: 0, output: 0 },
147
+ };
148
+ }
149
+ if (explicit === "ast-only")
150
+ return null;
151
+ // Auto-detect from API keys (priority order)
152
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
153
+ return { backend: "gemini", model: explicitModel ?? "gemini-3-flash-preview", pricing: BACKEND_PRICING.gemini };
154
+ }
155
+ if (process.env.DEEPSEEK_API_KEY) {
156
+ return { backend: "deepseek", model: explicitModel ?? "deepseek-v4-flash", pricing: BACKEND_PRICING.deepseek };
157
+ }
158
+ if (process.env.OPENAI_API_KEY) {
159
+ return { backend: "openai", model: explicitModel ?? "gpt-4.1-mini", pricing: BACKEND_PRICING.openai };
160
+ }
161
+ if (process.env.ANTHROPIC_API_KEY) {
162
+ return { backend: "claude", model: explicitModel ?? "claude-sonnet-4-6", pricing: BACKEND_PRICING.claude };
163
+ }
164
+ if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) {
165
+ return { backend: "kimi", model: explicitModel ?? "kimi-k2.6", pricing: BACKEND_PRICING.kimi };
166
+ }
167
+ if (process.env.GRAPHIFY_CUSTOM_BASE_URL && process.env.GRAPHIFY_CUSTOM_API_KEY) {
168
+ return {
169
+ backend: "custom",
170
+ model: explicitModel ?? process.env.GRAPHIFY_CUSTOM_MODEL ?? "gpt-4.1-mini",
171
+ pricing: { input: 0, output: 0 },
172
+ };
173
+ }
174
+ // Ollama with generation model
175
+ const ollamaModel = await detectOllamaModel();
176
+ if (ollamaModel) {
177
+ return { backend: "ollama", model: ollamaModel.model, pricing: BACKEND_PRICING.ollama };
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * Estimate extraction cost for a cloud backend.
183
+ */
184
+ function estimateCost(sourceFileCount, pricing) {
185
+ if (pricing.input === 0 && pricing.output === 0)
186
+ return "$0.00";
187
+ // Rough estimate: ~200 tokens per file input, ~100 tokens per file output
188
+ const inputTokens = sourceFileCount * 200;
189
+ const outputTokens = sourceFileCount * 100;
190
+ const cost = (inputTokens / 1_000_000) * pricing.input + (outputTokens / 1_000_000) * pricing.output;
191
+ return `$${cost.toFixed(3)}`;
192
+ }
193
+ /**
194
+ * Check if `graphify` CLI is available. Returns the command name or throws.
195
+ * Caches result for the process lifetime.
196
+ */
197
+ export async function ensureGraphify() {
198
+ if (graphifyCli !== undefined) {
199
+ if (graphifyCli === null)
200
+ throw new Error("graphify CLI not found. Install with: uv tool install graphifyy");
201
+ return graphifyCli;
202
+ }
203
+ for (const cmd of ["graphify", "python", "python3"]) {
204
+ const args = cmd === "graphify" ? ["--version"] : ["-m", "graphify", "--version"];
205
+ try {
206
+ await new Promise((resolve, reject) => {
207
+ execFile(cmd, args, { timeout: 5000 }, (err) => err ? reject(err) : resolve());
208
+ });
209
+ graphifyCli = cmd === "graphify" ? cmd : `${cmd} -m graphify`;
210
+ return graphifyCli;
211
+ }
212
+ catch { /* try next */ }
213
+ }
214
+ graphifyCli = null;
215
+ throw new Error("graphify CLI not found. Install with: uv tool install graphifyy");
216
+ }
217
+ async function isGraphDone(graphDir, extraGraphJsonPaths) {
218
+ if (await exists(graphSentinelDone(graphDir)))
219
+ return "done";
220
+ if (await exists(graphSentinelErr(graphDir)))
221
+ return "error";
222
+ // Fallback: check if graph.json exists (in case sentinel was missed)
223
+ if (await exists(join(graphDir, "graphify-out", "graph.json")))
224
+ return "done";
225
+ if (extraGraphJsonPaths) {
226
+ for (const p of extraGraphJsonPaths) {
227
+ if (await exists(join(p, "graph.json")))
228
+ return "done";
229
+ }
230
+ }
231
+ if (await exists(graphRunning(graphDir)))
232
+ return "running";
233
+ return "not_started";
234
+ }
235
+ /** Find the first directory that contains graph.json */
236
+ async function findGraphOutput(graphDir, extra) {
237
+ const legacy = join(graphDir, "graphify-out");
238
+ if (await exists(join(legacy, "graph.json")))
239
+ return legacy;
240
+ for (const p of extra) {
241
+ if (await exists(join(p, "graph.json")))
242
+ return p;
243
+ }
244
+ return null;
245
+ }
246
+ /**
247
+ * Build a knowledge graph for a mod. Prefers downloaded source (has docs/comments)
248
+ * over decompiled source (bare Java). Spawns graphify in background.
249
+ * When a semantic backend is available, uses `extract` for richer graphs.
250
+ */
251
+ export async function buildModGraph(dbId, force = false, backendOverride) {
252
+ validateDbId(dbId);
253
+ const mod = await findModById(dbId);
254
+ if (!mod)
255
+ throw new Error(`Mod #${dbId} not found`);
256
+ const graphDir = paths.graphs(mod.modId, mod.version);
257
+ await mkdir(graphDir, { recursive: true });
258
+ // Possible output locations: source dir or decomp dir (graphify writes graphify-out/ inside the target)
259
+ const candidateGraphPaths = [
260
+ mod.sourcePath ? join(mod.sourcePath, "graphify-out") : null,
261
+ mod.decompPath ? join(mod.decompPath, "graphify-out") : null,
262
+ ].filter(Boolean);
263
+ // Check existing state
264
+ if (!force) {
265
+ const state = await isGraphDone(graphDir, candidateGraphPaths);
266
+ if (state === "done") {
267
+ // Find the actual output dir
268
+ const outDir = await findGraphOutput(graphDir, candidateGraphPaths);
269
+ if (outDir)
270
+ await updateMod(dbId, { graphPath: outDir });
271
+ return { status: "done", graphDir: outDir ?? graphDir, sourceType: "cached", message: "Graph already built. Use graph_query to search it." };
272
+ }
273
+ if (state === "running") {
274
+ return { status: "running", graphDir, sourceType: "unknown", message: "Graph build in progress. Poll graph_status to check." };
275
+ }
276
+ }
277
+ // Pick the best source directory — authored source preferred over decompiled
278
+ let sourceDir;
279
+ let sourceType;
280
+ if (mod.sourcePath && await exists(mod.sourcePath)) {
281
+ sourceDir = mod.sourcePath;
282
+ sourceType = "source";
283
+ }
284
+ else if (mod.decompPath && await exists(mod.decompPath)) {
285
+ sourceDir = mod.decompPath;
286
+ sourceType = "decompiled";
287
+ }
288
+ else {
289
+ throw new Error(`No source available for mod #${dbId} (${mod.modId}). ` +
290
+ `Decompile first (mod action=decompile) or download source (platform action=download_source).`);
291
+ }
292
+ const cli = await ensureGraphify();
293
+ // Clean stale sentinels (stored alongside source dir)
294
+ await unlink(graphSentinelDone(graphDir)).catch(() => { });
295
+ await unlink(graphSentinelErr(graphDir)).catch(() => { });
296
+ await unlink(graphRunning(graphDir)).catch(() => { });
297
+ // Write running indicator
298
+ await writeFile(graphRunning(graphDir), String(Date.now()));
299
+ // Detect backend for semantic extraction
300
+ let resolved = null;
301
+ let hint;
302
+ if (backendOverride === "ast-only") {
303
+ // Force AST-only
304
+ }
305
+ else if (backendOverride) {
306
+ if (hasBackendKey(backendOverride)) {
307
+ resolved = {
308
+ backend: backendOverride,
309
+ model: process.env.GRAPHIFY_MODEL ?? BACKEND_DEFAULT_MODELS[backendOverride] ?? "unknown",
310
+ pricing: BACKEND_PRICING[backendOverride] ?? { input: 0, output: 0 },
311
+ };
312
+ }
313
+ // else: key missing — fall through to resolved=null → AST-only
314
+ }
315
+ else {
316
+ resolved = await detectBackend();
317
+ }
318
+ // Build graphify command and env
319
+ const outDir = join(sourceDir, "graphify-out");
320
+ const extraEnv = {};
321
+ let args;
322
+ if (resolved) {
323
+ // Semantic extraction via LLM
324
+ args = ["extract", sourceDir, "--no-viz"];
325
+ if (resolved.backend === "custom") {
326
+ // Route through openai backend with custom base URL
327
+ extraEnv.OPENAI_API_KEY = process.env.GRAPHIFY_CUSTOM_API_KEY;
328
+ extraEnv.OPENAI_BASE_URL = process.env.GRAPHIFY_CUSTOM_BASE_URL;
329
+ args.push("--backend", "openai", "--model", resolved.model);
330
+ }
331
+ else if (resolved.backend === "ollama") {
332
+ const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
333
+ extraEnv.OLLAMA_BASE_URL = `${ollamaUrl}/v1`;
334
+ extraEnv.OLLAMA_MODEL = resolved.model;
335
+ extraEnv.GRAPHIFY_OLLAMA_KEEP_ALIVE = "30m";
336
+ args.push("--backend", "ollama", "--max-concurrency", "1");
337
+ }
338
+ else {
339
+ args.push("--backend", resolved.backend);
340
+ if (process.env.GRAPHIFY_MODEL) {
341
+ args.push("--model", process.env.GRAPHIFY_MODEL);
342
+ }
343
+ // Bridge Kimi/Moonshot API key naming: setup writes KIMI_API_KEY,
344
+ // but the graphify CLI may read MOONSHOT_API_KEY (Moonshot SDK convention).
345
+ if (resolved.backend === "kimi") {
346
+ const kimiKey = process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY;
347
+ if (kimiKey) {
348
+ extraEnv.KIMI_API_KEY = kimiKey;
349
+ extraEnv.MOONSHOT_API_KEY = kimiKey;
350
+ }
351
+ }
352
+ // Cloud backends can handle parallel requests
353
+ args.push("--max-concurrency", "4");
354
+ }
355
+ args.push("--token-budget", "30000");
356
+ }
357
+ else {
358
+ // AST-only extraction (tree-sitter) — no LLM needed
359
+ args = ["update", sourceDir, "--no-cluster"];
360
+ // Check if Ollama is up but has no generation model — give a hint
361
+ const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
362
+ try {
363
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(1000) });
364
+ if (res.ok) {
365
+ hint = "For richer graphs with semantic relationships, pull a coder model: ollama pull qwen2.5-coder:7b (~4.7GB)";
366
+ }
367
+ }
368
+ catch { /* Ollama not reachable — fine */ }
369
+ }
370
+ // Spawn detached — same pattern as Vineflower in java-tools.ts
371
+ const cmdParts = cli.split(" ");
372
+ const proc = spawn(cmdParts[0], [...cmdParts.slice(1), ...args], {
373
+ stdio: "ignore",
374
+ detached: true,
375
+ cwd: graphDir,
376
+ env: { ...process.env, ...extraEnv },
377
+ });
378
+ proc.unref();
379
+ // Background sentinel writer — NOT awaited by caller
380
+ (async () => {
381
+ await new Promise((resolve) => {
382
+ proc.on("close", async (code) => {
383
+ await unlink(graphRunning(graphDir)).catch(() => { });
384
+ const sentinel = code === 0
385
+ ? graphSentinelDone(graphDir)
386
+ : graphSentinelErr(graphDir);
387
+ await writeFile(sentinel, String(code ?? "signal")).catch(() => { });
388
+ if (code === 0) {
389
+ await updateMod(dbId, { graphPath: outDir }).catch(() => { });
390
+ }
391
+ resolve();
392
+ });
393
+ proc.on("error", async () => {
394
+ await unlink(graphRunning(graphDir)).catch(() => { });
395
+ await writeFile(graphSentinelErr(graphDir), "spawn-error").catch(() => { });
396
+ resolve();
397
+ });
398
+ });
399
+ })();
400
+ // Brief wait to confirm spawn
401
+ await new Promise((res) => setTimeout(res, 300));
402
+ const mode = resolved ? `semantic (${resolved.backend}/${resolved.model})` : "AST-only";
403
+ const cost = resolved ? estimateCost(100, resolved.pricing) : undefined;
404
+ return {
405
+ status: "started",
406
+ graphDir: outDir,
407
+ sourceType,
408
+ backend: resolved?.backend ?? "ast-only",
409
+ estimatedCost: cost,
410
+ hint,
411
+ message: `Graphify building ${mode} graph of ${sourceType} source for ${mod.modId}. Poll graph_status to check progress.`,
412
+ };
413
+ }
414
+ export async function graphBuildStatus(dbId) {
415
+ validateDbId(dbId);
416
+ const mod = await findModById(dbId);
417
+ if (!mod)
418
+ throw new Error(`Mod #${dbId} not found`);
419
+ const graphDir = paths.graphs(mod.modId, mod.version);
420
+ const candidateGraphPaths = [
421
+ mod.sourcePath ? join(mod.sourcePath, "graphify-out") : null,
422
+ mod.decompPath ? join(mod.decompPath, "graphify-out") : null,
423
+ ].filter(Boolean);
424
+ const state = await isGraphDone(graphDir, candidateGraphPaths);
425
+ switch (state) {
426
+ case "done": {
427
+ const outDir = await findGraphOutput(graphDir, candidateGraphPaths);
428
+ if (outDir && !mod.graphPath) {
429
+ await updateMod(dbId, { graphPath: outDir });
430
+ }
431
+ return { status: "done", graphDir: outDir ?? graphDir, message: "Graph ready. Use graph_query to search it." };
432
+ }
433
+ case "error":
434
+ return { status: "error", graphDir, message: "Graphify exited with an error. Check .graph.error sentinel." };
435
+ case "running":
436
+ return { status: "running", graphDir, message: "Graph build still running..." };
437
+ default:
438
+ return { status: "not_started", graphDir, message: "No graph build found. Use graph_build to start one." };
439
+ }
440
+ }
441
+ /**
442
+ * Query a mod's knowledge graph. Returns a compact subgraph (~500 tokens)
443
+ * instead of raw source files (~10-50K tokens).
444
+ */
445
+ export async function queryModGraph(dbId, query, budget) {
446
+ validateDbId(dbId);
447
+ const mod = await findModById(dbId);
448
+ if (!mod)
449
+ throw new Error(`Mod #${dbId} not found`);
450
+ const graphPath = mod.graphPath;
451
+ if (!graphPath || !await exists(join(graphPath, "graph.json"))) {
452
+ return {
453
+ answer: `No graph available for ${mod.modId}. Build one first with: mod action=graph_build modId="${mod.modId}"`,
454
+ graphUsed: false,
455
+ hint: "Use mod action=source to browse raw source files instead.",
456
+ };
457
+ }
458
+ const cli = await ensureGraphify();
459
+ const cmdParts = cli.split(" ");
460
+ const args = [
461
+ ...cmdParts.slice(1),
462
+ "query", query,
463
+ "--graph", join(graphPath, "graph.json"),
464
+ "--budget", String(budget ?? 2000),
465
+ ];
466
+ const result = await new Promise((resolve, reject) => {
467
+ execFile(cmdParts[0], args, { timeout: 30000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
468
+ if (err)
469
+ reject(new Error(`graphify query failed: ${stderr || err.message}`));
470
+ else
471
+ resolve(stdout);
472
+ });
473
+ });
474
+ return {
475
+ answer: result.trim(),
476
+ graphUsed: true,
477
+ hint: "For full source of specific files mentioned above, use: mod action=source dbId=<id> path=<file>",
478
+ };
479
+ }
480
+ /**
481
+ * Return the GRAPH_REPORT.md for a mod — contains god nodes, surprising
482
+ * connections, and suggested questions. Much cheaper than reading source.
483
+ */
484
+ export async function getGraphReport(dbId) {
485
+ validateDbId(dbId);
486
+ const mod = await findModById(dbId);
487
+ if (!mod)
488
+ throw new Error(`Mod #${dbId} not found`);
489
+ const graphPath = mod.graphPath;
490
+ if (!graphPath)
491
+ throw new Error(`No graph for ${mod.modId}. Run graph_build first.`);
492
+ const reportPath = join(graphPath, "GRAPH_REPORT.md");
493
+ if (!await exists(reportPath))
494
+ throw new Error("GRAPH_REPORT.md not found in graph output.");
495
+ const content = await readFile(reportPath, "utf8");
496
+ return content.slice(0, 30_000); // cap at 30KB (reports are typically 2-5KB)
497
+ }
498
+ // ── Graph enrichment (Plan 1: Chat-Agent-Driven) ──────────────────────────────
499
+ const ENRICHMENT_SCHEMA = `Return JSON with this exact structure:
500
+ {
501
+ "nodes": [{"id": "ClassName", "type": "class", "src": "path/to/File.java", "loc": "L42", "community": "subsystem_name"}],
502
+ "edges": [{"source": "ClassA", "target": "ClassB", "relation": "implements_pattern", "context": "Singleton pattern via lazy init"}]
503
+ }
504
+
505
+ Valid relation types for semantic edges: implements_pattern, belongs_to_subsystem, delegates_to, adapts, wraps, event_handler_for, config_for, registry_entry, extends_behavior.
506
+ Node types: class, interface, enum, annotation, package, subsystem.
507
+ Focus on design patterns, subsystem groupings, and architectural relationships — NOT call graphs (those are already in the AST layer).`;
508
+ const CHUNK_SIZE = 20; // files per chunk
509
+ /**
510
+ * Return the next un-enriched chunk of source files for the chat agent to process.
511
+ */
512
+ export async function enrichNextChunk(dbId) {
513
+ validateDbId(dbId);
514
+ const mod = await findModById(dbId);
515
+ if (!mod)
516
+ throw new Error(`Mod #${dbId} not found`);
517
+ const graphPath = mod.graphPath;
518
+ if (!graphPath || !await exists(join(graphPath, "graph.json"))) {
519
+ throw new Error(`No graph for ${mod.modId}. Run graph_build first.`);
520
+ }
521
+ // Read existing graph
522
+ const graph = JSON.parse(await readFile(join(graphPath, "graph.json"), "utf8"));
523
+ // Collect source files from nodes
524
+ const sourceFiles = [...new Set(graph.nodes.map(n => n.src).filter(Boolean))];
525
+ const totalChunks = Math.max(1, Math.ceil(sourceFiles.length / CHUNK_SIZE));
526
+ // Read cursor
527
+ let cursor = 0;
528
+ const cursorPath = enrichCursorFile(graphPath);
529
+ if (await exists(cursorPath)) {
530
+ cursor = parseInt(await readFile(cursorPath, "utf8"), 10) || 0;
531
+ }
532
+ if (cursor >= totalChunks) {
533
+ return { chunkIndex: cursor, totalChunks, prompt: "", schema: "", done: true };
534
+ }
535
+ // Get chunk files
536
+ const chunkFiles = sourceFiles.slice(cursor * CHUNK_SIZE, (cursor + 1) * CHUNK_SIZE);
537
+ // Build prompt with AST context for these files
538
+ const nodesInChunk = graph.nodes.filter(n => n.src && chunkFiles.includes(n.src));
539
+ const edgesInChunk = graph.edges.filter(e => nodesInChunk.some(n => n.id === e.source || n.id === e.target));
540
+ const astContext = {
541
+ files: chunkFiles,
542
+ existingNodes: nodesInChunk.map(n => ({ id: n.id, type: n.type, src: n.src })),
543
+ existingEdges: edgesInChunk.map(e => ({ source: e.source, target: e.target, relation: e.relation })),
544
+ };
545
+ const prompt = `Analyze these ${chunkFiles.length} source files from mod "${mod.modId}" and identify semantic relationships.
546
+
547
+ AST context (structural relationships already captured):
548
+ ${JSON.stringify(astContext, null, 2)}
549
+
550
+ Add SEMANTIC relationships on top — design patterns, subsystem groupings, architectural intent.
551
+ Do NOT duplicate existing structural edges (calls, imports, extends, implements).`;
552
+ return {
553
+ chunkIndex: cursor,
554
+ totalChunks,
555
+ prompt,
556
+ schema: ENRICHMENT_SCHEMA,
557
+ done: false,
558
+ };
559
+ }
560
+ /**
561
+ * Merge the chat agent's semantic response back into graph.json.
562
+ */
563
+ export async function submitEnrichment(dbId, chunkIndex, nodes, edges) {
564
+ validateDbId(dbId);
565
+ const mod = await findModById(dbId);
566
+ if (!mod)
567
+ throw new Error(`Mod #${dbId} not found`);
568
+ const graphPath = mod.graphPath;
569
+ if (!graphPath || !await exists(join(graphPath, "graph.json"))) {
570
+ throw new Error(`No graph for ${mod.modId}. Run graph_build first.`);
571
+ }
572
+ // Validate submitted data
573
+ const { validateGraphEntries } = await import("../security.js");
574
+ const validation = validateGraphEntries(nodes, edges);
575
+ if (!validation.valid) {
576
+ throw new Error(`Enrichment validation failed: ${validation.reason}. ${validation.flaggedEntries?.length ? `Flagged: ${validation.flaggedEntries.join(", ")}` : ""}`);
577
+ }
578
+ // Read existing graph
579
+ const graph = JSON.parse(await readFile(join(graphPath, "graph.json"), "utf8"));
580
+ // Merge nodes (deduplicate by id)
581
+ const existingIds = new Set(graph.nodes.map(n => n.id));
582
+ let newNodes = 0;
583
+ for (const node of nodes) {
584
+ if (!existingIds.has(node.id)) {
585
+ graph.nodes.push(node);
586
+ existingIds.add(node.id);
587
+ newNodes++;
588
+ }
589
+ }
590
+ // Append edges
591
+ let newEdges = 0;
592
+ for (const edge of edges) {
593
+ if (!edge.source || !edge.target || !edge.relation)
594
+ continue;
595
+ graph.edges.push(edge);
596
+ newEdges++;
597
+ }
598
+ // Write updated graph
599
+ await writeFile(join(graphPath, "graph.json"), JSON.stringify(graph, null, 2));
600
+ // Advance cursor
601
+ const sourceFiles = [...new Set(graph.nodes.map(n => n.src).filter(Boolean))];
602
+ const totalChunks = Math.max(1, Math.ceil(sourceFiles.length / CHUNK_SIZE));
603
+ const nextChunk = chunkIndex + 1;
604
+ await writeFile(enrichCursorFile(graphPath), String(nextChunk));
605
+ return { merged: { newNodes, newEdges }, nextChunk, totalChunks };
606
+ }
607
+ // ── Graph registry download (Plan 1) ─────────────────────────────────────────
608
+ const GRAPH_REGISTRY_URL = process.env.MODLENS_GRAPH_REGISTRY_URL ??
609
+ "https://raw.githubusercontent.com/Mattabase/modlens-graphs/main/index.json";
610
+ let cachedRegistry = null;
611
+ const REGISTRY_CACHE_TTL = 60 * 60 * 1000; // 1 hour
612
+ async function fetchGraphRegistry() {
613
+ if (cachedRegistry && Date.now() - cachedRegistry.fetchedAt < REGISTRY_CACHE_TTL) {
614
+ return cachedRegistry.data;
615
+ }
616
+ const res = await fetch(GRAPH_REGISTRY_URL, { signal: AbortSignal.timeout(10_000) });
617
+ if (res.status === 404) {
618
+ const empty = { version: 1, graphs: [] };
619
+ cachedRegistry = { data: empty, fetchedAt: Date.now() };
620
+ return empty;
621
+ }
622
+ if (!res.ok)
623
+ throw new Error(`Failed to fetch graph registry: ${res.status}`);
624
+ const data = await res.json();
625
+ const { validateGraphRegistryIndex } = await import("../security.js");
626
+ const indexCheck = validateGraphRegistryIndex(data);
627
+ if (!indexCheck.valid) {
628
+ throw new Error(`Invalid graph registry: ${indexCheck.reason}`);
629
+ }
630
+ cachedRegistry = { data, fetchedAt: Date.now() };
631
+ return data;
632
+ }
633
+ /**
634
+ * Download a pre-built graph from the public registry.
635
+ */
636
+ export async function downloadGraph(reqOrDbId) {
637
+ const req = typeof reqOrDbId === "number"
638
+ ? { targetType: "mod", dbId: reqOrDbId }
639
+ : reqOrDbId;
640
+ const targetType = req.targetType ?? "mod";
641
+ let resolvedTargetId = req.targetId ?? "";
642
+ let resolvedTargetVersion = req.targetVersion ?? "";
643
+ let installDir = "";
644
+ let modDbIdForUpdate = null;
645
+ if (targetType === "mod") {
646
+ if (req.dbId != null) {
647
+ validateDbId(req.dbId);
648
+ const mod = await findModById(req.dbId);
649
+ if (!mod)
650
+ throw new Error(`Mod #${req.dbId} not found`);
651
+ resolvedTargetId = resolvedTargetId || mod.modId;
652
+ resolvedTargetVersion = resolvedTargetVersion || mod.version;
653
+ installDir = mod.sourcePath
654
+ ? join(mod.sourcePath, "graphify-out")
655
+ : mod.decompPath
656
+ ? join(mod.decompPath, "graphify-out")
657
+ : paths.graphs(mod.modId, mod.version);
658
+ modDbIdForUpdate = mod.id;
659
+ }
660
+ else {
661
+ if (!resolvedTargetId || !resolvedTargetVersion) {
662
+ throw new Error("graph_download targetType=mod requires dbId or targetId+targetVersion");
663
+ }
664
+ installDir = join(paths.graphs("mod", "external"), resolvedTargetId, resolvedTargetVersion);
665
+ }
666
+ }
667
+ else {
668
+ if (!resolvedTargetId || !resolvedTargetVersion) {
669
+ throw new Error("graph_download for vanilla/modloader requires targetId and targetVersion");
670
+ }
671
+ installDir = join(paths.graphs(targetType, "external"), resolvedTargetId, resolvedTargetVersion);
672
+ }
673
+ let registry;
674
+ try {
675
+ registry = await fetchGraphRegistry();
676
+ }
677
+ catch (e) {
678
+ return { status: "registry_unavailable", source: e.message };
679
+ }
680
+ // Find matching entry
681
+ const entry = registry.graphs.find((g) => {
682
+ const regType = g.targetType ?? "mod";
683
+ const regId = g.targetId ?? g.modId;
684
+ const regVersion = g.targetVersion ?? g.version;
685
+ return regType === targetType
686
+ && regId === resolvedTargetId
687
+ && regVersion === resolvedTargetVersion;
688
+ });
689
+ if (!entry) {
690
+ return { status: "not_found" };
691
+ }
692
+ // Resolve URL (relative URLs are relative to registry base)
693
+ let graphUrl = entry.graphUrl;
694
+ if (!graphUrl.startsWith("http")) {
695
+ const base = GRAPH_REGISTRY_URL.substring(0, GRAPH_REGISTRY_URL.lastIndexOf("/") + 1);
696
+ graphUrl = base + graphUrl;
697
+ }
698
+ // Validate URL scheme
699
+ if (!graphUrl.startsWith("https://")) {
700
+ throw new Error("Graph URL must use HTTPS");
701
+ }
702
+ // Download
703
+ const res = await fetch(graphUrl, { signal: AbortSignal.timeout(30_000) });
704
+ if (!res.ok)
705
+ throw new Error(`Failed to download graph: ${res.status}`);
706
+ const contentLength = parseInt(res.headers.get("content-length") ?? "0");
707
+ if (contentLength > 100_000_000) {
708
+ throw new Error("Graph file too large (>100MB)");
709
+ }
710
+ const graphData = await res.text();
711
+ // Verify SHA-256
712
+ const hash = createHash("sha256").update(graphData).digest("hex");
713
+ if (hash !== entry.sha256) {
714
+ throw new Error(`SHA-256 mismatch: expected ${entry.sha256}, got ${hash}`);
715
+ }
716
+ // Parse and validate
717
+ const graphJson = JSON.parse(graphData);
718
+ const { validateGraphBundle } = await import("../security.js");
719
+ const validation = validateGraphBundle(graphJson);
720
+ if (!validation.valid) {
721
+ throw new Error(`Graph validation failed: ${validation.reason}`);
722
+ }
723
+ await mkdir(installDir, { recursive: true });
724
+ await writeFile(join(installDir, "graph.json"), graphData);
725
+ if (modDbIdForUpdate != null) {
726
+ await updateMod(modDbIdForUpdate, { graphPath: installDir });
727
+ }
728
+ return {
729
+ status: "downloaded",
730
+ source: "registry",
731
+ nodeCount: entry.nodeCount,
732
+ edgeCount: entry.edgeCount,
733
+ enriched: entry.enriched,
734
+ backend: entry.backend,
735
+ model: entry.model,
736
+ targetType: entry.targetType ?? "mod",
737
+ };
738
+ }
739
+ // ── Export ─────────────────────────────────────────────────────────────────────
740
+ /**
741
+ * Export a mod's local graph as a shareable gzipped JSON bundle with metadata.
742
+ */
743
+ export async function exportGraph(dbId, outputDir) {
744
+ validateDbId(dbId);
745
+ const mod = await findModById(dbId);
746
+ if (!mod)
747
+ throw new Error(`Mod #${dbId} not found`);
748
+ const graphPath = mod.graphPath;
749
+ if (!graphPath || !await exists(join(graphPath, "graph.json"))) {
750
+ throw new Error(`No graph available for ${mod.modId}. Build one first with graph_build.`);
751
+ }
752
+ const raw = await readFile(join(graphPath, "graph.json"), "utf-8");
753
+ const graph = JSON.parse(raw);
754
+ // Build export bundle with metadata
755
+ const bundle = {
756
+ version: 1,
757
+ targetType: "mod",
758
+ targetId: mod.modId,
759
+ targetVersion: mod.version,
760
+ loader: mod.loader,
761
+ mcVersion: mod.mcVersion,
762
+ nodeCount: graph.nodes.length,
763
+ edgeCount: graph.edges.length,
764
+ enriched: graph.nodes.some((n) => n.community != null),
765
+ generatedAt: new Date().toISOString(),
766
+ graph,
767
+ };
768
+ const json = JSON.stringify(bundle);
769
+ const { gzipSync } = await import("zlib");
770
+ const compressed = gzipSync(Buffer.from(json));
771
+ await mkdir(outputDir, { recursive: true });
772
+ const safeId = mod.modId.replace(/[^a-zA-Z0-9._-]/g, "_");
773
+ const safeVer = mod.version.replace(/[^a-zA-Z0-9._-]/g, "_");
774
+ const outPath = join(outputDir, `${safeId}-${safeVer}.graph.json.gz`);
775
+ await writeFile(outPath, compressed);
776
+ const sha256 = createHash("sha256").update(compressed).digest("hex");
777
+ return {
778
+ path: outPath,
779
+ nodeCount: graph.nodes.length,
780
+ edgeCount: graph.edges.length,
781
+ enriched: bundle.enriched,
782
+ sha256,
783
+ sizeBytes: compressed.length,
784
+ };
785
+ }
786
+ /**
787
+ * Submit a locally-built graph for inclusion in the public registry.
788
+ * NOT YET IMPLEMENTED — returns a stub response with groundwork details.
789
+ */
790
+ export async function submitGraph(_submission) {
791
+ return {
792
+ status: "not_implemented",
793
+ message: "Community graph submission is planned but not yet available. "
794
+ + "For now, export your graph with graph_export and share the bundle file manually. "
795
+ + "Future versions will support direct submission to the public registry.",
796
+ };
797
+ }
798
+ //# sourceMappingURL=graphify.js.map