gitnexus 1.6.11-rc.2 → 1.6.11-rc.4

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.
Files changed (35) hide show
  1. package/README.md +16 -0
  2. package/dist/cli/i18n/en.d.ts +1 -1
  3. package/dist/cli/i18n/en.js +1 -1
  4. package/dist/cli/i18n/resources.d.ts +1 -1
  5. package/dist/cli/i18n/zh-CN.js +1 -1
  6. package/dist/cli/index.js +1 -1
  7. package/dist/cli/wiki.js +15 -2
  8. package/dist/core/group/config-parser.js +12 -1
  9. package/dist/core/group/extractors/fs-utils.d.ts +2 -0
  10. package/dist/core/group/extractors/fs-utils.js +86 -0
  11. package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
  12. package/dist/core/group/extractors/graphql-extractor.js +652 -0
  13. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  14. package/dist/core/group/extractors/manifest-extractor.js +1 -1
  15. package/dist/core/group/matching.js +8 -1
  16. package/dist/core/group/storage.js +1 -0
  17. package/dist/core/group/sync.js +12 -0
  18. package/dist/core/group/types.d.ts +7 -4
  19. package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
  20. package/dist/core/ingestion/utils/symbol-labels.js +2 -2
  21. package/dist/core/wiki/generator.js +8 -0
  22. package/dist/core/wiki/grok-client.d.ts +21 -0
  23. package/dist/core/wiki/grok-client.js +287 -0
  24. package/dist/core/wiki/llm-client.d.ts +1 -1
  25. package/dist/core/wiki/llm-client.js +5 -2
  26. package/dist/core/wiki/local-cli-client.d.ts +11 -0
  27. package/dist/core/wiki/local-cli-client.js +22 -9
  28. package/dist/mcp/local/local-backend.js +34 -4
  29. package/dist/mcp/local/pdg-impact.d.ts +8 -4
  30. package/dist/mcp/local/pdg-impact.js +7 -2
  31. package/dist/mcp/tools.js +1 -1
  32. package/dist/storage/repo-manager.d.ts +2 -1
  33. package/package.json +2 -1
  34. package/scripts/cross-platform-tests.ts +2 -0
  35. 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
@@ -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 opencode (default: minimax)";
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)";
@@ -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 opencode (default: minimax)',
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 opencode (default: minimax)";
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)";
@@ -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 或 opencode(默认:minimax)',
187
+ 'help.option.wiki.provider': 'LLM 提供商:minimax、openai、openrouter、azure、custom、cursor、claude、codex、opencodegrok(默认: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 opencode (default: minimax)')
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 detect = { ...DEFAULT_DETECT, ...(raw.detect || {}) };
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 {};