gitnexushub 0.2.1

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/api.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Hub API Client
3
+ *
4
+ * Fetch-based, zero-dependency API client for GitNexus Hub.
5
+ */
6
+ export interface UserProfile {
7
+ id: string;
8
+ name: string;
9
+ email: string;
10
+ }
11
+ export interface HubRepo {
12
+ id: string;
13
+ name: string;
14
+ fullName: string;
15
+ status: string;
16
+ stats?: {
17
+ nodes?: number;
18
+ edges?: number;
19
+ processes?: number;
20
+ };
21
+ }
22
+ export interface ConnectContext {
23
+ claudeMd: string;
24
+ agentsMd: string;
25
+ skills: Array<{
26
+ name: string;
27
+ content: string;
28
+ }>;
29
+ }
30
+ export interface IndexResult {
31
+ id: string;
32
+ name: string;
33
+ fullName: string;
34
+ status: string;
35
+ }
36
+ export interface RepoDetail {
37
+ id: string;
38
+ name: string;
39
+ fullName: string;
40
+ status: string;
41
+ stats?: {
42
+ nodes?: number;
43
+ edges?: number;
44
+ processes?: number;
45
+ };
46
+ error?: string;
47
+ job?: {
48
+ status: string;
49
+ progress: number;
50
+ phase: string;
51
+ } | null;
52
+ }
53
+ export declare class HubAPI {
54
+ private hubUrl;
55
+ private token;
56
+ constructor(hubUrl: string, token: string);
57
+ private request;
58
+ private post;
59
+ getMe(): Promise<UserProfile>;
60
+ listRepos(): Promise<HubRepo[]>;
61
+ getConnectContext(repoFullName: string): Promise<ConnectContext>;
62
+ indexRepo(fullName: string): Promise<IndexResult>;
63
+ getRepo(repoId: string): Promise<RepoDetail>;
64
+ }
package/dist/api.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Hub API Client
3
+ *
4
+ * Fetch-based, zero-dependency API client for GitNexus Hub.
5
+ */
6
+ export class HubAPI {
7
+ hubUrl;
8
+ token;
9
+ constructor(hubUrl, token) {
10
+ this.hubUrl = hubUrl;
11
+ this.token = token;
12
+ }
13
+ async request(path) {
14
+ const url = `${this.hubUrl}${path}`;
15
+ const res = await fetch(url, {
16
+ headers: { Authorization: `Bearer ${this.token}` },
17
+ });
18
+ if (!res.ok) {
19
+ const body = await res.json().catch(() => ({ error: res.statusText }));
20
+ throw new Error(body.error || `HTTP ${res.status}`);
21
+ }
22
+ return res.json();
23
+ }
24
+ async post(path, body) {
25
+ const url = `${this.hubUrl}${path}`;
26
+ const res = await fetch(url, {
27
+ method: 'POST',
28
+ headers: {
29
+ Authorization: `Bearer ${this.token}`,
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify(body),
33
+ });
34
+ if (!res.ok) {
35
+ const data = await res.json().catch(() => ({ error: res.statusText }));
36
+ throw new Error(data.error || `HTTP ${res.status}`);
37
+ }
38
+ return res.json();
39
+ }
40
+ async getMe() {
41
+ return this.request('/auth/me');
42
+ }
43
+ async listRepos() {
44
+ return this.request('/api/repos');
45
+ }
46
+ async getConnectContext(repoFullName) {
47
+ return this.request(`/api/connect/context?repo=${encodeURIComponent(repoFullName)}`);
48
+ }
49
+ async indexRepo(fullName) {
50
+ return this.post('/api/repos/public', { fullName });
51
+ }
52
+ async getRepo(repoId) {
53
+ return this.request(`/api/repos/${repoId}`);
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Config Manager
3
+ *
4
+ * Persists Hub token and URL in ~/.gitnexus/config.json.
5
+ */
6
+ interface ConnectConfig {
7
+ hubToken?: string;
8
+ hubUrl?: string;
9
+ }
10
+ export declare function loadConfig(): Promise<ConnectConfig>;
11
+ export declare function saveConfig(updates: Partial<ConnectConfig>): Promise<void>;
12
+ export declare function clearConfig(): Promise<void>;
13
+ export declare function getConfigPath(): string;
14
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Config Manager
3
+ *
4
+ * Persists Hub token and URL in ~/.gitnexus/config.json.
5
+ */
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { readJsonFile, writeJsonFile } from './utils.js';
9
+ const CONFIG_DIR = path.join(os.homedir(), '.gitnexus');
10
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
11
+ export async function loadConfig() {
12
+ const data = await readJsonFile(CONFIG_PATH);
13
+ return data || {};
14
+ }
15
+ export async function saveConfig(updates) {
16
+ const existing = await loadConfig();
17
+ const merged = { ...existing, ...updates };
18
+ await writeJsonFile(CONFIG_PATH, merged);
19
+ }
20
+ export async function clearConfig() {
21
+ try {
22
+ const fs = await import('fs/promises');
23
+ await fs.rm(CONFIG_DIR, { recursive: true, force: true });
24
+ }
25
+ catch {
26
+ // Already gone
27
+ }
28
+ }
29
+ export function getConfigPath() {
30
+ return CONFIG_PATH;
31
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Content Generator
3
+ *
4
+ * Generates CLAUDE.md, AGENTS.md content and loads bundled skills.
5
+ * Self-contained — no Hub API dependency for content generation.
6
+ */
7
+ import type { ConnectContext } from './api.js';
8
+ /** Skills to include for Hub users (excludes gitnexus-cli) */
9
+ export declare const HUB_SKILLS: string[];
10
+ interface RepoStats {
11
+ nodes?: number;
12
+ edges?: number;
13
+ processes?: number;
14
+ }
15
+ /**
16
+ * Generate full ConnectContext from repo stats + bundled skills.
17
+ * No Hub API round-trip needed.
18
+ */
19
+ export declare function generateConnectContext(repoFullName: string, stats: RepoStats): Promise<ConnectContext>;
20
+ export {};
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Content Generator
3
+ *
4
+ * Generates CLAUDE.md, AGENTS.md content and loads bundled skills.
5
+ * Self-contained — no Hub API dependency for content generation.
6
+ */
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const GITNEXUS_START_MARKER = '<!-- gitnexus:start -->';
13
+ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
14
+ /** Skills to include for Hub users (excludes gitnexus-cli) */
15
+ export const HUB_SKILLS = [
16
+ 'gitnexus-exploring',
17
+ 'gitnexus-debugging',
18
+ 'gitnexus-impact-analysis',
19
+ 'gitnexus-refactoring',
20
+ 'gitnexus-guide',
21
+ 'gitnexus-pr-review',
22
+ ];
23
+ /**
24
+ * Generate Hub-flavored CLAUDE.md / AGENTS.md content.
25
+ * No detect_changes or rename references (not available via Hub MCP).
26
+ */
27
+ function generateHubContent(projectName, stats) {
28
+ return `${GITNEXUS_START_MARKER}
29
+ # GitNexus — Code Intelligence
30
+
31
+ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
32
+
33
+ > Re-indexing is managed from the GitNexus Hub dashboard.
34
+
35
+ ## Always Do
36
+
37
+ - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
38
+ - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
39
+ - When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
40
+ - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`gitnexus_context({name: "symbolName"})\`.
41
+
42
+ ## When Debugging
43
+
44
+ 1. \`gitnexus_query({query: "<error or symptom>"})\` — find execution flows related to the issue
45
+ 2. \`gitnexus_context({name: "<suspect function>"})\` — see all callers, callees, and process participation
46
+ 3. \`READ gitnexus://repo/${projectName}/process/{processName}\` — trace the full execution flow step by step
47
+
48
+ ## When Refactoring
49
+
50
+ - **Extracting/Splitting**: MUST run \`gitnexus_context({name: "target"})\` to see all incoming/outgoing refs, then \`gitnexus_impact({target: "target", direction: "upstream"})\` to find all external callers before moving code.
51
+
52
+ ## Never Do
53
+
54
+ - NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
55
+ - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
56
+
57
+ ## Tools Quick Reference
58
+
59
+ | Tool | When to use | Command |
60
+ |------|-------------|---------|
61
+ | \`query\` | Find code by concept | \`gitnexus_query({query: "auth validation"})\` |
62
+ | \`context\` | 360-degree view of one symbol | \`gitnexus_context({name: "validateUser"})\` |
63
+ | \`impact\` | Blast radius before editing | \`gitnexus_impact({target: "X", direction: "upstream"})\` |
64
+ | \`cypher\` | Custom graph queries | \`gitnexus_cypher({query: "MATCH ..."})\` |
65
+
66
+ ## Impact Risk Levels
67
+
68
+ | Depth | Meaning | Action |
69
+ |-------|---------|--------|
70
+ | d=1 | WILL BREAK — direct callers/importers | MUST update these |
71
+ | d=2 | LIKELY AFFECTED — indirect deps | Should test |
72
+ | d=3 | MAY NEED TESTING — transitive | Test if critical path |
73
+
74
+ ## Resources
75
+
76
+ | Resource | Use for |
77
+ |----------|---------|
78
+ | \`gitnexus://repo/${projectName}/context\` | Codebase overview, check index freshness |
79
+ | \`gitnexus://repo/${projectName}/clusters\` | All functional areas |
80
+ | \`gitnexus://repo/${projectName}/processes\` | All execution flows |
81
+ | \`gitnexus://repo/${projectName}/process/{name}\` | Step-by-step execution trace |
82
+
83
+ ## Self-Check Before Finishing
84
+
85
+ Before completing any code modification task, verify:
86
+ 1. \`gitnexus_impact\` was run for all modified symbols
87
+ 2. No HIGH/CRITICAL risk warnings were ignored
88
+ 3. All d=1 (WILL BREAK) dependents were updated
89
+
90
+ ${GITNEXUS_END_MARKER}`;
91
+ }
92
+ /**
93
+ * Replace CLI references with Hub dashboard instructions.
94
+ */
95
+ function adaptSkillForHub(content) {
96
+ return content
97
+ .replace(/run `npx gitnexus analyze` in terminal\.?/g, 'trigger re-indexing from the Hub dashboard.')
98
+ .replace(/run `npx gitnexus analyze` in the terminal first\.?/g, 'trigger re-indexing from the Hub dashboard.')
99
+ .replace(/> If "Index is stale" → run `npx gitnexus analyze` in terminal\.?/g, '> If "Index is stale" → trigger re-indexing from the Hub dashboard.')
100
+ .replace(/> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal\.?/g, '> If step 2 says "Index is stale" → trigger re-indexing from the Hub dashboard.')
101
+ .replace(/> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first\.?/g, '> If step 1 warns the index is stale, trigger re-indexing from the Hub dashboard.')
102
+ .replace(/> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first\.?/g, '> Re-indexing is managed from the GitNexus Hub dashboard.');
103
+ }
104
+ /**
105
+ * Load bundled skills from the package's own skills/ directory.
106
+ * Skills ship with the npm package — no Hub API needed.
107
+ */
108
+ async function loadBundledSkills() {
109
+ // Skills are at ../skills/ relative to dist/ (or ../../skills/ relative to src/)
110
+ const skillsDir = path.join(__dirname, '..', 'skills');
111
+ const skills = [];
112
+ for (const skillName of HUB_SKILLS) {
113
+ try {
114
+ const skillPath = path.join(skillsDir, `${skillName}.md`);
115
+ const raw = await fs.readFile(skillPath, 'utf-8');
116
+ skills.push({ name: skillName, content: adaptSkillForHub(raw) });
117
+ }
118
+ catch {
119
+ // Skill file not found — skip
120
+ }
121
+ }
122
+ return skills;
123
+ }
124
+ /**
125
+ * Generate full ConnectContext from repo stats + bundled skills.
126
+ * No Hub API round-trip needed.
127
+ */
128
+ export async function generateConnectContext(repoFullName, stats) {
129
+ const projectName = repoFullName.split('/').pop() || repoFullName;
130
+ const content = generateHubContent(projectName, stats);
131
+ const skills = await loadBundledSkills();
132
+ return {
133
+ claudeMd: content,
134
+ agentsMd: content,
135
+ skills,
136
+ };
137
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Project Context Writer
3
+ *
4
+ * Writes CLAUDE.md, AGENTS.md, and skills into the current project.
5
+ */
6
+ import type { ConnectContext } from './api.js';
7
+ export interface ContextResult {
8
+ files: string[];
9
+ }
10
+ export declare function writeProjectContext(projectDir: string, ctx: ConnectContext): Promise<ContextResult>;
11
+ export declare function removeProjectContext(projectDir: string): Promise<string[]>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Project Context Writer
3
+ *
4
+ * Writes CLAUDE.md, AGENTS.md, and skills into the current project.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { upsertMarkedSection, removeMarkedSection } from './utils.js';
9
+ export async function writeProjectContext(projectDir, ctx) {
10
+ const files = [];
11
+ // Write CLAUDE.md
12
+ const claudePath = path.join(projectDir, 'CLAUDE.md');
13
+ const claudeResult = await upsertMarkedSection(claudePath, ctx.claudeMd);
14
+ files.push(`CLAUDE.md (${claudeResult})`);
15
+ // Write AGENTS.md
16
+ const agentsPath = path.join(projectDir, 'AGENTS.md');
17
+ const agentsResult = await upsertMarkedSection(agentsPath, ctx.agentsMd);
18
+ files.push(`AGENTS.md (${agentsResult})`);
19
+ // Write skills to .claude/skills/gitnexus/
20
+ if (ctx.skills.length > 0) {
21
+ const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
22
+ for (const skill of ctx.skills) {
23
+ const skillDir = path.join(skillsDir, skill.name);
24
+ await fs.mkdir(skillDir, { recursive: true });
25
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
26
+ }
27
+ files.push(`.claude/skills/gitnexus/ (${ctx.skills.length} skills)`);
28
+ }
29
+ return { files };
30
+ }
31
+ export async function removeProjectContext(projectDir) {
32
+ const removed = [];
33
+ // Remove marked sections from CLAUDE.md and AGENTS.md
34
+ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
35
+ const filePath = path.join(projectDir, name);
36
+ if (await removeMarkedSection(filePath)) {
37
+ removed.push(name);
38
+ }
39
+ }
40
+ // Remove .claude/skills/gitnexus/ directory
41
+ const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
42
+ try {
43
+ await fs.rm(skillsDir, { recursive: true, force: true });
44
+ removed.push('.claude/skills/gitnexus/');
45
+ }
46
+ catch {
47
+ // Already gone
48
+ }
49
+ return removed;
50
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Claude Code Editor Setup
3
+ *
4
+ * Runs `claude mcp add` CLI command, or falls back to writing ~/.claude.json.
5
+ * Skills are installed to ~/.claude/skills/.
6
+ */
7
+ import type { EditorConfig } from './types.js';
8
+ export declare const claudeCodeEditor: EditorConfig;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Claude Code Editor Setup
3
+ *
4
+ * Runs `claude mcp add` CLI command, or falls back to writing ~/.claude.json.
5
+ * Skills are installed to ~/.claude/skills/.
6
+ */
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+ import { execSync } from 'child_process';
11
+ import { readJsonFile, writeJsonFile } from '../utils.js';
12
+ import { HUB_SKILLS } from '../content.js';
13
+ const GITNEXUS_SKILL_NAMES = HUB_SKILLS;
14
+ function isClaudeOnPath() {
15
+ try {
16
+ execSync('claude --version', { stdio: 'pipe' });
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ async function configure(hubUrl, token) {
24
+ const mcpUrl = `${hubUrl}/mcp`;
25
+ const headerValue = `Authorization: Bearer ${token}`;
26
+ // Try `claude mcp add` first
27
+ if (isClaudeOnPath()) {
28
+ try {
29
+ const cmd = `claude mcp add gitnexus --transport streamable-http --url "${mcpUrl}" --header "${headerValue}"`;
30
+ if (process.platform === 'win32') {
31
+ execSync(`cmd /c ${cmd}`, { stdio: 'pipe' });
32
+ }
33
+ else {
34
+ execSync(cmd, { stdio: 'pipe' });
35
+ }
36
+ return { success: true, message: 'MCP configured via `claude mcp add`' };
37
+ }
38
+ catch {
39
+ // Fall through to file-based config
40
+ }
41
+ }
42
+ // Fallback: write ~/.claude.json
43
+ try {
44
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
45
+ const existing = (await readJsonFile(claudeJsonPath)) || {};
46
+ if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
47
+ existing.mcpServers = {};
48
+ }
49
+ existing.mcpServers.gitnexus = {
50
+ type: 'streamable-http',
51
+ url: mcpUrl,
52
+ headers: { Authorization: `Bearer ${token}` },
53
+ };
54
+ await writeJsonFile(claudeJsonPath, existing);
55
+ return { success: true, message: 'MCP configured in ~/.claude.json' };
56
+ }
57
+ catch (err) {
58
+ return { success: false, message: `Failed: ${err.message}` };
59
+ }
60
+ }
61
+ async function installSkills(skills) {
62
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills');
63
+ let installed = 0;
64
+ for (const skill of skills) {
65
+ try {
66
+ const skillDir = path.join(skillsDir, skill.name);
67
+ await fs.mkdir(skillDir, { recursive: true });
68
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
69
+ installed++;
70
+ }
71
+ catch {
72
+ // Skip on error
73
+ }
74
+ }
75
+ return installed;
76
+ }
77
+ async function unconfigure() {
78
+ // Try `claude mcp remove` first
79
+ if (isClaudeOnPath()) {
80
+ try {
81
+ const cmd = 'claude mcp remove gitnexus';
82
+ if (process.platform === 'win32') {
83
+ execSync(`cmd /c ${cmd}`, { stdio: 'pipe' });
84
+ }
85
+ else {
86
+ execSync(cmd, { stdio: 'pipe' });
87
+ }
88
+ return { success: true, message: 'MCP removed via `claude mcp remove`' };
89
+ }
90
+ catch {
91
+ // Fall through to file-based removal
92
+ }
93
+ }
94
+ // Fallback: remove from ~/.claude.json
95
+ try {
96
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
97
+ const existing = await readJsonFile(claudeJsonPath);
98
+ if (existing?.mcpServers?.gitnexus) {
99
+ delete existing.mcpServers.gitnexus;
100
+ await writeJsonFile(claudeJsonPath, existing);
101
+ }
102
+ return { success: true, message: 'MCP removed from ~/.claude.json' };
103
+ }
104
+ catch (err) {
105
+ return { success: false, message: `Failed: ${err.message}` };
106
+ }
107
+ }
108
+ async function removeSkills() {
109
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills');
110
+ let removed = 0;
111
+ for (const name of GITNEXUS_SKILL_NAMES) {
112
+ try {
113
+ await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
114
+ removed++;
115
+ }
116
+ catch {
117
+ // Already gone
118
+ }
119
+ }
120
+ return removed;
121
+ }
122
+ export const claudeCodeEditor = {
123
+ id: 'claude-code',
124
+ name: 'Claude Code',
125
+ configure,
126
+ unconfigure,
127
+ installSkills,
128
+ removeSkills,
129
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Cursor Editor Setup
3
+ *
4
+ * Writes MCP config to ~/.cursor/mcp.json
5
+ * Installs skills to ~/.cursor/skills/
6
+ */
7
+ import type { EditorConfig } from './types.js';
8
+ export declare const cursorEditor: EditorConfig;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cursor Editor Setup
3
+ *
4
+ * Writes MCP config to ~/.cursor/mcp.json
5
+ * Installs skills to ~/.cursor/skills/
6
+ */
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+ import { readJsonFile, writeJsonFile } from '../utils.js';
11
+ import { HUB_SKILLS } from '../content.js';
12
+ function getMcpConfig(hubUrl, token) {
13
+ return {
14
+ type: 'streamable-http',
15
+ url: `${hubUrl}/mcp`,
16
+ headers: { Authorization: `Bearer ${token}` },
17
+ };
18
+ }
19
+ async function configure(hubUrl, token) {
20
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
21
+ try {
22
+ const existing = (await readJsonFile(mcpPath)) || {};
23
+ if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
24
+ existing.mcpServers = {};
25
+ }
26
+ existing.mcpServers.gitnexus = getMcpConfig(hubUrl, token);
27
+ await writeJsonFile(mcpPath, existing);
28
+ return { success: true, message: 'MCP configured in ~/.cursor/mcp.json' };
29
+ }
30
+ catch (err) {
31
+ return { success: false, message: `Failed: ${err.message}` };
32
+ }
33
+ }
34
+ async function installSkills(skills) {
35
+ const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
36
+ let installed = 0;
37
+ for (const skill of skills) {
38
+ try {
39
+ const skillDir = path.join(skillsDir, skill.name);
40
+ await fs.mkdir(skillDir, { recursive: true });
41
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
42
+ installed++;
43
+ }
44
+ catch {
45
+ // Skip on error
46
+ }
47
+ }
48
+ return installed;
49
+ }
50
+ async function unconfigure() {
51
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
52
+ try {
53
+ const existing = await readJsonFile(mcpPath);
54
+ if (existing?.mcpServers?.gitnexus) {
55
+ delete existing.mcpServers.gitnexus;
56
+ await writeJsonFile(mcpPath, existing);
57
+ }
58
+ return { success: true, message: 'MCP removed from ~/.cursor/mcp.json' };
59
+ }
60
+ catch (err) {
61
+ return { success: false, message: `Failed: ${err.message}` };
62
+ }
63
+ }
64
+ async function removeSkills() {
65
+ const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
66
+ let removed = 0;
67
+ for (const name of HUB_SKILLS) {
68
+ try {
69
+ await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
70
+ removed++;
71
+ }
72
+ catch { /* already gone */ }
73
+ }
74
+ return removed;
75
+ }
76
+ export const cursorEditor = {
77
+ id: 'cursor',
78
+ name: 'Cursor',
79
+ configure,
80
+ unconfigure,
81
+ installSkills,
82
+ removeSkills,
83
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Editor Auto-Detection
3
+ *
4
+ * Probes well-known directories to detect which editors are installed.
5
+ */
6
+ import type { EditorId } from './types.js';
7
+ export declare function detectInstalledEditors(): Promise<EditorId[]>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Editor Auto-Detection
3
+ *
4
+ * Probes well-known directories to detect which editors are installed.
5
+ */
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { dirExists } from '../utils.js';
9
+ const PROBES = [
10
+ {
11
+ id: 'cursor',
12
+ name: 'Cursor',
13
+ dirs: [path.join(os.homedir(), '.cursor')],
14
+ },
15
+ {
16
+ id: 'claude-code',
17
+ name: 'Claude Code',
18
+ dirs: [path.join(os.homedir(), '.claude')],
19
+ },
20
+ {
21
+ id: 'windsurf',
22
+ name: 'Windsurf',
23
+ dirs: [path.join(os.homedir(), '.codeium', 'windsurf')],
24
+ },
25
+ {
26
+ id: 'opencode',
27
+ name: 'OpenCode',
28
+ dirs: [path.join(os.homedir(), '.config', 'opencode')],
29
+ },
30
+ ];
31
+ export async function detectInstalledEditors() {
32
+ const found = [];
33
+ for (const probe of PROBES) {
34
+ for (const dir of probe.dirs) {
35
+ if (await dirExists(dir)) {
36
+ found.push(probe.id);
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ return found;
42
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * OpenCode Editor Setup
3
+ *
4
+ * Writes MCP config to ~/.config/opencode/config.json (under `mcp` key)
5
+ * Installs skills to ~/.config/opencode/skill/
6
+ */
7
+ import type { EditorConfig } from './types.js';
8
+ export declare const opencodeEditor: EditorConfig;