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.
@@ -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 use
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
- * - Use MUST/ALWAYS language — agents skip "examples" and "suggestions"
23
- * - Keep reference material (tools, resources, schema) in AGENTS.md as a quick lookup
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, ${clusterCount} clusters, ${stats.processes || 0} processes).
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 adds a knowledge graph layer to this codebase — use it to understand architecture, trace call chains, analyze blast radius, and navigate code by meaning rather than just filenames. It complements your existing tools (read, grep, etc.) with structural and semantic intelligence.
32
+ GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
33
33
 
34
- IMPORTANT: GitNexus MCP tool usage is highly encouraged, especially for gathering deep and accurate codebase knowledge.
34
+ ## Always Start Here
35
35
 
36
- > **Staleness:** The \`gitnexus://repo/{name}/context\` resource warns when the index is out of date. Refresh with \`npx gitnexus analyze\` in the terminal.
36
+ For any task involving code understanding, debugging, impact analysis, or refactoring, you must:
37
37
 
38
- ## Skills
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
- For these tasks, read the matching skill and follow its workflow:
44
+ ## Skills
41
45
 
42
- | Task | Skill | Path |
43
- |------|-------|------|
44
- | Understand architecture / "How does X work?" | **Exploring** | \`.claude/skills/gitnexus/exploring/SKILL.md\` |
45
- | Blast radius / "What breaks if I change X?" | **Impact Analysis** | \`.claude/skills/gitnexus/impact-analysis/SKILL.md\` |
46
- | Trace bugs / "Why is X failing?" | **Debugging** | \`.claude/skills/gitnexus/debugging/SKILL.md\` |
47
- | Rename / extract / split / refactor | **Refactoring** | \`.claude/skills/gitnexus/refactoring/SKILL.md\` |
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
- | \`search\` | Semantic + keyword code search with cluster context |
54
- | \`explore\` | Symbol deep divecallers, callees, cluster membership, processes |
55
- | \`impact\` | Blast radius — what breaks at depth 1/2/3 with confidence scores |
56
- | \`overview\` | All clusters and processes at a glance |
57
+ | \`query\` | Process-grouped code intelligence execution flows related to a concept |
58
+ | \`context\` | 360-degree symbol viewcategorized 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 clusters with cohesion scores |
68
- | \`gitnexus://repo/{name}/cluster/{clusterName}\` | Cluster members |
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 |
@@ -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 ora from 'ora';
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 { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
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
- const spinner = ora('Checking repository...').start();
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
- spinner.fail('Not inside a git repository');
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
- spinner.fail('Not a git repository');
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
- spinner.succeed('Repository already up to date');
52
+ console.log('Repository already up to date\n');
42
53
  return;
43
54
  }
44
- // Run ingestion pipeline
45
- spinner.text = 'Running ingestion pipeline...';
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
- spinner.text = `${progress.phase}: ${progress.percent}%`;
65
+ const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
66
+ progressBar.update(progress.percent, { phase: phaseLabel });
48
67
  });
49
- // Load graph into KuzuDB
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
- // Create FTS indexes for keyword search
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
- // FTS index creation may fail if tables are empty (no data for that type)
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
- spinner.text = 'Generating embeddings...';
92
+ progressBar.update(100, { phase: 'Generating embeddings...' });
82
93
  await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
83
- spinner.text = `Embeddings: ${progress.percent}%`;
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
- // Generate AI context files
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
- spinner.succeed('Repository indexed successfully');
129
- console.log(` Path: ${repoPath}`);
130
- console.log(` Storage: ${storagePath}`);
131
- console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges`);
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(` AI Context: ${aiContext.files.join(', ')}`);
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
- // Registry didn't exist before this run suggest setup
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');