Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "gitnexus-marketplace",
"name": "gitnexus",
"owner": {
"name": "GitNexus",
"email": "nico@gitnexus.dev"
},
"metadata": {
"description": "Code intelligence powered by a knowledge graph — execution flows, blast radius, and semantic search",
"homepage": "https://github.com/nicosxt/gitnexus"
"homepage": "https://github.com/abhigyanpatwari/GitNexus"
},
"plugins": [
{
"name": "gitnexus",
"version": "1.3.3",
"version": "1.3.6",
"source": "./gitnexus-claude-plugin",
"description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase."
}
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ npx gitnexus analyze

That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command.

To configure MCP for your editor, run `npx gitnexus setup` once — or set it up manually below.
To configure MCP, install agent skills, and register hooks for your editor, run `npx gitnexus setup` once — or set it up manually below.

> **Claude Code plugin users:** If you installed GitNexus as a [Claude Code plugin](https://docs.anthropic.com/en/docs/claude-code/plugins), you can skip `gitnexus setup` entirely — the plugin bundles MCP, skills, and hooks automatically. Just run `gitnexus analyze` to index your repos.

### MCP Setup

`gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
`gitnexus setup` auto-detects your editors and writes the correct global MCP config, installs agent skills, and registers hooks. You only need to run it once.

### Editor Support

Expand Down
93 changes: 0 additions & 93 deletions gitnexus/src/cli/ai-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@

import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { type GeneratedSkillInfo } from './skill-gen.js';

// ESM equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

interface RepoStats {
files?: number;
nodes?: number;
Expand Down Expand Up @@ -212,88 +207,6 @@ async function upsertGitNexusSection(
return 'appended';
}

/**
* Install GitNexus skills to .claude/skills/gitnexus/
* Works natively with Claude Code, Cursor, and GitHub Copilot
*/
async function installSkills(repoPath: string): Promise<string[]> {
const skillsDir = path.join(repoPath, '.claude', 'skills', 'gitnexus');
const installedSkills: string[] = [];

// Skill definitions bundled with the package
const skills = [
{
name: 'gitnexus-exploring',
description:
'Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: "How does X work?", "What calls this function?", "Show me the auth flow"',
},
{
name: 'gitnexus-debugging',
description:
'Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: "Why is X failing?", "Where does this error come from?", "Trace this bug"',
},
{
name: 'gitnexus-impact-analysis',
description:
'Use when the user wants to know what will break if they change something, or needs safety analysis before editing code. Examples: "Is it safe to change X?", "What depends on this?", "What will break?"',
},
{
name: 'gitnexus-refactoring',
description:
'Use when the user wants to rename, extract, split, move, or restructure code safely. Examples: "Rename this function", "Extract this into a module", "Refactor this class", "Move this to a separate file"',
},
{
name: 'gitnexus-guide',
description:
'Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: "What GitNexus tools are available?", "How do I use GitNexus?"',
},
{
name: 'gitnexus-cli',
description:
'Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: "Index this repo", "Reanalyze the codebase", "Generate a wiki"',
},
];

for (const skill of skills) {
const skillDir = path.join(skillsDir, skill.name);
const skillPath = path.join(skillDir, 'SKILL.md');

try {
// Create skill directory
await fs.mkdir(skillDir, { recursive: true });

// Try to read from package skills directory
const packageSkillPath = path.join(__dirname, '..', '..', 'skills', `${skill.name}.md`);
let skillContent: string;

try {
skillContent = await fs.readFile(packageSkillPath, 'utf-8');
} catch {
// Fallback: generate minimal skill content
skillContent = `---
name: ${skill.name}
description: ${skill.description}
---

# ${skill.name.charAt(0).toUpperCase() + skill.name.slice(1)}

${skill.description}

Use GitNexus tools to accomplish this task.
`;
}

await fs.writeFile(skillPath, skillContent, 'utf-8');
installedSkills.push(skill.name);
} catch (err) {
// Skip on error, don't fail the whole process
console.warn(`Warning: Could not install skill ${skill.name}:`, err);
}
}

return installedSkills;
}

/**
* Generate AI context files after indexing
*/
Expand Down Expand Up @@ -323,11 +236,5 @@ export async function generateAIContextFiles(
createdFiles.push('CLAUDE.md (skipped via --skip-agents-md)');
}

// Install skills to .claude/skills/gitnexus/
const installedSkills = await installSkills(repoPath);
if (installedSkills.length > 0) {
createdFiles.push(`.claude/skills/gitnexus/ (${installedSkills.length} skills)`);
}

return { files: createdFiles };
}
29 changes: 28 additions & 1 deletion gitnexus/src/cli/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ function ensureHeap(): boolean {
return true;
}

/**
* Check for stale project-local skills left by a previous `analyze` run.
* Prints a deprecation notice but does NOT delete — cleanup is handled by `setup`.
*/
export async function checkStaleProjectSkills(repoPath: string): Promise<boolean> {
const skillsDir = path.join(repoPath, '.claude', 'skills', 'gitnexus');
try {
const stat = await fs.stat(skillsDir);
if (stat.isDirectory()) {
console.log(
` Note: Skills are no longer installed by analyze. Run 'gitnexus setup' to manage skills globally.`,
);
return true;
}
} catch {
// Directory doesn't exist — nothing to warn about
}
return false;
}

export interface AnalyzeOptions {
force?: boolean;
embeddings?: boolean;
Expand Down Expand Up @@ -193,6 +213,8 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption
console.warn = origWarn;
console.error = origError;
bar.stop();
// Keep migration notice visible even on no-op analyze runs
await checkStaleProjectSkills(repoPath);
console.log(' Already up to date\n');
// Safe to return without process.exit(0) — the early-return path in
// runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
Expand Down Expand Up @@ -268,10 +290,15 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption
);
console.log(` ${repoPath}`);

// Warn if stale project-local skills exist from a previous analyze run
await checkStaleProjectSkills(repoPath);

try {
await fs.access(getGlobalRegistryPath());
} catch {
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
console.log(
'\n Tip: Run `gitnexus setup` to configure MCP and install agent skills for your editor.',
);
}

console.log('');
Expand Down
53 changes: 53 additions & 0 deletions gitnexus/src/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,56 @@ async function installCodexSkills(result: SetupResult): Promise<void> {
}
}

// ─── Project-local skill cleanup ───────────────────────────────────

/**
* Find the nearest git repository root by walking upward and checking for
* a .git marker (directory for standard repos, file for worktrees/submodules).
*/
async function findRepoRoot(startPath: string): Promise<string | null> {
let current = path.resolve(startPath);
const root = path.parse(current).root;

while (true) {
const gitMarker = path.join(current, '.git');
try {
const stat = await fs.stat(gitMarker);
if (stat.isDirectory() || stat.isFile()) {
return current;
}
} catch {
// Keep walking up
}

if (current === root) return null;
current = path.dirname(current);
}
}

/**
* Remove stale project-local skills left by previous `analyze` runs.
* Cleans up at repo root and supports both .git directories and files.
*/
async function cleanupProjectLocalSkills(result: SetupResult): Promise<void> {
const repoRoot = await findRepoRoot(process.cwd());
if (!repoRoot) return; // Not inside a git repo

const localSkillsDir = path.join(repoRoot, '.claude', 'skills', 'gitnexus');
try {
const stat = await fs.stat(localSkillsDir);
if (!stat.isDirectory()) return;
} catch {
return; // No project-local skills
}

try {
await fs.rm(localSkillsDir, { recursive: true, force: true });
result.configured.push('Removed project-local skills (now installed globally)');
} catch (err: any) {
result.errors.push(`Project-local skill cleanup: ${err.message}`);
}
}

// ─── Main command ──────────────────────────────────────────────────

export const setupCommand = async () => {
Expand Down Expand Up @@ -469,6 +519,9 @@ export const setupCommand = async () => {
await installOpenCodeSkills(result);
await installCodexSkills(result);

// Clean up stale project-local skills left by previous `analyze` runs
await cleanupProjectLocalSkills(result);

// Print results
if (result.configured.length > 0) {
console.log(' Configured:');
Expand Down
57 changes: 45 additions & 12 deletions gitnexus/test/unit/ai-context.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
Expand All @@ -8,13 +8,13 @@ describe('generateAIContextFiles', () => {
let tmpDir: string;
let storagePath: string;

beforeAll(async () => {
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-ai-ctx-test-'));
storagePath = path.join(tmpDir, '.gitnexus');
await fs.mkdir(storagePath, { recursive: true });
});

afterAll(async () => {
afterEach(async () => {
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
Expand Down Expand Up @@ -66,18 +66,51 @@ describe('generateAIContextFiles', () => {
expect(starts).toBe(1);
});

it('installs skills files', async () => {
it('does NOT install skills after refactor', async () => {
const stats = { nodes: 10 };
const result = await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);

// Should have installed skill files
const skillsDir = path.join(tmpDir, '.claude', 'skills', 'gitnexus');
try {
const entries = await fs.readdir(skillsDir, { recursive: true });
expect(entries.length).toBeGreaterThan(0);
} catch {
// Skills dir may not be created if skills source doesn't exist in test context
}
await expect(fs.stat(skillsDir)).rejects.toThrow();
});

it('return value does not mention skills after refactor', async () => {
const stats = { nodes: 10 };
const result = await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
const skillFiles = result.files.filter((f) => f.includes('skills'));
expect(skillFiles).toHaveLength(0);
});

it('preserves existing CLAUDE.md content', async () => {
const claudePath = path.join(tmpDir, 'CLAUDE.md');
await fs.writeFile(claudePath, '# My Custom Instructions\n\nDo not remove this.\n', 'utf-8');

const stats = { nodes: 10 };
await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);

const content = await fs.readFile(claudePath, 'utf-8');
expect(content).toContain('My Custom Instructions');
expect(content).toContain('Do not remove this.');
expect(content).toContain('gitnexus:start');
});

it('existing CLAUDE.md with gitnexus section but no skills dir works', async () => {
const claudePath = path.join(tmpDir, 'CLAUDE.md');
await fs.writeFile(
claudePath,
'<!-- gitnexus:start -->\nold content\n<!-- gitnexus:end -->\n',
'utf-8',
);

const stats = { nodes: 99 };
await generateAIContextFiles(tmpDir, storagePath, 'UpdatedProject', stats);

const content = await fs.readFile(claudePath, 'utf-8');
expect(content).not.toContain('old content');
expect(content).toContain('99 symbols');
const starts = (content.match(/gitnexus:start/g) || []).length;
expect(starts).toBe(1);
await expect(fs.stat(path.join(tmpDir, '.claude', 'skills', 'gitnexus'))).rejects.toThrow();
});

it('preserves manual AGENTS.md and CLAUDE.md edits when skipAgentsMd is enabled', async () => {
Expand Down
Loading
Loading