gitnexus 1.6.6-rc.60 → 1.6.6-rc.61

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
4
4
 
5
- Works with **Cursor**, **Claude Code**, **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
5
+ Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/gitnexus.svg)](https://www.npmjs.com/package/gitnexus)
8
8
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0/)
@@ -34,6 +34,7 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
34
34
  |--------|-----|--------|---------------------|---------|
35
35
  | **Claude Code** | Yes | Yes | Yes (PreToolUse) | **Full** |
36
36
  | **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
37
+ | **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
37
38
  | **Codex** | Yes | Yes | — | MCP + Skills |
38
39
  | **Windsurf** | Yes | — | — | MCP |
39
40
  | **OpenCode** | Yes | Yes | — | MCP + Skills |
package/dist/cli/index.js CHANGED
@@ -13,7 +13,7 @@ const program = new Command();
13
13
  program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
14
14
  program
15
15
  .command('setup')
16
- .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex')
16
+ .description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, Codex')
17
17
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
18
18
  program
19
19
  .command('analyze [path]')
package/dist/cli/setup.js CHANGED
@@ -12,7 +12,6 @@ import { execFile, execFileSync } from 'child_process';
12
12
  import { createRequire } from 'module';
13
13
  import { promisify } from 'util';
14
14
  import { fileURLToPath } from 'url';
15
- import { glob } from 'glob';
16
15
  import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
17
16
  import { getGlobalDir } from '../storage/repo-manager.js';
18
17
  const __filename = fileURLToPath(import.meta.url);
@@ -224,12 +223,12 @@ async function installClaudeCodeSkills(result) {
224
223
  /**
225
224
  * Check whether an event array already contains a gitnexus-hook entry.
226
225
  */
227
- function hasGitnexusHook(hooksObj, eventName) {
226
+ function hasGitnexusHook(hooksObj, eventName, commandFragment = 'gitnexus-hook') {
228
227
  const entries = hooksObj?.[eventName];
229
228
  if (!Array.isArray(entries))
230
229
  return false;
231
230
  return entries.some((h) => Array.isArray(h.hooks) &&
232
- h.hooks.some((hh) => typeof hh.command === 'string' && hh.command.includes('gitnexus-hook')));
231
+ h.hooks.some((hh) => typeof hh.command === 'string' && hh.command.includes(commandFragment)));
233
232
  }
234
233
  /**
235
234
  * Merge hook entries into a JSONC settings file, preserving comments and formatting.
@@ -397,6 +396,172 @@ async function installClaudeCodeHooks(result) {
397
396
  result.errors.push(`Claude Code hooks: ${err.message}`);
398
397
  }
399
398
  }
399
+ // ─── Antigravity (Google) ──────────────────────────────────────────
400
+ //
401
+ // Antigravity stores its MCP config under ~/.gemini/antigravity/mcp_config.json
402
+ // and inherits Gemini CLI's hooks contract
403
+ // (https://geminicli.com/docs/hooks/reference/), which lives at
404
+ // ~/.gemini/settings.json under the canonical `hooks.<EventName>` array layout.
405
+ //
406
+ // We register a single AfterTool entry matching Gemini's built-in search/shell
407
+ // tools (search_file_content|glob|run_shell_command). BeforeTool is not used:
408
+ // the Gemini contract provides no documented context-injection channel for it,
409
+ // so augmentation runs in AfterTool where `hookSpecificOutput.additionalContext`
410
+ // is appended to the tool result the agent reads. See the antigravity hook
411
+ // adapter for the stdin/stdout contract details.
412
+ async function setupAntigravity(result) {
413
+ const antigravityDir = path.join(os.homedir(), '.gemini', 'antigravity');
414
+ if (!(await dirExists(antigravityDir))) {
415
+ result.skipped.push('Antigravity (not installed)');
416
+ return;
417
+ }
418
+ const mcpPath = path.join(antigravityDir, 'mcp_config.json');
419
+ try {
420
+ const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
421
+ if (ok) {
422
+ result.configured.push('Antigravity');
423
+ }
424
+ else {
425
+ result.errors.push('Antigravity: mcp_config.json is corrupt — skipping to preserve existing content');
426
+ }
427
+ }
428
+ catch (err) {
429
+ result.errors.push(`Antigravity: ${err.message}`);
430
+ }
431
+ }
432
+ /**
433
+ * Install GitNexus skills to ~/.gemini/antigravity/skills/ (global scope,
434
+ * per https://codelabs.developers.google.com/getting-started-with-antigravity-skills).
435
+ * Each skill is laid out as {skillName}/SKILL.md just like the other editors.
436
+ */
437
+ async function installAntigravitySkills(result) {
438
+ const antigravityDir = path.join(os.homedir(), '.gemini', 'antigravity');
439
+ if (!(await dirExists(antigravityDir)))
440
+ return;
441
+ const skillsDir = path.join(antigravityDir, 'skills');
442
+ try {
443
+ const installed = await installSkillsTo(skillsDir);
444
+ if (installed.length > 0) {
445
+ result.configured.push(`Antigravity skills (${installed.length} skills → ~/.gemini/antigravity/skills/)`);
446
+ }
447
+ }
448
+ catch (err) {
449
+ result.errors.push(`Antigravity skills: ${err.message}`);
450
+ }
451
+ }
452
+ /**
453
+ * Install the Antigravity/Gemini-CLI hook adapter to
454
+ * ~/.gemini/config/hooks/gitnexus/ and register an AfterTool entry in
455
+ * ~/.gemini/settings.json under `hooks.AfterTool`.
456
+ *
457
+ * Why AfterTool (and not BeforeTool): the Gemini hooks reference
458
+ * (https://geminicli.com/docs/hooks/reference/) does not provide a context-
459
+ * injection channel for BeforeTool. AfterTool's
460
+ * `hookSpecificOutput.additionalContext` is the only documented way to
461
+ * append text the agent will read.
462
+ */
463
+ async function installAntigravityHooks(result) {
464
+ const antigravityDir = path.join(os.homedir(), '.gemini', 'antigravity');
465
+ if (!(await dirExists(antigravityDir)))
466
+ return;
467
+ const geminiDir = path.join(os.homedir(), '.gemini');
468
+ const settingsPath = path.join(geminiDir, 'settings.json');
469
+ const destHooksDir = path.join(geminiDir, 'config', 'hooks', 'gitnexus');
470
+ // The antigravity adapter shares its lock/probe helpers with the claude
471
+ // adapter — same DB, same concurrency rules — so we reuse those CJS files
472
+ // from gitnexus/hooks/claude/ rather than duplicating them.
473
+ const pluginAntigravityDir = path.join(__dirname, '..', '..', 'hooks', 'antigravity');
474
+ const pluginClaudeDir = path.join(__dirname, '..', '..', 'hooks', 'claude');
475
+ try {
476
+ await fs.mkdir(destHooksDir, { recursive: true });
477
+ // Adapter script: rewrite the dist path baked into the file so it resolves
478
+ // to the installed gitnexus CLI rather than the cwd-relative dev path.
479
+ const adapterSrc = path.join(pluginAntigravityDir, 'gitnexus-antigravity-hook.cjs');
480
+ const adapterDest = path.join(destHooksDir, 'gitnexus-antigravity-hook.cjs');
481
+ try {
482
+ let content = await fs.readFile(adapterSrc, 'utf-8');
483
+ const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
484
+ const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
485
+ const jsonCli = JSON.stringify(normalizedCli);
486
+ content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
487
+ await fs.writeFile(adapterDest, content, 'utf-8');
488
+ }
489
+ catch {
490
+ // Adapter not found in source — skip
491
+ }
492
+ // Bail out if the adapter was not written — registering the hook entry
493
+ // without the script would crash on every tool invocation (top-level
494
+ // require() of sibling helpers fails with MODULE_NOT_FOUND).
495
+ try {
496
+ await fs.access(adapterDest);
497
+ }
498
+ catch {
499
+ result.errors.push('Antigravity hooks: adapter script was not installed — skipping hook registration');
500
+ return;
501
+ }
502
+ // Shared helpers (copied from hooks/claude/). win-rm-list-json.ps1 is
503
+ // required by hook-db-lock-probe.cjs on Windows — without it, the MCP
504
+ // server ownership probe silently fails open and the hook may contend
505
+ // with the MCP server on the LadybugDB.
506
+ for (const helper of ['hook-lock.cjs', 'hook-db-lock-probe.cjs', 'win-rm-list-json.ps1']) {
507
+ try {
508
+ await fs.copyFile(path.join(pluginClaudeDir, helper), path.join(destHooksDir, helper));
509
+ }
510
+ catch {
511
+ result.errors.push(`Antigravity hooks: failed to copy ${helper} — hook may crash at runtime`);
512
+ }
513
+ }
514
+ const hookPath = path.join(destHooksDir, 'gitnexus-antigravity-hook.cjs').replace(/\\/g, '/');
515
+ const escapedHookPath = hookPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
516
+ const hookCmd = `node "${escapedHookPath}"`;
517
+ const parsed = await (async () => {
518
+ try {
519
+ const r = await fs.readFile(settingsPath, 'utf-8');
520
+ return parseJsonc(r);
521
+ }
522
+ catch {
523
+ return null;
524
+ }
525
+ })();
526
+ const hookEntries = [];
527
+ if (!hasGitnexusHook(parsed?.hooks, 'AfterTool', 'gitnexus-antigravity-hook')) {
528
+ // Matcher follows the Gemini CLI built-in tool naming (snake_case).
529
+ // search_file_content / glob cover content + filename search; run_shell_command
530
+ // catches rg/grep invocations and the git commit family for stale-index hints.
531
+ hookEntries.push({
532
+ eventName: 'AfterTool',
533
+ value: {
534
+ matcher: 'search_file_content|glob|run_shell_command',
535
+ hooks: [
536
+ {
537
+ type: 'command',
538
+ command: hookCmd,
539
+ name: 'gitnexus',
540
+ // ms — Gemini CLI uses milliseconds (default 60000); Claude Code
541
+ // uses seconds. 10000 ms = 10 s.
542
+ timeout: 10000,
543
+ description: 'GitNexus graph context + stale-index hints',
544
+ },
545
+ ],
546
+ },
547
+ });
548
+ }
549
+ if (hookEntries.length === 0) {
550
+ result.configured.push('Antigravity hooks (already configured)');
551
+ return;
552
+ }
553
+ const ok = await mergeHooksJsonc(settingsPath, hookEntries);
554
+ if (ok) {
555
+ result.configured.push('Antigravity hooks (AfterTool)');
556
+ }
557
+ else {
558
+ result.errors.push('Antigravity hooks: settings.json is corrupt — skipping to preserve existing content');
559
+ }
560
+ }
561
+ catch (err) {
562
+ result.errors.push(`Antigravity hooks: ${err.message}`);
563
+ }
564
+ }
400
565
  async function setupOpenCode(result) {
401
566
  const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
402
567
  if (!(await dirExists(opencodeDir))) {
@@ -484,14 +649,30 @@ async function setupCodex(result) {
484
649
  */
485
650
  async function installSkillsTo(targetDir) {
486
651
  const installed = [];
487
- const skillsRoot = path.join(__dirname, '..', '..', 'skills');
652
+ // GITNEXUS_TEST_SKILLS_ROOT lets tests stage a fixture skills tree without
653
+ // depending on __dirname resolution under Vitest.
654
+ const skillsRoot = process.env.GITNEXUS_TEST_SKILLS_ROOT ?? path.join(__dirname, '..', '..', 'skills');
655
+ // Was glob('*.md') + glob('*/SKILL.md'); replaced with fs.readdir because
656
+ // glob v13's cwd handling did not match the fixture path on Windows runners
657
+ // (absolute temp paths containing the 8.3 short-name `RUNNER~1` returned
658
+ // zero matches). fs.readdir has no such path quirks.
488
659
  let flatFiles = [];
489
660
  let dirSkillFiles = [];
490
661
  try {
491
- [flatFiles, dirSkillFiles] = await Promise.all([
492
- glob('*.md', { cwd: skillsRoot }),
493
- glob('*/SKILL.md', { cwd: skillsRoot }),
494
- ]);
662
+ const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
663
+ flatFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.md')).map((e) => e.name);
664
+ const subdirSkillFiles = await Promise.all(entries
665
+ .filter((e) => e.isDirectory())
666
+ .map(async (e) => {
667
+ try {
668
+ await fs.access(path.join(skillsRoot, e.name, 'SKILL.md'));
669
+ return path.join(e.name, 'SKILL.md');
670
+ }
671
+ catch {
672
+ return null;
673
+ }
674
+ }));
675
+ dirSkillFiles = subdirSkillFiles.filter((p) => p !== null);
495
676
  }
496
677
  catch {
497
678
  return [];
@@ -616,11 +797,14 @@ export const setupCommand = async () => {
616
797
  // Detect and configure each editor's MCP
617
798
  await setupCursor(result);
618
799
  await setupClaudeCode(result);
800
+ await setupAntigravity(result);
619
801
  await setupOpenCode(result);
620
802
  await setupCodex(result);
621
803
  // Install global skills for platforms that support them
622
804
  await installClaudeCodeSkills(result);
623
805
  await installClaudeCodeHooks(result);
806
+ await installAntigravitySkills(result);
807
+ await installAntigravityHooks(result);
624
808
  await installCursorSkills(result);
625
809
  await installOpenCodeSkills(result);
626
810
  await installCodexSkills(result);
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitNexus Antigravity / Gemini CLI Hook Adapter
4
+ *
5
+ * Bridges the Gemini CLI hooks contract (also used by Antigravity 2.0 — see
6
+ * https://geminicli.com/docs/hooks/reference/) to the same graph-aware
7
+ * augmentation / staleness signals the Claude Code hook provides.
8
+ *
9
+ * Schema differences from the Claude adapter:
10
+ * - Events are BeforeTool / AfterTool (not PreToolUse / PostToolUse).
11
+ * - Tool names are snake_case (run_shell_command, search_file_content, glob).
12
+ * - BeforeTool cannot inject context — decision: "allow" provides no channel
13
+ * to surface text to the agent. Augmentation therefore runs in AfterTool,
14
+ * where `hookSpecificOutput.additionalContext` is appended to the tool
15
+ * result the agent sees.
16
+ * - Stale-index hints after git commit/merge/rebase/cherry-pick/pull are
17
+ * surfaced via the same `additionalContext` channel (so the agent reads
18
+ * them, not only the user) and mirrored to stderr for terminal users.
19
+ * - Stdin uses `tool_name`, `tool_input`, and `tool_response`
20
+ * (with `llmContent`, `returnDisplay`, optional `error`).
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { spawnSync } = require('child_process');
26
+ const { acquireHookSlot } = require('./hook-lock.cjs');
27
+ const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
28
+
29
+ function readInput() {
30
+ try {
31
+ const data = fs.readFileSync(0, 'utf-8');
32
+ return JSON.parse(data);
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ function isGlobalRegistryDir(candidate) {
39
+ if (fs.existsSync(path.join(candidate, 'meta.json'))) return false;
40
+ return (
41
+ fs.existsSync(path.join(candidate, 'registry.json')) ||
42
+ fs.existsSync(path.join(candidate, 'repos'))
43
+ );
44
+ }
45
+
46
+ function walkForGitNexusDir(startDir) {
47
+ let dir = startDir;
48
+ for (let i = 0; i < 5; i++) {
49
+ const candidate = path.join(dir, '.gitnexus');
50
+ if (fs.existsSync(candidate)) {
51
+ if (!isGlobalRegistryDir(candidate)) return candidate;
52
+ }
53
+ const parent = path.dirname(dir);
54
+ if (parent === dir) break;
55
+ dir = parent;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function findCanonicalRepoRoot(cwd) {
61
+ try {
62
+ const result = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
63
+ encoding: 'utf-8',
64
+ timeout: 2000,
65
+ cwd,
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ windowsHide: true,
68
+ });
69
+ if (result.error || result.status !== 0) return null;
70
+ const commonDir = (result.stdout || '').trim();
71
+ if (!commonDir || !path.isAbsolute(commonDir)) return null;
72
+ return path.dirname(commonDir);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function findGitNexusDir(startDir) {
79
+ const cwd = startDir || process.cwd();
80
+ const fromCwd = walkForGitNexusDir(cwd);
81
+ if (fromCwd) return fromCwd;
82
+ const canonicalRoot = findCanonicalRepoRoot(cwd);
83
+ if (canonicalRoot && canonicalRoot !== cwd) {
84
+ return walkForGitNexusDir(canonicalRoot);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function hasGitNexusServerOwner(gitNexusDir) {
90
+ return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
91
+ }
92
+
93
+ function extractAugmentContext(stderr) {
94
+ const output = (stderr || '').trim();
95
+ const marker = output.indexOf('[GitNexus]');
96
+ const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
97
+ if (debug && output.length > 0) {
98
+ // Emit the FULL discarded prefix (everything before the marker, or all of
99
+ // it when no marker is present) so suppressed diagnostics — LadybugDB lock
100
+ // warnings, parser errors, etc. — remain recoverable on the hook's own
101
+ // stderr. Mirrors the Claude adapter's debug behavior.
102
+ const discarded = marker === -1 ? output : output.slice(0, marker).trim();
103
+ if (discarded.length > 0) {
104
+ process.stderr.write(`[GitNexus hook] augment stderr discarded prefix:\n${discarded}\n`);
105
+ }
106
+ }
107
+ return marker === -1 ? '' : output.slice(marker).trim();
108
+ }
109
+
110
+ /**
111
+ * Extract a usable search token from a tool invocation.
112
+ * - search_file_content / glob: top-level `pattern` (sometimes `query`).
113
+ * - run_shell_command: parse rg/grep argv, returning the first non-flag
114
+ * positional ≥ 3 chars.
115
+ * Returns null when the tool is not a recognized search or the pattern is
116
+ * too short.
117
+ */
118
+ function extractPattern(toolName, toolInput) {
119
+ if (toolName === 'search_file_content') {
120
+ const q = toolInput.pattern || toolInput.query || '';
121
+ return typeof q === 'string' && q.length >= 3 ? q : null;
122
+ }
123
+
124
+ if (toolName === 'glob') {
125
+ const raw = toolInput.pattern || '';
126
+ const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
127
+ return match ? match[1] : null;
128
+ }
129
+
130
+ if (toolName === 'run_shell_command') {
131
+ const cmd = toolInput.command || '';
132
+ if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
133
+
134
+ const tokens = cmd.split(/\s+/);
135
+ let foundCmd = false;
136
+ let skipNext = false;
137
+ const flagsWithValues = new Set([
138
+ '-e',
139
+ '-f',
140
+ '-m',
141
+ '-A',
142
+ '-B',
143
+ '-C',
144
+ '-g',
145
+ '--glob',
146
+ '-t',
147
+ '--type',
148
+ '--include',
149
+ '--exclude',
150
+ ]);
151
+
152
+ for (const token of tokens) {
153
+ if (skipNext) {
154
+ skipNext = false;
155
+ continue;
156
+ }
157
+ if (!foundCmd) {
158
+ if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
159
+ continue;
160
+ }
161
+ if (token.startsWith('-')) {
162
+ if (flagsWithValues.has(token)) skipNext = true;
163
+ continue;
164
+ }
165
+ const cleaned = token.replace(/['"]/g, '');
166
+ return cleaned.length >= 3 ? cleaned : null;
167
+ }
168
+ return null;
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ function resolveCliPath() {
175
+ const fromEnv = process.env.GITNEXUS_HOOK_CLI_PATH;
176
+ if (fromEnv !== undefined && String(fromEnv).trim() && fs.existsSync(String(fromEnv))) {
177
+ return String(fromEnv);
178
+ }
179
+ let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
180
+ if (!fs.existsSync(cliPath)) {
181
+ try {
182
+ cliPath = require.resolve('gitnexus/dist/cli/index.js');
183
+ } catch {
184
+ cliPath = '';
185
+ }
186
+ }
187
+ return cliPath;
188
+ }
189
+
190
+ function runGitNexusCli(cliPath, args, cwd, timeout) {
191
+ const isWin = process.platform === 'win32';
192
+ if (cliPath) {
193
+ return spawnSync(process.execPath, [cliPath, ...args], {
194
+ encoding: 'utf-8',
195
+ timeout,
196
+ cwd,
197
+ stdio: ['pipe', 'pipe', 'pipe'],
198
+ windowsHide: true,
199
+ });
200
+ }
201
+ return spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], {
202
+ encoding: 'utf-8',
203
+ timeout: timeout + 5000,
204
+ cwd,
205
+ stdio: ['pipe', 'pipe', 'pipe'],
206
+ windowsHide: true,
207
+ });
208
+ }
209
+
210
+ function writeAdditionalContext(text) {
211
+ process.stdout.write(
212
+ JSON.stringify({
213
+ hookSpecificOutput: {
214
+ hookEventName: 'AfterTool',
215
+ additionalContext: text,
216
+ },
217
+ }),
218
+ );
219
+ }
220
+
221
+ function toolSucceeded(toolResponse) {
222
+ if (!toolResponse || typeof toolResponse !== 'object') return true;
223
+ if (toolResponse.error) return false;
224
+ if (toolResponse.exit_code != null && Number(toolResponse.exit_code) !== 0) return false;
225
+ return true;
226
+ }
227
+
228
+ /**
229
+ * Compute the additionalContext for a tool result, if any.
230
+ * 1. Graph augment for search-like tools (search_file_content, glob,
231
+ * run_shell_command-with-rg/grep) that completed successfully.
232
+ * 2. Stale-index hint after a successful git commit/merge/rebase/cherry-
233
+ * pick/pull.
234
+ * Returns null when nothing is to be appended.
235
+ */
236
+ function buildAfterToolContext(input) {
237
+ const cwd = input.cwd || process.cwd();
238
+ if (!path.isAbsolute(cwd)) return null;
239
+ const gitNexusDir = findGitNexusDir(cwd);
240
+ if (!gitNexusDir) return null;
241
+
242
+ const toolName = input.tool_name || '';
243
+ const toolInput = input.tool_input || {};
244
+ const toolResponse = input.tool_response || {};
245
+ const parts = [];
246
+
247
+ if (toolSucceeded(toolResponse)) {
248
+ const pattern = extractPattern(toolName, toolInput);
249
+ if (pattern) {
250
+ const augmentText = runAugment(gitNexusDir, cwd, pattern);
251
+ if (augmentText) parts.push(augmentText);
252
+ }
253
+ }
254
+
255
+ if (toolName === 'run_shell_command' && toolSucceeded(toolResponse)) {
256
+ const command = toolInput.command || '';
257
+ if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
258
+ const hint = buildStaleIndexHint(gitNexusDir, cwd);
259
+ if (hint) {
260
+ process.stderr.write(`${hint}\n`);
261
+ parts.push(hint);
262
+ }
263
+ }
264
+ }
265
+
266
+ return parts.length > 0 ? parts.join('\n\n') : null;
267
+ }
268
+
269
+ function runAugment(gitNexusDir, cwd, pattern) {
270
+ if (hasGitNexusServerOwner(gitNexusDir)) {
271
+ process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
272
+ return '';
273
+ }
274
+ const release = acquireHookSlot(gitNexusDir);
275
+ if (!release) return '';
276
+ const cliPath = resolveCliPath();
277
+ try {
278
+ const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
279
+ if (!child.error && child.status === 0) {
280
+ return extractAugmentContext(child.stderr || '');
281
+ }
282
+ } catch {
283
+ /* graceful failure */
284
+ } finally {
285
+ release();
286
+ }
287
+ return '';
288
+ }
289
+
290
+ function buildStaleIndexHint(gitNexusDir, cwd) {
291
+ let currentHead = '';
292
+ try {
293
+ const headResult = spawnSync('git', ['rev-parse', 'HEAD'], {
294
+ encoding: 'utf-8',
295
+ timeout: 3000,
296
+ cwd,
297
+ stdio: ['pipe', 'pipe', 'pipe'],
298
+ windowsHide: true,
299
+ });
300
+ currentHead = (headResult.stdout || '').trim();
301
+ } catch {
302
+ return '';
303
+ }
304
+ if (!currentHead) return '';
305
+
306
+ let lastCommit = '';
307
+ let hadEmbeddings = false;
308
+ try {
309
+ const meta = JSON.parse(fs.readFileSync(path.join(gitNexusDir, 'meta.json'), 'utf-8'));
310
+ lastCommit = meta.lastCommit || '';
311
+ hadEmbeddings = meta.stats && meta.stats.embeddings > 0;
312
+ } catch {
313
+ /* no meta — treat as stale */
314
+ }
315
+
316
+ if (currentHead === lastCommit) return '';
317
+
318
+ const analyzeCmd = `npx gitnexus analyze${hadEmbeddings ? ' --embeddings' : ''}`;
319
+ return (
320
+ `[GitNexus] index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
321
+ `Run \`${analyzeCmd}\` to refresh the knowledge graph.`
322
+ );
323
+ }
324
+
325
+ function handleAfterTool(input) {
326
+ const context = buildAfterToolContext(input);
327
+ if (context) writeAdditionalContext(context);
328
+ }
329
+
330
+ const handlers = {
331
+ AfterTool: handleAfterTool,
332
+ };
333
+
334
+ function main() {
335
+ try {
336
+ const input = readInput();
337
+ const handler = handlers[input.hook_event_name || ''];
338
+ if (handler) handler(input);
339
+ } catch (err) {
340
+ if (process.env.GITNEXUS_DEBUG) {
341
+ console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
342
+ }
343
+ }
344
+ }
345
+
346
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.60",
3
+ "version": "1.6.6-rc.61",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -29,6 +29,7 @@ const PLATFORM_LOGIC = [
29
29
  'test/unit/setup.test.ts',
30
30
  'test/unit/setup-jsonc.test.ts',
31
31
  'test/unit/setup-codex.test.ts',
32
+ 'test/unit/setup-antigravity.test.ts',
32
33
  'test/unit/platform-capabilities.test.ts',
33
34
  'test/unit/worker-pool-windows-quarantine.test.ts',
34
35
  'test/unit/lbug-pool-win-fts-probe.test.ts',
@@ -79,6 +80,8 @@ const SPAWN_CLI = [
79
80
  'test/integration/group/group-cli.test.ts',
80
81
  'test/integration/cli/tool-no-index-stderr.test.ts',
81
82
  'test/integration/setup-skills.test.ts',
83
+ 'test/integration/setup-antigravity.test.ts',
84
+ 'test/integration/antigravity-hook-e2e.test.ts',
82
85
  'test/unit/local-cli-subprocess.test.ts',
83
86
  ];
84
87