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 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: { Authorization: `Bearer ${this.token}` },
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
- Authorization: `Bearer ${this.token}`,
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: { Authorization: `Bearer ${token}` },
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 };
@@ -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: { Authorization: `Bearer ${token}` },
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 { /* already gone */ }
82
+ catch {
83
+ /* already gone */
84
+ }
74
85
  }
75
86
  return removed;
76
87
  }
@@ -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: { Authorization: `Bearer ${token}` },
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 { success: true, message: 'MCP configured in ~/.config/opencode/opencode.json', overrodeCli };
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 { /* already gone */ }
82
+ catch {
83
+ /* already gone */
84
+ }
74
85
  }
75
86
  return removed;
76
87
  }