gitnexushub 0.2.12 → 0.4.0
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 +31 -0
- package/dist/api.js +53 -2
- package/dist/cli-helpers.d.ts +23 -0
- package/dist/cli-helpers.js +57 -0
- package/dist/connect-command.d.ts +29 -0
- package/dist/connect-command.js +169 -0
- package/dist/content.js +62 -62
- package/dist/editors/claude-code.js +14 -1
- package/dist/editors/cursor.js +14 -3
- package/dist/editors/opencode.js +14 -3
- package/dist/editors/windsurf.js +14 -3
- package/dist/fingerprint.d.ts +11 -0
- package/dist/fingerprint.js +18 -0
- package/dist/hooks-installer.d.ts +33 -0
- package/dist/hooks-installer.js +114 -0
- package/dist/index.js +23 -171
- package/dist/registry.d.ts +41 -0
- package/dist/registry.js +92 -0
- package/dist/sync-command.d.ts +16 -0
- package/dist/sync-command.js +169 -0
- package/dist/tarball.d.ts +17 -0
- package/dist/tarball.js +75 -0
- package/hooks/gitnexus-enterprise-hook.cjs +415 -0
- package/package.json +58 -53
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-refactoring.md +121 -121
package/dist/api.d.ts
CHANGED
|
@@ -50,10 +50,35 @@ export interface RepoDetail {
|
|
|
50
50
|
phase: string;
|
|
51
51
|
} | null;
|
|
52
52
|
}
|
|
53
|
+
export interface RepoMeta {
|
|
54
|
+
full_name: string;
|
|
55
|
+
last_commit: string | null;
|
|
56
|
+
indexed_at: string | null;
|
|
57
|
+
graph_epoch: number;
|
|
58
|
+
stats: Record<string, number>;
|
|
59
|
+
}
|
|
60
|
+
export interface SyncMetadata {
|
|
61
|
+
local_head: string;
|
|
62
|
+
local_branch?: string;
|
|
63
|
+
dirty?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface SyncResult {
|
|
66
|
+
status: 'queued' | 'already_fresh';
|
|
67
|
+
job_id: string | null;
|
|
68
|
+
}
|
|
69
|
+
export interface SyncJobStatus {
|
|
70
|
+
status: string;
|
|
71
|
+
progress: number;
|
|
72
|
+
phase: string | null;
|
|
73
|
+
error: string | null;
|
|
74
|
+
}
|
|
53
75
|
export declare class HubAPI {
|
|
54
76
|
private hubUrl;
|
|
55
77
|
private token;
|
|
78
|
+
private fingerprint;
|
|
79
|
+
private deviceName;
|
|
56
80
|
constructor(hubUrl: string, token: string);
|
|
81
|
+
private get authHeaders();
|
|
57
82
|
private request;
|
|
58
83
|
private post;
|
|
59
84
|
getMe(): Promise<UserProfile>;
|
|
@@ -61,4 +86,10 @@ export declare class HubAPI {
|
|
|
61
86
|
getConnectContext(repoFullName: string): Promise<ConnectContext>;
|
|
62
87
|
indexRepo(fullName: string): Promise<IndexResult>;
|
|
63
88
|
getRepo(repoId: string): Promise<RepoDetail>;
|
|
89
|
+
meta(repoId: string): Promise<RepoMeta>;
|
|
90
|
+
sync(repoId: string, params: {
|
|
91
|
+
metadata: SyncMetadata;
|
|
92
|
+
tarball: NodeJS.ReadableStream;
|
|
93
|
+
}): Promise<SyncResult>;
|
|
94
|
+
syncStatus(repoId: string, jobId: string): Promise<SyncJobStatus>;
|
|
64
95
|
}
|
package/dist/api.js
CHANGED
|
@@ -3,17 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fetch-based, zero-dependency API client for GitNexus Hub.
|
|
5
5
|
*/
|
|
6
|
+
import { computeFingerprint, getDeviceName } from './fingerprint.js';
|
|
6
7
|
export class HubAPI {
|
|
7
8
|
hubUrl;
|
|
8
9
|
token;
|
|
10
|
+
fingerprint;
|
|
11
|
+
deviceName;
|
|
9
12
|
constructor(hubUrl, token) {
|
|
10
13
|
this.hubUrl = hubUrl;
|
|
11
14
|
this.token = token;
|
|
15
|
+
this.fingerprint = computeFingerprint();
|
|
16
|
+
this.deviceName = getDeviceName();
|
|
17
|
+
}
|
|
18
|
+
get authHeaders() {
|
|
19
|
+
return {
|
|
20
|
+
Authorization: `Bearer ${this.token}`,
|
|
21
|
+
'X-Device-Fingerprint': this.fingerprint,
|
|
22
|
+
'X-Device-Name': this.deviceName,
|
|
23
|
+
};
|
|
12
24
|
}
|
|
13
25
|
async request(path) {
|
|
14
26
|
const url = `${this.hubUrl}${path}`;
|
|
15
27
|
const res = await fetch(url, {
|
|
16
|
-
headers:
|
|
28
|
+
headers: this.authHeaders,
|
|
17
29
|
});
|
|
18
30
|
if (!res.ok) {
|
|
19
31
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
@@ -26,7 +38,7 @@ export class HubAPI {
|
|
|
26
38
|
const res = await fetch(url, {
|
|
27
39
|
method: 'POST',
|
|
28
40
|
headers: {
|
|
29
|
-
|
|
41
|
+
...this.authHeaders,
|
|
30
42
|
'Content-Type': 'application/json',
|
|
31
43
|
},
|
|
32
44
|
body: JSON.stringify(body),
|
|
@@ -52,4 +64,43 @@ export class HubAPI {
|
|
|
52
64
|
async getRepo(repoId) {
|
|
53
65
|
return this.request(`/api/repos/${repoId}`);
|
|
54
66
|
}
|
|
67
|
+
async meta(repoId) {
|
|
68
|
+
return this.request(`/api/repos/${repoId}/meta`);
|
|
69
|
+
}
|
|
70
|
+
async sync(repoId, params) {
|
|
71
|
+
// Gzip the tarball stream into a Buffer. For now, buffer in memory —
|
|
72
|
+
// typical repo size is <200MB. Streaming multipart via fetch is nontrivial
|
|
73
|
+
// in Node and not worth the complexity for the first cut.
|
|
74
|
+
const { createGzip } = await import('zlib');
|
|
75
|
+
const gz = params.tarball.pipe(createGzip());
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of gz) {
|
|
78
|
+
chunks.push(chunk);
|
|
79
|
+
}
|
|
80
|
+
const gzBuf = Buffer.concat(chunks);
|
|
81
|
+
const form = new FormData();
|
|
82
|
+
form.append('metadata', JSON.stringify(params.metadata));
|
|
83
|
+
// Blob + filename triggers fetch to set up the file-part headers correctly
|
|
84
|
+
form.append('tarball', new Blob([gzBuf]), 'tarball.tar.gz');
|
|
85
|
+
const url = `${this.hubUrl}/api/repos/${repoId}/sync`;
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
// IMPORTANT: do NOT set Content-Type — fetch sets it with the multipart boundary
|
|
89
|
+
headers: this.authHeaders,
|
|
90
|
+
body: form,
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
94
|
+
// Preserve the HTTP status so the CLI can branch on 503/409
|
|
95
|
+
// (at-capacity / conflict) and present friendlier hints rather
|
|
96
|
+
// than a generic "Sync failed" line.
|
|
97
|
+
const err = new Error(body.error || `HTTP ${res.status}`);
|
|
98
|
+
err.statusCode = res.status;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
return res.json();
|
|
102
|
+
}
|
|
103
|
+
async syncStatus(repoId, jobId) {
|
|
104
|
+
return this.request(`/api/repos/${repoId}/sync/${jobId}`);
|
|
105
|
+
}
|
|
55
106
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI helpers — log output and auth resolution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so the individual command-flow files
|
|
5
|
+
* (connect-command.ts, etc.) can be imported and unit-tested without
|
|
6
|
+
* pulling in commander's top-level program.parse() side effect.
|
|
7
|
+
*/
|
|
8
|
+
import { HubAPI } from './api.js';
|
|
9
|
+
import type { EditorId, EditorConfig } from './editors/types.js';
|
|
10
|
+
export declare const EDITORS: Record<EditorId, EditorConfig>;
|
|
11
|
+
export declare const ok: (msg: string) => void;
|
|
12
|
+
export declare const info: (msg: string) => void;
|
|
13
|
+
export declare const warn: (msg: string) => void;
|
|
14
|
+
export declare const fail: (msg: string) => void;
|
|
15
|
+
export declare const DEFAULT_HUB_URL: string;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve token + Hub URL from args/config. Exits on failure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveAuth(tokenArg?: string, hubOpt?: string): Promise<{
|
|
20
|
+
api: HubAPI;
|
|
21
|
+
hubUrl: string;
|
|
22
|
+
token: string;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI helpers — log output and auth resolution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so the individual command-flow files
|
|
5
|
+
* (connect-command.ts, etc.) can be imported and unit-tested without
|
|
6
|
+
* pulling in commander's top-level program.parse() side effect.
|
|
7
|
+
*/
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
10
|
+
import { HubAPI } from './api.js';
|
|
11
|
+
import { claudeCodeEditor } from './editors/claude-code.js';
|
|
12
|
+
import { cursorEditor } from './editors/cursor.js';
|
|
13
|
+
import { windsurfEditor } from './editors/windsurf.js';
|
|
14
|
+
import { opencodeEditor } from './editors/opencode.js';
|
|
15
|
+
export const EDITORS = {
|
|
16
|
+
'claude-code': claudeCodeEditor,
|
|
17
|
+
cursor: cursorEditor,
|
|
18
|
+
windsurf: windsurfEditor,
|
|
19
|
+
opencode: opencodeEditor,
|
|
20
|
+
};
|
|
21
|
+
export const ok = (msg) => console.log(` ${pc.green('✔')} ${msg}`);
|
|
22
|
+
export const info = (msg) => console.log(` ${pc.cyan('ℹ')} ${msg}`);
|
|
23
|
+
export const warn = (msg) => console.log(` ${pc.yellow('⚠')} ${msg}`);
|
|
24
|
+
export const fail = (msg) => console.error(` ${pc.red('✗')} ${msg}`);
|
|
25
|
+
export const DEFAULT_HUB_URL = process.env.GITNEXUS_HUB_URL || 'https://gitnexus-enterprise-production.up.railway.app';
|
|
26
|
+
/**
|
|
27
|
+
* Resolve token + Hub URL from args/config. Exits on failure.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveAuth(tokenArg, hubOpt) {
|
|
30
|
+
const config = await loadConfig();
|
|
31
|
+
const token = tokenArg || config.hubToken;
|
|
32
|
+
const hubUrl = hubOpt || config.hubUrl || DEFAULT_HUB_URL;
|
|
33
|
+
if (!token) {
|
|
34
|
+
fail('No API token provided.');
|
|
35
|
+
console.error('');
|
|
36
|
+
console.error(` Usage: ${pc.cyan('npx gitnexushub gnx_YOUR_TOKEN --editor cursor')}`);
|
|
37
|
+
console.error(` Generate a token at: ${pc.cyan(hubUrl + '/settings/tokens')}`);
|
|
38
|
+
console.error('');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
if (!token.startsWith('gnx_')) {
|
|
42
|
+
fail(`Invalid token format. Tokens must start with ${pc.bold('gnx_')}`);
|
|
43
|
+
console.error('');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const api = new HubAPI(hubUrl, token);
|
|
47
|
+
const user = await api.getMe().catch((err) => {
|
|
48
|
+
fail(`Authentication failed: ${err.message}`);
|
|
49
|
+
console.error(` Check your token and try again.`);
|
|
50
|
+
console.error('');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
ok(`Hello, ${pc.bold(user.name)}!`);
|
|
54
|
+
console.log('');
|
|
55
|
+
await saveConfig({ hubToken: token, hubUrl });
|
|
56
|
+
return { api, hubUrl, token };
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnx connect` flow — register Hub MCP, install skills, install hooks,
|
|
3
|
+
* and write project context files.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from index.ts so it can be imported (and unit-tested) without
|
|
6
|
+
* triggering commander's top-level program.parse() side effect.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the path to the shipped hook script (`gitnexus-enterprise-hook.cjs`).
|
|
10
|
+
*
|
|
11
|
+
* Lives at `hooks/` in the package root, sibling to `dist/` and `src/`:
|
|
12
|
+
* npm install → <npm-prefix>/lib/node_modules/gitnexushub/hooks/gitnexus-enterprise-hook.cjs
|
|
13
|
+
* dev (tsx) → <repo>/gitnexus-connect/hooks/gitnexus-enterprise-hook.cjs
|
|
14
|
+
*
|
|
15
|
+
* Both resolve via `..` from the file's own location. For the dev case,
|
|
16
|
+
* `import.meta.url` points at `src/connect-command.ts`, so `..` from `src/` →
|
|
17
|
+
* package root. For the dist case, `import.meta.url` points at
|
|
18
|
+
* `dist/connect-command.js`, so `..` from `dist/` → package root.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveHookScriptPath(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Connect flow entry point — called from `gnx connect` CLI action and
|
|
23
|
+
* (for testability) directly from unit tests.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runConnect(tokenArg: string | undefined, opts: {
|
|
26
|
+
editor?: string;
|
|
27
|
+
hub: string;
|
|
28
|
+
skipProject?: boolean;
|
|
29
|
+
}): Promise<undefined>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnx connect` flow — register Hub MCP, install skills, install hooks,
|
|
3
|
+
* and write project context files.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from index.ts so it can be imported (and unit-tested) without
|
|
6
|
+
* triggering commander's top-level program.parse() side effect.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
12
|
+
import { generateConnectContext } from './content.js';
|
|
13
|
+
import { isGitRepo, getGitRemoteUrl, parseGitRemote, matchRepo } from './project.js';
|
|
14
|
+
import { writeProjectContext } from './context.js';
|
|
15
|
+
import { detectInstalledEditors } from './editors/detect.js';
|
|
16
|
+
import { installClaudeCodeHook, installCursorHook, installOpenCodeHook, } from './hooks-installer.js';
|
|
17
|
+
import { ok, info, warn, fail, resolveAuth, EDITORS } from './cli-helpers.js';
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the path to the shipped hook script (`gitnexus-enterprise-hook.cjs`).
|
|
20
|
+
*
|
|
21
|
+
* Lives at `hooks/` in the package root, sibling to `dist/` and `src/`:
|
|
22
|
+
* npm install → <npm-prefix>/lib/node_modules/gitnexushub/hooks/gitnexus-enterprise-hook.cjs
|
|
23
|
+
* dev (tsx) → <repo>/gitnexus-connect/hooks/gitnexus-enterprise-hook.cjs
|
|
24
|
+
*
|
|
25
|
+
* Both resolve via `..` from the file's own location. For the dev case,
|
|
26
|
+
* `import.meta.url` points at `src/connect-command.ts`, so `..` from `src/` →
|
|
27
|
+
* package root. For the dist case, `import.meta.url` points at
|
|
28
|
+
* `dist/connect-command.js`, so `..` from `dist/` → package root.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveHookScriptPath() {
|
|
31
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
return path.resolve(here, '..', 'hooks', 'gitnexus-enterprise-hook.cjs');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Connect flow entry point — called from `gnx connect` CLI action and
|
|
36
|
+
* (for testability) directly from unit tests.
|
|
37
|
+
*/
|
|
38
|
+
export async function runConnect(tokenArg, opts) {
|
|
39
|
+
const { api, hubUrl, token } = await resolveAuth(tokenArg, opts.hub);
|
|
40
|
+
// ── Resolve editor ─────────────────────────────────────────────
|
|
41
|
+
const config = await loadConfig();
|
|
42
|
+
let editorId;
|
|
43
|
+
if (opts.editor) {
|
|
44
|
+
if (!(opts.editor in EDITORS)) {
|
|
45
|
+
fail(`Unknown editor: ${pc.bold(opts.editor)}`);
|
|
46
|
+
console.error(` Supported: ${pc.cyan(Object.keys(EDITORS).join(', '))}`);
|
|
47
|
+
console.error('');
|
|
48
|
+
return process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
editorId = opts.editor;
|
|
51
|
+
}
|
|
52
|
+
else if (!config.hubToken) {
|
|
53
|
+
const detected = await detectInstalledEditors();
|
|
54
|
+
if (detected.length === 1) {
|
|
55
|
+
editorId = detected[0];
|
|
56
|
+
info(`Auto-detected editor: ${pc.bold(EDITORS[editorId].name)}`);
|
|
57
|
+
}
|
|
58
|
+
else if (detected.length > 1) {
|
|
59
|
+
warn('Multiple editors detected. Please specify one:');
|
|
60
|
+
for (const id of detected) {
|
|
61
|
+
console.error(` ${pc.cyan('--editor ' + id)}`);
|
|
62
|
+
}
|
|
63
|
+
console.error('');
|
|
64
|
+
return process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
fail('No editor detected. Please specify one:');
|
|
68
|
+
console.error(` ${pc.cyan('--editor claude-code | cursor | windsurf | opencode')}`);
|
|
69
|
+
console.error('');
|
|
70
|
+
return process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Configure editor MCP ────────────────────────────────────────
|
|
74
|
+
let skills = [];
|
|
75
|
+
if (editorId) {
|
|
76
|
+
const editor = EDITORS[editorId];
|
|
77
|
+
info(`Configuring ${pc.bold(editor.name)}...`);
|
|
78
|
+
const result = await editor.configure(hubUrl, token);
|
|
79
|
+
if (result.success) {
|
|
80
|
+
ok(result.message);
|
|
81
|
+
if (result.overrodeCli) {
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(` ${pc.cyan('⬆')} ${pc.bold('GitNexus open-source detected — upgraded to Hub')}`);
|
|
84
|
+
console.log(` ${pc.dim('Remote indexing. Deeper analysis. PR blast radius. Auto-reindex on push.')}`);
|
|
85
|
+
console.log(` ${pc.dim('Your tools (query, context, impact) are unchanged — just faster and smarter.')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fail(result.message);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const bundled = await generateConnectContext('_', {});
|
|
93
|
+
skills = bundled.skills;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Skills load failed — continue without
|
|
97
|
+
}
|
|
98
|
+
if (editor.installSkills && skills.length > 0) {
|
|
99
|
+
const count = await editor.installSkills(skills);
|
|
100
|
+
if (count > 0) {
|
|
101
|
+
ok(`${count} skills installed`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Install editor hooks (PreToolUse augmentation + PostToolUse staleness) ──
|
|
105
|
+
try {
|
|
106
|
+
const hookScriptPath = resolveHookScriptPath();
|
|
107
|
+
if (editorId === 'claude-code') {
|
|
108
|
+
await installClaudeCodeHook({ hookScriptPath });
|
|
109
|
+
ok('Hooks installed');
|
|
110
|
+
}
|
|
111
|
+
else if (editorId === 'cursor') {
|
|
112
|
+
await installCursorHook({ hookScriptPath });
|
|
113
|
+
ok('Hooks installed');
|
|
114
|
+
}
|
|
115
|
+
else if (editorId === 'opencode') {
|
|
116
|
+
await installOpenCodeHook({ hookScriptPath });
|
|
117
|
+
ok('Hooks installed');
|
|
118
|
+
}
|
|
119
|
+
// windsurf: no hook system, skip silently
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
warn(`Hook install failed: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── Write project context ───────────────────────────────────────
|
|
126
|
+
if (!opts.skipProject && isGitRepo()) {
|
|
127
|
+
const remoteUrl = getGitRemoteUrl();
|
|
128
|
+
const remoteFullName = remoteUrl ? parseGitRemote(remoteUrl) : null;
|
|
129
|
+
if (remoteFullName) {
|
|
130
|
+
console.log('');
|
|
131
|
+
info(`Project: ${pc.bold(remoteFullName)}`);
|
|
132
|
+
try {
|
|
133
|
+
const repos = await api.listRepos();
|
|
134
|
+
const matched = matchRepo(remoteFullName, repos);
|
|
135
|
+
if (matched) {
|
|
136
|
+
if (matched.status === 'ready') {
|
|
137
|
+
const ctx = await generateConnectContext(matched.fullName, matched.stats || {});
|
|
138
|
+
const result = await writeProjectContext(process.cwd(), ctx);
|
|
139
|
+
for (const file of result.files) {
|
|
140
|
+
ok(file);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
warn(`Repo status: ${pc.yellow(matched.status)} ${pc.dim('(waiting for indexing)')}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
warn('Repo not indexed on Hub yet');
|
|
149
|
+
console.log(` Add it at: ${pc.cyan(hubUrl)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
fail(`Failed to fetch project context: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log('');
|
|
158
|
+
warn('No GitHub remote found — skipping project context');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (!opts.skipProject) {
|
|
162
|
+
console.log('');
|
|
163
|
+
info('Not inside a git repo — skipping project context');
|
|
164
|
+
}
|
|
165
|
+
// ── Summary ────────────────────────────────────────────────────
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(` ${pc.green('✔')} ${pc.bold('Done!')} Open your editor — GitNexus MCP is ready.`);
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
package/dist/content.js
CHANGED
|
@@ -25,68 +25,68 @@ export const HUB_SKILLS = [
|
|
|
25
25
|
* No detect_changes or rename references (not available via Hub MCP).
|
|
26
26
|
*/
|
|
27
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
|
-
|
|
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
90
|
${GITNEXUS_END_MARKER}`;
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
@@ -9,6 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
11
11
|
import { HUB_SKILLS } from '../content.js';
|
|
12
|
+
import { computeFingerprint, getDeviceName } from '../fingerprint.js';
|
|
12
13
|
async function configure(hubUrl, token) {
|
|
13
14
|
const mcpUrl = `${hubUrl}/mcp`;
|
|
14
15
|
try {
|
|
@@ -18,10 +19,22 @@ async function configure(hubUrl, token) {
|
|
|
18
19
|
existing.mcpServers = {};
|
|
19
20
|
}
|
|
20
21
|
const overrodeCli = !!(existing.mcpServers.gitnexus && 'command' in existing.mcpServers.gitnexus);
|
|
22
|
+
// Claude Code's `~/.claude.json` accepts `type: "http"` for
|
|
23
|
+
// remote HTTP MCP servers. This is the shape that
|
|
24
|
+
// `claude mcp add --transport http <url>` writes and the format
|
|
25
|
+
// documented in the official docs (e.g. Stripe + Notion remote
|
|
26
|
+
// examples). The MCP registry spec uses `"streamable-http"`
|
|
27
|
+
// internally, but the CLI config format uses the short `"http"`
|
|
28
|
+
// alias — commit c602f2e crossed the wires and used the registry
|
|
29
|
+
// value, which silently breaks on current Claude Code versions.
|
|
21
30
|
existing.mcpServers.gitnexus = {
|
|
22
31
|
type: 'http',
|
|
23
32
|
url: mcpUrl,
|
|
24
|
-
headers: {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${token}`,
|
|
35
|
+
'X-Device-Fingerprint': computeFingerprint(),
|
|
36
|
+
'X-Device-Name': getDeviceName(),
|
|
37
|
+
},
|
|
25
38
|
};
|
|
26
39
|
await writeJsonFile(claudeJsonPath, existing);
|
|
27
40
|
return { success: true, message: 'MCP configured in ~/.claude.json', overrodeCli };
|
package/dist/editors/cursor.js
CHANGED
|
@@ -9,11 +9,20 @@ import path from 'path';
|
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
11
11
|
import { HUB_SKILLS } from '../content.js';
|
|
12
|
+
import { computeFingerprint, getDeviceName } from '../fingerprint.js';
|
|
12
13
|
function getMcpConfig(hubUrl, token) {
|
|
14
|
+
// Cursor's `~/.cursor/mcp.json` identifies remote HTTP servers by
|
|
15
|
+
// the presence of `url` — the docs at cursor.com/docs/context/mcp
|
|
16
|
+
// do NOT document a `type` field for remote entries, so we omit
|
|
17
|
+
// it rather than send a registry-shape value that Cursor may
|
|
18
|
+
// start validating later.
|
|
13
19
|
return {
|
|
14
|
-
type: 'streamable-http',
|
|
15
20
|
url: `${hubUrl}/mcp`,
|
|
16
|
-
headers: {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${token}`,
|
|
23
|
+
'X-Device-Fingerprint': computeFingerprint(),
|
|
24
|
+
'X-Device-Name': getDeviceName(),
|
|
25
|
+
},
|
|
17
26
|
};
|
|
18
27
|
}
|
|
19
28
|
async function configure(hubUrl, token) {
|
|
@@ -70,7 +79,9 @@ async function removeSkills() {
|
|
|
70
79
|
await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
|
|
71
80
|
removed++;
|
|
72
81
|
}
|
|
73
|
-
catch {
|
|
82
|
+
catch {
|
|
83
|
+
/* already gone */
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
return removed;
|
|
76
87
|
}
|
package/dist/editors/opencode.js
CHANGED
|
@@ -9,11 +9,16 @@ import path from 'path';
|
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
11
11
|
import { HUB_SKILLS } from '../content.js';
|
|
12
|
+
import { computeFingerprint, getDeviceName } from '../fingerprint.js';
|
|
12
13
|
function getMcpConfig(hubUrl, token) {
|
|
13
14
|
return {
|
|
14
15
|
type: 'remote',
|
|
15
16
|
url: `${hubUrl}/mcp`,
|
|
16
|
-
headers: {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
'X-Device-Fingerprint': computeFingerprint(),
|
|
20
|
+
'X-Device-Name': getDeviceName(),
|
|
21
|
+
},
|
|
17
22
|
};
|
|
18
23
|
}
|
|
19
24
|
async function configure(hubUrl, token) {
|
|
@@ -26,7 +31,11 @@ async function configure(hubUrl, token) {
|
|
|
26
31
|
const overrodeCli = !!(existing.mcp.gitnexus && 'command' in existing.mcp.gitnexus);
|
|
27
32
|
existing.mcp.gitnexus = getMcpConfig(hubUrl, token);
|
|
28
33
|
await writeJsonFile(configPath, existing);
|
|
29
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
message: 'MCP configured in ~/.config/opencode/opencode.json',
|
|
37
|
+
overrodeCli,
|
|
38
|
+
};
|
|
30
39
|
}
|
|
31
40
|
catch (err) {
|
|
32
41
|
return { success: false, message: `Failed: ${err.message}` };
|
|
@@ -70,7 +79,9 @@ async function removeSkills() {
|
|
|
70
79
|
await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
|
|
71
80
|
removed++;
|
|
72
81
|
}
|
|
73
|
-
catch {
|
|
82
|
+
catch {
|
|
83
|
+
/* already gone */
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
return removed;
|
|
76
87
|
}
|