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
|
[](https://www.npmjs.com/package/gitnexus)
|
|
8
8
|
[](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(
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
@@ -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
|
|