mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-21 00:21:30 +00:00
fix(claude): skip augment hook when server owns db
This commit is contained in:
parent
d69eadfb7f
commit
f9c70fcf81
5 changed files with 222 additions and 9 deletions
|
|
@ -102,6 +102,43 @@ function findGitNexusDir(startDir) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function isGitNexusServerCommand(command) {
|
||||
const hasServerMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(command);
|
||||
const hasGitNexus = /(?:^|[/\\\s])gitnexus(?:\.cmd)?(?:\s|$)/.test(command) ||
|
||||
/node_modules[/\\]gitnexus[/\\]/.test(command);
|
||||
return hasServerMode && hasGitNexus;
|
||||
}
|
||||
|
||||
function hasGitNexusServerOwner(gitNexusDir) {
|
||||
const dbPath = path.join(gitNexusDir, 'lbug');
|
||||
if (process.platform === 'win32' || !fs.existsSync(dbPath)) return false;
|
||||
|
||||
const lsof = spawnSync('lsof', ['-nP', '-t', '--', dbPath], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 1000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
|
||||
|
||||
const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
|
||||
for (const pid of pids) {
|
||||
if (Number(pid) === process.pid) continue;
|
||||
const ps = spawnSync('ps', ['-p', pid, '-o', 'command='], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 500,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (!ps.error && isGitNexusServerCommand(ps.stdout || '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractAugmentContext(stderr) {
|
||||
const output = (stderr || '').trim();
|
||||
const marker = output.indexOf('[GitNexus]');
|
||||
return marker === -1 ? '' : output.slice(marker).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract search pattern from tool input.
|
||||
*/
|
||||
|
|
@ -217,7 +254,8 @@ function sendHookResponse(hookEventName, message) {
|
|||
function handlePreToolUse(input) {
|
||||
const cwd = input.cwd || process.cwd();
|
||||
if (!path.isAbsolute(cwd)) return;
|
||||
if (!findGitNexusDir(cwd)) return;
|
||||
const gitNexusDir = findGitNexusDir(cwd);
|
||||
if (!gitNexusDir) return;
|
||||
|
||||
const toolName = input.tool_name || '';
|
||||
const toolInput = input.tool_input || {};
|
||||
|
|
@ -226,19 +264,20 @@ function handlePreToolUse(input) {
|
|||
|
||||
const pattern = extractPattern(toolName, toolInput);
|
||||
if (!pattern || pattern.length < 3) return;
|
||||
if (hasGitNexusServerOwner(gitNexusDir)) return;
|
||||
|
||||
let result = '';
|
||||
try {
|
||||
const child = runGitNexusCli(['augment', '--', pattern], cwd, 7000);
|
||||
if (!child.error && child.status === 0) {
|
||||
result = child.stderr || '';
|
||||
result = extractAugmentContext(child.stderr || '');
|
||||
}
|
||||
} catch {
|
||||
/* graceful failure */
|
||||
}
|
||||
|
||||
if (result && result.trim()) {
|
||||
sendHookResponse('PreToolUse', result.trim());
|
||||
if (result) {
|
||||
sendHookResponse('PreToolUse', result);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,43 @@ function findGitNexusDir(startDir) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function isGitNexusServerCommand(command) {
|
||||
const hasServerMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(command);
|
||||
const hasGitNexus = /(?:^|[/\\\s])gitnexus(?:\.cmd)?(?:\s|$)/.test(command) ||
|
||||
/node_modules[/\\]gitnexus[/\\]/.test(command);
|
||||
return hasServerMode && hasGitNexus;
|
||||
}
|
||||
|
||||
function hasGitNexusServerOwner(gitNexusDir) {
|
||||
const dbPath = path.join(gitNexusDir, 'lbug');
|
||||
if (process.platform === 'win32' || !fs.existsSync(dbPath)) return false;
|
||||
|
||||
const lsof = spawnSync('lsof', ['-nP', '-t', '--', dbPath], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 1000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
|
||||
|
||||
const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
|
||||
for (const pid of pids) {
|
||||
if (Number(pid) === process.pid) continue;
|
||||
const ps = spawnSync('ps', ['-p', pid, '-o', 'command='], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 500,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (!ps.error && isGitNexusServerCommand(ps.stdout || '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractAugmentContext(stderr) {
|
||||
const output = (stderr || '').trim();
|
||||
const marker = output.indexOf('[GitNexus]');
|
||||
return marker === -1 ? '' : output.slice(marker).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract search pattern from tool input.
|
||||
*/
|
||||
|
|
@ -167,6 +204,9 @@ function extractPattern(toolName, toolInput) {
|
|||
* 3. Fall back to npx (returns empty string)
|
||||
*/
|
||||
function resolveCliPath() {
|
||||
if (process.env.GITNEXUS_HOOK_CLI_PATH !== undefined) {
|
||||
return process.env.GITNEXUS_HOOK_CLI_PATH;
|
||||
}
|
||||
let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
|
||||
if (!fs.existsSync(cliPath)) {
|
||||
try {
|
||||
|
|
@ -207,7 +247,8 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|||
function handlePreToolUse(input) {
|
||||
const cwd = input.cwd || process.cwd();
|
||||
if (!path.isAbsolute(cwd)) return;
|
||||
if (!findGitNexusDir(cwd)) return;
|
||||
const gitNexusDir = findGitNexusDir(cwd);
|
||||
if (!gitNexusDir) return;
|
||||
|
||||
const toolName = input.tool_name || '';
|
||||
const toolInput = input.tool_input || {};
|
||||
|
|
@ -216,20 +257,21 @@ function handlePreToolUse(input) {
|
|||
|
||||
const pattern = extractPattern(toolName, toolInput);
|
||||
if (!pattern || pattern.length < 3) return;
|
||||
if (hasGitNexusServerOwner(gitNexusDir)) return;
|
||||
|
||||
const cliPath = resolveCliPath();
|
||||
let result = '';
|
||||
try {
|
||||
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
||||
if (!child.error && child.status === 0) {
|
||||
result = child.stderr || '';
|
||||
result = extractAugmentContext(child.stderr || '');
|
||||
}
|
||||
} catch {
|
||||
/* graceful failure */
|
||||
}
|
||||
|
||||
if (result && result.trim()) {
|
||||
sendHookResponse('PreToolUse', result.trim());
|
||||
if (result) {
|
||||
sendHookResponse('PreToolUse', result);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
gitnexus/package-lock.json
generated
2
gitnexus/package-lock.json
generated
|
|
@ -63,7 +63,7 @@
|
|||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import type { WriteFileOptions } from 'fs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
|
@ -99,6 +100,45 @@ function createGlobalRegistry(homeDir: string, marker: 'both' | 'registry' | 're
|
|||
}
|
||||
}
|
||||
|
||||
function writeExecutable(filePath: string, content: string) {
|
||||
fs.writeFileSync(filePath, content, { mode: 0o755 } as WriteFileOptions);
|
||||
}
|
||||
|
||||
function createHookToolDir(options: {
|
||||
gitnexusStderr?: string;
|
||||
gitnexusMarkerPath?: string;
|
||||
lsofOutput?: string;
|
||||
psOutput?: string;
|
||||
}) {
|
||||
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-hook-bin-'));
|
||||
const gitnexusStderr = JSON.stringify(options.gitnexusStderr ?? '');
|
||||
const markerPath = JSON.stringify(options.gitnexusMarkerPath ?? '');
|
||||
|
||||
const fakeGitNexus = `#!/usr/bin/env node\nconst fs = require('fs');\nconst marker = ${markerPath};\nif (marker) fs.writeFileSync(marker, 'called');\nprocess.stderr.write(${gitnexusStderr});\n`;
|
||||
writeExecutable(path.join(binDir, 'gitnexus'), fakeGitNexus);
|
||||
writeExecutable(path.join(binDir, 'gitnexus-cli.js'), fakeGitNexus);
|
||||
|
||||
writeExecutable(
|
||||
path.join(binDir, 'lsof'),
|
||||
`#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(options.lsofOutput ?? '')});\nprocess.exit(0);\n`,
|
||||
);
|
||||
|
||||
writeExecutable(
|
||||
path.join(binDir, 'ps'),
|
||||
`#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(options.psOutput ?? '')});\nprocess.exit(0);\n`,
|
||||
);
|
||||
|
||||
return binDir;
|
||||
}
|
||||
|
||||
function hookEnv(binDir: string) {
|
||||
return {
|
||||
...process.env,
|
||||
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
GITNEXUS_HOOK_CLI_PATH: path.join(binDir, 'gitnexus-cli.js'),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Both hook files should exist ───────────────────────────────────
|
||||
|
||||
describe('Hook files exist', () => {
|
||||
|
|
@ -294,6 +334,96 @@ describe('Git mutation regex', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// ─── Integration: PreToolUse augmentation filtering ─────────────────
|
||||
|
||||
describe('PreToolUse augmentation filtering (integration)', () => {
|
||||
for (const [label, hookPath] of [
|
||||
['CJS', CJS_HOOK],
|
||||
['Plugin', PLUGIN_HOOK],
|
||||
] as const) {
|
||||
it(`${label}: emits valid GitNexus augmentation context`, () => {
|
||||
const binDir = createHookToolDir({
|
||||
gitnexusStderr: '[GitNexus] 1 related symbol found:\n\nvalidateUser (src/auth.ts)\n',
|
||||
});
|
||||
try {
|
||||
const result = runHook(
|
||||
hookPath,
|
||||
{
|
||||
hook_event_name: 'PreToolUse',
|
||||
tool_name: 'Grep',
|
||||
tool_input: { pattern: 'validateUser' },
|
||||
cwd: tmpDir,
|
||||
},
|
||||
undefined,
|
||||
{ env: hookEnv(binDir) },
|
||||
);
|
||||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.hookEventName).toBe('PreToolUse');
|
||||
expect(output!.additionalContext).toContain('[GitNexus] 1 related symbol found');
|
||||
} finally {
|
||||
fs.rmSync(binDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it(`${label}: suppresses LadybugDB lock warnings from augment stderr`, () => {
|
||||
const binDir = createHookToolDir({
|
||||
gitnexusStderr:
|
||||
'GitNexus: FTS extension load failed: IO exception: Could not set lock on file : /tmp/repo/.gitnexus/lbug\n',
|
||||
});
|
||||
try {
|
||||
const result = runHook(
|
||||
hookPath,
|
||||
{
|
||||
hook_event_name: 'PreToolUse',
|
||||
tool_name: 'Grep',
|
||||
tool_input: { pattern: 'validateUser' },
|
||||
cwd: tmpDir,
|
||||
},
|
||||
undefined,
|
||||
{ env: hookEnv(binDir) },
|
||||
);
|
||||
|
||||
expect(result.stdout.trim()).toBe('');
|
||||
} finally {
|
||||
fs.rmSync(binDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it(`${label}: skips augment when a GitNexus MCP process owns the repo DB`, () => {
|
||||
const markerPath = path.join(os.tmpdir(), `gitnexus-hook-called-${process.pid}-${label}`);
|
||||
const lbugPath = path.join(gitNexusDir, 'lbug');
|
||||
fs.writeFileSync(lbugPath, '');
|
||||
fs.rmSync(markerPath, { force: true });
|
||||
const binDir = createHookToolDir({
|
||||
gitnexusMarkerPath: markerPath,
|
||||
lsofOutput: '12345\n',
|
||||
psOutput: 'node /tmp/node_modules/.bin/gitnexus mcp\n',
|
||||
});
|
||||
try {
|
||||
const result = runHook(
|
||||
hookPath,
|
||||
{
|
||||
hook_event_name: 'PreToolUse',
|
||||
tool_name: 'Grep',
|
||||
tool_input: { pattern: 'validateUser' },
|
||||
cwd: tmpDir,
|
||||
},
|
||||
undefined,
|
||||
{ env: hookEnv(binDir) },
|
||||
);
|
||||
|
||||
expect(result.stdout.trim()).toBe('');
|
||||
expect(fs.existsSync(markerPath)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(markerPath, { force: true });
|
||||
fs.rmSync(binDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Integration: PostToolUse staleness detection ───────────────────
|
||||
|
||||
describe('PostToolUse staleness detection (integration)', () => {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ export function runHook(
|
|||
hookPath: string,
|
||||
input: Record<string, any>,
|
||||
cwd?: string,
|
||||
options: { env?: NodeJS.ProcessEnv } = {},
|
||||
): { stdout: string; stderr: string; status: number | null } {
|
||||
const result = spawnSync(process.execPath, [hookPath], {
|
||||
input: JSON.stringify(input),
|
||||
encoding: 'utf-8',
|
||||
timeout: 10000,
|
||||
cwd,
|
||||
env: options.env,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue