gitnexus 1.1.7 → 1.1.9
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 +29 -24
- package/dist/cli/analyze.js +50 -40
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/claude-hooks.d.ts +22 -0
- package/dist/cli/claude-hooks.js +97 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/setup.js +147 -5
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +213 -0
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.js +1 -1
- package/dist/core/ingestion/import-processor.js +1 -1
- package/dist/core/ingestion/pipeline.js +1 -1
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/kuzu/kuzu-adapter.js +13 -9
- package/dist/core/search/bm25-index.js +13 -15
- package/dist/mcp/local/local-backend.d.ts +54 -1
- package/dist/mcp/local/local-backend.js +707 -169
- package/dist/mcp/resources.d.ts +1 -1
- package/dist/mcp/resources.js +111 -73
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +90 -21
- package/dist/mcp/tools.js +80 -61
- package/hooks/claude/gitnexus-hook.cjs +135 -0
- package/hooks/claude/pre-tool-use.sh +78 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +4 -2
- package/skills/debugging.md +24 -22
- package/skills/exploring.md +26 -24
- package/skills/impact-analysis.md +19 -13
- package/skills/refactoring.md +37 -26
package/dist/cli/ai-context.js
CHANGED
|
@@ -17,55 +17,60 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
|
17
17
|
* Generate the full GitNexus context content.
|
|
18
18
|
*
|
|
19
19
|
* Design principles (learned from real agent behavior):
|
|
20
|
-
* - AGENTS.md is the ROUTER — it tells the agent WHICH skill to
|
|
20
|
+
* - AGENTS.md is the ROUTER — it tells the agent WHICH skill to read
|
|
21
21
|
* - Skills contain the actual workflows — AGENTS.md does NOT duplicate them
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
22
|
+
* - Bold **IMPORTANT** block + "Skills — Read First" heading — agents skip soft suggestions
|
|
23
|
+
* - One-line quick start (read context resource) gives agents an entry point
|
|
24
|
+
* - Tools/Resources sections are labeled "Reference" — agents treat them as lookup, not workflow
|
|
24
25
|
*/
|
|
25
26
|
function generateGitNexusContent(projectName, stats) {
|
|
26
|
-
const clusterCount = stats.clusters || stats.communities || 0;
|
|
27
27
|
return `${GITNEXUS_START_MARKER}
|
|
28
28
|
# GitNexus MCP
|
|
29
29
|
|
|
30
|
-
This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${
|
|
30
|
+
This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows).
|
|
31
31
|
|
|
32
|
-
GitNexus
|
|
32
|
+
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
## Always Start Here
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
For any task involving code understanding, debugging, impact analysis, or refactoring, you must:
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
1. **Read \`gitnexus://repo/{name}/context\`** — codebase overview + check index freshness
|
|
39
|
+
2. **Match your task to a skill below** and **read that skill file**
|
|
40
|
+
3. **Follow the skill's workflow and checklist**
|
|
41
|
+
|
|
42
|
+
> If step 1 warns the index is stale, run \`npx gitnexus analyze\` in the terminal first.
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
## Skills
|
|
41
45
|
|
|
42
|
-
| Task |
|
|
43
|
-
|
|
44
|
-
| Understand architecture / "How does X work?" |
|
|
45
|
-
| Blast radius / "What breaks if I change X?" |
|
|
46
|
-
| Trace bugs / "Why is X failing?" |
|
|
47
|
-
| Rename / extract / split / refactor |
|
|
46
|
+
| Task | Read this skill file |
|
|
47
|
+
|------|---------------------|
|
|
48
|
+
| Understand architecture / "How does X work?" | \`.claude/skills/gitnexus/exploring/SKILL.md\` |
|
|
49
|
+
| Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus/impact-analysis/SKILL.md\` |
|
|
50
|
+
| Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus/debugging/SKILL.md\` |
|
|
51
|
+
| Rename / extract / split / refactor | \`.claude/skills/gitnexus/refactoring/SKILL.md\` |
|
|
48
52
|
|
|
49
|
-
## Tools
|
|
53
|
+
## Tools Reference
|
|
50
54
|
|
|
51
55
|
| Tool | What it gives you |
|
|
52
56
|
|------|-------------------|
|
|
53
|
-
| \`
|
|
54
|
-
| \`
|
|
55
|
-
| \`impact\` |
|
|
56
|
-
| \`
|
|
57
|
+
| \`query\` | Process-grouped code intelligence — execution flows related to a concept |
|
|
58
|
+
| \`context\` | 360-degree symbol view — categorized refs, processes it participates in |
|
|
59
|
+
| \`impact\` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
|
|
60
|
+
| \`detect_changes\` | Git-diff impact — what do your current changes affect |
|
|
61
|
+
| \`rename\` | Multi-file coordinated rename with confidence-tagged edits |
|
|
57
62
|
| \`cypher\` | Raw graph queries (read \`gitnexus://repo/{name}/schema\` first) |
|
|
58
63
|
| \`list_repos\` | Discover indexed repos |
|
|
59
64
|
|
|
60
|
-
## Resources
|
|
65
|
+
## Resources Reference
|
|
61
66
|
|
|
62
67
|
Lightweight reads (~100-500 tokens) for navigation:
|
|
63
68
|
|
|
64
69
|
| Resource | Content |
|
|
65
70
|
|----------|---------|
|
|
66
71
|
| \`gitnexus://repo/{name}/context\` | Stats, staleness check |
|
|
67
|
-
| \`gitnexus://repo/{name}/clusters\` | All
|
|
68
|
-
| \`gitnexus://repo/{name}/cluster/{clusterName}\` |
|
|
72
|
+
| \`gitnexus://repo/{name}/clusters\` | All functional areas with cohesion scores |
|
|
73
|
+
| \`gitnexus://repo/{name}/cluster/{clusterName}\` | Area members |
|
|
69
74
|
| \`gitnexus://repo/{name}/processes\` | All execution flows |
|
|
70
75
|
| \`gitnexus://repo/{name}/process/{processName}\` | Step-by-step trace |
|
|
71
76
|
| \`gitnexus://repo/{name}/schema\` | Graph schema for Cypher |
|
package/dist/cli/analyze.js
CHANGED
|
@@ -4,17 +4,29 @@
|
|
|
4
4
|
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
5
|
*/
|
|
6
6
|
import path from 'path';
|
|
7
|
-
import
|
|
7
|
+
import cliProgress from 'cli-progress';
|
|
8
8
|
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
9
9
|
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex } from '../core/kuzu/kuzu-adapter.js';
|
|
10
10
|
import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
|
|
11
|
-
import {
|
|
11
|
+
import { disposeEmbedder } from '../core/embeddings/embedder.js';
|
|
12
|
+
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, getGlobalDir } from '../storage/repo-manager.js';
|
|
12
13
|
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
13
14
|
import { generateAIContextFiles } from './ai-context.js';
|
|
14
15
|
import fs from 'fs/promises';
|
|
16
|
+
import { registerClaudeHook } from './claude-hooks.js';
|
|
17
|
+
const PHASE_LABELS = {
|
|
18
|
+
extracting: 'Scanning files',
|
|
19
|
+
structure: 'Building structure',
|
|
20
|
+
parsing: 'Parsing code',
|
|
21
|
+
imports: 'Resolving imports',
|
|
22
|
+
calls: 'Tracing calls',
|
|
23
|
+
heritage: 'Extracting inheritance',
|
|
24
|
+
communities: 'Detecting communities',
|
|
25
|
+
processes: 'Detecting processes',
|
|
26
|
+
complete: 'Complete',
|
|
27
|
+
};
|
|
15
28
|
export const analyzeCommand = async (inputPath, options) => {
|
|
16
|
-
|
|
17
|
-
// If path provided, use it directly. Otherwise, find git root from cwd.
|
|
29
|
+
console.log('\n GitNexus Analyzer\n');
|
|
18
30
|
let repoPath;
|
|
19
31
|
if (inputPath) {
|
|
20
32
|
repoPath = path.resolve(inputPath);
|
|
@@ -22,36 +34,39 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
22
34
|
else {
|
|
23
35
|
const gitRoot = getGitRoot(process.cwd());
|
|
24
36
|
if (!gitRoot) {
|
|
25
|
-
|
|
37
|
+
console.log(' ✗ Not inside a git repository\n');
|
|
26
38
|
process.exitCode = 1;
|
|
27
39
|
return;
|
|
28
40
|
}
|
|
29
41
|
repoPath = gitRoot;
|
|
30
42
|
}
|
|
31
43
|
if (!isGitRepo(repoPath)) {
|
|
32
|
-
|
|
44
|
+
console.log(' ✗ Not a git repository\n');
|
|
33
45
|
process.exitCode = 1;
|
|
34
46
|
return;
|
|
35
47
|
}
|
|
36
48
|
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
|
37
49
|
const currentCommit = getCurrentCommit(repoPath);
|
|
38
50
|
const existingMeta = await loadMeta(storagePath);
|
|
39
|
-
// Skip if already indexed at same commit
|
|
40
51
|
if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
|
|
41
|
-
|
|
52
|
+
console.log(' ✓ Repository already up to date\n');
|
|
42
53
|
return;
|
|
43
54
|
}
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
const multibar = new cliProgress.MultiBar({
|
|
56
|
+
format: ' {bar} {percentage}% | {phase}',
|
|
57
|
+
barCompleteChar: '█',
|
|
58
|
+
barIncompleteChar: '░',
|
|
59
|
+
hideCursor: true,
|
|
60
|
+
barGlue: '',
|
|
61
|
+
autopadding: true,
|
|
62
|
+
}, cliProgress.Presets.shades_grey);
|
|
63
|
+
const progressBar = multibar.create(100, 0, { phase: 'Initializing...' });
|
|
46
64
|
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
47
|
-
|
|
65
|
+
const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
66
|
+
progressBar.update(progress.percent, { phase: phaseLabel });
|
|
48
67
|
});
|
|
49
|
-
|
|
50
|
-
// Always start fresh - remove existing kuzu DB to avoid stale/corrupt data
|
|
51
|
-
spinner.text = 'Loading graph into KuzuDB...';
|
|
68
|
+
progressBar.update(100, { phase: 'Loading graph into KuzuDB...' });
|
|
52
69
|
await closeKuzu();
|
|
53
|
-
// Kuzu 0.11 stores databases as: <name> (main file) + <name>.wal (WAL file)
|
|
54
|
-
// BOTH must be deleted or kuzu will find the orphaned WAL and corrupt the database
|
|
55
70
|
const fsClean = await import('fs/promises');
|
|
56
71
|
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
|
|
57
72
|
for (const f of kuzuFiles) {
|
|
@@ -62,9 +77,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
62
77
|
}
|
|
63
78
|
await initKuzu(kuzuPath);
|
|
64
79
|
await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
|
|
65
|
-
|
|
66
|
-
// Indexes searchable content on: File, Function, Class, Method
|
|
67
|
-
spinner.text = 'Creating FTS indexes...';
|
|
80
|
+
progressBar.update(100, { phase: 'Creating search indexes...' });
|
|
68
81
|
try {
|
|
69
82
|
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
70
83
|
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
@@ -73,17 +86,14 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
73
86
|
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
|
74
87
|
}
|
|
75
88
|
catch (e) {
|
|
76
|
-
|
|
77
|
-
console.error('Note: Some FTS indexes may not have been created:', e.message);
|
|
89
|
+
console.error(' Note: Some FTS indexes may not have been created:', e.message);
|
|
78
90
|
}
|
|
79
|
-
// Generate embeddings
|
|
80
91
|
if (!options?.skipEmbeddings) {
|
|
81
|
-
|
|
92
|
+
progressBar.update(100, { phase: 'Generating embeddings...' });
|
|
82
93
|
await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
|
|
83
|
-
|
|
94
|
+
progressBar.update(progress.percent, { phase: `Embeddings ${progress.percent}%` });
|
|
84
95
|
});
|
|
85
96
|
}
|
|
86
|
-
// Save metadata
|
|
87
97
|
const stats = await getKuzuStats();
|
|
88
98
|
const meta = {
|
|
89
99
|
repoPath,
|
|
@@ -98,14 +108,11 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
98
108
|
},
|
|
99
109
|
};
|
|
100
110
|
await saveMeta(storagePath, meta);
|
|
101
|
-
// Register in global registry
|
|
102
111
|
await registerRepo(repoPath, meta);
|
|
103
|
-
// Add .gitnexus to .gitignore
|
|
104
112
|
await addToGitignore(repoPath);
|
|
105
|
-
//
|
|
113
|
+
// Auto-register Claude Code hook (idempotent)
|
|
114
|
+
const hookResult = await registerClaudeHook();
|
|
106
115
|
const projectName = path.basename(repoPath);
|
|
107
|
-
// Compute aggregated cluster count (grouped by heuristicLabel, >=5 symbols)
|
|
108
|
-
// This matches the aggregation logic in local-backend.ts for tool output consistency.
|
|
109
116
|
let aggregatedClusterCount = 0;
|
|
110
117
|
if (pipelineResult.communityResult?.communities) {
|
|
111
118
|
const groups = new Map();
|
|
@@ -123,22 +130,25 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
123
130
|
clusters: aggregatedClusterCount,
|
|
124
131
|
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
125
132
|
});
|
|
126
|
-
// Close database
|
|
127
133
|
await closeKuzu();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log(
|
|
131
|
-
console.log(`
|
|
134
|
+
await disposeEmbedder();
|
|
135
|
+
multibar.stop();
|
|
136
|
+
console.log('\n ✓ Repository indexed successfully\n');
|
|
137
|
+
console.log(` Path: ${repoPath}`);
|
|
138
|
+
console.log(` Storage: ${storagePath}`);
|
|
139
|
+
console.log(` Registry: ${getGlobalDir()}`);
|
|
140
|
+
console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges, ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters, ${pipelineResult.processResult?.stats.totalProcesses || 0} processes`);
|
|
132
141
|
if (aiContext.files.length > 0) {
|
|
133
|
-
console.log(`
|
|
142
|
+
console.log(` Context: ${aiContext.files.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
if (hookResult.registered) {
|
|
145
|
+
console.log(` Hooks: ${hookResult.message}`);
|
|
134
146
|
}
|
|
135
|
-
// Hint about setup if it hasn't been run
|
|
136
147
|
try {
|
|
137
148
|
await fs.access(getGlobalRegistryPath());
|
|
138
149
|
}
|
|
139
150
|
catch {
|
|
140
|
-
|
|
141
|
-
console.log('');
|
|
142
|
-
console.log(' Tip: Run `gitnexus setup` to configure MCP for your editor.');
|
|
151
|
+
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
|
|
143
152
|
}
|
|
153
|
+
console.log('');
|
|
144
154
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Augment CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Fast-path command for platform hooks.
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus augment <pattern>
|
|
8
|
+
* Returns enriched text to stdout.
|
|
9
|
+
*
|
|
10
|
+
* Performance: Must cold-start fast (<500ms).
|
|
11
|
+
* Skips unnecessary initialization (no web server, no full DB warmup).
|
|
12
|
+
*/
|
|
13
|
+
export declare function augmentCommand(pattern: string): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Augment CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Fast-path command for platform hooks.
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus augment <pattern>
|
|
8
|
+
* Returns enriched text to stdout.
|
|
9
|
+
*
|
|
10
|
+
* Performance: Must cold-start fast (<500ms).
|
|
11
|
+
* Skips unnecessary initialization (no web server, no full DB warmup).
|
|
12
|
+
*/
|
|
13
|
+
import { augment } from '../core/augmentation/engine.js';
|
|
14
|
+
export async function augmentCommand(pattern) {
|
|
15
|
+
if (!pattern || pattern.length < 3) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const result = await augment(pattern, process.cwd());
|
|
20
|
+
if (result) {
|
|
21
|
+
// IMPORTANT: Write to stderr, NOT stdout.
|
|
22
|
+
// KuzuDB's native module captures stdout fd at OS level during init,
|
|
23
|
+
// which makes stdout permanently broken in subprocess contexts.
|
|
24
|
+
// stderr is never captured, so it works reliably everywhere.
|
|
25
|
+
// The hook reads from the subprocess's stderr.
|
|
26
|
+
process.stderr.write(result + '\n');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Graceful failure — never break the calling hook
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Hook Registration
|
|
3
|
+
*
|
|
4
|
+
* Registers the GitNexus PreToolUse hook in ~/.claude/hooks.json
|
|
5
|
+
* so that grep/glob/bash calls are automatically augmented with
|
|
6
|
+
* knowledge graph context.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent — safe to call multiple times.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Register (or verify) the GitNexus hook in Claude Code's global hooks.json.
|
|
12
|
+
*
|
|
13
|
+
* - Creates ~/.claude/ and hooks.json if they don't exist
|
|
14
|
+
* - Preserves existing hooks from other tools
|
|
15
|
+
* - Skips if GitNexus hook is already registered
|
|
16
|
+
*
|
|
17
|
+
* Returns a status message for the CLI output.
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerClaudeHook(): Promise<{
|
|
20
|
+
registered: boolean;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Hook Registration
|
|
3
|
+
*
|
|
4
|
+
* Registers the GitNexus PreToolUse hook in ~/.claude/hooks.json
|
|
5
|
+
* so that grep/glob/bash calls are automatically augmented with
|
|
6
|
+
* knowledge graph context.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent — safe to call multiple times.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
/**
|
|
17
|
+
* Get the absolute path to the gitnexus-hook.js file.
|
|
18
|
+
* Works for both local dev and npm-installed packages.
|
|
19
|
+
*/
|
|
20
|
+
function getHookScriptPath() {
|
|
21
|
+
// From dist/cli/claude-hooks.js → hooks/claude/gitnexus-hook.js
|
|
22
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
23
|
+
return path.join(packageRoot, 'hooks', 'claude', 'gitnexus-hook.cjs');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register (or verify) the GitNexus hook in Claude Code's global hooks.json.
|
|
27
|
+
*
|
|
28
|
+
* - Creates ~/.claude/ and hooks.json if they don't exist
|
|
29
|
+
* - Preserves existing hooks from other tools
|
|
30
|
+
* - Skips if GitNexus hook is already registered
|
|
31
|
+
*
|
|
32
|
+
* Returns a status message for the CLI output.
|
|
33
|
+
*/
|
|
34
|
+
export async function registerClaudeHook() {
|
|
35
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
36
|
+
const hooksFile = path.join(claudeDir, 'hooks.json');
|
|
37
|
+
const hookScript = getHookScriptPath();
|
|
38
|
+
// Check if the hook script exists
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(hookScript);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { registered: false, message: 'Hook script not found (package may be incomplete)' };
|
|
44
|
+
}
|
|
45
|
+
// Build the hook command — use node + absolute path for reliability
|
|
46
|
+
const hookCommand = `node "${hookScript}"`;
|
|
47
|
+
// Check if ~/.claude/ exists (user has Claude Code installed)
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(claudeDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// No Claude Code installation — skip silently
|
|
53
|
+
return { registered: false, message: 'Claude Code not detected (~/.claude/ not found)' };
|
|
54
|
+
}
|
|
55
|
+
// Read existing hooks.json or start fresh
|
|
56
|
+
let hooksConfig = {};
|
|
57
|
+
try {
|
|
58
|
+
const existing = await fs.readFile(hooksFile, 'utf-8');
|
|
59
|
+
hooksConfig = JSON.parse(existing);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// File doesn't exist or is invalid — we'll create it
|
|
63
|
+
}
|
|
64
|
+
// Ensure the hooks structure exists
|
|
65
|
+
if (!hooksConfig.hooks) {
|
|
66
|
+
hooksConfig.hooks = {};
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(hooksConfig.hooks.PreToolUse)) {
|
|
69
|
+
hooksConfig.hooks.PreToolUse = [];
|
|
70
|
+
}
|
|
71
|
+
// Check if GitNexus hook is already registered
|
|
72
|
+
const existingEntry = hooksConfig.hooks.PreToolUse.find((entry) => {
|
|
73
|
+
if (!entry.hooks || !Array.isArray(entry.hooks))
|
|
74
|
+
return false;
|
|
75
|
+
return entry.hooks.some((h) => h.command && (h.command.includes('gitnexus-hook') ||
|
|
76
|
+
h.command.includes('gitnexus augment')));
|
|
77
|
+
});
|
|
78
|
+
if (existingEntry) {
|
|
79
|
+
return { registered: true, message: 'Claude Code hook already registered' };
|
|
80
|
+
}
|
|
81
|
+
// Add the GitNexus hook entry
|
|
82
|
+
hooksConfig.hooks.PreToolUse.push({
|
|
83
|
+
matcher: {
|
|
84
|
+
tool_name: "Grep|Glob|Bash"
|
|
85
|
+
},
|
|
86
|
+
hooks: [
|
|
87
|
+
{
|
|
88
|
+
type: "command",
|
|
89
|
+
command: hookCommand,
|
|
90
|
+
timeout: 8000
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
});
|
|
94
|
+
// Write back
|
|
95
|
+
await fs.writeFile(hooksFile, JSON.stringify(hooksConfig, null, 2) + '\n', 'utf-8');
|
|
96
|
+
return { registered: true, message: 'Claude Code hook registered' };
|
|
97
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { statusCommand } from './status.js';
|
|
|
7
7
|
import { mcpCommand } from './mcp.js';
|
|
8
8
|
import { cleanCommand } from './clean.js';
|
|
9
9
|
import { setupCommand } from './setup.js';
|
|
10
|
+
import { augmentCommand } from './augment.js';
|
|
10
11
|
const program = new Command();
|
|
11
12
|
program
|
|
12
13
|
.name('gitnexus')
|
|
@@ -45,4 +46,8 @@ program
|
|
|
45
46
|
.option('-f, --force', 'Skip confirmation prompt')
|
|
46
47
|
.option('--all', 'Clean all indexed repos')
|
|
47
48
|
.action(cleanCommand);
|
|
49
|
+
program
|
|
50
|
+
.command('augment <pattern>')
|
|
51
|
+
.description('Augment a search pattern with knowledge graph context (used by hooks)')
|
|
52
|
+
.action(augmentCommand);
|
|
48
53
|
program.parse(process.argv);
|
package/dist/cli/setup.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
11
12
|
import { getGlobalDir } from '../storage/repo-manager.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
12
15
|
/**
|
|
13
16
|
* The MCP server entry for all editors
|
|
14
17
|
*/
|
|
@@ -82,8 +85,6 @@ async function setupCursor(result) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
async function setupClaudeCode(result) {
|
|
85
|
-
// Claude Code uses `claude mcp add` — we just print the command
|
|
86
|
-
// Check for common Claude Code indicators
|
|
87
88
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
88
89
|
const hasClaude = await dirExists(claudeDir);
|
|
89
90
|
if (!hasClaude) {
|
|
@@ -92,11 +93,82 @@ async function setupClaudeCode(result) {
|
|
|
92
93
|
}
|
|
93
94
|
// Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add
|
|
94
95
|
console.log('');
|
|
95
|
-
console.log(' Claude Code detected. Run this command to add GitNexus:');
|
|
96
|
+
console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
|
|
96
97
|
console.log('');
|
|
97
98
|
console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
|
|
98
99
|
console.log('');
|
|
99
|
-
result.configured.push('Claude Code (manual step printed)');
|
|
100
|
+
result.configured.push('Claude Code (MCP manual step printed)');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Install GitNexus skills to ~/.claude/skills/ for Claude Code.
|
|
104
|
+
*/
|
|
105
|
+
async function installClaudeCodeSkills(result) {
|
|
106
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
107
|
+
if (!(await dirExists(claudeDir)))
|
|
108
|
+
return;
|
|
109
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
110
|
+
try {
|
|
111
|
+
const installed = await installSkillsTo(skillsDir);
|
|
112
|
+
if (installed.length > 0) {
|
|
113
|
+
result.configured.push(`Claude Code skills (${installed.length} skills → ~/.claude/skills/)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
result.errors.push(`Claude Code skills: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
122
|
+
* Merges hook config without overwriting existing hooks.
|
|
123
|
+
*/
|
|
124
|
+
async function installClaudeCodeHooks(result) {
|
|
125
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
126
|
+
if (!(await dirExists(claudeDir)))
|
|
127
|
+
return;
|
|
128
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
129
|
+
// Source hooks bundled within the gitnexus package (hooks/claude/)
|
|
130
|
+
const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
|
|
131
|
+
// Copy unified hook script to ~/.claude/hooks/gitnexus/
|
|
132
|
+
const destHooksDir = path.join(claudeDir, 'hooks', 'gitnexus');
|
|
133
|
+
try {
|
|
134
|
+
await fs.mkdir(destHooksDir, { recursive: true });
|
|
135
|
+
const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
|
|
136
|
+
const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
|
|
137
|
+
try {
|
|
138
|
+
const content = await fs.readFile(src, 'utf-8');
|
|
139
|
+
await fs.writeFile(dest, content, 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Script not found in source — skip
|
|
143
|
+
}
|
|
144
|
+
const hookCmd = `node "${path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/')}"`;
|
|
145
|
+
// Merge hook config into ~/.claude/settings.json
|
|
146
|
+
const existing = await readJsonFile(settingsPath) || {};
|
|
147
|
+
if (!existing.hooks)
|
|
148
|
+
existing.hooks = {};
|
|
149
|
+
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
|
|
150
|
+
// Session context is delivered via CLAUDE.md / skills instead.
|
|
151
|
+
// Add PreToolUse hook if not already present
|
|
152
|
+
if (!existing.hooks.PreToolUse)
|
|
153
|
+
existing.hooks.PreToolUse = [];
|
|
154
|
+
const hasPreToolHook = existing.hooks.PreToolUse.some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus')));
|
|
155
|
+
if (!hasPreToolHook) {
|
|
156
|
+
existing.hooks.PreToolUse.push({
|
|
157
|
+
matcher: 'Grep|Glob|Bash',
|
|
158
|
+
hooks: [{
|
|
159
|
+
type: 'command',
|
|
160
|
+
command: hookCmd,
|
|
161
|
+
timeout: 8000,
|
|
162
|
+
statusMessage: 'Enriching with GitNexus graph context...',
|
|
163
|
+
}],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
await writeJsonFile(settingsPath, existing);
|
|
167
|
+
result.configured.push('Claude Code hooks (PreToolUse)');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
171
|
+
}
|
|
100
172
|
}
|
|
101
173
|
async function setupOpenCode(result) {
|
|
102
174
|
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
@@ -118,6 +190,67 @@ async function setupOpenCode(result) {
|
|
|
118
190
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
119
191
|
}
|
|
120
192
|
}
|
|
193
|
+
// ─── Skill Installation ───────────────────────────────────────────
|
|
194
|
+
const SKILL_NAMES = ['exploring', 'debugging', 'impact-analysis', 'refactoring'];
|
|
195
|
+
/**
|
|
196
|
+
* Install GitNexus skills to a target directory.
|
|
197
|
+
* Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
|
|
198
|
+
* following the Agent Skills standard (both Cursor and Claude Code).
|
|
199
|
+
*/
|
|
200
|
+
async function installSkillsTo(targetDir) {
|
|
201
|
+
const installed = [];
|
|
202
|
+
for (const skillName of SKILL_NAMES) {
|
|
203
|
+
const sourcePath = path.join(__dirname, '..', '..', 'skills', `${skillName}.md`);
|
|
204
|
+
const skillDir = path.join(targetDir, `gitnexus-${skillName}`);
|
|
205
|
+
const destPath = path.join(skillDir, 'SKILL.md');
|
|
206
|
+
try {
|
|
207
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
208
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
209
|
+
await fs.writeFile(destPath, content, 'utf-8');
|
|
210
|
+
installed.push(skillName);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Source skill file not found — skip
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return installed;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Install global Cursor skills to ~/.cursor/skills/gitnexus/
|
|
220
|
+
*/
|
|
221
|
+
async function installCursorSkills(result) {
|
|
222
|
+
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
223
|
+
if (!(await dirExists(cursorDir)))
|
|
224
|
+
return;
|
|
225
|
+
const skillsDir = path.join(cursorDir, 'skills');
|
|
226
|
+
try {
|
|
227
|
+
const installed = await installSkillsTo(skillsDir);
|
|
228
|
+
if (installed.length > 0) {
|
|
229
|
+
result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
result.errors.push(`Cursor skills: ${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Install global OpenCode skills to ~/.config/opencode/skill/gitnexus/
|
|
238
|
+
*/
|
|
239
|
+
async function installOpenCodeSkills(result) {
|
|
240
|
+
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
241
|
+
if (!(await dirExists(opencodeDir)))
|
|
242
|
+
return;
|
|
243
|
+
const skillsDir = path.join(opencodeDir, 'skill');
|
|
244
|
+
try {
|
|
245
|
+
const installed = await installSkillsTo(skillsDir);
|
|
246
|
+
if (installed.length > 0) {
|
|
247
|
+
result.configured.push(`OpenCode skills (${installed.length} skills → ~/.config/opencode/skill/)`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
result.errors.push(`OpenCode skills: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
121
254
|
// ─── Main command ──────────────────────────────────────────────────
|
|
122
255
|
export const setupCommand = async () => {
|
|
123
256
|
console.log('');
|
|
@@ -132,10 +265,15 @@ export const setupCommand = async () => {
|
|
|
132
265
|
skipped: [],
|
|
133
266
|
errors: [],
|
|
134
267
|
};
|
|
135
|
-
// Detect and configure each editor
|
|
268
|
+
// Detect and configure each editor's MCP
|
|
136
269
|
await setupCursor(result);
|
|
137
270
|
await setupClaudeCode(result);
|
|
138
271
|
await setupOpenCode(result);
|
|
272
|
+
// Install global skills for platforms that support them
|
|
273
|
+
await installClaudeCodeSkills(result);
|
|
274
|
+
await installClaudeCodeHooks(result);
|
|
275
|
+
await installCursorSkills(result);
|
|
276
|
+
await installOpenCodeSkills(result);
|
|
139
277
|
// Print results
|
|
140
278
|
if (result.configured.length > 0) {
|
|
141
279
|
console.log(' Configured:');
|
|
@@ -158,6 +296,10 @@ export const setupCommand = async () => {
|
|
|
158
296
|
}
|
|
159
297
|
}
|
|
160
298
|
console.log('');
|
|
299
|
+
console.log(' Summary:');
|
|
300
|
+
console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
|
|
301
|
+
console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
|
|
302
|
+
console.log('');
|
|
161
303
|
console.log(' Next steps:');
|
|
162
304
|
console.log(' 1. cd into any git repo');
|
|
163
305
|
console.log(' 2. Run: gitnexus analyze');
|