gitnexus 1.3.10 → 1.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/ai-context.js +18 -0
- package/dist/cli/analyze.js +8 -0
- package/dist/cli/setup.js +17 -19
- package/dist/core/ingestion/pipeline.js +15 -2
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/kuzu/kuzu-adapter.js +2 -2
- package/dist/core/search/bm25-index.js +2 -1
- package/dist/mcp/core/kuzu-adapter.js +6 -18
- package/hooks/claude/gitnexus-hook.cjs +149 -66
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +1 -1
package/dist/cli/ai-context.js
CHANGED
|
@@ -96,6 +96,24 @@ Before completing any code modification task, verify:
|
|
|
96
96
|
3. \`gitnexus_detect_changes()\` confirms changes match expected scope
|
|
97
97
|
4. All d=1 (WILL BREAK) dependents were updated
|
|
98
98
|
|
|
99
|
+
## Keeping the Index Fresh
|
|
100
|
+
|
|
101
|
+
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
|
102
|
+
|
|
103
|
+
\`\`\`bash
|
|
104
|
+
npx gitnexus analyze
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
If the index previously included embeddings, preserve them by adding \`--embeddings\`:
|
|
108
|
+
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
npx gitnexus analyze --embeddings
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
To check whether embeddings exist, inspect \`.gitnexus/meta.json\` — the \`stats.embeddings\` field shows the count (0 means no embeddings). **Running analyze without \`--embeddings\` will delete any previously generated embeddings.**
|
|
114
|
+
|
|
115
|
+
> Claude Code users: A PostToolUse hook handles this automatically after \`git commit\` and \`git merge\`.
|
|
116
|
+
|
|
99
117
|
## CLI
|
|
100
118
|
|
|
101
119
|
- Re-index: \`npx gitnexus analyze\`
|
package/dist/cli/analyze.js
CHANGED
|
@@ -242,6 +242,13 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
242
242
|
}
|
|
243
243
|
// ── Phase 5: Finalize (98–100%) ───────────────────────────────────
|
|
244
244
|
updateBar(98, 'Saving metadata...');
|
|
245
|
+
// Count embeddings in the index (cached + newly generated)
|
|
246
|
+
let embeddingCount = 0;
|
|
247
|
+
try {
|
|
248
|
+
const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
|
|
249
|
+
embeddingCount = embResult?.[0]?.cnt ?? 0;
|
|
250
|
+
}
|
|
251
|
+
catch { /* table may not exist if embeddings never ran */ }
|
|
245
252
|
const meta = {
|
|
246
253
|
repoPath,
|
|
247
254
|
lastCommit: currentCommit,
|
|
@@ -252,6 +259,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
252
259
|
edges: stats.edges,
|
|
253
260
|
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
254
261
|
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
262
|
+
embeddings: embeddingCount,
|
|
255
263
|
},
|
|
256
264
|
};
|
|
257
265
|
await saveMeta(storagePath, meta);
|
package/dist/cli/setup.js
CHANGED
|
@@ -147,36 +147,34 @@ async function installClaudeCodeHooks(result) {
|
|
|
147
147
|
// even when it's no longer inside the npm package tree
|
|
148
148
|
const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
|
|
149
149
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
150
|
-
|
|
150
|
+
const jsonCli = JSON.stringify(normalizedCli);
|
|
151
|
+
content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
|
|
151
152
|
await fs.writeFile(dest, content, 'utf-8');
|
|
152
153
|
}
|
|
153
154
|
catch {
|
|
154
155
|
// Script not found in source — skip
|
|
155
156
|
}
|
|
156
|
-
const
|
|
157
|
+
const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
|
|
158
|
+
const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
|
|
157
159
|
// Merge hook config into ~/.claude/settings.json
|
|
158
160
|
const existing = await readJsonFile(settingsPath) || {};
|
|
159
161
|
if (!existing.hooks)
|
|
160
162
|
existing.hooks = {};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
type: 'command',
|
|
172
|
-
command: hookCmd,
|
|
173
|
-
timeout: 8000,
|
|
174
|
-
statusMessage: 'Enriching with GitNexus graph context...',
|
|
175
|
-
}],
|
|
176
|
-
});
|
|
163
|
+
function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
|
|
164
|
+
if (!existing.hooks[eventName])
|
|
165
|
+
existing.hooks[eventName] = [];
|
|
166
|
+
const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some(hh => hh.command?.includes('gitnexus-hook')));
|
|
167
|
+
if (!hasHook) {
|
|
168
|
+
existing.hooks[eventName].push({
|
|
169
|
+
matcher,
|
|
170
|
+
hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
177
173
|
}
|
|
174
|
+
ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
|
|
175
|
+
ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
|
|
178
176
|
await writeJsonFile(settingsPath, existing);
|
|
179
|
-
result.configured.push('Claude Code hooks (PreToolUse)');
|
|
177
|
+
result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
|
|
180
178
|
}
|
|
181
179
|
catch (err) {
|
|
182
180
|
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
@@ -12,6 +12,9 @@ import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
|
|
|
12
12
|
import { getLanguageFromFilename } from './utils.js';
|
|
13
13
|
import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
|
14
14
|
import { createWorkerPool } from './workers/worker-pool.js';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
18
|
const isDev = process.env.NODE_ENV === 'development';
|
|
16
19
|
/** Max bytes of source content to load per parse chunk. Each chunk's source +
|
|
17
20
|
* parsed ASTs + extracted records + worker serialization overhead all live in
|
|
@@ -124,11 +127,21 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
124
127
|
// Create worker pool once, reuse across chunks
|
|
125
128
|
let workerPool;
|
|
126
129
|
try {
|
|
127
|
-
|
|
130
|
+
let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
|
|
131
|
+
// When running under vitest, import.meta.url points to src/ where no .js exists.
|
|
132
|
+
// Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
|
|
133
|
+
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
|
134
|
+
if (!fs.existsSync(fileURLToPath(workerUrl))) {
|
|
135
|
+
const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
|
|
136
|
+
if (fs.existsSync(distWorker)) {
|
|
137
|
+
workerUrl = pathToFileURL(distWorker);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
128
140
|
workerPool = createWorkerPool(workerUrl);
|
|
129
141
|
}
|
|
130
142
|
catch (err) {
|
|
131
|
-
|
|
143
|
+
if (isDev)
|
|
144
|
+
console.warn('Worker pool creation failed, using sequential fallback:', err.message);
|
|
132
145
|
}
|
|
133
146
|
let filesParsedSoFar = 0;
|
|
134
147
|
// AST cache sized for one chunk (sequential fallback uses it for import/call/heritage)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
/**
|
|
4
6
|
* Max files to send to a worker in a single postMessage.
|
|
5
7
|
* Keeps structured-clone memory bounded per sub-batch.
|
|
@@ -12,6 +14,12 @@ const SUB_BATCH_TIMEOUT_MS = 30_000;
|
|
|
12
14
|
* Create a pool of worker threads.
|
|
13
15
|
*/
|
|
14
16
|
export const createWorkerPool = (workerUrl, poolSize) => {
|
|
17
|
+
// Validate worker script exists before spawning to prevent uncaught
|
|
18
|
+
// MODULE_NOT_FOUND crashes in worker threads (e.g. when running from src/ via vitest)
|
|
19
|
+
const workerPath = fileURLToPath(workerUrl);
|
|
20
|
+
if (!fs.existsSync(workerPath)) {
|
|
21
|
+
throw new Error(`Worker script not found: ${workerPath}`);
|
|
22
|
+
}
|
|
15
23
|
const size = poolSize ?? Math.min(8, Math.max(1, os.cpus().length - 1));
|
|
16
24
|
const workers = [];
|
|
17
25
|
for (let i = 0; i < size; i++) {
|
|
@@ -711,8 +711,8 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
|
|
|
711
711
|
if (!conn) {
|
|
712
712
|
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
713
713
|
}
|
|
714
|
-
// Escape single quotes
|
|
715
|
-
const escapedQuery = query.replace(/'/g, "''");
|
|
714
|
+
// Escape backslashes and single quotes to prevent Cypher injection
|
|
715
|
+
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
716
716
|
const cypher = `
|
|
717
717
|
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := ${conjunctive})
|
|
718
718
|
RETURN node, score
|
|
@@ -10,7 +10,8 @@ import { queryFTS } from '../kuzu/kuzu-adapter.js';
|
|
|
10
10
|
* Returns the same shape as core queryFTS.
|
|
11
11
|
*/
|
|
12
12
|
async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
|
|
13
|
-
|
|
13
|
+
// Escape single quotes and backslashes to prevent Cypher injection
|
|
14
|
+
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
14
15
|
const cypher = `
|
|
15
16
|
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := false)
|
|
16
17
|
RETURN node, score
|
|
@@ -64,26 +64,14 @@ function evictLRU() {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
67
|
+
* Remove a repo from the pool without calling native close methods.
|
|
68
|
+
*
|
|
69
|
+
* KuzuDB's native .closeSync() triggers N-API destructor hooks that
|
|
70
|
+
* segfault on Linux/macOS. Pool databases are opened read-only, so
|
|
71
|
+
* there is no WAL to flush — just deleting the pool entry and letting
|
|
72
|
+
* the GC (or process exit) reclaim native resources is safe.
|
|
68
73
|
*/
|
|
69
74
|
function closeOne(repoId) {
|
|
70
|
-
const entry = pool.get(repoId);
|
|
71
|
-
if (!entry)
|
|
72
|
-
return;
|
|
73
|
-
for (const conn of entry.available) {
|
|
74
|
-
try {
|
|
75
|
-
conn.close();
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
console.error('GitNexus [pool:close-conn]:', e instanceof Error ? e.message : e);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
entry.db.close();
|
|
83
|
-
}
|
|
84
|
-
catch (e) {
|
|
85
|
-
console.error('GitNexus [pool:close-db]:', e instanceof Error ? e.message : e);
|
|
86
|
-
}
|
|
87
75
|
pool.delete(repoId);
|
|
88
76
|
}
|
|
89
77
|
/**
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* GitNexus Claude Code Hook
|
|
4
4
|
*
|
|
5
|
-
* PreToolUse
|
|
6
|
-
*
|
|
5
|
+
* PreToolUse — intercepts Grep/Glob/Bash searches and augments
|
|
6
|
+
* with graph context from the GitNexus index.
|
|
7
|
+
* PostToolUse — detects stale index after git mutations and notifies
|
|
8
|
+
* the agent to reindex.
|
|
7
9
|
*
|
|
8
10
|
* NOTE: SessionStart hooks are broken on Windows (Claude Code bug).
|
|
9
11
|
* Session context is injected via CLAUDE.md / skills instead.
|
|
@@ -11,7 +13,7 @@
|
|
|
11
13
|
|
|
12
14
|
const fs = require('fs');
|
|
13
15
|
const path = require('path');
|
|
14
|
-
const {
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Read JSON input from stdin synchronously.
|
|
@@ -26,19 +28,19 @@ function readInput() {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
31
|
+
* Find the .gitnexus directory by walking up from startDir.
|
|
32
|
+
* Returns the path to .gitnexus/ or null if not found.
|
|
30
33
|
*/
|
|
31
|
-
function
|
|
34
|
+
function findGitNexusDir(startDir) {
|
|
32
35
|
let dir = startDir || process.cwd();
|
|
33
36
|
for (let i = 0; i < 5; i++) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
+
const candidate = path.join(dir, '.gitnexus');
|
|
38
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
37
39
|
const parent = path.dirname(dir);
|
|
38
40
|
if (parent === dir) break;
|
|
39
41
|
dir = parent;
|
|
40
42
|
}
|
|
41
|
-
return
|
|
43
|
+
return null;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/**
|
|
@@ -83,72 +85,153 @@ function extractPattern(toolName, toolInput) {
|
|
|
83
85
|
return null;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the gitnexus CLI path.
|
|
90
|
+
* 1. Relative path (works when script is inside npm package)
|
|
91
|
+
* 2. require.resolve (works when gitnexus is globally installed)
|
|
92
|
+
* 3. Fall back to npx (returns empty string)
|
|
93
|
+
*/
|
|
94
|
+
function resolveCliPath() {
|
|
95
|
+
let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
|
|
96
|
+
if (!fs.existsSync(cliPath)) {
|
|
97
|
+
try {
|
|
98
|
+
cliPath = require.resolve('gitnexus/dist/cli/index.js');
|
|
99
|
+
} catch {
|
|
100
|
+
cliPath = '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return cliPath;
|
|
104
|
+
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Spawn a gitnexus CLI command synchronously.
|
|
108
|
+
* Returns the stderr output (KuzuDB captures stdout at OS level).
|
|
109
|
+
*/
|
|
110
|
+
function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
111
|
+
const isWin = process.platform === 'win32';
|
|
112
|
+
if (cliPath) {
|
|
113
|
+
return spawnSync(
|
|
114
|
+
process.execPath,
|
|
115
|
+
[cliPath, ...args],
|
|
116
|
+
{ encoding: 'utf-8', timeout, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
// On Windows, invoke npx.cmd directly (no shell needed)
|
|
120
|
+
return spawnSync(
|
|
121
|
+
isWin ? 'npx.cmd' : 'npx',
|
|
122
|
+
['-y', 'gitnexus', ...args],
|
|
123
|
+
{ encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
/**
|
|
128
|
+
* PreToolUse handler — augment searches with graph context.
|
|
129
|
+
*/
|
|
130
|
+
function handlePreToolUse(input) {
|
|
131
|
+
const cwd = input.cwd || process.cwd();
|
|
132
|
+
if (!path.isAbsolute(cwd)) return;
|
|
133
|
+
if (!findGitNexusDir(cwd)) return;
|
|
98
134
|
|
|
99
|
-
|
|
135
|
+
const toolName = input.tool_name || '';
|
|
136
|
+
const toolInput = input.tool_input || {};
|
|
100
137
|
|
|
101
|
-
|
|
102
|
-
if (!pattern || pattern.length < 3) return;
|
|
138
|
+
if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
|
|
103
139
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// 2. require.resolve (works when gitnexus is globally installed)
|
|
107
|
-
// 3. Fall back to npx (works when neither is available)
|
|
108
|
-
let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
|
|
109
|
-
if (!fs.existsSync(cliPath)) {
|
|
110
|
-
try {
|
|
111
|
-
cliPath = require.resolve('gitnexus/dist/cli/index.js');
|
|
112
|
-
} catch {
|
|
113
|
-
cliPath = ''; // will use npx fallback
|
|
114
|
-
}
|
|
115
|
-
}
|
|
140
|
+
const pattern = extractPattern(toolName, toolInput);
|
|
141
|
+
if (!pattern || pattern.length < 3) return;
|
|
116
142
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let child;
|
|
123
|
-
if (cliPath) {
|
|
124
|
-
child = spawnSync(
|
|
125
|
-
process.execPath,
|
|
126
|
-
[cliPath, 'augment', pattern],
|
|
127
|
-
{ encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
128
|
-
);
|
|
129
|
-
} else {
|
|
130
|
-
// npx fallback
|
|
131
|
-
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
132
|
-
child = spawnSync(
|
|
133
|
-
cmd,
|
|
134
|
-
['-y', 'gitnexus', 'augment', pattern],
|
|
135
|
-
{ encoding: 'utf-8', timeout: 15000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
136
|
-
);
|
|
137
|
-
}
|
|
143
|
+
const cliPath = resolveCliPath();
|
|
144
|
+
let result = '';
|
|
145
|
+
try {
|
|
146
|
+
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
147
|
+
if (!child.error && child.status === 0) {
|
|
138
148
|
result = child.stderr || '';
|
|
139
|
-
} catch { /* graceful failure */ }
|
|
140
|
-
|
|
141
|
-
if (result && result.trim()) {
|
|
142
|
-
console.log(JSON.stringify({
|
|
143
|
-
hookSpecificOutput: {
|
|
144
|
-
hookEventName: 'PreToolUse',
|
|
145
|
-
additionalContext: result.trim()
|
|
146
|
-
}
|
|
147
|
-
}));
|
|
148
149
|
}
|
|
150
|
+
} catch { /* graceful failure */ }
|
|
151
|
+
|
|
152
|
+
if (result && result.trim()) {
|
|
153
|
+
sendHookResponse('PreToolUse', result.trim());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Emit a PostToolUse hook response with additional context for the agent.
|
|
159
|
+
*/
|
|
160
|
+
function sendHookResponse(hookEventName, message) {
|
|
161
|
+
console.log(JSON.stringify({
|
|
162
|
+
hookSpecificOutput: { hookEventName, additionalContext: message }
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* PostToolUse handler — detect index staleness after git mutations.
|
|
168
|
+
*
|
|
169
|
+
* Instead of spawning a full `gitnexus analyze` synchronously (which blocks
|
|
170
|
+
* the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
|
|
171
|
+
* lightweight staleness check: compare `git rev-parse HEAD` against the
|
|
172
|
+
* lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
|
|
173
|
+
* agent so it can decide when to reindex.
|
|
174
|
+
*/
|
|
175
|
+
function handlePostToolUse(input) {
|
|
176
|
+
const toolName = input.tool_name || '';
|
|
177
|
+
if (toolName !== 'Bash') return;
|
|
178
|
+
|
|
179
|
+
const command = (input.tool_input || {}).command || '';
|
|
180
|
+
if (!/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) return;
|
|
181
|
+
|
|
182
|
+
// Only proceed if the command succeeded
|
|
183
|
+
const toolOutput = input.tool_output || {};
|
|
184
|
+
if (toolOutput.exit_code !== undefined && toolOutput.exit_code !== 0) return;
|
|
185
|
+
|
|
186
|
+
const cwd = input.cwd || process.cwd();
|
|
187
|
+
if (!path.isAbsolute(cwd)) return;
|
|
188
|
+
const gitNexusDir = findGitNexusDir(cwd);
|
|
189
|
+
if (!gitNexusDir) return;
|
|
190
|
+
|
|
191
|
+
// Compare HEAD against last indexed commit — skip if unchanged
|
|
192
|
+
let currentHead = '';
|
|
193
|
+
try {
|
|
194
|
+
const headResult = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
195
|
+
encoding: 'utf-8', timeout: 3000, cwd, stdio: ['pipe', 'pipe', 'pipe'],
|
|
196
|
+
});
|
|
197
|
+
currentHead = (headResult.stdout || '').trim();
|
|
198
|
+
} catch { return; }
|
|
199
|
+
|
|
200
|
+
if (!currentHead) return;
|
|
201
|
+
|
|
202
|
+
let lastCommit = '';
|
|
203
|
+
let hadEmbeddings = false;
|
|
204
|
+
try {
|
|
205
|
+
const meta = JSON.parse(fs.readFileSync(path.join(gitNexusDir, 'meta.json'), 'utf-8'));
|
|
206
|
+
lastCommit = meta.lastCommit || '';
|
|
207
|
+
hadEmbeddings = (meta.stats && meta.stats.embeddings > 0);
|
|
208
|
+
} catch { /* no meta — treat as stale */ }
|
|
209
|
+
|
|
210
|
+
// If HEAD matches last indexed commit, no reindex needed
|
|
211
|
+
if (currentHead && currentHead === lastCommit) return;
|
|
212
|
+
|
|
213
|
+
const analyzeCmd = `npx gitnexus analyze${hadEmbeddings ? ' --embeddings' : ''}`;
|
|
214
|
+
sendHookResponse('PostToolUse',
|
|
215
|
+
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
|
216
|
+
`Run \`${analyzeCmd}\` to update the knowledge graph.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Dispatch map for hook events
|
|
221
|
+
const handlers = {
|
|
222
|
+
PreToolUse: handlePreToolUse,
|
|
223
|
+
PostToolUse: handlePostToolUse,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function main() {
|
|
227
|
+
try {
|
|
228
|
+
const input = readInput();
|
|
229
|
+
const handler = handlers[input.hook_event_name || ''];
|
|
230
|
+
if (handler) handler(input);
|
|
149
231
|
} catch (err) {
|
|
150
|
-
|
|
151
|
-
|
|
232
|
+
if (process.env.GITNEXUS_DEBUG) {
|
|
233
|
+
console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
|
|
234
|
+
}
|
|
152
235
|
}
|
|
153
236
|
}
|
|
154
237
|
|
package/package.json
CHANGED
package/skills/gitnexus-cli.md
CHANGED
|
@@ -22,7 +22,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
|
|
|
22
22
|
| `--force` | Force full re-index even if up to date |
|
|
23
23
|
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
|
24
24
|
|
|
25
|
-
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale.
|
|
25
|
+
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook runs `analyze` automatically after `git commit` and `git merge`, preserving embeddings if previously generated.
|
|
26
26
|
|
|
27
27
|
### status — Check index freshness
|
|
28
28
|
|