@duytransipher/gitnexus 1.2.2 → 1.3.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.
@@ -185,7 +185,7 @@ export const analyzeCommand = async (inputPath, options) => {
185
185
  // ── Phase 1.5: Blueprint Ingestion (optional) ────────────────────────
186
186
  try {
187
187
  const { ingestBlueprintsIntoGraph } = await import('../unreal/blueprint-ingestion.js');
188
- const bpResult = await ingestBlueprintsIntoGraph(pipelineResult.graph, storagePath);
188
+ const bpResult = await ingestBlueprintsIntoGraph(pipelineResult.graph, storagePath, repoPath);
189
189
  if (bpResult.nodesAdded > 0) {
190
190
  updateBar(61, `Indexed ${bpResult.nodesAdded} Blueprints (${bpResult.edgesAdded} edges)`);
191
191
  }
package/dist/cli/index.js CHANGED
@@ -106,6 +106,7 @@ program
106
106
  .command('unreal-sync')
107
107
  .description('Refresh the Unreal Blueprint asset manifest for the current indexed repo')
108
108
  .option('-r, --repo <name>', 'Target repository')
109
+ .option('--deep', 'Deep mode: load Blueprints fully for native_function_refs (slower, higher memory)')
109
110
  .action(createLazyAction(() => import('./tool.js'), 'syncUnrealAssetManifestCommand'));
110
111
  program
111
112
  .command('unreal-find-refs [functionName]')
@@ -39,6 +39,7 @@ export declare function cypherCommand(query: string, options?: {
39
39
  }): Promise<void>;
40
40
  export declare function syncUnrealAssetManifestCommand(options?: {
41
41
  repo?: string;
42
+ deep?: boolean;
42
43
  }): Promise<void>;
43
44
  export declare function findNativeBlueprintReferencesCommand(functionName: string, options?: {
44
45
  repo?: string;
package/dist/cli/tool.js CHANGED
@@ -129,13 +129,14 @@ export async function cypherCommand(query, options) {
129
129
  }
130
130
  export async function syncUnrealAssetManifestCommand(options) {
131
131
  const backend = await getBackend();
132
- const result = await withUnrealProgress(() => backend.callTool('sync_unreal_asset_manifest', { repo: options?.repo }), { phaseLabel: 'Syncing Unreal asset manifest', successLabel: 'Manifest synced', failLabel: 'Manifest sync failed', isError: isUnrealError });
132
+ const mode = options?.deep ? 'deep' : 'metadata';
133
+ const result = await withUnrealProgress(() => backend.callTool('sync_unreal_asset_manifest', { repo: options?.repo, deep: options?.deep }), { phaseLabel: `Syncing Unreal asset manifest (${mode} mode)`, successLabel: 'Manifest synced', failLabel: 'Manifest sync failed', isError: isUnrealError });
133
134
  if (result?.status === 'error') {
134
135
  console.error(`\n Error: ${result.error}\n`);
135
136
  process.exit(1);
136
137
  }
137
138
  if (result?.asset_count != null) {
138
- console.log(` ${result.asset_count.toLocaleString()} Blueprint assets indexed`);
139
+ console.log(` ${result.asset_count.toLocaleString()} Blueprint assets indexed (${mode} mode)`);
139
140
  if (result.manifest_path)
140
141
  console.log(` ${result.manifest_path}`);
141
142
  }
@@ -276,7 +276,7 @@ export class LocalBackend {
276
276
  case 'rename':
277
277
  return this.rename(repo, params);
278
278
  case 'sync_unreal_asset_manifest':
279
- return this.syncUnrealAssetManifestTool(repo);
279
+ return this.syncUnrealAssetManifestTool(repo, params);
280
280
  case 'find_native_blueprint_references':
281
281
  return this.findNativeBlueprintReferencesTool(repo, params);
282
282
  case 'expand_blueprint_chain':
@@ -426,7 +426,7 @@ export class LocalBackend {
426
426
  let manifest = refreshManifest ? null : await loadUnrealAssetManifest(repo.storagePath);
427
427
  let manifestRefreshed = false;
428
428
  if (!manifest) {
429
- const syncResult = await syncUnrealAssetManifest(repo.storagePath, config);
429
+ const syncResult = await syncUnrealAssetManifest(repo.storagePath, config, repo.repoPath);
430
430
  if (syncResult.status === 'error') {
431
431
  return { error: syncResult.error || 'Failed to build Unreal asset manifest.' };
432
432
  }
@@ -443,7 +443,7 @@ export class LocalBackend {
443
443
  manifestRefreshed,
444
444
  };
445
445
  }
446
- async syncUnrealAssetManifestTool(repo) {
446
+ async syncUnrealAssetManifestTool(repo, params) {
447
447
  const config = await loadUnrealConfig(repo.storagePath);
448
448
  if (!config) {
449
449
  const paths = getUnrealStoragePaths(repo.storagePath);
@@ -451,7 +451,7 @@ export class LocalBackend {
451
451
  error: `Unreal analyzer is not configured for this repo. Create ${paths.config_path} with editor_cmd and project_path.`,
452
452
  };
453
453
  }
454
- return syncUnrealAssetManifest(repo.storagePath, config);
454
+ return syncUnrealAssetManifest(repo.storagePath, config, repo.repoPath, params?.deep);
455
455
  }
456
456
  async findNativeBlueprintReferencesTool(repo, params) {
457
457
  const targetResult = await this.resolveNativeFunctionTarget(repo, params);
package/dist/mcp/tools.js CHANGED
@@ -7,14 +7,14 @@
7
7
  export const GITNEXUS_TOOLS = [
8
8
  {
9
9
  name: 'list_repos',
10
- description: `List all indexed repositories available to GitNexus.
11
-
12
- Returns each repo's name, path, indexed date, last commit, and stats.
13
-
14
- WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
15
- AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
16
-
17
- When multiple repos are indexed, you MUST specify the "repo" parameter
10
+ description: `List all indexed repositories available to GitNexus.
11
+
12
+ Returns each repo's name, path, indexed date, last commit, and stats.
13
+
14
+ WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
15
+ AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
16
+
17
+ When multiple repos are indexed, you MUST specify the "repo" parameter
18
18
  on other tools (query, context, impact, etc.) to target the correct one.`,
19
19
  inputSchema: {
20
20
  type: 'object',
@@ -29,6 +29,10 @@ on other tools (query, context, impact, etc.) to target the correct one.`,
29
29
  Uses the configured Unreal Editor commandlet to enumerate Blueprint assets and
30
30
  store a manifest under .gitnexus/unreal/asset-manifest.json.
31
31
 
32
+ Two modes:
33
+ - metadata (default): Uses AssetRegistry only — zero Blueprint loading, fast and safe for large projects.
34
+ - deep: Loads each Blueprint fully to extract native_function_refs and graph data. Uses batch GC. Pass deep=true.
35
+
32
36
  WHEN TO USE: Before Blueprint-reference queries, after large Blueprint changes, or
33
37
  to validate Unreal analyzer configuration.
34
38
  AFTER THIS: Use find_native_blueprint_references() or find_blueprints_derived_from_native_class().`,
@@ -36,6 +40,7 @@ AFTER THIS: Use find_native_blueprint_references() or find_blueprints_derived_fr
36
40
  type: 'object',
37
41
  properties: {
38
42
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
43
+ deep: { type: 'boolean', description: 'Deep mode: fully load Blueprints for native_function_refs (slower, higher memory). Default: false (metadata-only).' },
39
44
  },
40
45
  required: [],
41
46
  },
@@ -106,15 +111,15 @@ AFTER THIS: Use find_native_blueprint_references() for function-level graph refe
106
111
  name: 'query',
107
112
  description: `Query the code knowledge graph for execution flows related to a concept.
108
113
  Returns processes (call chains) ranked by relevance, each with its symbols and file locations.
109
-
110
- WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
111
- AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
112
-
113
- Returns results grouped by process (execution flow):
114
- - processes: ranked execution flows with relevance priority
115
- - process_symbols: all symbols in those flows with file locations and module (functional area)
116
- - definitions: standalone types/interfaces not in any process
117
-
114
+
115
+ WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
116
+ AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
117
+
118
+ Returns results grouped by process (execution flow):
119
+ - processes: ranked execution flows with relevance priority
120
+ - process_symbols: all symbols in those flows with file locations and module (functional area)
121
+ - definitions: standalone types/interfaces not in any process
122
+
118
123
  Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`,
119
124
  inputSchema: {
120
125
  type: 'object',
@@ -132,49 +137,49 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
132
137
  },
133
138
  {
134
139
  name: 'cypher',
135
- description: `Execute Cypher query against the code knowledge graph.
136
-
137
- WHEN TO USE: Complex structural queries that search/explore can't answer. READ gitnexus://repo/{name}/schema first for the full schema.
138
- AFTER THIS: Use context() on result symbols for deeper context.
139
-
140
- SCHEMA:
141
- - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
142
- - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
143
- - All edges via single CodeRelation table with 'type' property
144
- - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
145
- - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
146
-
147
- EXAMPLES:
148
- • Find callers of a function:
149
- MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
150
-
151
- • Find community members:
152
- MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name
153
-
154
- • Trace a process:
155
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
156
-
157
- • Find all methods of a class:
158
- MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
159
-
160
- • Find all properties of a class:
161
- MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.description
162
-
163
- • Find all writers of a field:
164
- MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
165
-
166
- • Find method overrides (MRO resolution):
167
- MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
168
-
169
- • Detect diamond inheritance:
170
- MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
171
-
172
- OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
173
-
174
- TIPS:
175
- - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
176
- - Community = auto-detected functional area (Leiden algorithm)
177
- - Process = execution flow trace from entry point to terminal
140
+ description: `Execute Cypher query against the code knowledge graph.
141
+
142
+ WHEN TO USE: Complex structural queries that search/explore can't answer. READ gitnexus://repo/{name}/schema first for the full schema.
143
+ AFTER THIS: Use context() on result symbols for deeper context.
144
+
145
+ SCHEMA:
146
+ - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
147
+ - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
148
+ - All edges via single CodeRelation table with 'type' property
149
+ - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
150
+ - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
151
+
152
+ EXAMPLES:
153
+ • Find callers of a function:
154
+ MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
155
+
156
+ • Find community members:
157
+ MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name
158
+
159
+ • Trace a process:
160
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
161
+
162
+ • Find all methods of a class:
163
+ MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
164
+
165
+ • Find all properties of a class:
166
+ MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.description
167
+
168
+ • Find all writers of a field:
169
+ MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
170
+
171
+ • Find method overrides (MRO resolution):
172
+ MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
173
+
174
+ • Detect diamond inheritance:
175
+ MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
176
+
177
+ OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
178
+
179
+ TIPS:
180
+ - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
181
+ - Community = auto-detected functional area (Leiden algorithm)
182
+ - Process = execution flow trace from entry point to terminal
178
183
  - Use heuristicLabel (not label) for human-readable community/process names`,
179
184
  inputSchema: {
180
185
  type: 'object',
@@ -187,14 +192,14 @@ TIPS:
187
192
  },
188
193
  {
189
194
  name: 'context',
190
- description: `360-degree view of a single code symbol.
191
- Shows categorized incoming/outgoing references (calls, imports, extends, implements, methods, properties, overrides), process participation, and file location.
192
-
193
- WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
194
- AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
195
-
196
- Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.
197
-
195
+ description: `360-degree view of a single code symbol.
196
+ Shows categorized incoming/outgoing references (calls, imports, extends, implements, methods, properties, overrides), process participation, and file location.
197
+
198
+ WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
199
+ AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
200
+
201
+ Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.
202
+
198
203
  NOTE: ACCESSES edges (field read/write tracking) are included in context results. Coverage: reads detected during call chain resolution (e.g., user.address.save() emits a read on 'address'). Standalone reads and writes require Phase 2.`,
199
204
  inputSchema: {
200
205
  type: 'object',
@@ -210,12 +215,12 @@ NOTE: ACCESSES edges (field read/write tracking) are included in context results
210
215
  },
211
216
  {
212
217
  name: 'detect_changes',
213
- description: `Analyze uncommitted git changes and find affected execution flows.
214
- Maps git diff hunks to indexed symbols, then traces which processes are impacted.
215
-
216
- WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
217
- AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
218
-
218
+ description: `Analyze uncommitted git changes and find affected execution flows.
219
+ Maps git diff hunks to indexed symbols, then traces which processes are impacted.
220
+
221
+ WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
222
+ AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
223
+
219
224
  Returns: changed symbols, affected processes, and a risk summary.`,
220
225
  inputSchema: {
221
226
  type: 'object',
@@ -229,14 +234,14 @@ Returns: changed symbols, affected processes, and a risk summary.`,
229
234
  },
230
235
  {
231
236
  name: 'rename',
232
- description: `Multi-file coordinated rename using the knowledge graph + text search.
233
- Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default.
234
-
235
- WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace.
236
- AFTER THIS: Run detect_changes() to verify no unexpected side effects.
237
-
238
- Each edit is tagged with confidence:
239
- - "graph": found via knowledge graph relationships (high confidence, safe to accept)
237
+ description: `Multi-file coordinated rename using the knowledge graph + text search.
238
+ Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default.
239
+
240
+ WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace.
241
+ AFTER THIS: Run detect_changes() to verify no unexpected side effects.
242
+
243
+ Each edit is tagged with confidence:
244
+ - "graph": found via knowledge graph relationships (high confidence, safe to accept)
240
245
  - "text_search": found via regex text search (lower confidence, review carefully)`,
241
246
  inputSchema: {
242
247
  type: 'object',
@@ -253,27 +258,27 @@ Each edit is tagged with confidence:
253
258
  },
254
259
  {
255
260
  name: 'impact',
256
- description: `Analyze the blast radius of changing a code symbol.
257
- Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
258
-
259
- WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
260
- AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
261
-
262
- Output includes:
263
- - risk: LOW / MEDIUM / HIGH / CRITICAL
264
- - summary: direct callers, processes affected, modules affected
265
- - affected_processes: which execution flows break and at which step
266
- - affected_modules: which functional areas are hit (direct vs indirect)
267
- - byDepth: all affected symbols grouped by traversal depth
268
-
269
- Depth groups:
270
- - d=1: WILL BREAK (direct callers/importers)
271
- - d=2: LIKELY AFFECTED (indirect)
272
- - d=3: MAY NEED TESTING (transitive)
273
-
274
- TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
275
-
276
- EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
261
+ description: `Analyze the blast radius of changing a code symbol.
262
+ Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
263
+
264
+ WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
265
+ AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
266
+
267
+ Output includes:
268
+ - risk: LOW / MEDIUM / HIGH / CRITICAL
269
+ - summary: direct callers, processes affected, modules affected
270
+ - affected_processes: which execution flows break and at which step
271
+ - affected_modules: which functional areas are hit (direct vs indirect)
272
+ - byDepth: all affected symbols grouped by traversal depth
273
+
274
+ Depth groups:
275
+ - d=1: WILL BREAK (direct callers/importers)
276
+ - d=2: LIKELY AFFECTED (indirect)
277
+ - d=3: MAY NEED TESTING (transitive)
278
+
279
+ TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
280
+
281
+ EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
277
282
  Confidence: 1.0 = certain, <0.8 = fuzzy match`,
278
283
  inputSchema: {
279
284
  type: 'object',
@@ -10,9 +10,16 @@ export interface BlueprintIngestionResult {
10
10
  nodesAdded: number;
11
11
  edgesAdded: number;
12
12
  }
13
+ /**
14
+ * Convert an Unreal asset path to a filesystem-relative path for ignore matching.
15
+ * "/Game/Characters/BP_Hero" → "Content/Characters/BP_Hero"
16
+ * "/MyPlugin/Maps/TestMap" → "Plugins/MyPlugin/Content/Maps/TestMap"
17
+ * "/Engine/BasicShapes/Cube" → "Engine/Content/BasicShapes/Cube"
18
+ */
19
+ export declare const assetPathToRelative: (assetPath: string) => string;
13
20
  /**
14
21
  * Ingest Blueprint assets from the Unreal asset manifest into the knowledge graph.
15
22
  * Creates Blueprint nodes and edges (EXTENDS, CALLS, IMPORTS) linking them to
16
23
  * existing C++ symbols in the graph.
17
24
  */
18
- export declare const ingestBlueprintsIntoGraph: (graph: KnowledgeGraph, storagePath: string) => Promise<BlueprintIngestionResult>;
25
+ export declare const ingestBlueprintsIntoGraph: (graph: KnowledgeGraph, storagePath: string, repoPath?: string) => Promise<BlueprintIngestionResult>;
@@ -8,6 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { generateId } from '../lib/utils.js';
11
+ import { shouldIgnorePath, loadIgnoreRules } from '../config/ignore-service.js';
11
12
  /** Extract a display name from an Unreal asset path (last segment). */
12
13
  const extractAssetName = (assetPath) => {
13
14
  // "/Game/Characters/BP_Hero" → "BP_Hero"
@@ -29,12 +30,33 @@ const extractClassName = (unrealPath) => {
29
30
  const slashIdx = unrealPath.lastIndexOf('/');
30
31
  return slashIdx >= 0 ? unrealPath.slice(slashIdx + 1) : unrealPath;
31
32
  };
33
+ /**
34
+ * Convert an Unreal asset path to a filesystem-relative path for ignore matching.
35
+ * "/Game/Characters/BP_Hero" → "Content/Characters/BP_Hero"
36
+ * "/MyPlugin/Maps/TestMap" → "Plugins/MyPlugin/Content/Maps/TestMap"
37
+ * "/Engine/BasicShapes/Cube" → "Engine/Content/BasicShapes/Cube"
38
+ */
39
+ export const assetPathToRelative = (assetPath) => {
40
+ // Strip leading slash
41
+ const trimmed = assetPath.startsWith('/') ? assetPath.slice(1) : assetPath;
42
+ const slashIdx = trimmed.indexOf('/');
43
+ if (slashIdx < 0)
44
+ return trimmed;
45
+ const mount = trimmed.slice(0, slashIdx);
46
+ const rest = trimmed.slice(slashIdx + 1);
47
+ if (mount === 'Game')
48
+ return `Content/${rest}`;
49
+ if (mount === 'Engine')
50
+ return `Engine/Content/${rest}`;
51
+ // Plugin mounts: /PluginName/X → Plugins/PluginName/Content/X
52
+ return `Plugins/${mount}/Content/${rest}`;
53
+ };
32
54
  /**
33
55
  * Ingest Blueprint assets from the Unreal asset manifest into the knowledge graph.
34
56
  * Creates Blueprint nodes and edges (EXTENDS, CALLS, IMPORTS) linking them to
35
57
  * existing C++ symbols in the graph.
36
58
  */
37
- export const ingestBlueprintsIntoGraph = async (graph, storagePath) => {
59
+ export const ingestBlueprintsIntoGraph = async (graph, storagePath, repoPath) => {
38
60
  const manifestPath = path.join(storagePath, 'unreal', 'asset-manifest.json');
39
61
  let manifest;
40
62
  try {
@@ -47,6 +69,16 @@ export const ingestBlueprintsIntoGraph = async (graph, storagePath) => {
47
69
  if (!manifest.assets || manifest.assets.length === 0) {
48
70
  return { nodesAdded: 0, edgesAdded: 0 };
49
71
  }
72
+ // ── Filter assets through ignore rules ─────────────────────────────
73
+ const ig = repoPath ? await loadIgnoreRules(repoPath) : null;
74
+ const assets = manifest.assets.filter(asset => {
75
+ const relPath = assetPathToRelative(asset.asset_path);
76
+ if (shouldIgnorePath(relPath))
77
+ return false;
78
+ if (ig && ig.ignores(relPath))
79
+ return false;
80
+ return true;
81
+ });
50
82
  // ── Build lookup indexes from existing graph nodes ──────────────────
51
83
  // Class/Struct nodes keyed by name (for parent class matching)
52
84
  const classByName = new Map();
@@ -101,7 +133,7 @@ export const ingestBlueprintsIntoGraph = async (graph, storagePath) => {
101
133
  let edgeCounter = 0;
102
134
  // Track created Blueprint IDs for second-pass dependency edges
103
135
  const blueprintIdByAssetPath = new Map();
104
- for (const asset of manifest.assets) {
136
+ for (const asset of assets) {
105
137
  const bpId = generateId('Blueprint', asset.asset_path);
106
138
  const bpName = extractAssetName(asset.asset_path);
107
139
  graph.addNode({
@@ -174,7 +206,7 @@ export const ingestBlueprintsIntoGraph = async (graph, storagePath) => {
174
206
  }
175
207
  }
176
208
  // ── Second pass: Blueprint-to-Blueprint IMPORTS edges ───────────────
177
- for (const asset of manifest.assets) {
209
+ for (const asset of assets) {
178
210
  const deps = asset.dependencies || [];
179
211
  if (deps.length === 0)
180
212
  continue;
@@ -1,4 +1,4 @@
1
1
  import type { ExpandBlueprintChainResult, FindNativeBlueprintReferencesResult, NativeFunctionTarget, SyncUnrealAssetManifestResult, UnrealBlueprintCandidate, UnrealConfig } from './types.js';
2
- export declare function syncUnrealAssetManifest(storagePath: string, config: UnrealConfig): Promise<SyncUnrealAssetManifestResult>;
2
+ export declare function syncUnrealAssetManifest(storagePath: string, config: UnrealConfig, repoPath?: string, deep?: boolean): Promise<SyncUnrealAssetManifestResult>;
3
3
  export declare function findNativeBlueprintReferences(storagePath: string, config: UnrealConfig, target: NativeFunctionTarget, candidateAssets: UnrealBlueprintCandidate[], manifestPath?: string): Promise<FindNativeBlueprintReferencesResult>;
4
4
  export declare function expandBlueprintChain(storagePath: string, config: UnrealConfig, assetPath: string, chainAnchorId: string, direction: 'upstream' | 'downstream', maxDepth: number): Promise<ExpandBlueprintChainResult>;
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { ensureUnrealStorage, saveUnrealAssetManifest } from './config.js';
7
+ import { loadIgnoreRules } from '../config/ignore-service.js';
7
8
  const execFileAsync = promisify(execFile);
8
9
  function buildBaseArgs(config, operation, outputPath) {
9
10
  return [
@@ -68,10 +69,74 @@ async function readOutputJson(outputPath, stdout) {
68
69
  return JSON.parse(stdout);
69
70
  }
70
71
  }
71
- export async function syncUnrealAssetManifest(storagePath, config) {
72
+ /**
73
+ * Build include/exclude prefix filters for the Unreal commandlet.
74
+ * Merges config `include_paths`/`exclude_paths` with .gitnexusignore patterns,
75
+ * mapping filesystem patterns to Unreal asset path prefixes.
76
+ */
77
+ async function buildFilterPrefixes(repoPath, config) {
78
+ const include_prefixes = [];
79
+ const exclude_prefixes = [];
80
+ // Add explicit include_paths (whitelist) from unreal config
81
+ if (config.include_paths && Array.isArray(config.include_paths)) {
82
+ for (const p of config.include_paths) {
83
+ include_prefixes.push(p);
84
+ }
85
+ }
86
+ // Add explicit exclude_paths from unreal config
87
+ if (config.exclude_paths && Array.isArray(config.exclude_paths)) {
88
+ for (const p of config.exclude_paths) {
89
+ exclude_prefixes.push(p);
90
+ }
91
+ }
92
+ // Derive exclude prefixes from .gitnexusignore patterns
93
+ if (repoPath) {
94
+ const ig = await loadIgnoreRules(repoPath);
95
+ if (ig) {
96
+ const projectDir = path.dirname(config.project_path);
97
+ try {
98
+ const contentDir = path.join(projectDir, 'Content');
99
+ const entries = await fs.readdir(contentDir, { withFileTypes: true });
100
+ for (const entry of entries) {
101
+ if (entry.isDirectory()) {
102
+ const relPath = `Content/${entry.name}`;
103
+ if (ig.ignores(relPath) || ig.ignores(relPath + '/')) {
104
+ exclude_prefixes.push(`/Game/${entry.name}`);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ catch { /* Content dir might not exist */ }
110
+ try {
111
+ const pluginsDir = path.join(projectDir, 'Plugins');
112
+ const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ if (entry.isDirectory()) {
115
+ const relPath = `Plugins/${entry.name}`;
116
+ if (ig.ignores(relPath) || ig.ignores(relPath + '/')) {
117
+ exclude_prefixes.push(`/${entry.name}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ catch { /* Plugins dir might not exist */ }
123
+ }
124
+ }
125
+ return { include_prefixes, exclude_prefixes };
126
+ }
127
+ export async function syncUnrealAssetManifest(storagePath, config, repoPath, deep) {
72
128
  const unrealPaths = await ensureUnrealStorage(storagePath);
73
129
  const { outputPath } = requestPaths(unrealPaths);
74
130
  const args = buildBaseArgs(config, 'SyncAssets', outputPath);
131
+ // Pass scan mode: metadata (default, zero loading) or deep (full Blueprint loading)
132
+ args.push(`-Mode=${deep ? 'deep' : 'metadata'}`);
133
+ // Build and pass filter prefixes (include + exclude)
134
+ const filters = await buildFilterPrefixes(repoPath, config);
135
+ if (filters.include_prefixes.length > 0 || filters.exclude_prefixes.length > 0) {
136
+ const filterJsonPath = path.join(unrealPaths.requests_dir, `filter-${randomUUID()}.json`);
137
+ await fs.writeFile(filterJsonPath, JSON.stringify(filters), 'utf-8');
138
+ args.push(`-FilterJson=${filterJsonPath}`);
139
+ }
75
140
  try {
76
141
  const { stdout } = await runCommand(config, 'SyncAssets', args);
77
142
  const manifest = await readOutputJson(outputPath, stdout);
@@ -19,6 +19,10 @@ export interface UnrealConfig {
19
19
  timeout_ms?: number;
20
20
  working_directory?: string;
21
21
  extra_args?: string[];
22
+ /** Unreal package path prefixes to exclude from sync (e.g., "/Game/ThirdParty", "/SomePlugin") */
23
+ exclude_paths?: string[];
24
+ /** Unreal package path prefixes to include (whitelist). If set, ONLY assets under these prefixes are scanned. */
25
+ include_paths?: string[];
22
26
  }
23
27
  export interface UnrealStoragePaths {
24
28
  root_dir: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duytransipher/gitnexus",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Sipher-maintained fork of GitNexus for graph-powered code intelligence via MCP and CLI.",
5
5
  "author": "DuyTranSipher",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",