@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.
- package/README.md +938 -0
- package/dist/access-flags.d.ts +58 -0
- package/dist/access-flags.d.ts.map +1 -0
- package/dist/access-flags.js +79 -0
- package/dist/access-flags.js.map +1 -0
- package/dist/access-flags.test.d.ts +2 -0
- package/dist/access-flags.test.d.ts.map +1 -0
- package/dist/access-flags.test.js +189 -0
- package/dist/access-flags.test.js.map +1 -0
- package/dist/batch-ingest.d.ts +2 -0
- package/dist/batch-ingest.d.ts.map +1 -0
- package/dist/batch-ingest.js +54 -0
- package/dist/batch-ingest.js.map +1 -0
- package/dist/batch-resolve-mixins.d.ts +2 -0
- package/dist/batch-resolve-mixins.d.ts.map +1 -0
- package/dist/batch-resolve-mixins.js +36 -0
- package/dist/batch-resolve-mixins.js.map +1 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +36 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1318 -0
- package/dist/cli.js.map +1 -0
- package/dist/curseforge.d.ts +53 -0
- package/dist/curseforge.d.ts.map +1 -0
- package/dist/curseforge.js +104 -0
- package/dist/curseforge.js.map +1 -0
- package/dist/db-backend.d.ts +17 -0
- package/dist/db-backend.d.ts.map +1 -0
- package/dist/db-backend.js +40 -0
- package/dist/db-backend.js.map +1 -0
- package/dist/db.d.ts +11 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +55 -0
- package/dist/db.js.map +1 -0
- package/dist/embed-queue.d.ts +12 -0
- package/dist/embed-queue.d.ts.map +1 -0
- package/dist/embed-queue.js +103 -0
- package/dist/embed-queue.js.map +1 -0
- package/dist/embeddings.d.ts +21 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +55 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/env-path.d.ts +19 -0
- package/dist/env-path.d.ts.map +1 -0
- package/dist/env-path.js +40 -0
- package/dist/env-path.js.map +1 -0
- package/dist/fetch-utils.d.ts +24 -0
- package/dist/fetch-utils.d.ts.map +1 -0
- package/dist/fetch-utils.js +73 -0
- package/dist/fetch-utils.js.map +1 -0
- package/dist/fetch-utils.test.d.ts +2 -0
- package/dist/fetch-utils.test.d.ts.map +1 -0
- package/dist/fetch-utils.test.js +133 -0
- package/dist/fetch-utils.test.js.map +1 -0
- package/dist/generated/sqlite/browser.d.ts +55 -0
- package/dist/generated/sqlite/browser.d.ts.map +1 -0
- package/dist/generated/sqlite/browser.js +18 -0
- package/dist/generated/sqlite/browser.js.map +1 -0
- package/dist/generated/sqlite/client.d.ts +72 -0
- package/dist/generated/sqlite/client.d.ts.map +1 -0
- package/dist/generated/sqlite/client.js +33 -0
- package/dist/generated/sqlite/client.js.map +1 -0
- package/dist/generated/sqlite/commonInputTypes.d.ts +387 -0
- package/dist/generated/sqlite/commonInputTypes.d.ts.map +1 -0
- package/dist/generated/sqlite/commonInputTypes.js +11 -0
- package/dist/generated/sqlite/commonInputTypes.js.map +1 -0
- package/dist/generated/sqlite/enums.d.ts +2 -0
- package/dist/generated/sqlite/enums.d.ts.map +1 -0
- package/dist/generated/sqlite/enums.js +11 -0
- package/dist/generated/sqlite/enums.js.map +1 -0
- package/dist/generated/sqlite/internal/class.d.ts +226 -0
- package/dist/generated/sqlite/internal/class.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/class.js +84 -0
- package/dist/generated/sqlite/internal/class.js.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts +1344 -0
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespace.js +220 -0
- package/dist/generated/sqlite/internal/prismaNamespace.js.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +204 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js +191 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js.map +1 -0
- package/dist/generated/sqlite/models/DocEntry.d.ts +1223 -0
- package/dist/generated/sqlite/models/DocEntry.d.ts.map +1 -0
- package/dist/generated/sqlite/models/DocEntry.js +2 -0
- package/dist/generated/sqlite/models/DocEntry.js.map +1 -0
- package/dist/generated/sqlite/models/McSourceFile.d.ts +1289 -0
- package/dist/generated/sqlite/models/McSourceFile.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McSourceFile.js +2 -0
- package/dist/generated/sqlite/models/McSourceFile.js.map +1 -0
- package/dist/generated/sqlite/models/McVersion.d.ts +1315 -0
- package/dist/generated/sqlite/models/McVersion.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McVersion.js +2 -0
- package/dist/generated/sqlite/models/McVersion.js.map +1 -0
- package/dist/generated/sqlite/models/McVersionDiff.d.ts +1061 -0
- package/dist/generated/sqlite/models/McVersionDiff.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McVersionDiff.js +2 -0
- package/dist/generated/sqlite/models/McVersionDiff.js.map +1 -0
- package/dist/generated/sqlite/models/Mod.d.ts +2396 -0
- package/dist/generated/sqlite/models/Mod.d.ts.map +1 -0
- package/dist/generated/sqlite/models/Mod.js +2 -0
- package/dist/generated/sqlite/models/Mod.js.map +1 -0
- package/dist/generated/sqlite/models/ModClass.d.ts +1254 -0
- package/dist/generated/sqlite/models/ModClass.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModClass.js +2 -0
- package/dist/generated/sqlite/models/ModClass.js.map +1 -0
- package/dist/generated/sqlite/models/ModSourceFile.d.ts +1283 -0
- package/dist/generated/sqlite/models/ModSourceFile.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModSourceFile.js +2 -0
- package/dist/generated/sqlite/models/ModSourceFile.js.map +1 -0
- package/dist/generated/sqlite/models/ModTag.d.ts +1278 -0
- package/dist/generated/sqlite/models/ModTag.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModTag.js +2 -0
- package/dist/generated/sqlite/models/ModTag.js.map +1 -0
- package/dist/generated/sqlite/models/ModVersionDiff.d.ts +1073 -0
- package/dist/generated/sqlite/models/ModVersionDiff.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModVersionDiff.js +2 -0
- package/dist/generated/sqlite/models/ModVersionDiff.js.map +1 -0
- package/dist/generated/sqlite/models/Primer.d.ts +1319 -0
- package/dist/generated/sqlite/models/Primer.d.ts.map +1 -0
- package/dist/generated/sqlite/models/Primer.js +2 -0
- package/dist/generated/sqlite/models/Primer.js.map +1 -0
- package/dist/generated/sqlite/models.d.ts +12 -0
- package/dist/generated/sqlite/models.d.ts.map +1 -0
- package/dist/generated/sqlite/models.js +2 -0
- package/dist/generated/sqlite/models.js.map +1 -0
- package/dist/jar.d.ts +7 -0
- package/dist/jar.d.ts.map +1 -0
- package/dist/jar.js +52 -0
- package/dist/jar.js.map +1 -0
- package/dist/java-tools.d.ts +46 -0
- package/dist/java-tools.d.ts.map +1 -0
- package/dist/java-tools.js +263 -0
- package/dist/java-tools.js.map +1 -0
- package/dist/launcher.d.ts +3 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +105 -0
- package/dist/launcher.js.map +1 -0
- package/dist/mappings.d.ts +95 -0
- package/dist/mappings.d.ts.map +1 -0
- package/dist/mappings.js +1094 -0
- package/dist/mappings.js.map +1 -0
- package/dist/mappings.test.d.ts +2 -0
- package/dist/mappings.test.d.ts.map +1 -0
- package/dist/mappings.test.js +161 -0
- package/dist/mappings.test.js.map +1 -0
- package/dist/minecraft.d.ts +21 -0
- package/dist/minecraft.d.ts.map +1 -0
- package/dist/minecraft.js +70 -0
- package/dist/minecraft.js.map +1 -0
- package/dist/modpacks-ch.d.ts +224 -0
- package/dist/modpacks-ch.d.ts.map +1 -0
- package/dist/modpacks-ch.js +146 -0
- package/dist/modpacks-ch.js.map +1 -0
- package/dist/modrinth.d.ts +67 -0
- package/dist/modrinth.d.ts.map +1 -0
- package/dist/modrinth.js +88 -0
- package/dist/modrinth.js.map +1 -0
- package/dist/platform-adapter.d.ts +23 -0
- package/dist/platform-adapter.d.ts.map +1 -0
- package/dist/platform-adapter.js +2 -0
- package/dist/platform-adapter.js.map +1 -0
- package/dist/platform.d.ts +47 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +175 -0
- package/dist/platform.js.map +1 -0
- package/dist/processor.d.ts +51 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +655 -0
- package/dist/processor.js.map +1 -0
- package/dist/processor.test.d.ts +2 -0
- package/dist/processor.test.d.ts.map +1 -0
- package/dist/processor.test.js +629 -0
- package/dist/processor.test.js.map +1 -0
- package/dist/repositories/embeddings-sqlite.d.ts +38 -0
- package/dist/repositories/embeddings-sqlite.d.ts.map +1 -0
- package/dist/repositories/embeddings-sqlite.js +208 -0
- package/dist/repositories/embeddings-sqlite.js.map +1 -0
- package/dist/repositories/embeddings.d.ts +40 -0
- package/dist/repositories/embeddings.d.ts.map +1 -0
- package/dist/repositories/embeddings.js +136 -0
- package/dist/repositories/embeddings.js.map +1 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +15 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/repositories/mcVersion.d.ts +16 -0
- package/dist/repositories/mcVersion.d.ts.map +1 -0
- package/dist/repositories/mcVersion.js +52 -0
- package/dist/repositories/mcVersion.js.map +1 -0
- package/dist/repositories/mod.d.ts +241 -0
- package/dist/repositories/mod.d.ts.map +1 -0
- package/dist/repositories/mod.js +342 -0
- package/dist/repositories/mod.js.map +1 -0
- package/dist/repositories/packs.d.ts +44 -0
- package/dist/repositories/packs.d.ts.map +1 -0
- package/dist/repositories/packs.js +138 -0
- package/dist/repositories/packs.js.map +1 -0
- package/dist/search-adapter.d.ts +34 -0
- package/dist/search-adapter.d.ts.map +1 -0
- package/dist/search-adapter.js +195 -0
- package/dist/search-adapter.js.map +1 -0
- package/dist/search.d.ts +6 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +54 -0
- package/dist/search.js.map +1 -0
- package/dist/search.test.d.ts +2 -0
- package/dist/search.test.d.ts.map +1 -0
- package/dist/search.test.js +71 -0
- package/dist/search.test.js.map +1 -0
- package/dist/security.d.ts +98 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +446 -0
- package/dist/security.js.map +1 -0
- package/dist/security.test.d.ts +2 -0
- package/dist/security.test.d.ts.map +1 -0
- package/dist/security.test.js +687 -0
- package/dist/security.test.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1465 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +1113 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/bytecode.d.ts +129 -0
- package/dist/tools/bytecode.d.ts.map +1 -0
- package/dist/tools/bytecode.js +679 -0
- package/dist/tools/bytecode.js.map +1 -0
- package/dist/tools/catalog.d.ts +54 -0
- package/dist/tools/catalog.d.ts.map +1 -0
- package/dist/tools/catalog.js +249 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/compat-check.d.ts +23 -0
- package/dist/tools/compat-check.d.ts.map +1 -0
- package/dist/tools/compat-check.js +250 -0
- package/dist/tools/compat-check.js.map +1 -0
- package/dist/tools/compat-check.test.d.ts +2 -0
- package/dist/tools/compat-check.test.d.ts.map +1 -0
- package/dist/tools/compat-check.test.js +179 -0
- package/dist/tools/compat-check.test.js.map +1 -0
- package/dist/tools/diagnostics.d.ts +10 -0
- package/dist/tools/diagnostics.d.ts.map +1 -0
- package/dist/tools/diagnostics.js +133 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/diagnostics.test.d.ts +2 -0
- package/dist/tools/diagnostics.test.d.ts.map +1 -0
- package/dist/tools/diagnostics.test.js +168 -0
- package/dist/tools/diagnostics.test.js.map +1 -0
- package/dist/tools/docs.d.ts +19 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/docs.js +230 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/embed-registry.d.ts +125 -0
- package/dist/tools/embed-registry.d.ts.map +1 -0
- package/dist/tools/embed-registry.js +560 -0
- package/dist/tools/embed-registry.js.map +1 -0
- package/dist/tools/gradle.d.ts +16 -0
- package/dist/tools/gradle.d.ts.map +1 -0
- package/dist/tools/gradle.js +182 -0
- package/dist/tools/gradle.js.map +1 -0
- package/dist/tools/graphify.d.ts +144 -0
- package/dist/tools/graphify.d.ts.map +1 -0
- package/dist/tools/graphify.js +798 -0
- package/dist/tools/graphify.js.map +1 -0
- package/dist/tools/graphify.test.d.ts +2 -0
- package/dist/tools/graphify.test.d.ts.map +1 -0
- package/dist/tools/graphify.test.js +39 -0
- package/dist/tools/graphify.test.js.map +1 -0
- package/dist/tools/ingest.d.ts +85 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +477 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/ingest.test.d.ts +2 -0
- package/dist/tools/ingest.test.d.ts.map +1 -0
- package/dist/tools/ingest.test.js +340 -0
- package/dist/tools/ingest.test.js.map +1 -0
- package/dist/tools/kubejs.d.ts +37 -0
- package/dist/tools/kubejs.d.ts.map +1 -0
- package/dist/tools/kubejs.js +245 -0
- package/dist/tools/kubejs.js.map +1 -0
- package/dist/tools/mappings.d.ts +17 -0
- package/dist/tools/mappings.d.ts.map +1 -0
- package/dist/tools/mappings.js +85 -0
- package/dist/tools/mappings.js.map +1 -0
- package/dist/tools/mc-fts.d.ts +101 -0
- package/dist/tools/mc-fts.d.ts.map +1 -0
- package/dist/tools/mc-fts.js +279 -0
- package/dist/tools/mc-fts.js.map +1 -0
- package/dist/tools/mcmeta.d.ts +41 -0
- package/dist/tools/mcmeta.d.ts.map +1 -0
- package/dist/tools/mcmeta.js +365 -0
- package/dist/tools/mcmeta.js.map +1 -0
- package/dist/tools/mixin-scan.d.ts +113 -0
- package/dist/tools/mixin-scan.d.ts.map +1 -0
- package/dist/tools/mixin-scan.js +433 -0
- package/dist/tools/mixin-scan.js.map +1 -0
- package/dist/tools/mod-data.d.ts +83 -0
- package/dist/tools/mod-data.d.ts.map +1 -0
- package/dist/tools/mod-data.js +630 -0
- package/dist/tools/mod-data.js.map +1 -0
- package/dist/tools/mod-tags.d.ts +59 -0
- package/dist/tools/mod-tags.d.ts.map +1 -0
- package/dist/tools/mod-tags.js +279 -0
- package/dist/tools/mod-tags.js.map +1 -0
- package/dist/tools/mod-tags.test.d.ts +2 -0
- package/dist/tools/mod-tags.test.d.ts.map +1 -0
- package/dist/tools/mod-tags.test.js +75 -0
- package/dist/tools/mod-tags.test.js.map +1 -0
- package/dist/tools/modpacks-ch.d.ts +257 -0
- package/dist/tools/modpacks-ch.d.ts.map +1 -0
- package/dist/tools/modpacks-ch.js +516 -0
- package/dist/tools/modpacks-ch.js.map +1 -0
- package/dist/tools/packtools.d.ts +113 -0
- package/dist/tools/packtools.d.ts.map +1 -0
- package/dist/tools/packtools.js +809 -0
- package/dist/tools/packtools.js.map +1 -0
- package/dist/tools/packtools.test.d.ts +2 -0
- package/dist/tools/packtools.test.d.ts.map +1 -0
- package/dist/tools/packtools.test.js +99 -0
- package/dist/tools/packtools.test.js.map +1 -0
- package/dist/tools/platform.d.ts +68 -0
- package/dist/tools/platform.d.ts.map +1 -0
- package/dist/tools/platform.js +452 -0
- package/dist/tools/platform.js.map +1 -0
- package/dist/tools/primers.d.ts +36 -0
- package/dist/tools/primers.d.ts.map +1 -0
- package/dist/tools/primers.js +451 -0
- package/dist/tools/primers.js.map +1 -0
- package/dist/tools/reports.d.ts +19 -0
- package/dist/tools/reports.d.ts.map +1 -0
- package/dist/tools/reports.js +573 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/source.d.ts +10 -0
- package/dist/tools/source.d.ts.map +1 -0
- package/dist/tools/source.js +101 -0
- package/dist/tools/source.js.map +1 -0
- package/dist/tools/vanilla-data.d.ts +127 -0
- package/dist/tools/vanilla-data.d.ts.map +1 -0
- package/dist/tools/vanilla-data.js +824 -0
- package/dist/tools/vanilla-data.js.map +1 -0
- package/dist/tools/vanilla.d.ts +171 -0
- package/dist/tools/vanilla.d.ts.map +1 -0
- package/dist/tools/vanilla.js +612 -0
- package/dist/tools/vanilla.js.map +1 -0
- package/dist/tools/version-diff.d.ts +113 -0
- package/dist/tools/version-diff.d.ts.map +1 -0
- package/dist/tools/version-diff.js +519 -0
- package/dist/tools/version-diff.js.map +1 -0
- package/dist/tools/version-diff.test.d.ts +2 -0
- package/dist/tools/version-diff.test.d.ts.map +1 -0
- package/dist/tools/version-diff.test.js +102 -0
- package/dist/tools/version-diff.test.js.map +1 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +54 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +73 -0
- package/dist/validate.test.js.map +1 -0
- package/package.json +62 -0
- package/prisma/backends/schema.sqlite.prisma +215 -0
- package/prisma/backends/template.db +0 -0
- package/prisma/schema.prisma +303 -0
- package/scripts/backup.mjs +151 -0
- package/scripts/build-template-db.mjs +38 -0
- package/scripts/check-atm10.mjs +27 -0
- package/scripts/check-sync.mjs +13 -0
- package/scripts/create-fts-index.mjs +50 -0
- package/scripts/enable-pgvector.mjs +90 -0
- package/scripts/enable-sqlite-vec.mjs +158 -0
- package/scripts/migrate-backend.mjs +188 -0
- package/scripts/migrate-embed-provenance.mjs +80 -0
- package/scripts/migrate-metadata-source.mjs +62 -0
- package/scripts/migrate-mod-source-files.mjs +67 -0
- package/scripts/post-update.mjs +77 -0
- 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
|