From 5e3531133d720538de2cfe2cb181a1c9547af41b Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 16 Jul 2026 17:51:46 +0700 Subject: [PATCH 01/11] test(embeddings): cover File-row deletion --- .../lbug-delete-nodes-for-files.test.ts | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts b/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts index 124bd6474..736dba3f5 100644 --- a/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts +++ b/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts @@ -92,10 +92,14 @@ withTestLbugDB('delete-nodes-for-files', (handle) => { // is plain schema (no VECTOR extension involved). const SURVIVOR_PATH = filePath(FILE_COUNT - 1); const survivorEmbeddingNodeId = `Function:${SURVIVOR_PATH}:fn${FILE_COUNT - 1}:1`; + const survivorFileEmbeddingNodeId = `File:${SURVIVOR_PATH}`; const seededEmbeddingNodeIds = [ `Function:${filePath(1)}:fn1:1`, // deleted, plain path + `File:${filePath(1)}`, // deleted fallback File embedding, plain path `Function:${QUOTED_PATH}:fn0:1`, // deleted, quoted path + `File:${QUOTED_PATH}`, // deleted fallback File embedding, quoted path survivorEmbeddingNodeId, // survives the delete + survivorFileEmbeddingNodeId, // fallback File embedding also survives ]; await batchInsertEmbeddings( executeWithReusedStatement, @@ -154,15 +158,17 @@ withTestLbugDB('delete-nodes-for-files', (handle) => { const embRows = (await executeQuery( `MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN e.nodeId AS nodeId`, )) as Array<{ nodeId: string }>; - expect(embRows.map((r) => String(r.nodeId))).toEqual([survivorEmbeddingNodeId]); + expect(embRows.map((r) => String(r.nodeId)).sort()).toEqual( + [survivorEmbeddingNodeId, survivorFileEmbeddingNodeId].sort(), + ); // Zero-match batch (all paths already gone) is a clean no-op. await expect(deleteNodesForFiles([QUOTED_PATH, filePath(1)])).resolves.toBeUndefined(); // …and it left the surviving embedding row alone. - expect(await count(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS c`)).toBe(1); + expect(await count(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS c`)).toBe(2); }, 120_000); - it('a file whose only nodes carry non-embeddable labels deletes cleanly and leaves other files’ embedding rows intact (FIX 4)', async () => { + it('a File node without an embedding deletes cleanly and leaves other files’ embedding rows intact (FIX 4)', async () => { const { deleteNodesForFiles, executeQuery } = await import('../../src/core/lbug/lbug-adapter.js'); const count = async (cypher: string): Promise => { @@ -170,9 +176,9 @@ withTestLbugDB('delete-nodes-for-files', (handle) => { return Number(rows[0]?.c ?? 0); }; - // File is NOT an embeddable label, so the label-scoped embedding join - // (FIX 4) never binds it — the delete must still remove the node rows - // without erroring, and embedding rows owned by OTHER files stay put. + // File can own fallback embeddings, but this fixture deliberately has + // none. The delete must still remove the node row without erroring, and + // embedding rows owned by OTHER files stay put. const ASSET_PATH = 'src/assets-only.txt'; await executeQuery( `CREATE (:File {id: 'File:${ASSET_PATH}', name: 'assets-only.txt', filePath: '${ASSET_PATH}'})`, @@ -190,6 +196,39 @@ withTestLbugDB('delete-nodes-for-files', (handle) => { embeddingsBefore, ); }, 120_000); + + it('deleteNodesForFile removes a fallback embedding owned by the File node', async () => { + const { deleteNodesForFile, executeQuery, executeWithReusedStatement } = + await import('../../src/core/lbug/lbug-adapter.js'); + const { batchInsertEmbeddings } = + await import('../../src/core/embeddings/embedding-pipeline.js'); + const count = async (cypher: string): Promise => { + const rows = (await executeQuery(cypher)) as Array<{ c: number | bigint }>; + return Number(rows[0]?.c ?? 0); + }; + + const filePath = 'docs/singular.md'; + const nodeId = `File:${filePath}`; + await executeQuery( + `CREATE (:File {id: '${nodeId}', name: 'singular.md', filePath: '${filePath}'})`, + ); + await batchInsertEmbeddings(executeWithReusedStatement, [ + { + nodeId, + chunkIndex: 0, + startLine: 1, + endLine: 1, + embedding: new Array(EMBEDDING_DIMS).fill(0), + }, + ]); + + await expect(deleteNodesForFile(filePath)).resolves.toEqual({ deletedNodes: 1 }); + expect( + await count( + `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = '${nodeId}' RETURN count(e) AS c`, + ), + ).toBe(0); + }, 120_000); }); }); From 42de243e9a64a6f4d8b7da407f0f69bb0229182e Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 16 Jul 2026 11:03:15 +0000 Subject: [PATCH 02/11] ci(release): sync plugin manifests on every version bump (#2445) The RC path bumped only gitnexus/package.json, so every v1.6.10-rc tag through rc.28 shipped the four plugin manifest surfaces frozen at 1.6.9 and failed its own unit suite. The npm version lifecycle script now runs a fail-closed sync whenever npm version executes, in CI or on a maintainer's laptop; publish.yml verifies the result and stages the surfaces into the detached release commit, and the stable path refuses to publish a tag whose manifests drifted. The sync is textual so a release commit carries a one-line change per surface instead of reformatting churn. Design follows the proposal by @100yenadmin in #2445, moved onto the standard npm version hook. Co-Authored-By: Claude Fable 5 --- .github/workflows/publish.yml | 21 +++ gitnexus/package.json | 3 +- gitnexus/scripts/sync-plugin-manifests.mjs | 141 ++++++++++++++++ .../test/unit/sync-plugin-manifests.test.ts | 155 ++++++++++++++++++ 4 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 gitnexus/scripts/sync-plugin-manifests.mjs create mode 100644 gitnexus/test/unit/sync-plugin-manifests.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a178376e..1623698a7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -423,6 +423,10 @@ jobs: echo "::error::Tag version (v$TAG_VERSION) does not match package.json version ($PKG_VERSION)" exit 1 fi + # Stable releases carry their version bump on main via the release + # PR, so the manifest surfaces must already be in sync — refuse to + # publish a stable whose manifests drifted (#2445). + node scripts/sync-plugin-manifests.mjs --check echo "Version verified: $PKG_VERSION" # ── RC-only: compute the next rc version against the live registry ── @@ -584,6 +588,17 @@ jobs: npm version "${{ steps.rc-version.outputs.rc_version }}" \ --no-git-tag-version --allow-same-version + # ── Verify the plugin manifest surfaces synced (#2445) ─────────────── + # The npm `version` lifecycle script in gitnexus/package.json syncs all + # four manifest surfaces whenever `npm version` runs (the step above, + # and a maintainer's laptop alike). This step only verifies fail-closed + # so a future removal of that wiring cannot ship a drifted RC again. + - name: Verify plugin manifests (rc) + if: needs.route.outputs.mode == 'rc' + shell: bash + working-directory: gitnexus + run: node scripts/sync-plugin-manifests.mjs --check + - name: Build gitnexus run: npm run build working-directory: gitnexus @@ -669,6 +684,12 @@ jobs: # pristine, but the v-tag's tree matches the published package # exactly (release-integrity). git add package.json package-lock.json 2>/dev/null || git add package.json + # The synced manifest surfaces (#2445) belong in the same detached + # release commit so the tag's tree passes its own version contract. + git add ../gitnexus-claude-plugin/.claude-plugin/plugin.json \ + ../.claude-plugin/marketplace.json \ + ../gitnexus-claude-plugin/.codex-plugin/plugin.json \ + ../.agents/plugins/marketplace.json git commit -m "release: ${VTAG}" --allow-empty RELEASE_SHA="$(git rev-parse HEAD)" echo "Detached release commit: $RELEASE_SHA" diff --git a/gitnexus/package.json b/gitnexus/package.json index 9b5015a49..860fad1b4 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -52,7 +52,8 @@ "postinstall": "node scripts/build-tree-sitter-grammars.cjs", "assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs", "prepare": "node scripts/build.js", - "prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js" + "prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js", + "version": "node scripts/sync-plugin-manifests.mjs" }, "dependencies": { "@ladybugdb/core": "^0.18.0", diff --git a/gitnexus/scripts/sync-plugin-manifests.mjs b/gitnexus/scripts/sync-plugin-manifests.mjs new file mode 100644 index 000000000..ff9383fe1 --- /dev/null +++ b/gitnexus/scripts/sync-plugin-manifests.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node +/** + * Fail-closed version sync for the plugin manifest surfaces (#2445). + * + * `publish.yml` bumps only `gitnexus/package.json` when it cuts an RC, so + * every RC tag through v1.6.10-rc.28 shipped manifests frozen at the last + * stable version and failed its own unit suite (the cli-commands version + * contract). This script pins all four manifest surfaces to the package + * version: + * + * - gitnexus-claude-plugin/.claude-plugin/plugin.json (top-level version) + * - .claude-plugin/marketplace.json (plugins[gitnexus]) + * - gitnexus-claude-plugin/.codex-plugin/plugin.json (top-level version) + * - .agents/plugins/marketplace.json (plugins[gitnexus]) + * + * Modes: + * node scripts/sync-plugin-manifests.mjs rewrite stale surfaces + * node scripts/sync-plugin-manifests.mjs --check verify only, exit 1 on drift + * + * Fail-closed: a missing file, unparseable JSON, an absent version field, or + * anything other than exactly one `gitnexus` marketplace entry aborts with a + * non-zero exit rather than letting a release ship a partial sync. + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const MANIFEST_SURFACES = [ + { file: 'gitnexus-claude-plugin/.claude-plugin/plugin.json', kind: 'plugin' }, + { file: '.claude-plugin/marketplace.json', kind: 'marketplace' }, + { file: 'gitnexus-claude-plugin/.codex-plugin/plugin.json', kind: 'plugin' }, + { file: '.agents/plugins/marketplace.json', kind: 'marketplace' }, +]; + +const PLUGIN_NAME = 'gitnexus'; + +function readJson(filePath) { + let raw; + try { + raw = readFileSync(filePath, 'utf8'); + } catch (err) { + throw new Error(`Cannot read manifest surface ${filePath}: ${err.message}`); + } + try { + return { raw, parsed: JSON.parse(raw) }; + } catch (err) { + throw new Error(`Manifest surface ${filePath} is not valid JSON: ${err.message}`); + } +} + +function versionTarget(manifest, kind, filePath) { + if (kind === 'plugin') { + if (typeof manifest.version !== 'string' || manifest.version.length === 0) { + throw new Error(`Manifest surface ${filePath} has no version field to sync`); + } + return manifest; + } + const entries = (Array.isArray(manifest.plugins) ? manifest.plugins : []).filter( + (plugin) => plugin?.name === PLUGIN_NAME, + ); + if (entries.length !== 1) { + throw new Error( + `Manifest surface ${filePath} must contain exactly one "${PLUGIN_NAME}" plugin entry, found ${entries.length}`, + ); + } + if (typeof entries[0].version !== 'string' || entries[0].version.length === 0) { + throw new Error(`Manifest surface ${filePath} has no version field to sync`); + } + return entries[0]; +} + +/** + * Sync (or with `check: true`, only inspect) every manifest surface under + * `rootDir`. Returns `{ version, synced, stale }` where `stale` lists the + * surfaces that did not match the package version when the run started. + */ +export function syncPluginManifests(rootDir, { check = false } = {}) { + const pkgPath = path.join(rootDir, 'gitnexus', 'package.json'); + const version = readJson(pkgPath).parsed.version; + if (typeof version !== 'string' || version.length === 0) { + throw new Error(`No version found in ${pkgPath}`); + } + + const synced = []; + const stale = []; + for (const { file, kind } of MANIFEST_SURFACES) { + const manifestPath = path.join(rootDir, file); + const { raw, parsed } = readJson(manifestPath); + const target = versionTarget(parsed, kind, manifestPath); + if (target.version === version) continue; + + stale.push({ file, from: target.version }); + if (check) continue; + + // Textual surgery instead of re-serializing: JSON.stringify would refold + // arrays and fight prettier, turning a one-line version bump into + // formatting churn inside the release commit. The needle is built from + // the parsed current version, and anything other than exactly one + // occurrence aborts rather than guessing. + const needle = `"version": "${target.version}"`; + const occurrences = raw.split(needle).length - 1; + if (occurrences !== 1) { + throw new Error( + `Manifest surface ${manifestPath} has ${occurrences} occurrences of ${needle}; ` + + 'expected exactly one, refusing to sync', + ); + } + writeFileSync(manifestPath, raw.replace(needle, `"version": "${version}"`)); + synced.push(file); + } + + return { version, synced, stale }; +} + +const invokedDirectly = + process.argv[1] !== undefined && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (invokedDirectly) { + const check = process.argv.includes('--check'); + const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + const result = syncPluginManifests(rootDir, { check }); + + if (check && result.stale.length > 0) { + for (const { file, from } of result.stale) { + console.error( + `::error::${file} is at ${from} but gitnexus/package.json is at ${result.version}. ` + + 'Run `node gitnexus/scripts/sync-plugin-manifests.mjs` and commit the result.', + ); + } + process.exit(1); + } + + for (const file of result.synced) { + console.log(`synced ${file} -> ${result.version}`); + } + console.log( + result.stale.length === 0 && result.synced.length === 0 + ? `all plugin manifests already at ${result.version}` + : `plugin manifests now at ${result.version}`, + ); +} diff --git a/gitnexus/test/unit/sync-plugin-manifests.test.ts b/gitnexus/test/unit/sync-plugin-manifests.test.ts new file mode 100644 index 000000000..d44f00a32 --- /dev/null +++ b/gitnexus/test/unit/sync-plugin-manifests.test.ts @@ -0,0 +1,155 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { syncPluginManifests } from '../../scripts/sync-plugin-manifests.mjs'; + +const SURFACES = [ + 'gitnexus-claude-plugin/.claude-plugin/plugin.json', + '.claude-plugin/marketplace.json', + 'gitnexus-claude-plugin/.codex-plugin/plugin.json', + '.agents/plugins/marketplace.json', +] as const; + +const tempRoots: string[] = []; + +afterEach(() => { + for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function writeJson(root: string, file: string, value: unknown): void { + const filePath = path.join(root, file); + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function makeRoot(packageVersion: string, manifestVersion: string): string { + const root = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-manifest-sync-')); + tempRoots.push(root); + writeJson(root, 'gitnexus/package.json', { name: 'gitnexus', version: packageVersion }); + writeJson(root, SURFACES[0], { name: 'gitnexus', version: manifestVersion }); + writeJson(root, SURFACES[1], { + name: 'gitnexus-marketplace', + plugins: [{ name: 'gitnexus', version: manifestVersion, source: './gitnexus-claude-plugin' }], + }); + writeJson(root, SURFACES[2], { name: 'gitnexus', version: manifestVersion }); + writeJson(root, SURFACES[3], { + name: 'gitnexus-marketplace', + plugins: [{ name: 'gitnexus', version: manifestVersion, category: 'Developer Tools' }], + }); + return root; +} + +function readVersions(root: string): string[] { + return SURFACES.map((file) => { + const manifest = JSON.parse(readFileSync(path.join(root, file), 'utf8')) as { + version?: string; + plugins?: Array<{ name: string; version: string }>; + }; + return ( + manifest.version ?? + manifest.plugins?.find((plugin) => plugin.name === 'gitnexus')?.version ?? + '' + ); + }); +} + +describe('syncPluginManifests (#2445)', () => { + it('rewrites all four surfaces to the package version and reports them', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + + const result = syncPluginManifests(root); + + expect(result.version).toBe('1.6.10-rc.29'); + expect(result.synced).toHaveLength(4); + expect(result.stale.map(({ from }) => from)).toEqual(['1.6.9', '1.6.9', '1.6.9', '1.6.9']); + expect(readVersions(root)).toEqual(Array(4).fill('1.6.10-rc.29')); + }); + + it('is idempotent once everything matches', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + syncPluginManifests(root); + + const second = syncPluginManifests(root); + + expect(second.synced).toHaveLength(0); + expect(second.stale).toHaveLength(0); + }); + + it('check mode reports drift without writing anything', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + + const result = syncPluginManifests(root, { check: true }); + + expect(result.stale).toHaveLength(4); + expect(result.synced).toHaveLength(0); + expect(readVersions(root)).toEqual(Array(4).fill('1.6.9')); + }); + + it('changes only the version text and preserves the surrounding formatting', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + const inlineFormatted = `{ + "name": "gitnexus", + "version": "1.6.9", + "keywords": ["code-intelligence", "knowledge-graph", "mcp"] +} +`; + writeFileSync(path.join(root, SURFACES[0]), inlineFormatted); + + syncPluginManifests(root); + + expect(readFileSync(path.join(root, SURFACES[0]), 'utf8')).toBe( + inlineFormatted.replace('"version": "1.6.9"', '"version": "1.6.10-rc.29"'), + ); + }); + + it('fails closed when the current version text is ambiguous in the file', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + writeFileSync( + path.join(root, SURFACES[0]), + `{ + "name": "gitnexus", + "version": "1.6.9", + "previous": { "version": "1.6.9" } +} +`, + ); + + expect(() => syncPluginManifests(root)).toThrow(/expected exactly one/); + }); + + it('fails closed when a marketplace has no gitnexus entry', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + writeJson(root, SURFACES[1], { name: 'gitnexus-marketplace', plugins: [] }); + + expect(() => syncPluginManifests(root)).toThrow(/exactly one "gitnexus" plugin entry/); + }); + + it('fails closed when a surface file is missing', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + rmSync(path.join(root, SURFACES[2])); + + expect(() => syncPluginManifests(root)).toThrow(/Cannot read manifest surface/); + }); + + it('fails closed on unparseable JSON', () => { + const root = makeRoot('1.6.10-rc.29', '1.6.9'); + writeFileSync(path.join(root, SURFACES[0]), '{ not json'); + + expect(() => syncPluginManifests(root)).toThrow(/not valid JSON/); + }); + + it('matches the real repository layout and passes the check on a synced tree', () => { + const repoRoot = path.resolve(__dirname, '..', '..', '..'); + + const result = syncPluginManifests(repoRoot, { check: true }); + + expect(result.stale).toEqual([]); + }); + + it('is wired into the npm version lifecycle so every bump syncs the manifests', async () => { + const pkg = await import('../../package.json', { with: { type: 'json' } }); + + expect(pkg.default.scripts.version).toBe('node scripts/sync-plugin-manifests.mjs'); + }); +}); From 573a777ef5ca944eb843ff4d02dd1f44eb6d4c55 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 16 Jul 2026 11:03:43 +0000 Subject: [PATCH 03/11] ci(tests): widen the Windows shard watchdog and keep exit diagnostics (#2449) The busiest Windows platform shard reached 14m57s against the 15 minute watchdog on the rc.19 green run and has timed out once since. CI now sets GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES=20 (the job timeout stays 25), the stale comfortably-under comment reflects reality, and the runner always logs status, signal, spawn code and elapsed time so the next status-null death is diagnosable. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci-tests.yml | 10 ++++++++-- gitnexus/scripts/run-cross-platform.ts | 25 ++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 276245d30..904d2f0b9 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -184,8 +184,10 @@ jobs: # spawns and Windows is ~5x slower than macOS at those, so the unsharded # run crept past the 15-min watchdog in run-cross-platform.ts. vitest # shards by file COUNT, not runtime, so the heaviest spawn suites can - # cluster on one shard; 3 shards keep even the busiest Windows shard - # comfortably under the watchdog (macOS had margin either way). + # cluster on one shard. The busiest Windows shard has grown to the old + # 15-minute watchdog (14m57s on the v1.6.10-rc.19 green run, one + # observed timeout since — #2449), so the job env below raises the + # per-shard watchdog to 20 minutes, still bounded by timeout-minutes. # Shard indices come from the shard-plan job (single source of truth): # its TOTAL drives this list and the /N in the job name + --shard arg. shard: ${{ fromJSON(needs.shard-plan.outputs.shards) }} @@ -204,6 +206,10 @@ jobs: env: GITNEXUS_REQUIRE_FTS: '1' GITNEXUS_E2E_CLI: dist + # #2449: hosted Windows runners intermittently push the busiest shard past + # the default 15-minute watchdog. 20 minutes restores real headroom while + # the 25-minute job timeout above still bounds a genuine hang. + GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES: '20' steps: # persist-credentials: false — runs tests only, never pushes (zizmor # credential-persistence / artipacked audit). diff --git a/gitnexus/scripts/run-cross-platform.ts b/gitnexus/scripts/run-cross-platform.ts index c1eed5c9a..3b829e3ca 100644 --- a/gitnexus/scripts/run-cross-platform.ts +++ b/gitnexus/scripts/run-cross-platform.ts @@ -48,10 +48,11 @@ try { // Per-shard watchdog, default 15 min. Sharding splits the file list by COUNT, not // runtime, so the heaviest spawn suites can cluster on one shard — what this -// bounds is the *busiest* shard, not an even 1/n of wall-clock. With 3 shards -// even that shard clears the watchdog, where the whole unsharded Windows run -// used to trip it. Allow CI/manual runs to add headroom without editing the -// script again. +// bounds is the *busiest* shard, not an even 1/n of wall-clock. The busiest +// Windows shard has grown to the default (14m57s on the v1.6.10-rc.19 green +// run, one observed timeout since — #2449), so CI raises the budget to 20 +// minutes via GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES; the default stays 15 +// for local runs. const DEFAULT_TIMEOUT_MIN = 15; const timeoutMinutes = Number.parseInt( process.env.GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES ?? String(DEFAULT_TIMEOUT_MIN), @@ -67,6 +68,7 @@ console.log( `${shardArg ? ` (${shardArg.replace('--shard=', 'shard ')})` : ''}...\n`, ); +const startedAt = Date.now(); try { execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM, ...(shardArg ? [shardArg] : [])], { cwd: ROOT, @@ -76,9 +78,22 @@ try { }); } catch (err) { // execFileSync sets `killed`/`signal` when the watchdog above kills vitest. - const e = err as { killed?: boolean; signal?: NodeJS.Signals | null }; + const e = err as { + killed?: boolean; + signal?: NodeJS.Signals | null; + status?: number | null; + code?: string; + }; if (e.killed || e.signal) { console.error(`vitest timed out after ${Math.round(timeoutMs / 60_000)} minutes`); } + // #2449: Windows shards have died with a bare `status: null`, empty stderr + // and nothing to triage from. Always leave the child's exit facts behind. + const elapsedSec = Math.round((Date.now() - startedAt) / 1000); + console.error( + `vitest exited abnormally: status=${e.status ?? 'null'} signal=${e.signal ?? 'none'} ` + + `killed=${e.killed === true} spawnCode=${e.code ?? 'none'} elapsed=${elapsedSec}s ` + + `budget=${Math.round(timeoutMs / 60_000)}min`, + ); process.exit(1); } From 3dd553b34579e57f03b6fa79034d3207cb079b40 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Thu, 16 Jul 2026 13:11:57 +0100 Subject: [PATCH 04/11] feat(taint): expand TS/JS sink model (#2490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(taint): expand TS/JS sink model Co-authored-by: Cursor * test(taint): cover TS sink disambiguation end-to-end Add a real-pipeline integration test proving the expanded TS/JS taint sinks only emit findings for intended imported and receiver-conventional symbols. Co-authored-by: Cursor --------- Co-authored-by: Cursor Co-authored-by: Gergő Magyar --- .../core/ingestion/taint/typescript-model.ts | 36 ++++++++- .../test/integration/taint-explain.test.ts | 77 +++++++++++++++++++ gitnexus/test/unit/taint/model-match.test.ts | 41 ++++++++++ gitnexus/test/unit/taint/propagate.test.ts | 48 ++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/taint/typescript-model.ts b/gitnexus/src/core/ingestion/taint/typescript-model.ts index bfa41c49c..6ef39cca9 100644 --- a/gitnexus/src/core/ingestion/taint/typescript-model.ts +++ b/gitnexus/src/core/ingestion/taint/typescript-model.ts @@ -38,10 +38,14 @@ export const TS_JS_TAINT_MODEL: SourceSinkSanitizerSpec = { }, ], sinks: [ - // Command execution — the command string is argument 0. + // Command execution — shell strings are arg 0; argv-form APIs also treat + // the argv array at arg 1 as command/option injection surface. { name: 'exec', kind: 'command-injection', args: [0], module: 'child_process' }, { name: 'execSync', kind: 'command-injection', args: [0], module: 'child_process' }, - { name: 'spawn', kind: 'command-injection', args: [0], module: 'child_process' }, + { name: 'spawn', kind: 'command-injection', args: [0, 1], module: 'child_process' }, + { name: 'spawnSync', kind: 'command-injection', args: [0, 1], module: 'child_process' }, + { name: 'execFile', kind: 'command-injection', args: [0, 1], module: 'child_process' }, + { name: 'execFileSync', kind: 'command-injection', args: [0, 1], module: 'child_process' }, // Code evaluation. `eval` takes code at 0; `new Function(...)` treats // EVERY argument as source text (params + body), so `args` is omitted // (= all positions) rather than pinned to 0. @@ -56,9 +60,37 @@ export const TS_JS_TAINT_MODEL: SourceSinkSanitizerSpec = { // (mysql2/pg/knex handles go by many names; receiver-conventional). { name: 'query', kind: 'sql-injection', args: [0], anyReceiver: true }, { name: 'execute', kind: 'sql-injection', args: [0], anyReceiver: true }, + // Modern DB libraries expose shorter method names with high collision + // rates (`map.get`, `task.run`, ...), so keep these receiver-conventional. + { + name: 'run', + kind: 'sql-injection', + args: [0], + receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'], + }, + { + name: 'all', + kind: 'sql-injection', + args: [0], + receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'], + }, + { + name: 'get', + kind: 'sql-injection', + args: [0], + receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'], + }, + { + name: 'values', + kind: 'sql-injection', + args: [0], + receivers: ['db', 'database', 'conn', 'client', 'pool'], + }, + { name: 'raw', kind: 'sql-injection', args: [0], receivers: ['db', 'knex', 'sequelize'] }, // Reflected XSS — Express response writes, conventional receiver `res`. { name: 'send', kind: 'xss', args: [0], receivers: ['res'] }, { name: 'write', kind: 'xss', args: [0], receivers: ['res'] }, + { name: 'render', kind: 'xss', args: [0, 1], receivers: ['res'] }, ], sanitizers: [ // URL-encoding: neutralizes markup injection AND path separators diff --git a/gitnexus/test/integration/taint-explain.test.ts b/gitnexus/test/integration/taint-explain.test.ts index 7f500679b..28478051e 100644 --- a/gitnexus/test/integration/taint-explain.test.ts +++ b/gitnexus/test/integration/taint-explain.test.ts @@ -26,6 +26,7 @@ import { LocalBackend } from '../../src/mcp/local/local-backend.js'; import { listRegisteredRepos, loadMeta } from '../../src/storage/repo-manager.js'; import { withTestLbugDB } from '../helpers/test-indexed-db.js'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import { decodeTaintPath } from '../../src/core/ingestion/taint/path-codec.js'; vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => { const actual = await importOriginal(); @@ -42,6 +43,82 @@ vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => { const FIXTURE = path.join(__dirname, 'cfg', 'fixtures', 'pdg-repo'); +describe('TS/JS taint model sink disambiguation — real pipeline', () => { + it('emits findings only for the intended imported/receiver-conventional sinks', async () => { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-taint-disambig-')); + try { + fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true }); + fs.writeFileSync( + path.join(repoDir, 'src', 'app.ts'), + `import { execFile as childExecFile, spawnSync } from 'node:child_process'; + +function execFile(cmd: string, args: string[]) { + return { cmd, args }; +} + +export function handle(req: any, db: any, map: Map, task: any, res: any, out: any) { + const value = req.body; + childExecFile('git', [value]); + spawnSync(value, []); + execFile(value, [value]); + db.run(value); + map.get(value); + task.run(value); + res.render(value, {}); + out.render(value, {}); +} +`, + ); + + const result = await runPipelineFromRepo(repoDir, () => {}, { pdg: true }); + const findings = [...result.graph.iterRelationships()] + .filter((rel) => rel.type === 'TAINTED') + .map((rel) => { + const sink = result.graph.getNode(rel.targetId); + const decoded = decodeTaintPath(rel.reason); + if (!decoded.ok) { + throw new Error(`invalid TAINTED reason for ${rel.id}: ${decoded.error}`); + } + return { + kind: decoded.kind, + sinkLine: decoded.hops.at(-1)?.line, + sinkText: String(sink?.properties.text ?? ''), + }; + }); + + expect(findings.map((f) => f.kind).sort()).toEqual([ + 'command-injection', + 'command-injection', + 'sql-injection', + 'xss', + ]); + const findingSites = findings + .map((f) => `${f.kind}@${f.sinkLine}`) + .sort((a, b) => Number(a.split('@')[1]) - Number(b.split('@')[1])); + expect(findingSites).toEqual([ + 'command-injection@9', + 'command-injection@10', + 'sql-injection@12', + 'xss@15', + ]); + expect(findings.map((f) => f.sinkLine).sort((a, b) => Number(a) - Number(b))).not.toContain( + 11, + ); + expect(findings.map((f) => f.sinkLine).sort((a, b) => Number(a) - Number(b))).not.toContain( + 13, + ); + expect(findings.map((f) => f.sinkLine).sort((a, b) => Number(a) - Number(b))).not.toContain( + 14, + ); + expect(findings.map((f) => f.sinkLine).sort((a, b) => Number(a) - Number(b))).not.toContain( + 16, + ); + } finally { + fs.rmSync(repoDir, { recursive: true, force: true }); + } + }); +}); + // ─── Block 1: a --pdg index with real taint findings ───────────────── withTestLbugDB( diff --git a/gitnexus/test/unit/taint/model-match.test.ts b/gitnexus/test/unit/taint/model-match.test.ts index 3c275eda7..7d89eeb98 100644 --- a/gitnexus/test/unit/taint/model-match.test.ts +++ b/gitnexus/test/unit/taint/model-match.test.ts @@ -99,6 +99,22 @@ function f(c) { execSync(c); }`); expect(allSinks(m).map((s) => s.entry.name)).toEqual(['execSync']); }); + it('argv-form child_process sinks match command injection on arg 0', () => { + const m = matchesOf(`import { execFile, execFileSync, spawnSync } from 'node:child_process'; +function f(cmd, arg) { + execFile(cmd, [arg]); + execFileSync(cmd, [arg]); + spawnSync(cmd, [arg]); +}`); + expect(allSinks(m).map((s) => s.entry.name)).toEqual(['execFile', 'execFileSync', 'spawnSync']); + expect(allSinks(m).map((s) => [...s.argPositions])).toEqual([ + [0, 1], + [0, 1], + [0, 1], + ]); + expect(allSinks(m).every((s) => s.entry.kind === 'command-injection')).toBe(true); + }); + it('an in-FUNCTION local `exec` shadows the import — no match', () => { const m = matchesOf(`import { exec } from 'child_process'; function f(c) { function exec(x) { return x; } exec(c); }`); @@ -225,10 +241,35 @@ describe('receiver-conventional sinks', () => { expect(allSinks(m).every((s) => s.entry.kind === 'xss')).toBe(true); }); + it('res.render matches template and data args; out.render does not', () => { + const m = matchesOf(`function f(res, out, template, data) { + res.render(template, data); + out.render(template, data); + }`); + const sinks = allSinks(m); + expect(sinks.map((s) => s.entry.name)).toEqual(['render']); + expect(sinks.map((s) => [...s.argPositions])).toEqual([[0, 1]]); + expect(sinks[0].entry.kind).toBe('xss'); + }); + it('.query/.execute match sql-injection on ANY receiver', () => { const m = matchesOf(`function f(db, pool, x) { db.query(x); pool.execute(x); }`); expect(allSinks(m).map((s) => s.entry.kind)).toEqual(['sql-injection', 'sql-injection']); }); + + it('modern DB method sinks match only conventional DB receivers', () => { + const m = matchesOf(`function f(db, stmt, knex, map, task, x) { + db.run(x); + db.all(x); + stmt.get(x); + knex.raw(x); + db.values(x); + map.get(x); + task.run(x); + }`); + expect(allSinks(m).map((s) => s.entry.name)).toEqual(['run', 'all', 'get', 'raw', 'values']); + expect(allSinks(m).every((s) => s.entry.kind === 'sql-injection')).toBe(true); + }); }); describe('sanitizers — import-aware only, kind-scoped', () => { diff --git a/gitnexus/test/unit/taint/propagate.test.ts b/gitnexus/test/unit/taint/propagate.test.ts index 2e8f9a549..a1bb61e3d 100644 --- a/gitnexus/test/unit/taint/propagate.test.ts +++ b/gitnexus/test/unit/taint/propagate.test.ts @@ -539,6 +539,54 @@ describe('multi-source identity — distinct sources do not merge at one def', ( // ── kind-set exclusion model (real built-in model) ────────────────────────── describe('kind-set exclusions — sanitizers neutralize their kinds only', () => { + it('built-in model catches argv-form child_process command sinks', () => { + const r = analyze( + `import { execFileSync } from 'node:child_process'; +function f(req) { + const tool = req.body; + const arg = req.query; + execFileSync(tool, ['--version']); + execFileSync('git', [arg]); +}`, + { spec: TS_JS_TAINT_MODEL }, + ); + expect(r.findings.map((finding) => finding.sinkKind)).toEqual([ + 'command-injection', + 'command-injection', + ]); + }); + + it('built-in model catches conventional modern DB receiver methods', () => { + const r = analyze( + `function f(req, db, stmt, knex) { + const name = req.body; + db.run(name); + db.all(name); + stmt.get(name); + knex.raw(name); +}`, + { spec: TS_JS_TAINT_MODEL }, + ); + expect(r.findings.map((finding) => finding.sinkKind)).toEqual([ + 'sql-injection', + 'sql-injection', + 'sql-injection', + 'sql-injection', + ]); + }); + + it('built-in model catches Express render template and data sinks', () => { + const r = analyze( + `function f(req, res) { + const template = req.body; + const viewData = req.query; + res.render(template, viewData); +}`, + { spec: TS_JS_TAINT_MODEL }, + ); + expect(r.findings.map((finding) => finding.sinkKind)).toEqual(['xss', 'xss']); + }); + it('escape(req.body) → res.send(b) suppressed (xss neutralized) BUT db.query(b) fires (sql not)', () => { const r = analyze( `import { escape } from 'validator'; From a333d94a00eecccefd59bb0da561e67c8e549d69 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Thu, 16 Jul 2026 13:53:58 +0100 Subject: [PATCH 05/11] feat(wiki): allow explicit HTTP LLM hosts (#2491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(wiki): allow explicit HTTP LLM hosts Keep wiki LLM HTTP endpoints fail-closed by default while adding a narrow exact-host opt-in for LAN/self-hosted models. Co-authored-by: Cursor * fix(wiki): simplify insecure LLM flag name Rename the wiki HTTP opt-in flag to --allow-insecure-connection per review feedback. Co-authored-by: Cursor * fix(wiki): simplify insecure connection env Rename the wiki HTTP allowlist environment variable and align validation errors with the CLI flag naming. Co-authored-by: Cursor --------- Co-authored-by: Cursor Co-authored-by: Gergő Magyar --- README.md | 186 +++++++++++---------- gitnexus/README.md | 30 ++-- gitnexus/src/cli/help-i18n.ts | 1 + gitnexus/src/cli/i18n/en.ts | 2 + gitnexus/src/cli/i18n/zh-CN.ts | 2 + gitnexus/src/cli/index.ts | 4 + gitnexus/src/cli/wiki.ts | 13 +- gitnexus/src/core/wiki/llm-client.ts | 56 ++++++- gitnexus/test/unit/wiki-flags.test.ts | 40 ++++- gitnexus/test/unit/wiki-llm-client.test.ts | 47 ++++++ 10 files changed, 264 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 670ea3f4a..cb4ac5fac 100644 --- a/README.md +++ b/README.md @@ -82,15 +82,15 @@ That's it. `analyze` indexes the codebase, installs agent skills, registers Clau ## Two Ways to Use GitNexus -| | **CLI + MCP** (recommended) | **Web UI** | -| ----------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- | -| **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser | -| **For** | Daily development with Cursor, Claude Code, Antigravity, Codex, Windsurf, OpenCode | Quick exploration, demos, one-off analysis | -| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode | -| **Install** | `npm install -g gitnexus` | No install — [gitnexus.vercel.app](https://gitnexus.vercel.app) | -| **Storage** | LadybugDB native (fast, persistent) | LadybugDB WASM (in-memory, per session) | -| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM | -| **Privacy** | Everything local, no network | Everything in-browser, no server | +| | **CLI + MCP** (recommended) | **Web UI** | +| ----------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser | +| **For** | Daily development with Cursor, Claude Code, Antigravity, Codex, Windsurf, OpenCode | Quick exploration, demos, one-off analysis | +| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode | +| **Install** | `npm install -g gitnexus` | No install — [gitnexus.vercel.app](https://gitnexus.vercel.app) | +| **Storage** | LadybugDB native (fast, persistent) | LadybugDB WASM (in-memory, per session) | +| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM | +| **Privacy** | Everything local, no network | Everything in-browser, no server | > **Bridge mode:** `gitnexus serve` connects the two — the web UI auto-detects the local server and can browse all your CLI-indexed repos without re-uploading or re-indexing. @@ -137,49 +137,49 @@ flowchart TB ### 17 MCP tools (15 per-repo + 2 group) -| Tool | What It Does | -| ---------------- | --------------------------------------------------------------------- | -| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) | -| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) | -| `context` | 360-degree symbol view — categorized refs, process participation | -| `impact` | Blast radius analysis with depth grouping and confidence | -| `trace` | Shortest directed path between two symbols (call + class-member edges)| -| `detect_changes` | Git-diff impact — maps changed lines to affected processes | -| `check` | Read-only structural checks against the indexed graph | -| `rename` | Multi-file coordinated rename with graph + text search | -| `cypher` | Raw Cypher graph queries | -| `route_map` | API route map — which components fetch which endpoints, and handlers | -| `tool_map` | MCP/RPC tool definitions — where they're defined and handled | -| `shape_check` | Validate API response shapes against consumers' property accesses | -| `api_impact` | Pre-change impact report for an API route handler | -| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) | -| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) | -| `group_list` | List configured repository groups | -| `group_sync` | Rebuild a group's Contract Registry and cross-repo links | +| Tool | What It Does | +| ---------------- | ---------------------------------------------------------------------- | +| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) | +| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) | +| `context` | 360-degree symbol view — categorized refs, process participation | +| `impact` | Blast radius analysis with depth grouping and confidence | +| `trace` | Shortest directed path between two symbols (call + class-member edges) | +| `detect_changes` | Git-diff impact — maps changed lines to affected processes | +| `check` | Read-only structural checks against the indexed graph | +| `rename` | Multi-file coordinated rename with graph + text search | +| `cypher` | Raw Cypher graph queries | +| `route_map` | API route map — which components fetch which endpoints, and handlers | +| `tool_map` | MCP/RPC tool definitions — where they're defined and handled | +| `shape_check` | Validate API response shapes against consumers' property accesses | +| `api_impact` | Pre-change impact report for an API route handler | +| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) | +| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) | +| `group_list` | List configured repository groups | +| `group_sync` | Rebuild a group's Contract Registry and cross-repo links | > Per-repo tools take an optional `repo` parameter (omit it when only one repo is indexed) and an optional `branch` for indexes pinned with `gitnexus analyze --branch`. Omitting `branch` queries the workspace index, which follows your checked-out working tree — switching branches and re-running `gitnexus analyze` updates it incrementally. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`. ### Resources for instant context -| Resource | Purpose | -| ---------------------------------------- | ---------------------------------------------------- | -| `gitnexus://repos` | List all indexed repositories (read this first) | -| `gitnexus://setup` | Setup and usage guidance for agents | -| `gitnexus://repo/{name}/context` | Codebase stats, staleness check, and available tools | -| `gitnexus://repo/{name}/clusters` | All functional clusters with cohesion scores | -| `gitnexus://repo/{name}/cluster/{name}` | Cluster members and details | -| `gitnexus://repo/{name}/processes` | All execution flows | -| `gitnexus://repo/{name}/process/{name}` | Full process trace with steps | -| `gitnexus://repo/{name}/schema` | Graph schema for Cypher queries | -| `gitnexus://group/{name}/contracts` | A group's extracted contracts and cross-links | -| `gitnexus://group/{name}/status` | Staleness of repos in a group | +| Resource | Purpose | +| --------------------------------------- | ---------------------------------------------------- | +| `gitnexus://repos` | List all indexed repositories (read this first) | +| `gitnexus://setup` | Setup and usage guidance for agents | +| `gitnexus://repo/{name}/context` | Codebase stats, staleness check, and available tools | +| `gitnexus://repo/{name}/clusters` | All functional clusters with cohesion scores | +| `gitnexus://repo/{name}/cluster/{name}` | Cluster members and details | +| `gitnexus://repo/{name}/processes` | All execution flows | +| `gitnexus://repo/{name}/process/{name}` | Full process trace with steps | +| `gitnexus://repo/{name}/schema` | Graph schema for Cypher queries | +| `gitnexus://group/{name}/contracts` | A group's extracted contracts and cross-links | +| `gitnexus://group/{name}/status` | Staleness of repos in a group | ### 2 MCP prompts for guided workflows -| Prompt | What It Does | -| --------------- | -------------------------------------------------------------------------- | -| `detect_impact` | Pre-commit change analysis — scope, affected processes, risk level | -| `generate_map` | Architecture documentation from the knowledge graph with mermaid diagrams | +| Prompt | What It Does | +| --------------- | ------------------------------------------------------------------------- | +| `detect_impact` | Pre-commit change analysis — scope, affected processes, risk level | +| `generate_map` | Architecture documentation from the knowledge graph with mermaid diagrams | ### 6 agent skills installed to `.claude/skills/` automatically @@ -196,20 +196,21 @@ flowchart TB `gitnexus setup` auto-detects your editors and writes the correct global MCP config. Run it once. To configure only selected integrations, pass `--coding-agent`/`-c` with a comma-separated list, e.g. `gitnexus setup -c cursor,codex`. -| Editor | MCP | Skills | Hooks (auto-augment) | Support | -| ------------------------ | --- | ------ | ---------------------------------------------------------------------------------------- | ------------ | -| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** | -| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](gitnexus-cursor-integration/README.md#hook-install)) | **Full** | +| Editor | MCP | Skills | Hooks (auto-augment) | Support | +| ------------------------ | --- | ------ | ----------------------------------------------------------------------------------------------------------------- | ------------ | +| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** | +| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](gitnexus-cursor-integration/README.md#hook-install)) | **Full** | | **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/))[¹](#fn-antigravity-hooks) | **Full** | -| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** | -| **OpenCode** | Yes | Yes | — | MCP + Skills | -| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills | -| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills | -| **Windsurf** | Yes | — | — | MCP | +| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** | +| **OpenCode** | Yes | Yes | — | MCP + Skills | +| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills | +| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills | +| **Windsurf** | Yes | — | — | MCP | > **Claude Code** and **Codex** get the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex. + > ¹ **Antigravity hooks** follow the [Gemini CLI hooks reference](https://geminicli.com/docs/hooks/reference/) (Antigravity 2.0 is the documented successor to Gemini CLI). Augmentation runs in `AfterTool` because `BeforeTool` has no context-injection channel in the Gemini contract — the agent sees graph context appended to the tool result via `hookSpecificOutput.additionalContext`. Stale-index hints land in the same channel after a successful `git commit/merge/rebase/cherry-pick/pull`. The schema may evolve if Antigravity-specific hook docs diverge from Gemini CLI's; the implementation will track those changes.
@@ -446,7 +447,7 @@ Commit a `.gitnexusrc` JSON file at the repo root to preconfigure recurring `ana "skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md "skipSkills": true, // don't install standard .claude/skills/gitnexus-* skills "embeddings": true, // generate embeddings by default - "workerTimeout": 60 + "workerTimeout": 60, } ``` @@ -470,32 +471,32 @@ Notes: Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max-file-size`, `--verbose`). Use the env-var form when you'd otherwise repeat the same flag every run, or when invoking GitNexus from a long-running host (MCP server, eval-server, CI shell) that already manages its own environment. CLI flags take precedence over env vars; env vars take precedence over built-in defaults. -| Variable | Default | Effect | Tune when… | -| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size (must be ≥ 1). Equivalent to `--workers `. The worker pool is the sole parse path — there is no sequential parser, so `0` is rejected with an actionable error (the pool self-heals via quarantine + respawn). | Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set `1` for a single-worker pool — not `0`. | -| `GITNEXUS_PARSE_CHUNK_CONCURRENCY` | `2` | Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. | Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock. | -| `GITNEXUS_VERBOSE` | unset | When `1`, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to `--verbose`. | Debugging an analyze that "completed" but seems to have missed files; tuning `--workers` / chunk concurrency against observable throughput. | -| `GITNEXUS_AUTH_TOKEN` | unset | Bearer token required when `eval-server` binds beyond loopback. May also be read from `.env.local` or `.env`; shell values take precedence. | Exposing the evaluation HTTP tools to a container, VM, or LAN. | -| `GITNEXUS_PROFILE_DEFERRED` | unset | When `1`, emits `[deferred-profile]` timing/progress logs for the post-chunk deferred resolution band (imports → heritage → buildHeritageMap → legacy call resolution). Implied by `GITNEXUS_VERBOSE`. | Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise. | -| `GITNEXUS_PROFILE_DEFERRED_SLOW_MS` | `3000` (verbose) / `5000` | Per-file threshold in ms above which `processCallsFromExtracted` emits a `slow file …` log line. Parsed via `Number()`: accepts integers (`5000`), scientific notation (`2.5e3`), decimals (`.5`), and hex (`0x10`). Non-finite or non-positive values fall back to the default. | Hunting a few outlier files dominating the deferred call-resolution stage; lower to surface more, raise to focus only on the worst. | -| `PROF_LBUG_LOAD` | unset | When `1`, emits one `[lbug-load prof]` summary line per `loadGraphToLbug` call breaking the graph-DB persistence wall into stages (`csv-emit` / `copy-nodes` / `copy-rels` / `fallback` / `total`) plus node & edge counts. Zero-cost when unset. | Attributing large-repo analyze wall time across CSV generation vs. LadybugDB `COPY` (issue #2203) — the analyze "emit" timing is the scope-resolution bucket, not this DB-write path. | -| `GITNEXUS_MAX_FILE_SIZE` | `512` (KB) | Walker skip threshold in KB. Hard cap is `32768` (tree-sitter buffer ceiling). Equivalent to `--max-file-size `. | Indexing repos with intentionally-large source files (generated parsers, vendored bundles) that should still be parsed. | -| `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS` | `30000` | Worker idle timeout in milliseconds before retry/fallback. Equivalent to `--worker-timeout ` × 1000. | Slow-parsing files (large minified JS, deeply-nested TS types) that legitimately need more than 30s. | -| `GITNEXUS_FTS_STEMMER` | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` for matching repository comments. Re-run `gitnexus analyze --repair-fts` after changing it. | Keyword search quality is poor for non-English comments or identifiers under English stemming. | -| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to `--wal-checkpoint-threshold `. `-1` keeps LadybugDB's stock threshold (~16 MiB). Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. | You need a larger or smaller WAL auto-checkpoint threshold for your analyze workload. | -| `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` | `8388608` (8 MB) | Per-job byte budget the pool will send to a worker in one `postMessage`. | Very large individual files; mostly diagnostic — bumping past 8 MB risks structured-clone memory pressure. | -| `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per worker slot before the slot is dropped from the active rotation. Bounds respawn loops on a chronically-crashing slot. | Hosts where a flaky worker should retry more (raise) or fail-fast (lower) before the slot is dropped. | -| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Combined with `timeoutBackoffFactor`, prevents exponentially-growing retries from stalling for hours. | Slow files that legitimately need long total retry windows; lower to fail-fast on stalls. | -| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD`| `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, every subsequent dispatch rejects until a fresh pool is created. | Hosts where a SIGSEGV-prone native grammar should trip the breaker sooner; CI runners that should fail loudly. | -| `GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS` | `30000` | Max wait at pool shutdown for a retired worker still inside native code. The worker is terminated at its next JS-safe point instead of mid-native-call (which aborts the whole process with `Napi::Error`, #2432); on expiry it is left running, unref'd, and terminated when it surfaces. | Shutdown latency matters more than draining a wedged worker (lower), or a legitimately-slow native grammar needs longer to surface (raise). | -| `GITNEXUS_CPP_CAPTURE_BUDGET_MS` | `20000` | Per-file wall-clock budget for C++ capture extraction. On breach the file keeps the captures accumulated so far and logs a warning — the worker returns to JS instead of stalling in native-heavy loops (#2432). `0` expires immediately. | Pathological generated C++ that still exceeds the budget after the indexed lookups; raise for completeness, lower to fail-fast. | -| `GITNEXUS_CHUNK_BYTE_BUDGET` | `2097152` (2 MB) | Chunk boundary used for cache-key composition and dispatch. Smaller = finer-grained cache hits but more dispatch overhead. | Tuning incremental-analyze cache behavior on monorepos. | -| `GITNEXUS_NO_GITIGNORE` | unset | When set, skips `.gitignore` parsing. `.gitnexusignore` is still honored. | Indexing a repo whose `.gitignore` excludes files you actually want indexed (e.g., generated code committed for cross-repo lookup). | -| `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` | unset | When `=1` strictly, skips the vendored grammar materialize for `tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`, and `tree-sitter-kotlin` at install time (and the Dart/Proto source builds). Those four won't be parsed; the install still succeeds. | Installing on a host without a C++ toolchain or where the vendored prebuilds don't match; willing to skip Dart/Proto/Swift/Kotlin parsing. | -| `GITNEXUS_MCP_READ_ONLY` | unset | Set to `1` to expose only proven single-repository read tools and resources; `0` disables the policy and any other value fails startup. | The MCP server runs in an environment where graph mutation, raw Cypher, and cross-repository group routing must be unavailable. | -| `GITNEXUS_MCP_ALLOWED_REPOS` | unset | Comma-separated allowlist of canonical indexed repository names or absolute paths. Invalid, ambiguous, or blank entries fail startup. | One MCP process must expose only a bounded subset of the repositories in the global registry. | -| `GITNEXUS_MCP_DEFAULT_REPO` | unset | Canonical indexed repository name or absolute path used when a tool or resource omits its repository. Must belong to the allowlist when one is set. | Several repositories are available but unqualified MCP calls should resolve deterministically. | -| `GITNEXUS_MCP_DEFAULT_MAX_TOKENS` | unset | Default positive-integer response budget for MCP `query`, `context`, and `impact`, estimated at four UTF-8 bytes per token. Explicit `maxTokens` wins. | Long MCP responses consume too much model context and callers cannot reliably add a per-request budget. | +| Variable | Default | Effect | Tune when… | +| ----------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size (must be ≥ 1). Equivalent to `--workers `. The worker pool is the sole parse path — there is no sequential parser, so `0` is rejected with an actionable error (the pool self-heals via quarantine + respawn). | Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set `1` for a single-worker pool — not `0`. | +| `GITNEXUS_PARSE_CHUNK_CONCURRENCY` | `2` | Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. | Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock. | +| `GITNEXUS_VERBOSE` | unset | When `1`, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to `--verbose`. | Debugging an analyze that "completed" but seems to have missed files; tuning `--workers` / chunk concurrency against observable throughput. | +| `GITNEXUS_AUTH_TOKEN` | unset | Bearer token required when `eval-server` binds beyond loopback. May also be read from `.env.local` or `.env`; shell values take precedence. | Exposing the evaluation HTTP tools to a container, VM, or LAN. | +| `GITNEXUS_PROFILE_DEFERRED` | unset | When `1`, emits `[deferred-profile]` timing/progress logs for the post-chunk deferred resolution band (imports → heritage → buildHeritageMap → legacy call resolution). Implied by `GITNEXUS_VERBOSE`. | Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise. | +| `GITNEXUS_PROFILE_DEFERRED_SLOW_MS` | `3000` (verbose) / `5000` | Per-file threshold in ms above which `processCallsFromExtracted` emits a `slow file …` log line. Parsed via `Number()`: accepts integers (`5000`), scientific notation (`2.5e3`), decimals (`.5`), and hex (`0x10`). Non-finite or non-positive values fall back to the default. | Hunting a few outlier files dominating the deferred call-resolution stage; lower to surface more, raise to focus only on the worst. | +| `PROF_LBUG_LOAD` | unset | When `1`, emits one `[lbug-load prof]` summary line per `loadGraphToLbug` call breaking the graph-DB persistence wall into stages (`csv-emit` / `copy-nodes` / `copy-rels` / `fallback` / `total`) plus node & edge counts. Zero-cost when unset. | Attributing large-repo analyze wall time across CSV generation vs. LadybugDB `COPY` (issue #2203) — the analyze "emit" timing is the scope-resolution bucket, not this DB-write path. | +| `GITNEXUS_MAX_FILE_SIZE` | `512` (KB) | Walker skip threshold in KB. Hard cap is `32768` (tree-sitter buffer ceiling). Equivalent to `--max-file-size `. | Indexing repos with intentionally-large source files (generated parsers, vendored bundles) that should still be parsed. | +| `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS` | `30000` | Worker idle timeout in milliseconds before retry/fallback. Equivalent to `--worker-timeout ` × 1000. | Slow-parsing files (large minified JS, deeply-nested TS types) that legitimately need more than 30s. | +| `GITNEXUS_FTS_STEMMER` | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` for matching repository comments. Re-run `gitnexus analyze --repair-fts` after changing it. | Keyword search quality is poor for non-English comments or identifiers under English stemming. | +| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to `--wal-checkpoint-threshold `. `-1` keeps LadybugDB's stock threshold (~16 MiB). Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. | You need a larger or smaller WAL auto-checkpoint threshold for your analyze workload. | +| `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` | `8388608` (8 MB) | Per-job byte budget the pool will send to a worker in one `postMessage`. | Very large individual files; mostly diagnostic — bumping past 8 MB risks structured-clone memory pressure. | +| `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per worker slot before the slot is dropped from the active rotation. Bounds respawn loops on a chronically-crashing slot. | Hosts where a flaky worker should retry more (raise) or fail-fast (lower) before the slot is dropped. | +| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Combined with `timeoutBackoffFactor`, prevents exponentially-growing retries from stalling for hours. | Slow files that legitimately need long total retry windows; lower to fail-fast on stalls. | +| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, every subsequent dispatch rejects until a fresh pool is created. | Hosts where a SIGSEGV-prone native grammar should trip the breaker sooner; CI runners that should fail loudly. | +| `GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS` | `30000` | Max wait at pool shutdown for a retired worker still inside native code. The worker is terminated at its next JS-safe point instead of mid-native-call (which aborts the whole process with `Napi::Error`, #2432); on expiry it is left running, unref'd, and terminated when it surfaces. | Shutdown latency matters more than draining a wedged worker (lower), or a legitimately-slow native grammar needs longer to surface (raise). | +| `GITNEXUS_CPP_CAPTURE_BUDGET_MS` | `20000` | Per-file wall-clock budget for C++ capture extraction. On breach the file keeps the captures accumulated so far and logs a warning — the worker returns to JS instead of stalling in native-heavy loops (#2432). `0` expires immediately. | Pathological generated C++ that still exceeds the budget after the indexed lookups; raise for completeness, lower to fail-fast. | +| `GITNEXUS_CHUNK_BYTE_BUDGET` | `2097152` (2 MB) | Chunk boundary used for cache-key composition and dispatch. Smaller = finer-grained cache hits but more dispatch overhead. | Tuning incremental-analyze cache behavior on monorepos. | +| `GITNEXUS_NO_GITIGNORE` | unset | When set, skips `.gitignore` parsing. `.gitnexusignore` is still honored. | Indexing a repo whose `.gitignore` excludes files you actually want indexed (e.g., generated code committed for cross-repo lookup). | +| `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` | unset | When `=1` strictly, skips the vendored grammar materialize for `tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`, and `tree-sitter-kotlin` at install time (and the Dart/Proto source builds). Those four won't be parsed; the install still succeeds. | Installing on a host without a C++ toolchain or where the vendored prebuilds don't match; willing to skip Dart/Proto/Swift/Kotlin parsing. | +| `GITNEXUS_MCP_READ_ONLY` | unset | Set to `1` to expose only proven single-repository read tools and resources; `0` disables the policy and any other value fails startup. | The MCP server runs in an environment where graph mutation, raw Cypher, and cross-repository group routing must be unavailable. | +| `GITNEXUS_MCP_ALLOWED_REPOS` | unset | Comma-separated allowlist of canonical indexed repository names or absolute paths. Invalid, ambiguous, or blank entries fail startup. | One MCP process must expose only a bounded subset of the repositories in the global registry. | +| `GITNEXUS_MCP_DEFAULT_REPO` | unset | Canonical indexed repository name or absolute path used when a tool or resource omits its repository. Must belong to the allowlist when one is set. | Several repositories are available but unqualified MCP calls should resolve deterministically. | +| `GITNEXUS_MCP_DEFAULT_MAX_TOKENS` | unset | Default positive-integer response budget for MCP `query`, `context`, and `impact`, estimated at four UTF-8 bytes per token. Explicit `maxTokens` wins. | Long MCP responses consume too much model context and callers cannot reliably add a per-request budget. |
@@ -739,10 +740,17 @@ gitnexus wiki --force gitnexus wiki --timeout # LLM request timeout in seconds (default: disabled) gitnexus wiki --retries # Max LLM retry attempts per request (default: 3) +# Allow a specific LAN/self-hosted HTTP LLM host (HTTPS is preferred for remote endpoints) +gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local +# Or set a comma-separated host allowlist: +GITNEXUS_ALLOW_INSECURE_CONNECTION=llama-box.local,192.168.1.23 + # Change the output language gitnexus wiki --lang # e.g. english, chinese, spanish, japanese ``` +For safety, `http://` LLM base URLs are allowed by default only for loopback hosts (`localhost`, `127.0.0.1`, `::1`). `--allow-insecure-connection` and `GITNEXUS_ALLOW_INSECURE_CONNECTION` accept exact hostnames or IP addresses only; do not include schemes, ports, paths, credentials, or wildcards. + The wiki generator reads the indexed graph structure, groups files into modules via LLM, generates per-module documentation pages, and creates an overview page — all with cross-references to the knowledge graph. ## Web UI (browser-based) @@ -781,10 +789,10 @@ This starts the server on `http://localhost:4747` and the web UI on `http://loca The official setup ships **two signed images**, published identically to **GitHub Container Registry** (GHCR) and **Docker Hub** — same build, same digest, same Cosign signature: -| Purpose | GHCR (default in `docker-compose.yaml`) | Docker Hub mirror | -| ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------- | -| CLI / `gitnexus serve` backend (HTTP API on port `4747`, MCP, indexer) | `ghcr.io/abhigyanpatwari/gitnexus:latest` | `akonlabs/gitnexus:latest` | -| Static web UI (port `4173`) | `ghcr.io/abhigyanpatwari/gitnexus-web:latest` | `akonlabs/gitnexus-web:latest` | +| Purpose | GHCR (default in `docker-compose.yaml`) | Docker Hub mirror | +| ---------------------------------------------------------------------- | --------------------------------------------- | ------------------------------ | +| CLI / `gitnexus serve` backend (HTTP API on port `4747`, MCP, indexer) | `ghcr.io/abhigyanpatwari/gitnexus:latest` | `akonlabs/gitnexus:latest` | +| Static web UI (port `4173`) | `ghcr.io/abhigyanpatwari/gitnexus-web:latest` | `akonlabs/gitnexus-web:latest` | A named volume (`gitnexus-data`) persists the global registry, indexes, and cloned repos at `/data/gitnexus` inside the server container. To make repos on your host machine indexable, set `WORKSPACE_DIR` before bringing the stack up: @@ -914,11 +922,11 @@ Enterprise includes: Built by the community — not officially maintained, but worth checking out. -| Project | Author | Description | -| ------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------ | -| [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) | [@tintinweb](https://github.com/tintinweb) | GitNexus plugin for [pi](https://pi.dev) — `pi install npm:pi-gitnexus` | -| [gitnexus-stable-ops](https://github.com/ShunsukeHayashi/gitnexus-stable-ops) | [@ShunsukeHayashi](https://github.com/ShunsukeHayashi) | Stable ops & deployment workflows (Miyabi ecosystem) | -| [KiloCode MCP workflow](Documentation/kilo-code-mcp.md) | [@oktanishq](https://github.com/oktanishq) | Guide to connect GitNexus MCP to Kilo Code and verify tools. | +| Project | Author | Description | +| ----------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------- | +| [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) | [@tintinweb](https://github.com/tintinweb) | GitNexus plugin for [pi](https://pi.dev) — `pi install npm:pi-gitnexus` | +| [gitnexus-stable-ops](https://github.com/ShunsukeHayashi/gitnexus-stable-ops) | [@ShunsukeHayashi](https://github.com/ShunsukeHayashi) | Stable ops & deployment workflows (Miyabi ecosystem) | +| [KiloCode MCP workflow](Documentation/kilo-code-mcp.md) | [@oktanishq](https://github.com/oktanishq) | Guide to connect GitNexus MCP to Kilo Code and verify tools. | > Have a project built on GitNexus? Open a PR to add it here! diff --git a/gitnexus/README.md b/gitnexus/README.md index c9b7db121..6c83b27dc 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -249,6 +249,8 @@ gitnexus clean # Delete index for current repo gitnexus clean --all --force # Delete all indexes gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph gitnexus wiki --model # Wiki with custom LLM model (default: minimax/minimax-m2.5) +gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local + # Allow an exact LAN/self-hosted HTTP LLM host; env: GITNEXUS_ALLOW_INSECURE_CONNECTION gitnexus doctor # Show runtime platform capabilities and embedding configuration # Direct graph queries — the same tools the MCP server exposes, no MCP daemon needed @@ -461,14 +463,14 @@ GitNexus uses optional DuckDB extensions for BM25 and vector search. The `gitnex Configure the behavior with these environment variables: -| Variable | Values | Default | Effect | -| -------------------------------------------- | ---------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded install if LOAD fails — a plain `INSTALL`, escalating to `FORCE INSTALL` only when the LOAD error shows the present extension file is broken. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. | -| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process extension-install child before it is killed. | -| `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. | -| `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. | -| `GITNEXUS_COMMUNITY_ENGINE` | `graphology`, `icebug`, `auto` | `graphology` | Community-detection engine used during analyze. `graphology` uses the bundled default path. `icebug` and `auto` currently behave identically: both try the experimental Icebug CSR path and fall back to Graphology if the optional native module is unavailable or incompatible. | -| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. | +| Variable | Values | Default | Effect | +| -------------------------------------------- | ------------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded install if LOAD fails — a plain `INSTALL`, escalating to `FORCE INSTALL` only when the LOAD error shows the present extension file is broken. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. | +| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process extension-install child before it is killed. | +| `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. | +| `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. | +| `GITNEXUS_COMMUNITY_ENGINE` | `graphology`, `icebug`, `auto` | `graphology` | Community-detection engine used during analyze. `graphology` uses the bundled default path. `icebug` and `auto` currently behave identically: both try the experimental Icebug CSR path and fall back to Graphology if the optional native module is unavailable or incompatible. | +| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. | ```bash # Offline/airgapped: never reach the network for extensions @@ -533,13 +535,13 @@ For repositories with very large source files, `GITNEXUS_WORKER_SUB_BATCH_MAX_BY Three env vars expose the pool's resilience layers (respawn budget, cumulative-timeout cap, circuit breaker). Defaults are tuned for typical repos; bump them when an analyze legitimately needs more retries, or lower them to fail-fast on a known-bad shape. -| Variable | Default | Effect | -| ----------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per slot before the slot is dropped from the active rotation. | -| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. | -| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. | +| Variable | Default | Effect | +| ----------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per slot before the slot is dropped from the active rotation. | +| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. | +| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. | | `GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS` | `30000` | Max wait at pool shutdown for a retired worker still inside native code — terminated at its next JS-safe point instead of mid-native-call, which would abort the process (`Napi::Error`, #2432). | -| `GITNEXUS_CPP_CAPTURE_BUDGET_MS` | `20000` | Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning (#2432). `0` expires immediately. | +| `GITNEXUS_CPP_CAPTURE_BUDGET_MS` | `20000` | Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning (#2432). `0` expires immediately. | ### Graph cleanup tuning diff --git a/gitnexus/src/cli/help-i18n.ts b/gitnexus/src/cli/help-i18n.ts index d9a385455..209b48758 100644 --- a/gitnexus/src/cli/help-i18n.ts +++ b/gitnexus/src/cli/help-i18n.ts @@ -96,6 +96,7 @@ const OPTION_DESCRIPTION_KEYS = { 'wiki|--concurrency ': 'help.option.wiki.concurrency', 'wiki|--timeout ': 'help.option.wiki.timeout', 'wiki|--retries ': 'help.option.wiki.retries', + 'wiki|--allow-insecure-connection ': 'help.option.wiki.allowInsecureConnection', 'wiki|--gist': 'help.option.wiki.gist', 'wiki|-v, --verbose': 'help.option.verbose', 'wiki|--review': 'help.option.wiki.review', diff --git a/gitnexus/src/cli/i18n/en.ts b/gitnexus/src/cli/i18n/en.ts index f60825593..3f49d25bd 100644 --- a/gitnexus/src/cli/i18n/en.ts +++ b/gitnexus/src/cli/i18n/en.ts @@ -235,6 +235,8 @@ export const en = { 'help.option.wiki.concurrency': 'Parallel LLM calls (default: 3)', 'help.option.wiki.timeout': 'LLM request timeout in seconds (default: disabled)', 'help.option.wiki.retries': 'Max LLM retry attempts per request (default: 3)', + 'help.option.wiki.allowInsecureConnection': + 'Allow exact host(s) for http:// LLM base URLs (comma-separated; HTTPS is preferred)', 'help.option.wiki.gist': 'Publish wiki as a public GitHub Gist after generation', 'help.option.wiki.review': 'Stop after grouping to review module structure before generating pages', diff --git a/gitnexus/src/cli/i18n/zh-CN.ts b/gitnexus/src/cli/i18n/zh-CN.ts index a1d9a37dc..8eb218f28 100644 --- a/gitnexus/src/cli/i18n/zh-CN.ts +++ b/gitnexus/src/cli/i18n/zh-CN.ts @@ -222,6 +222,8 @@ export const zhCN = { 'help.option.wiki.concurrency': '并行 LLM 调用数(默认:3)', 'help.option.wiki.timeout': 'LLM 请求超时时间(秒,默认:禁用)', 'help.option.wiki.retries': '每个请求的最大 LLM 重试次数(默认:3)', + 'help.option.wiki.allowInsecureConnection': + '允许 http:// LLM base URL 使用的精确主机(逗号分隔;推荐使用 HTTPS)', 'help.option.wiki.gist': '生成后发布 Wiki 为公开 GitHub Gist', 'help.option.wiki.review': '分组后停止,以便在生成页面前审查模块结构', 'help.option.wiki.lang': '生成文档的输出语言(如 english、chinese、spanish、japanese)', diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index 83ac9ce44..b92cc5a26 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -304,6 +304,10 @@ program .option('--concurrency ', 'Parallel LLM calls (default: 3)', '3') .option('--timeout ', 'LLM request timeout in seconds (default: disabled)') .option('--retries ', 'Max LLM retry attempts per request (default: 3)') + .option( + '--allow-insecure-connection ', + 'Allow exact host(s) for http:// LLM base URLs (comma-separated; HTTPS is preferred)', + ) .option('--gist', 'Publish wiki as a public GitHub Gist after generation') .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)') .option('--review', 'Stop after grouping to review module structure before generating pages') diff --git a/gitnexus/src/cli/wiki.ts b/gitnexus/src/cli/wiki.ts index 446b83d30..ef6776fbd 100644 --- a/gitnexus/src/cli/wiki.ts +++ b/gitnexus/src/cli/wiki.ts @@ -17,7 +17,11 @@ import { saveCLIConfig, } from '../storage/repo-manager.js'; import { WikiGenerator, type WikiOptions } from '../core/wiki/generator.js'; -import { resolveLLMConfig, type LLMProvider } from '../core/wiki/llm-client.js'; +import { + parseLLMAllowedInsecureHttpHosts, + resolveLLMConfig, + type LLMProvider, +} from '../core/wiki/llm-client.js'; import { detectCursorCLI } from '../core/wiki/cursor-client.js'; import { detectLocalCLI } from '../core/wiki/local-cli-client.js'; import { logger } from '../core/logger.js'; @@ -37,6 +41,7 @@ export interface WikiCommandOptions { timeout?: string; retries?: string; lang?: string; + allowInsecureConnection?: string; } function parsePositiveIntegerOption( @@ -185,9 +190,14 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions) let timeoutSeconds: number | undefined; let retries: number | undefined; + let allowedInsecureHttpHosts: string[] | undefined; try { timeoutSeconds = parsePositiveIntegerOption(options?.timeout, '--timeout', 1000); retries = parsePositiveIntegerOption(options?.retries, '--retries'); + allowedInsecureHttpHosts = + options?.allowInsecureConnection === undefined + ? undefined + : parseLLMAllowedInsecureHttpHosts(options.allowInsecureConnection); } catch (error) { console.log(` Error: ${(error as Error).message}\n`); process.exitCode = 1; @@ -245,6 +255,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions) provider: options?.provider, apiVersion: options?.apiVersion, isReasoningModel: options?.reasoningModel, + allowedInsecureHttpHosts, }); // Run interactive setup if no saved config and no CLI flags provided diff --git a/gitnexus/src/core/wiki/llm-client.ts b/gitnexus/src/core/wiki/llm-client.ts index 786fcbea5..2fe42cdf0 100644 --- a/gitnexus/src/core/wiki/llm-client.ts +++ b/gitnexus/src/core/wiki/llm-client.ts @@ -35,6 +35,8 @@ export interface LLMConfig { requestTimeoutMs?: number; /** Max fetch attempts before giving up (default: 3). */ maxAttempts?: number; + /** Exact hostnames allowed for explicit http:// LLM endpoints. */ + allowedInsecureHttpHosts?: readonly string[]; } export interface LLMResponse { @@ -94,6 +96,9 @@ export async function resolveLLMConfig(overrides?: Partial): Promise< apiVersion: overrides?.apiVersion || process.env.GITNEXUS_AZURE_API_VERSION || savedConfig.apiVersion, isReasoningModel: overrides?.isReasoningModel ?? savedConfig.isReasoningModel, + allowedInsecureHttpHosts: + overrides?.allowedInsecureHttpHosts ?? + parseLLMAllowedInsecureHttpHosts(process.env[LLM_ALLOW_INSECURE_CONNECTION_ENV]), }; } @@ -117,6 +122,38 @@ function isTimeoutLikeError(err: unknown): boolean { return /time(d)?\s*out|timeout/i.test(err.message); } +export const LLM_ALLOW_INSECURE_CONNECTION_ENV = 'GITNEXUS_ALLOW_INSECURE_CONNECTION'; + +function normalizeAllowedInsecureHttpHost(host: string): string { + const trimmed = host.trim().toLowerCase(); + const fail = () => { + throw new Error( + `--allow-insecure-connection / ${LLM_ALLOW_INSECURE_CONNECTION_ENV} entries must be exact hostnames or IP addresses`, + ); + }; + if (!trimmed || /[/@?#]/.test(trimmed)) fail(); + + if (trimmed.startsWith('[')) { + if (!trimmed.endsWith(']')) fail(); + const normalized = trimmed.slice(1, -1); + if (!normalized || /[\[\]]/.test(normalized)) fail(); + return normalized; + } + + if (/[\[\]]/.test(trimmed)) fail(); + if ((trimmed.match(/:/g)?.length ?? 0) === 1) { + // URL.hostname never includes the port, so accepting "host:port" would + // create a confusing no-op allowlist entry. + fail(); + } + return trimmed; +} + +export function parseLLMAllowedInsecureHttpHosts(value: string | undefined): string[] { + if (value === undefined || value.trim() === '') return []; + return [...new Set(value.split(',').map(normalizeAllowedInsecureHttpHost))]; +} + /** * Validate that a base URL supplied for LLM API calls is a safe HTTP/HTTPS * endpoint (CWE-918 / CodeQL js/http-to-file-access). @@ -124,15 +161,22 @@ function isTimeoutLikeError(err: unknown): boolean { * Allowed: * - https:// with any hostname (public LLM APIs, Azure, OpenRouter, …) * - http:// restricted to localhost / 127.0.0.1 (local servers: Ollama, LiteLLM, …) + * - http:// to exact hosts explicitly allowlisted for LAN/self-hosted LLMs * * Rejected: * - file://, data:, javascript:, and any other non-HTTP scheme - * - http:// aimed at non-loopback hosts (avoids SSRF against internal networks) + * - http:// aimed at non-loopback hosts unless explicitly allowlisted + * (avoids SSRF against internal networks by default) * * Throws with a descriptive message on validation failure so callers surface a * clear error rather than an opaque network error. */ -export function validateLLMBaseUrl(baseUrl: string): void { +export function validateLLMBaseUrl( + baseUrl: string, + allowedInsecureHttpHosts: readonly string[] = parseLLMAllowedInsecureHttpHosts( + process.env[LLM_ALLOW_INSECURE_CONNECTION_ENV], + ), +): void { let parsed: URL; try { parsed = new URL(baseUrl); @@ -150,10 +194,12 @@ export function validateLLMBaseUrl(baseUrl: string): void { // Node's URL parser preserves IPv6 brackets in hostname (e.g. "[::1]"), // so strip them before comparing to bare address literals. const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, ''); - if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1') { + const allowedHosts = new Set(allowedInsecureHttpHosts.map(normalizeAllowedInsecureHttpHost)); + if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1' && !allowedHosts.has(host)) { // Use parsed.origin (scheme+host+port, no credentials) instead of the full URL. throw new Error( - `Insecure http:// LLM base URLs are only allowed for localhost/127.0.0.1. ` + + `Insecure http:// LLM base URLs are only allowed for localhost/127.0.0.1 ` + + `or hosts listed by --allow-insecure-connection / ${LLM_ALLOW_INSECURE_CONNECTION_ENV}. ` + `Use https:// for remote endpoints (got ${parsed.origin})`, ); } @@ -212,7 +258,7 @@ export async function callLLM( options?: CallLLMOptions, ): Promise { // Validate base URL before any fetch (CodeQL js/http-to-file-access) - validateLLMBaseUrl(config.baseUrl); + validateLLMBaseUrl(config.baseUrl, config.allowedInsecureHttpHosts); const messages: Array<{ role: string; content: string }> = []; if (systemPrompt) { diff --git a/gitnexus/test/unit/wiki-flags.test.ts b/gitnexus/test/unit/wiki-flags.test.ts index 165ca2e87..43d2a070a 100644 --- a/gitnexus/test/unit/wiki-flags.test.ts +++ b/gitnexus/test/unit/wiki-flags.test.ts @@ -579,6 +579,17 @@ describe('wikiCommand --timeout mapping', () => { async function loadWikiCommandHarness() { let capturedConfig: Record | undefined; + const resolveLLMConfig = vi.fn().mockImplementation((overrides = {}) => + Promise.resolve({ + apiKey: 'sk-test', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-4o', + maxTokens: 16_384, + temperature: 0, + provider: 'openai', + ...overrides, + }), + ); const generatorCtor = vi .fn() .mockImplementation(function (_repoPath, _storagePath, _lbugPath, config) { @@ -609,14 +620,7 @@ describe('wikiCommand --timeout mapping', () => { const actual = await importOriginal(); return { ...actual, - resolveLLMConfig: vi.fn().mockResolvedValue({ - apiKey: 'sk-test', - baseUrl: 'https://api.openai.com/v1', - model: 'gpt-4o', - maxTokens: 16_384, - temperature: 0, - provider: 'openai', - }), + resolveLLMConfig, }; }); vi.doMock('../../src/core/wiki/generator.js', () => ({ @@ -642,6 +646,7 @@ describe('wikiCommand --timeout mapping', () => { generatorCtor, consoleSpy, getCapturedConfig: () => capturedConfig, + resolveLLMConfig, }; } @@ -671,6 +676,25 @@ describe('wikiCommand --timeout mapping', () => { expect(harness.generatorCtor).toHaveBeenCalledTimes(1); expect(harness.getCapturedConfig()?.maxAttempts).toBe(5); }); + + it('maps --allow-insecure-connection to allowedInsecureHttpHosts', async () => { + const harness = await loadWikiCommandHarness(); + + await harness.wikiCommand('/tmp/repo', { + allowInsecureConnection: 'llama-box.local,192.168.1.23,llama-box.local', + }); + + expect(harness.resolveLLMConfig).toHaveBeenCalledWith( + expect.objectContaining({ + allowedInsecureHttpHosts: ['llama-box.local', '192.168.1.23'], + }), + ); + expect(harness.generatorCtor).toHaveBeenCalledTimes(1); + expect(harness.getCapturedConfig()?.allowedInsecureHttpHosts).toEqual([ + 'llama-box.local', + '192.168.1.23', + ]); + }); }); describe('wikiCommand timeout messaging', () => { diff --git a/gitnexus/test/unit/wiki-llm-client.test.ts b/gitnexus/test/unit/wiki-llm-client.test.ts index 5b6a827c3..91f8660db 100644 --- a/gitnexus/test/unit/wiki-llm-client.test.ts +++ b/gitnexus/test/unit/wiki-llm-client.test.ts @@ -2,9 +2,12 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; // Import the function we'll add in the next step import { + LLM_ALLOW_INSECURE_CONNECTION_ENV, isAzureProvider, isReasoningModel, buildRequestUrl, + parseLLMAllowedInsecureHttpHosts, + resolveLLMConfig, validateLLMBaseUrl, } from '../../src/core/wiki/llm-client.js'; @@ -470,6 +473,10 @@ describe('readSSEStream — content_filter handling', () => { }); describe('validateLLMBaseUrl', () => { + afterEach(() => { + delete process.env[LLM_ALLOW_INSECURE_CONNECTION_ENV]; + }); + it('allows https:// for any public host', () => { expect(() => validateLLMBaseUrl('https://api.openai.com/v1')).not.toThrow(); expect(() => validateLLMBaseUrl('https://openrouter.ai/api/v1')).not.toThrow(); @@ -498,6 +505,46 @@ describe('validateLLMBaseUrl', () => { ); }); + it('allows explicit http:// hosts only when exactly allowlisted', () => { + expect(() => + validateLLMBaseUrl('http://llama-box.local:8080/v1', ['llama-box.local']), + ).not.toThrow(); + expect(() => + validateLLMBaseUrl('http://LLAMA-BOX.local:8080/v1', [' llama-box.LOCAL ']), + ).not.toThrow(); + expect(() => validateLLMBaseUrl('http://llama-box.local.evil/v1', ['llama-box.local'])).toThrow( + 'Insecure http://', + ); + expect(() => validateLLMBaseUrl('http://192.168.1.23:8080/v1', ['192.168.1.23'])).not.toThrow(); + }); + + it('parses and validates comma-separated insecure HTTP host allowlists', () => { + expect( + parseLLMAllowedInsecureHttpHosts(' llama-box.local,192.168.1.23,llama-box.local '), + ).toEqual(['llama-box.local', '192.168.1.23']); + expect(parseLLMAllowedInsecureHttpHosts('[fe80::1]')).toEqual(['fe80::1']); + expect(() => parseLLMAllowedInsecureHttpHosts('http://llama-box.local')).toThrow( + 'exact hostnames or IP addresses', + ); + expect(() => parseLLMAllowedInsecureHttpHosts('llama-box.local/path')).toThrow( + 'exact hostnames or IP addresses', + ); + expect(() => parseLLMAllowedInsecureHttpHosts('llama-box.local:8080')).toThrow( + 'exact hostnames or IP addresses', + ); + expect(() => parseLLMAllowedInsecureHttpHosts('[fe80::1]:8080')).toThrow( + 'exact hostnames or IP addresses', + ); + }); + + it('resolveLLMConfig reads insecure HTTP hosts from env when no override is passed', async () => { + process.env[LLM_ALLOW_INSECURE_CONNECTION_ENV] = 'llama-box.local,192.168.1.23'; + + const config = await resolveLLMConfig(); + + expect(config.allowedInsecureHttpHosts).toEqual(['llama-box.local', '192.168.1.23']); + }); + it('rejects http:// hostname-spoofing attempts', () => { // Full-hostname comparison prevents prefix/suffix attacks expect(() => validateLLMBaseUrl('http://localhost.evil.com/v1')).toThrow('Insecure http://'); From b85f1ace7a447bffa4f65288d903a740a3b01ab2 Mon Sep 17 00:00:00 2001 From: Parafee41 Date: Thu, 16 Jul 2026 22:20:22 +0800 Subject: [PATCH 06/11] fix(mcp): avoid api impact schema combinators (#2489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gergő Magyar --- gitnexus/src/mcp/tools.ts | 9 --------- gitnexus/test/unit/tools.test.ts | 13 +++++++------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index a34587e5d..1d634d206 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -28,12 +28,6 @@ export interface ToolDefinition { } >; required: string[]; - /** - * JSON-Schema `anyOf` for cross-property constraints `required` cannot express - * — e.g. "at least one of route/file". Forwarded verbatim to clients by the - * server's ListTools handler, so MCP clients see the constraint. - */ - anyOf?: Array<{ required: string[] }>; }; } @@ -796,9 +790,6 @@ Response shape is keyed on how many routes match, not on the data: exactly one m repo: { type: 'string', description: 'Repository name or path.' }, }, required: [], - // Exactly one lookup key is needed, but either works (route wins if both - // are passed) — so the structural constraint is "at least one of route/file". - anyOf: [{ required: ['route'] }, { required: ['file'] }], }, }, { diff --git a/gitnexus/test/unit/tools.test.ts b/gitnexus/test/unit/tools.test.ts index 23bb79606..368101628 100644 --- a/gitnexus/test/unit/tools.test.ts +++ b/gitnexus/test/unit/tools.test.ts @@ -134,14 +134,15 @@ describe('GITNEXUS_TOOLS', () => { expect(contextTool.inputSchema.properties.file).toMatchObject({ type: 'string' }); }); - it('api_impact tool expresses the route-or-file requirement via anyOf (#2308)', () => { + it('api_impact tool avoids top-level schema combinators for Bedrock compatibility (#2487)', () => { const apiImpactTool = GITNEXUS_TOOLS.find((t) => t.name === 'api_impact')!; - expect(apiImpactTool.inputSchema.anyOf).toEqual([ - { required: ['route'] }, - { required: ['file'] }, - ]); - // route/file stay optional in `required` (anyOf carries the cross-field rule) + expect(apiImpactTool.inputSchema).not.toHaveProperty('anyOf'); + expect(apiImpactTool.inputSchema).not.toHaveProperty('oneOf'); + expect(apiImpactTool.inputSchema).not.toHaveProperty('allOf'); + // route/file stay optional in the transport schema; callTool keeps the + // runtime guard so providers that reject top-level combinators can load it. expect(apiImpactTool.inputSchema.required).toEqual([]); + expect(apiImpactTool.description).toContain('Requires at least "route" or "file"'); }); it('impact tool requires direction and advertises target, name, or symbol without combinators', () => { From f45e89e6b61f1e5346143c32242dae46dc3c870e Mon Sep 17 00:00:00 2001 From: Parafee41 Date: Thu, 16 Jul 2026 22:21:03 +0800 Subject: [PATCH 07/11] fix(embeddings): make batch inserts retry-safe (#2453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(embeddings): make batch inserts retry-safe * fix(types): cover optional transformers dependency * Fix embedding restore test expectation * test(embeddings): count checkpoint creates --------- Co-authored-by: Gergő Magyar --- .../src/core/embeddings/embedding-pipeline.ts | 8 ++- .../src/types/huggingface-transformers.d.ts | 47 +++++++++++++++++ gitnexus/test/unit/embedding-pipeline.test.ts | 50 ++++++++++++++++--- .../test/unit/run-analyze-fts-repair.test.ts | 10 +++- 4 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 gitnexus/src/types/huggingface-transformers.d.ts diff --git a/gitnexus/src/core/embeddings/embedding-pipeline.ts b/gitnexus/src/core/embeddings/embedding-pipeline.ts index 0b460c6de..ffb5b1b13 100644 --- a/gitnexus/src/core/embeddings/embedding-pipeline.ts +++ b/gitnexus/src/core/embeddings/embedding-pipeline.ts @@ -243,7 +243,6 @@ export const batchInsertEmbeddings = async ( contentHash?: string; }>, ): Promise => { - const cypher = `CREATE (e:${EMBEDDING_TABLE_NAME} {id: $id, nodeId: $nodeId, chunkIndex: $chunkIndex, startLine: $startLine, endLine: $endLine, embedding: $embedding, contentHash: $contentHash})`; const paramsList = updates.map((u) => ({ id: `${u.nodeId}:${u.chunkIndex}`, nodeId: u.nodeId, @@ -253,6 +252,13 @@ export const batchInsertEmbeddings = async ( embedding: u.embedding, contentHash: u.contentHash ?? STALE_HASH_SENTINEL, })); + if (paramsList.length === 0) return; + + await executeWithReusedStatement( + `MATCH (e:${EMBEDDING_TABLE_NAME} {id: $id}) DELETE e`, + paramsList.map(({ id }) => ({ id })), + ); + const cypher = `CREATE (e:${EMBEDDING_TABLE_NAME} {id: $id, nodeId: $nodeId, chunkIndex: $chunkIndex, startLine: $startLine, endLine: $endLine, embedding: $embedding, contentHash: $contentHash})`; await executeWithReusedStatement(cypher, paramsList); }; diff --git a/gitnexus/src/types/huggingface-transformers.d.ts b/gitnexus/src/types/huggingface-transformers.d.ts new file mode 100644 index 000000000..c8ac779c0 --- /dev/null +++ b/gitnexus/src/types/huggingface-transformers.d.ts @@ -0,0 +1,47 @@ +// This ambient shim intentionally shadows the optional package's bundled types +// in dependency-pruned CI. Mirror any newly used transformer API surface here. +declare module '@huggingface/transformers' { + export interface ProgressInfo { + status?: string; + file?: string; + progress?: number; + loaded?: number; + total?: number; + } + + export interface FeatureExtractionResult { + data: ArrayLike; + } + + export interface FeatureExtractionOptions { + pooling?: string; + normalize?: boolean; + } + + export interface FeatureExtractionPipeline { + ( + input: string | string[], + options?: FeatureExtractionOptions, + ): Promise; + dispose?: () => void | Promise; + } + + export interface PipelineOptions { + device?: string; + dtype?: string; + progress_callback?: (progress: ProgressInfo) => void; + session_options?: Record; + } + + export function pipeline( + task: 'feature-extraction', + model: string, + options?: PipelineOptions, + ): Promise; + + export const env: { + allowLocalModels: boolean; + cacheDir: string; + remoteHost: string; + }; +} diff --git a/gitnexus/test/unit/embedding-pipeline.test.ts b/gitnexus/test/unit/embedding-pipeline.test.ts index 8a874e47e..e34c41be2 100644 --- a/gitnexus/test/unit/embedding-pipeline.test.ts +++ b/gitnexus/test/unit/embedding-pipeline.test.ts @@ -428,6 +428,38 @@ describe('runEmbeddingPipeline incremental filter', () => { expect(insertParams[0].contentHash).toMatch(/^[0-9a-f]{40}$/); }); + it('deletes exact embedding row ids before inserting a batch (#2452)', async () => { + mockEmbedderSetup(); + + const node = makeNode({ + id: 'Function:retry:src/retry.ts', + name: 'retry', + filePath: 'src/retry.ts', + }); + const executeQuery = mockExecuteQuery([node]); + const executeWithReusedStatement = mockExecuteWithReusedStatement(); + + const { runEmbeddingPipeline } = + await import('../../src/core/embeddings/embedding-pipeline.js'); + + await runEmbeddingPipeline( + executeQuery, + executeWithReusedStatement, + onProgress, + {}, + undefined, + new Map(), + ); + + const rowDeleteIndex = stmtCalls.findIndex( + (c) => c.cypher.includes('{id: $id}') && c.cypher.includes('DELETE'), + ); + const createIndex = stmtCalls.findIndex((c) => c.cypher.includes('CREATE')); + expect(rowDeleteIndex).toBeGreaterThanOrEqual(0); + expect(createIndex).toBeGreaterThan(rowDeleteIndex); + expect(stmtCalls[rowDeleteIndex].params).toContainEqual({ id: `${node.id}:0` }); + }); + it('maps positional query rows with description/isExported columns correctly', async () => { const embedBatchSpy = vi .fn() @@ -536,7 +568,7 @@ describe('runEmbeddingPipeline incremental filter', () => { ); // Should have a DELETE call for the stale node - const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('DELETE')); + const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('{nodeId: $nodeId}')); expect(deleteCalls.length).toBeGreaterThanOrEqual(1); expect(deleteCalls[0].params.some((p: any) => p.nodeId === node.id)).toBe(true); @@ -568,7 +600,7 @@ describe('runEmbeddingPipeline incremental filter', () => { ); // Should have a DELETE call (stale) - const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('DELETE')); + const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('{nodeId: $nodeId}')); expect(deleteCalls.length).toBeGreaterThanOrEqual(1); // Should also have a CREATE (re-embed) @@ -604,7 +636,7 @@ describe('runEmbeddingPipeline incremental filter', () => { // U6 / KTD7: per-batch interleaving means TWO separate DELETE calls (one per // batch), not one up-front bulk delete of both stale rows. - const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('DELETE')); + const deleteCalls = stmtCalls.filter((c) => c.cypher.includes('{nodeId: $nodeId}')); expect(deleteCalls.length).toBe(2); // Ordering proof: batch 1's INSERT lands BEFORE batch 2's DELETE. An up-front @@ -614,7 +646,7 @@ describe('runEmbeddingPipeline incremental filter', () => { (c) => c.cypher.includes('CREATE') && c.params.some((p) => p.nodeId === n1.id), ); const deleteN2 = stmtCalls.findIndex( - (c) => c.cypher.includes('DELETE') && c.params.some((p) => p.nodeId === n2.id), + (c) => c.cypher.includes('{nodeId: $nodeId}') && c.params.some((p) => p.nodeId === n2.id), ); expect(insertN1).toBeGreaterThanOrEqual(0); expect(deleteN2).toBeGreaterThanOrEqual(0); @@ -750,7 +782,7 @@ describe('runEmbeddingPipeline incremental filter', () => { const executeQuery = mockExecuteQuery([first, second, third]); const executeWithReusedStatement = mockExecuteWithReusedStatement(); const windows: string[][] = []; - const mutationCountsAtWindowStart: number[] = []; + const createCountsAtWindowStart: number[] = []; const checkpoints: number[] = []; const { runEmbeddingPipeline } = await import('../../src/core/embeddings/embedding-pipeline.js'); @@ -766,7 +798,9 @@ describe('runEmbeddingPipeline incremental filter', () => { checkpointEveryNodes: 2, onCheckpointWindowStart: async ({ nodeIds }) => { windows.push(nodeIds); - mutationCountsAtWindowStart.push(stmtCalls.length); + createCountsAtWindowStart.push( + stmtCalls.filter((call) => call.cypher.includes('CREATE')).length, + ); }, onCheckpoint: async ({ nodesProcessed }) => { checkpoints.push(nodesProcessed); @@ -775,7 +809,7 @@ describe('runEmbeddingPipeline incremental filter', () => { ); expect(windows).toEqual([[first.id, second.id], [third.id]]); - expect(mutationCountsAtWindowStart).toEqual([0, 2]); + expect(createCountsAtWindowStart).toEqual([0, 2]); expect(checkpoints).toEqual([2, 3]); }); @@ -833,7 +867,7 @@ describe('runEmbeddingPipeline incremental filter', () => { ); const deletedIds = stmtCalls - .filter((c) => c.cypher.includes('DELETE')) + .filter((c) => c.cypher.includes('{nodeId: $nodeId}')) .flatMap((c) => c.params.map((p) => p.nodeId)); expect(deletedIds).toContain(stale.id); expect(deletedIds).not.toContain(brandNew.id); diff --git a/gitnexus/test/unit/run-analyze-fts-repair.test.ts b/gitnexus/test/unit/run-analyze-fts-repair.test.ts index 86ed16cbb..b0c0b4c9d 100644 --- a/gitnexus/test/unit/run-analyze-fts-repair.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-repair.test.ts @@ -803,8 +803,14 @@ describe('runFullAnalysis wipe-and-restore vector-index stamp (tri-review 466951 // The recreation seam fired exactly once… expect(buildVectorIndex).toHaveBeenCalledTimes(1); - // …the restore actually submitted the cached row (one 200-row batch)… - expect(executeWithReusedStatement).toHaveBeenCalledTimes(1); + // …the restore first clears the exact target id, then submits the + // cached row (one 200-row batch)… + expect(executeWithReusedStatement).toHaveBeenCalledTimes(2); + const [deleteCall, restoreCall] = executeWithReusedStatement.mock.calls; + expect(deleteCall[0]).toContain('DELETE e'); + expect(deleteCall[1]).toEqual([{ id: `${RESTORED_NODE_ID}:0` }]); + expect(restoreCall[0]).toContain('CREATE (e:CodeEmbedding'); + expect(restoreCall[1]).toHaveLength(1); // …and the persisted stamp reflects the DB's ACTUAL state, not the // platform capability fallback. const meta = JSON.parse(await fs.readFile(`${storagePath}/meta.json`, 'utf-8')) as RepoMeta; From 8292b2bee4a1045d99280c1827bf25e99f7e7b67 Mon Sep 17 00:00:00 2001 From: Parafee41 Date: Thu, 16 Jul 2026 22:22:20 +0800 Subject: [PATCH 08/11] test(cli): lock native load guard for lazy actions (#2442) --- gitnexus/test/unit/lazy-action.test.ts | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/gitnexus/test/unit/lazy-action.test.ts b/gitnexus/test/unit/lazy-action.test.ts index 73b1b92b8..e02cf7f0d 100644 --- a/gitnexus/test/unit/lazy-action.test.ts +++ b/gitnexus/test/unit/lazy-action.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it, vi } from 'vitest'; import { createLazyAction } from '../../src/cli/lazy-action.js'; +const { checkLbugNativeMock } = vi.hoisted(() => ({ + checkLbugNativeMock: vi.fn(() => ({ ok: true })), +})); + +vi.mock('../../src/core/lbug/native-check.js', () => ({ + checkLbugNative: checkLbugNativeMock, +})); + describe('createLazyAction', () => { it('does not import target module until invoked', async () => { const loader = vi.fn(async () => ({ @@ -19,3 +27,34 @@ describe('createLazyAction', () => { await expect(action()).rejects.toThrow('notAFunction'); }); }); + +describe('createLbugLazyAction', () => { + it('fails before importing the target module when LadybugDB native cannot load', async () => { + checkLbugNativeMock.mockReturnValueOnce({ + ok: false, + message: + 'LadybugDB native binary (lbugjs.node) exists but failed to load:\n' + ' dlopen failed', + }); + const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + process.exitCode = undefined; + const loader = vi.fn(async () => ({ + run: vi.fn(async () => 'ok'), + })); + + try { + const { createLbugLazyAction } = await import('../../src/cli/lazy-action.js'); + const action = createLbugLazyAction(loader, 'run'); + + await expect(action('arg-1')).resolves.toBeUndefined(); + + expect(loader).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(1); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('LadybugDB native binary (lbugjs.node) exists but failed to load:'), + ); + } finally { + stderrSpy.mockRestore(); + process.exitCode = undefined; + } + }); +}); From d27b1ab8c45b5054b9e50611ec1a578f7c960b1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:51:25 +0100 Subject: [PATCH 09/11] chore(deps)(deps-dev): bump @babel/parser in /gitnexus (#2521) Bumps [@babel/parser](https://github.com/babel/babel/tree/HEAD/packages/babel-parser) from 7.29.7 to 8.0.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.0/packages/babel-parser) --- updated-dependencies: - dependency-name: "@babel/parser" dependency-version: 8.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus/package-lock.json | 110 +++++++++++++++++++++++++++++++++++-- gitnexus/package.json | 2 +- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index d33becd22..5715b617c 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -51,7 +51,7 @@ }, "devDependencies": { "@babel/generator": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^8.0.0", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", @@ -121,6 +121,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -152,19 +168,53 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0.tgz", + "integrity": "sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^8.0.0" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/template": { @@ -182,6 +232,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/traverse": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", @@ -201,6 +267,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -3978,6 +4060,22 @@ "source-map-js": "^1.2.1" } }, + "node_modules/magicast/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 860fad1b4..0dfb7f1ea 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -98,7 +98,7 @@ }, "devDependencies": { "@babel/generator": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^8.0.0", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", From 795f81127b0a202a42d55b9e326786c7c4052e88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:54:48 +0100 Subject: [PATCH 10/11] chore(deps)(deps-dev): bump tsx from 4.23.0 to 4.23.1 in /gitnexus (#2517) Bumps [tsx](https://github.com/privatenumber/tsx) from 4.23.0 to 4.23.1. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.23.0...v4.23.1) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.23.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 5715b617c..da8e6fc99 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -5472,9 +5472,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", - "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "dev": true, "license": "MIT", "dependencies": { From 91955e657639cdf3a098162a2a632b505a563ffa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:51:51 +0100 Subject: [PATCH 11/11] chore(deps)(deps-dev): bump @babel/traverse in /gitnexus (#2520) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.29.7 to 8.0.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.0/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-version: 8.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus/package-lock.json | 169 +++++++++++++++++++++++++------------ gitnexus/package.json | 2 +- 2 files changed, 117 insertions(+), 54 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index da8e6fc99..b73eaf40c 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -52,7 +52,7 @@ "devDependencies": { "@babel/generator": "^7.29.7", "@babel/parser": "^8.0.0", - "@babel/traverse": "^7.29.7", + "@babel/traverse": "^8.0.0", "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6", @@ -83,26 +83,28 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", + "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@babel/helper-validator-identifier": "^8.0.0", + "js-tokens": "^10.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/code-frame/node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/@babel/code-frame/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } }, "node_modules/@babel/generator": { "version": "7.29.7", @@ -138,13 +140,13 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz", + "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-string-parser": { @@ -218,69 +220,123 @@ } }, "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "node_modules/@babel/template/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz", + "integrity": "sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-globals": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.0", + "obug": "^2.1.1" }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/types": { @@ -2027,6 +2083,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 0dfb7f1ea..d1ae23b56 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -99,7 +99,7 @@ "devDependencies": { "@babel/generator": "^7.29.7", "@babel/parser": "^8.0.0", - "@babel/traverse": "^7.29.7", + "@babel/traverse": "^8.0.0", "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6",