gitnexus 1.6.4 → 1.6.5-rc.10
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/README.md +1 -1
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +30 -12
- package/dist/cli/analyze.d.ts +18 -0
- package/dist/cli/analyze.js +33 -4
- package/dist/cli/augment.d.ts +1 -1
- package/dist/cli/augment.js +1 -1
- package/dist/cli/index.js +6 -1
- package/dist/core/augmentation/engine.d.ts +2 -2
- package/dist/core/augmentation/engine.js +2 -2
- package/dist/core/embeddings/ast-utils.js +2 -1
- package/dist/core/embeddings/http-client.js +22 -6
- package/dist/core/group/extractors/grpc-extractor.js +2 -1
- package/dist/core/group/extractors/http-patterns/python.js +175 -0
- package/dist/core/group/extractors/http-route-extractor.js +2 -1
- package/dist/core/group/extractors/include-extractor.js +2 -1
- package/dist/core/group/extractors/thrift-extractor.js +2 -1
- package/dist/core/group/extractors/tree-sitter-scanner.js +2 -1
- package/dist/core/ingestion/call-processor.js +3 -2
- package/dist/core/ingestion/heritage-processor.js +3 -2
- package/dist/core/ingestion/import-processor.js +2 -1
- package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
- package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
- package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
- package/dist/core/ingestion/languages/c/arity.js +18 -0
- package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/c/captures.js +105 -0
- package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
- package/dist/core/ingestion/languages/c/header-scan.js +55 -0
- package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
- package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
- package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
- package/dist/core/ingestion/languages/c/import-target.js +57 -0
- package/dist/core/ingestion/languages/c/index.d.ts +11 -0
- package/dist/core/ingestion/languages/c/index.js +11 -0
- package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
- package/dist/core/ingestion/languages/c/interpret.js +48 -0
- package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
- package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
- package/dist/core/ingestion/languages/c/query.d.ts +3 -0
- package/dist/core/ingestion/languages/c/query.js +161 -0
- package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
- package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
- package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
- package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
- package/dist/core/ingestion/languages/c-cpp.js +10 -0
- package/dist/core/ingestion/languages/csharp/captures.js +2 -1
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +3 -2
- package/dist/core/ingestion/languages/go/captures.js +2 -1
- package/dist/core/ingestion/languages/go/range-binding.js +2 -1
- package/dist/core/ingestion/languages/python/captures.js +2 -1
- package/dist/core/ingestion/languages/typescript/captures.js +2 -1
- package/dist/core/ingestion/parsing-processor.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +11 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +2 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +13 -2
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +4 -1
- package/dist/core/ingestion/workers/parse-worker.js +2 -1
- package/dist/core/lbug/lbug-adapter.js +8 -0
- package/dist/core/lbug/pool-adapter.js +19 -2
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +5 -1
- package/dist/core/tree-sitter/safe-parse.d.ts +6 -0
- package/dist/core/tree-sitter/safe-parse.js +32 -0
- package/dist/server/git-clone.js +10 -13
- package/dist/storage/git.d.ts +17 -6
- package/dist/storage/git.js +46 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|
|
33
33
|
| Editor | MCP | Skills | Hooks (auto-augment) | Support |
|
|
34
34
|
|--------|-----|--------|---------------------|---------|
|
|
35
35
|
| **Claude Code** | Yes | Yes | Yes (PreToolUse) | **Full** |
|
|
36
|
-
| **Cursor** | Yes | Yes |
|
|
36
|
+
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
|
|
37
37
|
| **Codex** | Yes | Yes | — | MCP + Skills |
|
|
38
38
|
| **Windsurf** | Yes | — | — | MCP |
|
|
39
39
|
| **OpenCode** | Yes | Yes | — | MCP + Skills |
|
package/dist/cli/ai-context.d.ts
CHANGED
package/dist/cli/ai-context.js
CHANGED
|
@@ -68,20 +68,31 @@ async function findGroupsContainingRegistryName(registryName) {
|
|
|
68
68
|
}
|
|
69
69
|
return hits;
|
|
70
70
|
}
|
|
71
|
-
function generateGitNexusContent(projectName, stats, generatedSkills, groupNames, noStats) {
|
|
71
|
+
function generateGitNexusContent(projectName, stats, generatedSkills, groupNames, noStats, skipSkills) {
|
|
72
72
|
const generatedRows = generatedSkills && generatedSkills.length > 0
|
|
73
73
|
? generatedSkills
|
|
74
74
|
.map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`)
|
|
75
75
|
.join('\n')
|
|
76
76
|
: '';
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
// Standard skill rows reference files installed by installSkills(). When
|
|
78
|
+
// --skip-skills suppresses that install, these rows must be omitted — else
|
|
79
|
+
// AGENTS.md/CLAUDE.md would direct agents to read files that don't exist.
|
|
80
|
+
// Community skills (generatedRows) live in .claude/skills/generated/ and
|
|
81
|
+
// are independent of --skip-skills, so they remain when present.
|
|
82
|
+
const standardSkillsRows = skipSkills
|
|
83
|
+
? ''
|
|
84
|
+
: `| Understand architecture / "How does X work?" | \`.claude/skills/gitnexus/gitnexus-exploring/SKILL.md\` |
|
|
80
85
|
| Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md\` |
|
|
81
86
|
| Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus/gitnexus-debugging/SKILL.md\` |
|
|
82
87
|
| Rename / extract / split / refactor | \`.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md\` |
|
|
83
88
|
| Tools, resources, schema reference | \`.claude/skills/gitnexus/gitnexus-guide/SKILL.md\` |
|
|
84
|
-
| Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus/gitnexus-cli/SKILL.md\`
|
|
89
|
+
| Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus/gitnexus-cli/SKILL.md\` |`;
|
|
90
|
+
const tableBody = [standardSkillsRows, generatedRows].filter(Boolean).join('\n');
|
|
91
|
+
const skillsTable = tableBody
|
|
92
|
+
? `| Task | Read this skill file |
|
|
93
|
+
|------|---------------------|
|
|
94
|
+
${tableBody}`
|
|
95
|
+
: '';
|
|
85
96
|
return `${GITNEXUS_START_MARKER}
|
|
86
97
|
# GitNexus — Code Intelligence
|
|
87
98
|
|
|
@@ -119,11 +130,13 @@ ${groupNames && groupNames.length > 0
|
|
|
119
130
|
This repository is listed under GitNexus **group(s): ${groupNames.join(', ')}** (see \`~/.gitnexus/groups/\`). For cross-repo analysis, use MCP tools \`impact\`, \`query\`, and \`context\` with \`repo\` set to \`@<groupName>\` or \`@<groupName>/<memberPath>\` (paths match keys in that group’s \`group.yaml\`). Use \`group_list\` / \`group_sync\` for membership and sync. From the terminal: \`npx gitnexus group list\`, \`npx gitnexus group sync <name>\`, \`npx gitnexus group impact <name> --target <symbol> --repo <group-path>\`.
|
|
120
131
|
|
|
121
132
|
`
|
|
122
|
-
: ''}
|
|
133
|
+
: ''}${skillsTable
|
|
134
|
+
? `## CLI
|
|
123
135
|
|
|
124
136
|
${skillsTable}
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
`
|
|
139
|
+
: ''}${GITNEXUS_END_MARKER}`;
|
|
127
140
|
}
|
|
128
141
|
/**
|
|
129
142
|
* Check if a file exists
|
|
@@ -246,7 +259,7 @@ Use GitNexus tools to accomplish this task.
|
|
|
246
259
|
*/
|
|
247
260
|
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, generatedSkills, options) {
|
|
248
261
|
const groupNames = await findGroupsContainingRegistryName(projectName);
|
|
249
|
-
const content = generateGitNexusContent(projectName, stats, generatedSkills, groupNames, options?.noStats);
|
|
262
|
+
const content = generateGitNexusContent(projectName, stats, generatedSkills, groupNames, options?.noStats, options?.skipSkills);
|
|
250
263
|
const createdFiles = [];
|
|
251
264
|
if (!options?.skipAgentsMd) {
|
|
252
265
|
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
|
|
@@ -262,10 +275,15 @@ export async function generateAIContextFiles(repoPath, _storagePath, projectName
|
|
|
262
275
|
createdFiles.push('AGENTS.md (skipped via --skip-agents-md)');
|
|
263
276
|
createdFiles.push('CLAUDE.md (skipped via --skip-agents-md)');
|
|
264
277
|
}
|
|
265
|
-
// Install skills to .claude/skills/gitnexus/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
// Install skills to .claude/skills/gitnexus/ (unless --skip-skills)
|
|
279
|
+
if (!options?.skipSkills) {
|
|
280
|
+
const installedSkills = await installSkills(repoPath);
|
|
281
|
+
if (installedSkills.length > 0) {
|
|
282
|
+
createdFiles.push(`.claude/skills/gitnexus/ (${installedSkills.length} skills)`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
createdFiles.push('.claude/skills/gitnexus/ (skipped via --skip-skills)');
|
|
269
287
|
}
|
|
270
288
|
return { files: createdFiles };
|
|
271
289
|
}
|
package/dist/cli/analyze.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface AnalyzeOptions {
|
|
|
29
29
|
skipAgentsMd?: boolean;
|
|
30
30
|
/** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
|
|
31
31
|
noStats?: boolean;
|
|
32
|
+
/** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
|
|
33
|
+
skipSkills?: boolean;
|
|
34
|
+
/** Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills). */
|
|
35
|
+
indexOnly?: boolean;
|
|
32
36
|
/** Index the folder even when no .git directory is present. */
|
|
33
37
|
skipGit?: boolean;
|
|
34
38
|
/**
|
|
@@ -59,4 +63,18 @@ export interface AnalyzeOptions {
|
|
|
59
63
|
embeddingSubBatchSize?: string;
|
|
60
64
|
embeddingDevice?: string;
|
|
61
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the post-index skill step should run.
|
|
68
|
+
*
|
|
69
|
+
* The gated block does two things in sequence: (1) generates the community
|
|
70
|
+
* skill files from `--skills`, and (2) re-runs `generateAIContextFiles` so
|
|
71
|
+
* AGENTS.md/CLAUDE.md can reference the freshly written skills. Both are
|
|
72
|
+
* suppressed together — `--index-only` drops the entire step, not just the
|
|
73
|
+
* community-skill write. Name retained for the test contract; see call site
|
|
74
|
+
* in `analyzeCommand` for the AGENTS.md/CLAUDE.md re-generation it also gates.
|
|
75
|
+
*
|
|
76
|
+
* Kept as a pure helper so the `--index-only --skills` contract is unit-tested
|
|
77
|
+
* without booting the full analyze pipeline (#742 review).
|
|
78
|
+
*/
|
|
79
|
+
export declare const shouldGenerateCommunitySkillFiles: (options: Pick<AnalyzeOptions, "skills" | "indexOnly"> | undefined, pipelineResult: unknown) => boolean;
|
|
62
80
|
export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
|
package/dist/cli/analyze.js
CHANGED
|
@@ -86,6 +86,20 @@ function ensureHeap() {
|
|
|
86
86
|
}
|
|
87
87
|
return true;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether the post-index skill step should run.
|
|
91
|
+
*
|
|
92
|
+
* The gated block does two things in sequence: (1) generates the community
|
|
93
|
+
* skill files from `--skills`, and (2) re-runs `generateAIContextFiles` so
|
|
94
|
+
* AGENTS.md/CLAUDE.md can reference the freshly written skills. Both are
|
|
95
|
+
* suppressed together — `--index-only` drops the entire step, not just the
|
|
96
|
+
* community-skill write. Name retained for the test contract; see call site
|
|
97
|
+
* in `analyzeCommand` for the AGENTS.md/CLAUDE.md re-generation it also gates.
|
|
98
|
+
*
|
|
99
|
+
* Kept as a pure helper so the `--index-only --skills` contract is unit-tested
|
|
100
|
+
* without booting the full analyze pipeline (#742 review).
|
|
101
|
+
*/
|
|
102
|
+
export const shouldGenerateCommunitySkillFiles = (options, pipelineResult) => Boolean(options?.skills && pipelineResult && !options?.indexOnly);
|
|
89
103
|
export const analyzeCommand = async (inputPath, options) => {
|
|
90
104
|
if (ensureHeap())
|
|
91
105
|
return;
|
|
@@ -151,6 +165,15 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
151
165
|
process.env.GITNEXUS_EMBEDDING_DEVICE = options.embeddingDevice;
|
|
152
166
|
}
|
|
153
167
|
console.log('\n GitNexus Analyzer\n');
|
|
168
|
+
// `--index-only` is the stronger contract — it suppresses every form of file
|
|
169
|
+
// injection, including community skill writes that `--skills` would normally
|
|
170
|
+
// produce. Surface the override explicitly so users don't wonder why a
|
|
171
|
+
// pipeline re-index ran but no skill files appeared. The pipeline still
|
|
172
|
+
// re-runs (see `force: options?.force || options?.skills` below); the warning
|
|
173
|
+
// is purely about the dropped post-index write step.
|
|
174
|
+
if (options?.indexOnly && options?.skills) {
|
|
175
|
+
console.log(' Note: --index-only overrides --skills; community skill files will not be written.\n');
|
|
176
|
+
}
|
|
154
177
|
let repoPath;
|
|
155
178
|
if (inputPath) {
|
|
156
179
|
repoPath = path.resolve(inputPath);
|
|
@@ -285,6 +308,9 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
285
308
|
const t0 = Date.now();
|
|
286
309
|
// ── Run shared analysis orchestrator ───────────────────────────────
|
|
287
310
|
try {
|
|
311
|
+
const skipAll = options?.indexOnly;
|
|
312
|
+
const skipAgentsMd = skipAll || options?.skipAgentsMd;
|
|
313
|
+
const skipSkills = skipAll || options?.skipSkills;
|
|
288
314
|
const result = await runFullAnalysis(repoPath, {
|
|
289
315
|
// Pipeline re-index — OR'd with --skills because skill generation
|
|
290
316
|
// needs a fresh pipelineResult. Has no bearing on the registry
|
|
@@ -294,7 +320,8 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
294
320
|
embeddingsNodeLimit,
|
|
295
321
|
dropEmbeddings: options?.dropEmbeddings,
|
|
296
322
|
skipGit: options?.skipGit,
|
|
297
|
-
skipAgentsMd
|
|
323
|
+
skipAgentsMd,
|
|
324
|
+
skipSkills,
|
|
298
325
|
noStats: options?.noStats,
|
|
299
326
|
registryName: options?.name,
|
|
300
327
|
// Registry-collision bypass — its own CLI flag, intentionally NOT
|
|
@@ -335,8 +362,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
335
362
|
// exit code and an actionable error instead of being mistaken for
|
|
336
363
|
// a healthy index.
|
|
337
364
|
await assertAnalysisFinalized(repoPath);
|
|
338
|
-
// Skill generation (CLI-only, uses pipeline result from analysis)
|
|
339
|
-
|
|
365
|
+
// Skill generation (CLI-only, uses pipeline result from analysis).
|
|
366
|
+
// Gated so `--index-only --skills` skips community skill writes too
|
|
367
|
+
// (`shouldGenerateCommunitySkillFiles` — see unit test).
|
|
368
|
+
if (shouldGenerateCommunitySkillFiles(options, result.pipelineResult)) {
|
|
340
369
|
updateBar(99, 'Generating skill files...');
|
|
341
370
|
try {
|
|
342
371
|
const { generateSkillFiles } = await import('./skill-gen.js');
|
|
@@ -364,7 +393,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
364
393
|
communities: s.communities,
|
|
365
394
|
clusters: aggregatedClusterCount,
|
|
366
395
|
processes: s.processes,
|
|
367
|
-
}, skillResult.skills, { skipAgentsMd
|
|
396
|
+
}, skillResult.skills, { skipAgentsMd, skipSkills, noStats: options?.noStats });
|
|
368
397
|
}
|
|
369
398
|
}
|
|
370
399
|
catch {
|
package/dist/cli/augment.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Augment CLI Command
|
|
3
3
|
*
|
|
4
4
|
* Fast-path command for platform hooks.
|
|
5
|
-
* Shells out from Claude Code PreToolUse / Cursor
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor postToolUse hooks.
|
|
6
6
|
*
|
|
7
7
|
* Usage: gitnexus augment <pattern>
|
|
8
8
|
* Returns enriched text to stdout.
|
package/dist/cli/augment.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Augment CLI Command
|
|
3
3
|
*
|
|
4
4
|
* Fast-path command for platform hooks.
|
|
5
|
-
* Shells out from Claude Code PreToolUse / Cursor
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor postToolUse hooks.
|
|
6
6
|
*
|
|
7
7
|
* Usage: gitnexus augment <pattern>
|
|
8
8
|
* Returns enriched text to stdout.
|
package/dist/cli/index.js
CHANGED
|
@@ -21,9 +21,14 @@ program
|
|
|
21
21
|
'Optional [limit] overrides the 50,000-node safety cap; pass 0 to disable the cap entirely.')
|
|
22
22
|
.option('--drop-embeddings', 'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' +
|
|
23
23
|
'preserves any embeddings already present in the index.')
|
|
24
|
-
.option('--skills', 'Generate repo-specific skill files from detected communities'
|
|
24
|
+
.option('--skills', 'Generate repo-specific skill files from detected communities ' +
|
|
25
|
+
'(no-op when --index-only is also set).')
|
|
25
26
|
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
|
|
26
27
|
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
|
|
28
|
+
.option('--skip-skills', 'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. ' +
|
|
29
|
+
'Does not suppress community skills from --skills (those use .claude/skills/generated/). ' +
|
|
30
|
+
'Use --index-only to skip all AI-context file injection.')
|
|
31
|
+
.option('--index-only', 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)')
|
|
27
32
|
.option('--skip-git', 'Treat the provided path/cwd as the index root and skip parent git-root discovery')
|
|
28
33
|
.option('--name <alias>', 'Register this repo under a custom name in ~/.gitnexus/registry.json ' +
|
|
29
34
|
'(disambiguates repos whose paths share a basename, e.g. two different .../app folders)')
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Augmentation Engine
|
|
3
3
|
*
|
|
4
4
|
* Lightweight, fast-path enrichment of search patterns with knowledge graph context.
|
|
5
|
-
* Designed to be called from platform hooks (Claude Code PreToolUse, Cursor
|
|
6
|
-
* when an agent runs grep/glob/search.
|
|
5
|
+
* Designed to be called from platform hooks (Claude Code PreToolUse, Cursor postToolUse)
|
|
6
|
+
* when an agent runs grep/glob/read/search.
|
|
7
7
|
*
|
|
8
8
|
* Performance target: <500ms cold start, <200ms warm.
|
|
9
9
|
*
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Augmentation Engine
|
|
3
3
|
*
|
|
4
4
|
* Lightweight, fast-path enrichment of search patterns with knowledge graph context.
|
|
5
|
-
* Designed to be called from platform hooks (Claude Code PreToolUse, Cursor
|
|
6
|
-
* when an agent runs grep/glob/search.
|
|
5
|
+
* Designed to be called from platform hooks (Claude Code PreToolUse, Cursor postToolUse)
|
|
6
|
+
* when an agent runs grep/glob/read/search.
|
|
7
7
|
*
|
|
8
8
|
* Performance target: <500ms cold start, <200ms warm.
|
|
9
9
|
*
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { getLanguageFromFilename } from '../../_shared/index.js';
|
|
7
7
|
import { createParserForLanguage, isLanguageAvailable, resolveLanguageKey, } from '../tree-sitter/parser-loader.js';
|
|
8
|
+
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
|
8
9
|
const parserCache = new Map();
|
|
9
10
|
/**
|
|
10
11
|
* Ensure parser is initialized and language is loaded, then parse content.
|
|
@@ -22,7 +23,7 @@ export const ensureAndParse = async (content, filePath) => {
|
|
|
22
23
|
parserInstance = await createParserForLanguage(language, filePath);
|
|
23
24
|
parserCache.set(parserKey, parserInstance);
|
|
24
25
|
}
|
|
25
|
-
return parserInstance
|
|
26
|
+
return parseSourceSafe(parserInstance, content);
|
|
26
27
|
};
|
|
27
28
|
const FUNCTION_LIKE_TYPES = new Set([
|
|
28
29
|
'function_declaration',
|
|
@@ -30,8 +30,11 @@ const readConfig = () => {
|
|
|
30
30
|
const rawDims = process.env.GITNEXUS_EMBEDDING_DIMS;
|
|
31
31
|
let dimensions;
|
|
32
32
|
if (rawDims !== undefined) {
|
|
33
|
+
if (!/^\d+$/.test(rawDims)) {
|
|
34
|
+
throw new Error(`GITNEXUS_EMBEDDING_DIMS must be a positive integer, got "${rawDims}"`);
|
|
35
|
+
}
|
|
33
36
|
const parsed = parseInt(rawDims, 10);
|
|
34
|
-
if (
|
|
37
|
+
if (parsed <= 0) {
|
|
35
38
|
throw new Error(`GITNEXUS_EMBEDDING_DIMS must be a positive integer, got "${rawDims}"`);
|
|
36
39
|
}
|
|
37
40
|
dimensions = parsed;
|
|
@@ -73,9 +76,22 @@ const safeUrl = (url) => {
|
|
|
73
76
|
* @param model - Model name for the request body
|
|
74
77
|
* @param apiKey - Bearer token (only used in Authorization header)
|
|
75
78
|
* @param batchIndex - Logical batch number (for error context)
|
|
76
|
-
* @param
|
|
79
|
+
* @param dimensions - Optional output-vector size. When provided, sent as
|
|
80
|
+
* the `dimensions` field in the request body. Endpoints that implement
|
|
81
|
+
* Matryoshka truncation (OpenAI text-embedding-3-*, Cohere embed-v3,
|
|
82
|
+
* Voyage) return a truncated vector at that size; endpoints that do not
|
|
83
|
+
* recognise the field may ignore it or return 400. Leave
|
|
84
|
+
* `GITNEXUS_EMBEDDING_DIMS` unset for strict backends that reject
|
|
85
|
+
* unknown fields.
|
|
77
86
|
*/
|
|
78
|
-
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0) => {
|
|
87
|
+
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensions) => {
|
|
88
|
+
const requestBody = {
|
|
89
|
+
input: batch,
|
|
90
|
+
model,
|
|
91
|
+
};
|
|
92
|
+
if (dimensions !== undefined) {
|
|
93
|
+
requestBody.dimensions = dimensions;
|
|
94
|
+
}
|
|
79
95
|
let resp;
|
|
80
96
|
try {
|
|
81
97
|
resp = await resilientFetch(url, {
|
|
@@ -85,7 +101,7 @@ const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0) => {
|
|
|
85
101
|
'Content-Type': 'application/json',
|
|
86
102
|
Authorization: `Bearer ${apiKey}`,
|
|
87
103
|
},
|
|
88
|
-
body: JSON.stringify(
|
|
104
|
+
body: JSON.stringify(requestBody),
|
|
89
105
|
}, {
|
|
90
106
|
breakerKey: HTTP_BREAKER_KEY,
|
|
91
107
|
retry: { maxAttempts: HTTP_MAX_RETRIES + 1, baseDelayMs: HTTP_RETRY_BACKOFF_MS },
|
|
@@ -130,7 +146,7 @@ export const httpEmbed = async (texts) => {
|
|
|
130
146
|
for (let i = 0; i < texts.length; i += HTTP_BATCH_SIZE) {
|
|
131
147
|
const batch = texts.slice(i, i + HTTP_BATCH_SIZE);
|
|
132
148
|
const batchIndex = Math.floor(i / HTTP_BATCH_SIZE);
|
|
133
|
-
const items = await httpEmbedBatch(url, batch, config.model, config.apiKey, batchIndex);
|
|
149
|
+
const items = await httpEmbedBatch(url, batch, config.model, config.apiKey, batchIndex, config.dimensions);
|
|
134
150
|
if (items.length !== batch.length) {
|
|
135
151
|
throw new Error(`Embedding endpoint returned ${items.length} vectors for ${batch.length} texts ` +
|
|
136
152
|
`(${safeUrl(url)}, batch ${batchIndex})`);
|
|
@@ -164,7 +180,7 @@ export const httpEmbedQuery = async (text) => {
|
|
|
164
180
|
if (!config)
|
|
165
181
|
throw new Error('HTTP embedding not configured');
|
|
166
182
|
const url = `${config.baseUrl}/embeddings`;
|
|
167
|
-
const items = await httpEmbedBatch(url, [text], config.model, config.apiKey);
|
|
183
|
+
const items = await httpEmbedBatch(url, [text], config.model, config.apiKey, 0, config.dimensions);
|
|
168
184
|
if (!items.length) {
|
|
169
185
|
throw new Error(`Embedding endpoint returned empty response (${safeUrl(url)})`);
|
|
170
186
|
}
|
|
@@ -3,6 +3,7 @@ import { glob } from 'glob';
|
|
|
3
3
|
import Parser from 'tree-sitter';
|
|
4
4
|
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
|
5
5
|
import { readSafe } from './fs-utils.js';
|
|
6
|
+
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
6
7
|
import { logger } from '../../logger.js';
|
|
7
8
|
import { GRPC_SCAN_GLOB, getPluginForFile, hasProtoPlugin, } from './grpc-patterns/index.js';
|
|
8
9
|
/**
|
|
@@ -352,7 +353,7 @@ export class GrpcExtractor {
|
|
|
352
353
|
let detections = [];
|
|
353
354
|
try {
|
|
354
355
|
parser.setLanguage(plugin.language);
|
|
355
|
-
const tree = parser
|
|
356
|
+
const tree = parseSourceSafe(parser, content);
|
|
356
357
|
detections = plugin.scan(tree);
|
|
357
358
|
}
|
|
358
359
|
catch {
|
|
@@ -5,6 +5,7 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
|
|
|
5
5
|
* - FastAPI `@app.get("/path")` provider decorators
|
|
6
6
|
* - `requests.get/post/...("url")` consumer calls
|
|
7
7
|
* - Generic `requests.request("METHOD", "url")` consumer calls
|
|
8
|
+
* - `httpx.AsyncClient` instances calling `.get/.post/...("url")`
|
|
8
9
|
*/
|
|
9
10
|
const FASTAPI_VERBS = {
|
|
10
11
|
get: 'GET',
|
|
@@ -65,11 +66,142 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
|
|
|
65
66
|
},
|
|
66
67
|
],
|
|
67
68
|
});
|
|
69
|
+
// ─── Consumer: httpx.AsyncClient assignments ────────────────────────
|
|
70
|
+
// NOTE: This targeted detector only tracks explicit `httpx.AsyncClient(...)`
|
|
71
|
+
// construction. Direct imports (`from httpx import AsyncClient`) and module
|
|
72
|
+
// aliases (`import httpx as hx`) and annotated assignments (`client: httpx.AsyncClient = ...`)
|
|
73
|
+
// are intentionally left for a follow-up. Module-scope clients are only matched
|
|
74
|
+
// at module scope; calls inside functions require a function/class-local tracked
|
|
75
|
+
// client to avoid false positives from same-name local variables.
|
|
76
|
+
const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
|
|
77
|
+
name: 'python-httpx-async-client-assign',
|
|
78
|
+
language: Python,
|
|
79
|
+
patterns: [
|
|
80
|
+
{
|
|
81
|
+
meta: {},
|
|
82
|
+
query: `
|
|
83
|
+
(assignment
|
|
84
|
+
left: (_) @client
|
|
85
|
+
right: (call
|
|
86
|
+
function: (attribute
|
|
87
|
+
object: (identifier) @module (#eq? @module "httpx")
|
|
88
|
+
attribute: (identifier) @client_class (#eq? @client_class "AsyncClient"))))
|
|
89
|
+
`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
// ─── Consumer: async with httpx.AsyncClient() as client ──────────────
|
|
94
|
+
const HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS = compilePatterns({
|
|
95
|
+
name: 'python-httpx-async-client-with-alias',
|
|
96
|
+
language: Python,
|
|
97
|
+
patterns: [
|
|
98
|
+
{
|
|
99
|
+
meta: {},
|
|
100
|
+
query: `
|
|
101
|
+
(as_pattern
|
|
102
|
+
(call
|
|
103
|
+
function: (attribute
|
|
104
|
+
object: (identifier) @module (#eq? @module "httpx")
|
|
105
|
+
attribute: (identifier) @client_class (#eq? @client_class "AsyncClient")))
|
|
106
|
+
(as_pattern_target (identifier) @client))
|
|
107
|
+
`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
function getScopeKey(node, preferClass = false) {
|
|
112
|
+
if (preferClass) {
|
|
113
|
+
let current = node;
|
|
114
|
+
while (current) {
|
|
115
|
+
if (current.type === 'class_definition') {
|
|
116
|
+
return `class:${current.startIndex}:${current.endIndex}`;
|
|
117
|
+
}
|
|
118
|
+
current = current.parent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let current = node;
|
|
122
|
+
while (current) {
|
|
123
|
+
if (current.type === 'function_definition') {
|
|
124
|
+
return `function:${current.startIndex}:${current.endIndex}`;
|
|
125
|
+
}
|
|
126
|
+
current = current.parent;
|
|
127
|
+
}
|
|
128
|
+
return 'module';
|
|
129
|
+
}
|
|
130
|
+
function trackedClientScopeKey(clientNode) {
|
|
131
|
+
return getScopeKey(clientNode.parent, clientNode.text.includes('.'));
|
|
132
|
+
}
|
|
133
|
+
function callScopeKeys(clientNode) {
|
|
134
|
+
const keys = new Set();
|
|
135
|
+
const preferClass = clientNode.text.includes('.');
|
|
136
|
+
const nearestScope = getScopeKey(clientNode.parent, preferClass);
|
|
137
|
+
keys.add(nearestScope);
|
|
138
|
+
return [...keys];
|
|
139
|
+
}
|
|
140
|
+
function collectHttpxAsyncClients(tree) {
|
|
141
|
+
const clients = new Map();
|
|
142
|
+
const addClient = (clientNode) => {
|
|
143
|
+
if (!clientNode)
|
|
144
|
+
return;
|
|
145
|
+
const scopeKey = trackedClientScopeKey(clientNode);
|
|
146
|
+
const clientText = clientNode.text;
|
|
147
|
+
const scopes = clients.get(clientText) ?? new Set();
|
|
148
|
+
scopes.add(scopeKey);
|
|
149
|
+
clients.set(clientText, scopes);
|
|
150
|
+
};
|
|
151
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS, tree)) {
|
|
152
|
+
addClient(match.captures.client);
|
|
153
|
+
}
|
|
154
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS, tree)) {
|
|
155
|
+
addClient(match.captures.client);
|
|
156
|
+
}
|
|
157
|
+
return clients;
|
|
158
|
+
}
|
|
159
|
+
function hasTrackedHttpxAsyncClient(clients, clientNode) {
|
|
160
|
+
const scopes = clients.get(clientNode.text);
|
|
161
|
+
if (!scopes)
|
|
162
|
+
return false;
|
|
163
|
+
return callScopeKeys(clientNode).some((scopeKey) => scopes.has(scopeKey));
|
|
164
|
+
}
|
|
165
|
+
// ─── Consumer: httpx AsyncClient .get/.post/...("url") ──────────────
|
|
166
|
+
const HTTPX_ASYNC_CLIENT_VERB_PATTERNS = compilePatterns({
|
|
167
|
+
name: 'python-httpx-async-client-verb',
|
|
168
|
+
language: Python,
|
|
169
|
+
patterns: [
|
|
170
|
+
{
|
|
171
|
+
meta: {},
|
|
172
|
+
query: `
|
|
173
|
+
(call
|
|
174
|
+
function: (attribute
|
|
175
|
+
object: (_) @client
|
|
176
|
+
attribute: (identifier) @method (#match? @method "^(get|post|put|delete|patch)$"))
|
|
177
|
+
arguments: (argument_list . (string) @path))
|
|
178
|
+
`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
// ─── Consumer: httpx AsyncClient .request("METHOD", "url") ─────────
|
|
183
|
+
const HTTPX_ASYNC_CLIENT_GENERIC_PATTERNS = compilePatterns({
|
|
184
|
+
name: 'python-httpx-async-client-generic',
|
|
185
|
+
language: Python,
|
|
186
|
+
patterns: [
|
|
187
|
+
{
|
|
188
|
+
meta: {},
|
|
189
|
+
query: `
|
|
190
|
+
(call
|
|
191
|
+
function: (attribute
|
|
192
|
+
object: (_) @client
|
|
193
|
+
attribute: (identifier) @method (#eq? @method "request"))
|
|
194
|
+
arguments: (argument_list . (string) @http_method (string) @path))
|
|
195
|
+
`,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
});
|
|
68
199
|
export const PYTHON_HTTP_PLUGIN = {
|
|
69
200
|
name: 'python-http',
|
|
70
201
|
language: Python,
|
|
71
202
|
scan(tree) {
|
|
72
203
|
const out = [];
|
|
204
|
+
const httpxAsyncClients = collectHttpxAsyncClients(tree);
|
|
73
205
|
// Providers: FastAPI
|
|
74
206
|
for (const match of runCompiledPatterns(FASTAPI_PATTERNS, tree)) {
|
|
75
207
|
const methodNode = match.captures.method;
|
|
@@ -128,6 +260,49 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
128
260
|
confidence: 0.7,
|
|
129
261
|
});
|
|
130
262
|
}
|
|
263
|
+
// Consumers: httpx.AsyncClient.<verb>("url")
|
|
264
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_VERB_PATTERNS, tree)) {
|
|
265
|
+
const clientNode = match.captures.client;
|
|
266
|
+
const methodNode = match.captures.method;
|
|
267
|
+
const pathNode = match.captures.path;
|
|
268
|
+
if (!clientNode || !methodNode || !pathNode)
|
|
269
|
+
continue;
|
|
270
|
+
if (!hasTrackedHttpxAsyncClient(httpxAsyncClients, clientNode))
|
|
271
|
+
continue;
|
|
272
|
+
const path = unquoteLiteral(pathNode.text);
|
|
273
|
+
if (path === null)
|
|
274
|
+
continue;
|
|
275
|
+
out.push({
|
|
276
|
+
role: 'consumer',
|
|
277
|
+
framework: 'python-httpx',
|
|
278
|
+
method: methodNode.text.toUpperCase(),
|
|
279
|
+
path,
|
|
280
|
+
name: null,
|
|
281
|
+
confidence: 0.7,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Consumers: httpx.AsyncClient.request("METHOD", "url")
|
|
285
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_GENERIC_PATTERNS, tree)) {
|
|
286
|
+
const clientNode = match.captures.client;
|
|
287
|
+
const methodNode = match.captures.http_method;
|
|
288
|
+
const pathNode = match.captures.path;
|
|
289
|
+
if (!clientNode || !methodNode || !pathNode)
|
|
290
|
+
continue;
|
|
291
|
+
if (!hasTrackedHttpxAsyncClient(httpxAsyncClients, clientNode))
|
|
292
|
+
continue;
|
|
293
|
+
const methodRaw = unquoteLiteral(methodNode.text);
|
|
294
|
+
const path = unquoteLiteral(pathNode.text);
|
|
295
|
+
if (methodRaw === null || path === null)
|
|
296
|
+
continue;
|
|
297
|
+
out.push({
|
|
298
|
+
role: 'consumer',
|
|
299
|
+
framework: 'python-httpx',
|
|
300
|
+
method: methodRaw.toUpperCase(),
|
|
301
|
+
path,
|
|
302
|
+
name: null,
|
|
303
|
+
confidence: 0.7,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
131
306
|
return out;
|
|
132
307
|
},
|
|
133
308
|
};
|
|
@@ -3,6 +3,7 @@ import { glob } from 'glob';
|
|
|
3
3
|
import Parser from 'tree-sitter';
|
|
4
4
|
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
|
5
5
|
import { readSafe } from './fs-utils.js';
|
|
6
|
+
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
6
7
|
import { getPluginForFile, HTTP_SCAN_GLOB } from './http-patterns/index.js';
|
|
7
8
|
/**
|
|
8
9
|
* Language-agnostic orchestrator for HTTP route (provider + consumer)
|
|
@@ -154,7 +155,7 @@ export class HttpRouteExtractor {
|
|
|
154
155
|
}
|
|
155
156
|
try {
|
|
156
157
|
parser.setLanguage(plugin.language);
|
|
157
|
-
const tree = parser
|
|
158
|
+
const tree = parseSourceSafe(parser, content);
|
|
158
159
|
const detections = plugin.scan(tree);
|
|
159
160
|
cachedDetections.set(rel, detections);
|
|
160
161
|
return detections;
|
|
@@ -8,6 +8,7 @@ import { readSafe } from './fs-utils.js';
|
|
|
8
8
|
import { buildSuffixIndex } from '../../ingestion/import-resolvers/utils.js';
|
|
9
9
|
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
|
10
10
|
import { getMaxFileSizeBytes } from '../../ingestion/utils/max-file-size.js';
|
|
11
|
+
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
11
12
|
import { logger } from '../../logger.js';
|
|
12
13
|
/**
|
|
13
14
|
* Cross-repo C/C++ `#include` dependency extractor.
|
|
@@ -457,7 +458,7 @@ export class IncludeExtractor {
|
|
|
457
458
|
let extractionSource;
|
|
458
459
|
try {
|
|
459
460
|
parser.setLanguage(lang);
|
|
460
|
-
const tree = parser
|
|
461
|
+
const tree = parseSourceSafe(parser, content);
|
|
461
462
|
let matches;
|
|
462
463
|
try {
|
|
463
464
|
matches = query.matches(tree.rootNode);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { glob } from 'glob';
|
|
2
2
|
import Parser from 'tree-sitter';
|
|
3
3
|
import { readSafe } from './fs-utils.js';
|
|
4
|
+
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
4
5
|
import { getPluginForFile, THRIFT_SCAN_GLOB, } from './thrift-patterns/index.js';
|
|
5
6
|
function normalizeThriftPath(rel) {
|
|
6
7
|
return rel.replace(/\\/g, '/');
|
|
@@ -228,7 +229,7 @@ export class ThriftExtractor {
|
|
|
228
229
|
let detections = [];
|
|
229
230
|
try {
|
|
230
231
|
parser.setLanguage(plugin.language);
|
|
231
|
-
const tree = parser
|
|
232
|
+
const tree = parseSourceSafe(parser, content);
|
|
232
233
|
detections = plugin.scan(tree);
|
|
233
234
|
}
|
|
234
235
|
catch {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
+
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
2
3
|
/**
|
|
3
4
|
* Compile a LanguagePatterns bundle. Call this once per plugin, at
|
|
4
5
|
* module load time, and export the result. Throws if any pattern
|
|
@@ -60,7 +61,7 @@ export function scanFile(parser, plugin, content) {
|
|
|
60
61
|
let tree;
|
|
61
62
|
try {
|
|
62
63
|
parser.setLanguage(plugin.language);
|
|
63
|
-
tree = parser
|
|
64
|
+
tree = parseSourceSafe(parser, content);
|
|
64
65
|
}
|
|
65
66
|
catch {
|
|
66
67
|
return [];
|