gitnexus 1.6.11-rc.2 → 1.6.11-rc.3
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 +16 -0
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/wiki.js +15 -2
- package/dist/core/group/config-parser.js +12 -1
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.js +12 -0
- package/dist/core/group/types.d.ts +7 -4
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/storage/repo-manager.d.ts +2 -1
- package/package.json +2 -1
- package/scripts/cross-platform-tests.ts +2 -0
- package/skills/gitnexus-cli.md +6 -2
package/README.md
CHANGED
|
@@ -256,6 +256,7 @@ gitnexus clean # Delete index for current repo
|
|
|
256
256
|
gitnexus clean --all --force # Delete all indexes
|
|
257
257
|
gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph
|
|
258
258
|
gitnexus wiki --model <model> # Wiki with custom LLM model (default: minimax/minimax-m2.5)
|
|
259
|
+
gitnexus wiki --provider grok # Local Grok Build CLI (uses `grok login`, no API key)
|
|
259
260
|
gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
|
|
260
261
|
# Allow an exact LAN/self-hosted HTTP LLM host; env: GITNEXUS_ALLOW_INSECURE_CONNECTION
|
|
261
262
|
gitnexus doctor # Show runtime platform capabilities and embedding configuration
|
|
@@ -281,6 +282,21 @@ gitnexus group status <name> # Check staleness of repos in a group
|
|
|
281
282
|
gitnexus group impact <name> --target <symbol> --repo <groupPath> # Cross-repo blast radius
|
|
282
283
|
```
|
|
283
284
|
|
|
285
|
+
GraphQL contract matching is opt-in in the group's `group.yaml`:
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
detect:
|
|
289
|
+
graphql: true
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The initial exact-only slice matches methods and properties on top-level NestJS `@Resolver`
|
|
293
|
+
classes using imported `@Query`, `@Mutation`, and `@Subscription` decorators. Named
|
|
294
|
+
`.graphql`/`.gql` operations are anchored by generated `<OperationName>Document` declarations;
|
|
295
|
+
object, static `gql` template, and `TypedDocumentString` initializers must prove the operation name
|
|
296
|
+
and root fields. Dynamic decorator names, anonymous operations, and ambiguous or missing graph
|
|
297
|
+
anchors are deliberately omitted. Add common infrastructure fields such as `/health` to
|
|
298
|
+
`matching.exclude_links_paths` to keep those GraphQL contracts visible without cross-linking them.
|
|
299
|
+
|
|
284
300
|
> **`gitnexus uninstall`** reverses `gitnexus setup` — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified **by bundled gitnexus skill name** (e.g. `gitnexus-cli/`), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass `--force` to apply. Per-repo indexes (`gitnexus clean --all`) and the global npm package (`npm uninstall -g gitnexus`) are left for you to remove.
|
|
285
301
|
|
|
286
302
|
## Remote Embeddings
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -184,7 +184,7 @@ export declare const en: {
|
|
|
184
184
|
readonly 'help.option.clean.branch': "Delete only the named branch index (not the workspace index)";
|
|
185
185
|
readonly 'help.option.clean.lbugSidecars': "Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)";
|
|
186
186
|
readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
|
|
187
|
-
readonly 'help.option.wiki.provider': "LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or
|
|
187
|
+
readonly 'help.option.wiki.provider': "LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, opencode, or grok (default: minimax)";
|
|
188
188
|
readonly 'help.option.wiki.model': "LLM model or deployment name (default: MiniMax-M3)";
|
|
189
189
|
readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
|
|
190
190
|
readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -186,7 +186,7 @@ export const en = {
|
|
|
186
186
|
'help.option.clean.branch': 'Delete only the named branch index (not the workspace index)',
|
|
187
187
|
'help.option.clean.lbugSidecars': 'Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)',
|
|
188
188
|
'help.option.wiki.force': 'Force full regeneration even if up to date',
|
|
189
|
-
'help.option.wiki.provider': 'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or
|
|
189
|
+
'help.option.wiki.provider': 'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, opencode, or grok (default: minimax)',
|
|
190
190
|
'help.option.wiki.model': 'LLM model or deployment name (default: MiniMax-M3)',
|
|
191
191
|
'help.option.wiki.baseUrl': 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
|
192
192
|
'help.option.wiki.apiKey': 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)',
|
|
@@ -185,7 +185,7 @@ export declare const cliResources: {
|
|
|
185
185
|
readonly 'help.option.clean.branch': "Delete only the named branch index (not the workspace index)";
|
|
186
186
|
readonly 'help.option.clean.lbugSidecars': "Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)";
|
|
187
187
|
readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
|
|
188
|
-
readonly 'help.option.wiki.provider': "LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or
|
|
188
|
+
readonly 'help.option.wiki.provider': "LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, opencode, or grok (default: minimax)";
|
|
189
189
|
readonly 'help.option.wiki.model': "LLM model or deployment name (default: MiniMax-M3)";
|
|
190
190
|
readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
|
|
191
191
|
readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -184,7 +184,7 @@ export const zhCN = {
|
|
|
184
184
|
'help.option.clean.branch': '仅删除指定分支的索引(不影响工作区索引)',
|
|
185
185
|
'help.option.clean.lbugSidecars': '清理已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件与 dirty-recovery 暂存文件)',
|
|
186
186
|
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
|
|
187
|
-
'help.option.wiki.provider': 'LLM 提供商:minimax、openai、openrouter、azure、custom、cursor、claude、codex 或
|
|
187
|
+
'help.option.wiki.provider': 'LLM 提供商:minimax、openai、openrouter、azure、custom、cursor、claude、codex、opencode 或 grok(默认:minimax)',
|
|
188
188
|
'help.option.wiki.model': 'LLM 模型或 deployment 名称(默认:MiniMax-M3)',
|
|
189
189
|
'help.option.wiki.baseUrl': 'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
|
|
190
190
|
'help.option.wiki.apiKey': 'LLM API key 或 Azure api-key(保存到 ~/.gitnexus/config.json)',
|
package/dist/cli/index.js
CHANGED
|
@@ -187,7 +187,7 @@ program
|
|
|
187
187
|
.command('wiki [path]')
|
|
188
188
|
.description('Generate repository wiki from knowledge graph')
|
|
189
189
|
.option('-f, --force', 'Force full regeneration even if up to date')
|
|
190
|
-
.option('--provider <provider>', 'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or
|
|
190
|
+
.option('--provider <provider>', 'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, opencode, or grok (default: minimax)')
|
|
191
191
|
.option('--model <model>', 'LLM model or deployment name (default: MiniMax-M3)')
|
|
192
192
|
.option('--base-url <url>', 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1')
|
|
193
193
|
.option('--api-key <key>', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)')
|
package/dist/cli/wiki.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig, } from '../sto
|
|
|
13
13
|
import { WikiGenerator } from '../core/wiki/generator.js';
|
|
14
14
|
import { MINIMAX_MODEL_IDS, MINIMAX_OPENAI_BASE_URLS, parseLLMAllowedInsecureHttpHosts, resolveLLMConfig, } from '../core/wiki/llm-client.js';
|
|
15
15
|
import { detectCursorCLI } from '../core/wiki/cursor-client.js';
|
|
16
|
+
import { detectGrokCLI } from '../core/wiki/grok-client.js';
|
|
16
17
|
import { detectLocalCLI } from '../core/wiki/local-cli-client.js';
|
|
17
18
|
import { logger } from '../core/logger.js';
|
|
18
19
|
function parsePositiveIntegerOption(value, flag, multiplier = 1) {
|
|
@@ -32,7 +33,8 @@ function isLocalProvider(provider) {
|
|
|
32
33
|
return (provider === 'cursor' ||
|
|
33
34
|
provider === 'claude' ||
|
|
34
35
|
provider === 'codex' ||
|
|
35
|
-
provider === 'opencode'
|
|
36
|
+
provider === 'opencode' ||
|
|
37
|
+
provider === 'grok');
|
|
36
38
|
}
|
|
37
39
|
function localModelConfigKey(provider) {
|
|
38
40
|
if (provider === 'cursor')
|
|
@@ -43,6 +45,8 @@ function localModelConfigKey(provider) {
|
|
|
43
45
|
return 'codexModel';
|
|
44
46
|
if (provider === 'opencode')
|
|
45
47
|
return 'opencodeModel';
|
|
48
|
+
if (provider === 'grok')
|
|
49
|
+
return 'grokModel';
|
|
46
50
|
throw new Error(`Unsupported local provider: ${provider}`);
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
@@ -244,7 +248,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
244
248
|
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
|
245
249
|
console.log(' Error: No LLM API key found.');
|
|
246
250
|
console.log(' Set MINIMAX_API_KEY, GITNEXUS_API_KEY, or OPENAI_API_KEY,');
|
|
247
|
-
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
|
|
251
|
+
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode|grok.\n');
|
|
248
252
|
process.exitCode = 1;
|
|
249
253
|
return;
|
|
250
254
|
}
|
|
@@ -258,6 +262,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
258
262
|
const hasClaude = detectLocalCLI('claude');
|
|
259
263
|
const hasCodex = detectLocalCLI('codex');
|
|
260
264
|
const hasOpenCode = detectLocalCLI('opencode');
|
|
265
|
+
const hasGrok = detectGrokCLI();
|
|
261
266
|
const localChoices = [];
|
|
262
267
|
// Provider selection
|
|
263
268
|
console.log(' [1] OpenAI (api.openai.com)');
|
|
@@ -299,6 +304,14 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
299
304
|
});
|
|
300
305
|
console.log(` [${choice}] OpenCode CLI (local, uses your OpenCode login/config)`);
|
|
301
306
|
}
|
|
307
|
+
if (hasGrok) {
|
|
308
|
+
const choice = String(nextChoice++);
|
|
309
|
+
localChoices.push({
|
|
310
|
+
choice,
|
|
311
|
+
provider: 'grok',
|
|
312
|
+
});
|
|
313
|
+
console.log(` [${choice}] Grok CLI (local, uses your Grok Build login)`);
|
|
314
|
+
}
|
|
302
315
|
console.log('');
|
|
303
316
|
const maxChoice = String(nextChoice - 1);
|
|
304
317
|
const choice = await prompt(` Select provider (1/${maxChoice}): `);
|
|
@@ -22,6 +22,7 @@ const VALID_ROLES = ['provider', 'consumer'];
|
|
|
22
22
|
// repos that need cross-repo header tracking.
|
|
23
23
|
const DEFAULT_DETECT = {
|
|
24
24
|
http: true,
|
|
25
|
+
graphql: false,
|
|
25
26
|
grpc: true,
|
|
26
27
|
thrift: true,
|
|
27
28
|
topics: true,
|
|
@@ -77,7 +78,17 @@ export function parseGroupConfig(yamlContent) {
|
|
|
77
78
|
role: link.role,
|
|
78
79
|
};
|
|
79
80
|
});
|
|
80
|
-
const
|
|
81
|
+
const rawDetect = raw.detect;
|
|
82
|
+
if (rawDetect !== undefined &&
|
|
83
|
+
(!rawDetect || typeof rawDetect !== 'object' || Array.isArray(rawDetect))) {
|
|
84
|
+
throw new Error('detect must be a mapping of boolean flags');
|
|
85
|
+
}
|
|
86
|
+
for (const [key, value] of Object.entries(rawDetect || {})) {
|
|
87
|
+
if (key in DEFAULT_DETECT && typeof value !== 'boolean') {
|
|
88
|
+
throw new Error(`detect.${key} must be true or false`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const detect = { ...DEFAULT_DETECT, ...(rawDetect || {}) };
|
|
81
92
|
const matching = { ...DEFAULT_MATCHING, ...(raw.matching || {}) };
|
|
82
93
|
const packages = raw.packages || {};
|
|
83
94
|
return {
|
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
* sensitive) lives in exactly one place.
|
|
9
9
|
*/
|
|
10
10
|
export declare function readSafe(repoPath: string, rel: string): string | null;
|
|
11
|
+
/** Read a regular in-repo file without buffering more than `maxBytes`. */
|
|
12
|
+
export declare function readSafeBounded(repoPath: string, rel: string, maxBytes: number): Promise<string | null>;
|
|
@@ -22,3 +22,89 @@ export function readSafe(repoPath, rel) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/** Read a regular in-repo file without buffering more than `maxBytes`. */
|
|
26
|
+
export async function readSafeBounded(repoPath, rel, maxBytes) {
|
|
27
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
|
28
|
+
return null;
|
|
29
|
+
const abs = path.resolve(repoPath, rel);
|
|
30
|
+
const base = path.resolve(repoPath);
|
|
31
|
+
const relToBase = path.relative(base, abs);
|
|
32
|
+
if (relToBase.startsWith('..') || path.isAbsolute(relToBase))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const canonicalBase = await fs.promises.realpath(base);
|
|
36
|
+
const canonicalFile = await fs.promises.realpath(abs);
|
|
37
|
+
const canonicalRelative = path.relative(canonicalBase, canonicalFile);
|
|
38
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative))
|
|
39
|
+
return null;
|
|
40
|
+
const beforeOpen = await fs.promises.lstat(canonicalFile);
|
|
41
|
+
if (!beforeOpen.isFile() || beforeOpen.size > maxBytes)
|
|
42
|
+
return null;
|
|
43
|
+
if (maxBytes === 0)
|
|
44
|
+
return beforeOpen.size === 0 ? '' : null;
|
|
45
|
+
return await new Promise((resolve) => {
|
|
46
|
+
const stream = fs.createReadStream(canonicalFile, {
|
|
47
|
+
flags: 'r',
|
|
48
|
+
start: 0,
|
|
49
|
+
end: maxBytes,
|
|
50
|
+
autoClose: true,
|
|
51
|
+
});
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let totalBytes = 0;
|
|
54
|
+
let validated = false;
|
|
55
|
+
let settled = false;
|
|
56
|
+
const finish = (value) => {
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
resolve(value);
|
|
61
|
+
};
|
|
62
|
+
stream.pause();
|
|
63
|
+
stream.once('open', (fd) => {
|
|
64
|
+
try {
|
|
65
|
+
const opened = fs.fstatSync(fd);
|
|
66
|
+
if (!opened.isFile() || opened.size > maxBytes) {
|
|
67
|
+
finish(null);
|
|
68
|
+
stream.destroy();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const currentCanonical = fs.realpathSync(canonicalFile);
|
|
72
|
+
const currentRelative = path.relative(canonicalBase, currentCanonical);
|
|
73
|
+
const current = fs.statSync(currentCanonical);
|
|
74
|
+
if (currentRelative.startsWith('..') ||
|
|
75
|
+
path.isAbsolute(currentRelative) ||
|
|
76
|
+
opened.dev !== current.dev ||
|
|
77
|
+
opened.ino !== current.ino) {
|
|
78
|
+
finish(null);
|
|
79
|
+
stream.destroy();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
validated = true;
|
|
83
|
+
stream.resume();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
finish(null);
|
|
87
|
+
stream.destroy();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
stream.on('data', (chunk) => {
|
|
91
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
92
|
+
totalBytes += bytes.length;
|
|
93
|
+
if (totalBytes > maxBytes) {
|
|
94
|
+
finish(null);
|
|
95
|
+
stream.destroy();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
chunks.push(bytes);
|
|
99
|
+
});
|
|
100
|
+
stream.once('end', () => {
|
|
101
|
+
finish(validated ? Buffer.concat(chunks, totalBytes).toString('utf8') : null);
|
|
102
|
+
});
|
|
103
|
+
stream.once('error', () => finish(null));
|
|
104
|
+
stream.once('close', () => finish(null));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Parser from 'tree-sitter';
|
|
2
|
+
import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
|
|
3
|
+
import type { ExtractedContract, RepoHandle } from '../types.js';
|
|
4
|
+
type GraphqlOperationKind = 'query' | 'mutation' | 'subscription';
|
|
5
|
+
type GeneratedSymbolIndex = Map<string, Parser.SyntaxNode[]>;
|
|
6
|
+
export declare const RESOLVE_METHOD_QUERY = "\nMATCH (n)\nWHERE labels(n) IN ['Method','Function','Property','CodeElement']\n AND n.name = $name AND n.filePath = $filePath AND n.startLine = $startLine AND n.id <> ''\nRETURN n.id AS uid, n.name AS name, n.filePath AS filePath\nORDER BY n.id ASC\nLIMIT 2";
|
|
7
|
+
export declare const RESOLVE_GENERATED_SYMBOL_QUERY = "\nMATCH (n)\nWHERE labels(n) IN ['Const','Variable','Function','Method','CodeElement']\n AND n.name = $name AND n.filePath <> '' AND n.id <> ''\nRETURN n.id AS uid, n.name AS name, n.filePath AS filePath\nORDER BY n.id ASC\nLIMIT 2";
|
|
8
|
+
export declare function hasGeneratedDocumentProof(initializer: Parser.SyntaxNode, operationKind: GraphqlOperationKind, operationName: string, requiredFields: readonly string[]): boolean;
|
|
9
|
+
export declare function indexGeneratedDeclarators(root: Parser.SyntaxNode): GeneratedSymbolIndex;
|
|
10
|
+
export declare class GraphqlExtractor implements ContractExtractor {
|
|
11
|
+
type: "graphql";
|
|
12
|
+
canExtract(_repo: RepoHandle): Promise<boolean>;
|
|
13
|
+
extract(dbExecutor: CypherExecutor | null, repoPath: string, _repo: RepoHandle): Promise<ExtractedContract[]>;
|
|
14
|
+
private extractProviders;
|
|
15
|
+
private extractConsumers;
|
|
16
|
+
}
|
|
17
|
+
export {};
|