gitnexus 1.6.9 → 1.6.10-rc.2

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**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
5
+ Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, **CodeBuddy** (Tencent), **Qoder** (Alibaba), 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/)
@@ -43,11 +43,13 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
43
43
  | **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
44
44
  | **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
45
45
  | **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
46
- | **Codex** | Yes | Yes | — | MCP + Skills |
46
+ | **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** |
47
47
  | **OpenCode** | Yes | Yes | — | MCP + Skills |
48
+ | **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills |
49
+ | **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills |
48
50
  | **Windsurf** | Yes | — | — | MCP |
49
51
 
50
- > **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
52
+ > **Claude Code** and **Codex** get the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
51
53
 
52
54
  ### Community Integrations
53
55
 
@@ -69,12 +71,23 @@ claude mcp add gitnexus -- npx -y gitnexus@latest mcp
69
71
  claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
70
72
  ```
71
73
 
72
- ### Codex (full support — MCP + skills)
74
+ ### Codex (full support — MCP + skills + hooks)
73
75
 
74
76
  ```bash
75
77
  codex mcp add gitnexus -- npx -y gitnexus@latest mcp
76
78
  ```
77
79
 
80
+ Codex hooks (PreToolUse graph enrichment + PostToolUse stale-index detection in `~/.codex/hooks.json`, [same schema as Claude Code](https://developers.openai.com/codex/hooks)) need the bundled adapter script, so they are installed by `gitnexus setup -c codex` rather than manually.
81
+
82
+ Alternatively, install everything as a [Codex plugin](https://developers.openai.com/codex/plugins/build) (MCP + skills + hooks in one step):
83
+
84
+ ```bash
85
+ codex plugin marketplace add abhigyanpatwari/GitNexus
86
+ # then inside Codex: /plugins → install "GitNexus"
87
+ ```
88
+
89
+ > **Codex notes:** SessionStart is intentionally not registered — Codex reads [AGENTS.md natively](https://developers.openai.com/codex/guides/agents-md), which already carries the GitNexus context block. Newly installed hooks need a one-time approval in Codex via `/hooks` before they run. Pick **one** install route (`gitnexus setup -c codex` **or** the plugin): plugin hooks load alongside `~/.codex/hooks.json`, so installing both can fire duplicate hooks per tool call.
90
+
78
91
  ### Cursor / Windsurf
79
92
 
80
93
  Add to `~/.cursor/mcp.json` (global — works for all projects):
@@ -105,6 +118,36 @@ Add to `~/.config/opencode/config.json`:
105
118
  }
106
119
  ```
107
120
 
121
+ ### CodeBuddy
122
+
123
+ CodeBuddy reads only the **first existing file** in its config priority chain: `~/.codebuddy/.mcp.json` (recommended) → `~/.codebuddy/mcp.json` (deprecated) → `~/.codebuddy.json` (legacy). Edit the first non-empty file that exists — creating a higher-priority file would hide the servers in the ones below it. If none exist, create `~/.codebuddy/.mcp.json`:
124
+
125
+ ```json
126
+ {
127
+ "mcpServers": {
128
+ "gitnexus": {
129
+ "command": "npx",
130
+ "args": ["-y", "gitnexus@latest", "mcp"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ ### Qoder
137
+
138
+ Add to `~/.qoder.json`:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "gitnexus": {
144
+ "command": "npx",
145
+ "args": ["-y", "gitnexus@latest", "mcp"]
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
108
151
  ## How It Works
109
152
 
110
153
  GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:
@@ -125,24 +168,24 @@ The result is a **LadybugDB graph database** stored locally in `.gitnexus/` with
125
168
  Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
126
169
 
127
170
  | Tool | What It Does |
128
- | ---------------- | ----------------------------------------------------------------------- |
129
- | `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) |
130
- | `query` | Process-grouped hybrid search (BM25 + semantic + RRF) |
131
- | `context` | 360-degree symbol view — categorized refs, process participation |
132
- | `impact` | Blast radius analysis with depth grouping and confidence |
133
- | `trace` | Shortest directed path between two symbols (call + class-member edges) |
134
- | `detect_changes` | Git-diff impact — maps changed lines to affected processes |
135
- | `check` | Read-only structural checks against the indexed graph |
136
- | `rename` | Multi-file coordinated rename with graph + text search |
137
- | `cypher` | Raw Cypher graph queries |
138
- | `route_map` | API route map — which components fetch which endpoints, and handlers |
139
- | `tool_map` | MCP/RPC tool definitions — where they're defined and handled |
140
- | `shape_check` | Validate API response shapes against consumers' property accesses |
141
- | `api_impact` | Pre-change impact report for an API route handler |
142
- | `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) |
143
- | `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) |
144
- | `group_list` | List configured repository groups |
145
- | `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
171
+ | ---------------- | ---------------------------------------------------------------------- |
172
+ | `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) |
173
+ | `query` | Process-grouped hybrid search (BM25 + semantic + RRF) |
174
+ | `context` | 360-degree symbol view — categorized refs, process participation |
175
+ | `impact` | Blast radius analysis with depth grouping and confidence |
176
+ | `trace` | Shortest directed path between two symbols (call + class-member edges) |
177
+ | `detect_changes` | Git-diff impact — maps changed lines to affected processes |
178
+ | `check` | Read-only structural checks against the indexed graph |
179
+ | `rename` | Multi-file coordinated rename with graph + text search |
180
+ | `cypher` | Raw Cypher graph queries |
181
+ | `route_map` | API route map — which components fetch which endpoints, and handlers |
182
+ | `tool_map` | MCP/RPC tool definitions — where they're defined and handled |
183
+ | `shape_check` | Validate API response shapes against consumers' property accesses |
184
+ | `api_impact` | Pre-change impact report for an API route handler |
185
+ | `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) |
186
+ | `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) |
187
+ | `group_list` | List configured repository groups |
188
+ | `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
146
189
 
147
190
  > With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch`; omitting it queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
148
191
 
@@ -383,13 +426,13 @@ GitNexus uses optional DuckDB extensions for BM25 and vector search. The `gitnex
383
426
 
384
427
  Configure the behavior with these environment variables:
385
428
 
386
- | Variable | Values | Default | Effect |
387
- | -------------------------------------------- | ---------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
- | `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
389
- | `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
390
- | `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
429
+ | Variable | Values | Default | Effect |
430
+ | -------------------------------------------- | ---------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
431
+ | `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
432
+ | `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
433
+ | `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
391
434
  | `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. |
392
- | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
435
+ | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
393
436
 
394
437
  ```bash
395
438
  # Offline/airgapped: never reach the network for extensions
@@ -464,9 +507,9 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
464
507
 
465
508
  After scope resolution, analyze prunes inert block-local value symbols (a function-local `const`/`let`/`var` that ends up with only its structural `File→DEFINES` edge) to keep the graph focused on cross-symbol relationships. Module/file-scope symbols, class members, and any local with a real edge are always kept.
466
509
 
467
- | Variable | Default | Effect |
468
- | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------- |
469
- | `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
510
+ | Variable | Default | Effect |
511
+ | ----------------------------------- | ------- | ---------------------------------------------------------------------------------- |
512
+ | `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
470
513
 
471
514
  Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
472
515
 
@@ -2,7 +2,7 @@
2
2
  * AI Context Generator
3
3
  *
4
4
  * Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
5
- * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
5
+ * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, CodeBuddy, Qoder, etc.
6
6
  * CLAUDE.md is for Claude Code which only reads that file.
7
7
  */
8
8
  import { type GeneratedSkillInfo } from './skill-gen.js';
@@ -2,7 +2,7 @@
2
2
  * AI Context Generator
3
3
  *
4
4
  * Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
5
- * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
5
+ * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, CodeBuddy, Qoder, etc.
6
6
  * CLAUDE.md is for Claude Code which only reads that file.
7
7
  */
8
8
  import fs from 'fs/promises';
@@ -15,7 +15,7 @@
15
15
  * implementations remain behaviourally symmetrical on top of this shared
16
16
  * structure.
17
17
  */
18
- export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codex';
18
+ export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codebuddy' | 'qoder' | 'codex';
19
19
  /** An editor whose MCP config is a JSONC document (server keyed by name). */
20
20
  export interface McpJsoncTarget {
21
21
  id: EditorId;
@@ -29,6 +29,14 @@ export interface McpJsoncTarget {
29
29
  * without either side needing a cast.
30
30
  */
31
31
  keyPath: string[];
32
+ /**
33
+ * Older config locations the editor still reads when `file` is absent
34
+ * (CodeBuddy reads only the FIRST existing file in its priority chain).
35
+ * Setup writes into the first existing candidate of [file, ...legacyFiles]
36
+ * so it never shadows a user's servers living in a deprecated file;
37
+ * uninstall sweeps all of them.
38
+ */
39
+ legacyFiles?: string[];
32
40
  }
33
41
  /** Codex stores MCP config as a TOML table, not JSONC. */
34
42
  export interface CodexMcpTarget {
@@ -58,7 +66,7 @@ export interface HookTarget {
58
66
  scriptDir: string;
59
67
  }
60
68
  export interface EditorTargets {
61
- /** JSONC-format MCP entries: Cursor, Claude Code, Antigravity, OpenCode. */
69
+ /** JSONC-format MCP entries: Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder. */
62
70
  mcpJsonc: McpJsoncTarget[];
63
71
  /** Codex MCP (TOML). */
64
72
  codex: CodexMcpTarget;
@@ -80,6 +88,13 @@ export declare function mcpTarget(id: EditorId, home?: string): McpJsoncTarget;
80
88
  export declare function skillTarget(id: EditorId, home?: string): SkillTarget;
81
89
  /** Look up a single hook target by editor id (throws if unknown). */
82
90
  export declare function hookTarget(id: EditorId, home?: string): HookTarget;
91
+ /**
92
+ * True when err is a Node fs error with code ENOENT (file/dir absent).
93
+ * Shared by setup and uninstall: both must swallow ONLY absence when reading
94
+ * editor configs — any other read/stat failure (EACCES, EIO) is surfaced so an
95
+ * unreadable config is never treated as empty and rewritten gitnexus-only.
96
+ */
97
+ export declare function isEnoent(err: unknown): boolean;
83
98
  /**
84
99
  * Detect indentation style from file content so JSONC edits preserve the file's
85
100
  * existing formatting. Shared by setup (writes) and uninstall (removes).
@@ -50,6 +50,26 @@ export function getEditorTargets(home = os.homedir()) {
50
50
  // OpenCode nests servers under `mcp`, not `mcpServers`.
51
51
  keyPath: ['mcp', 'gitnexus'],
52
52
  },
53
+ {
54
+ id: 'codebuddy',
55
+ label: 'CodeBuddy',
56
+ // Recommended user-scope path per https://www.codebuddy.ai/docs/cli/mcp;
57
+ // CodeBuddy reads only the first existing file in this priority chain.
58
+ file: path.join(home, '.codebuddy', '.mcp.json'),
59
+ legacyFiles: [
60
+ path.join(home, '.codebuddy', 'mcp.json'), // deprecated
61
+ path.join(home, '.codebuddy.json'), // legacy
62
+ ],
63
+ keyPath: ['mcpServers', 'gitnexus'],
64
+ },
65
+ {
66
+ id: 'qoder',
67
+ label: 'Qoder',
68
+ // Qoder's documented user-scope MCP config (https://docs.qoder.com/cli/using-cli);
69
+ // the IDE manages MCP via its Settings UI with no documented file path.
70
+ file: path.join(home, '.qoder.json'),
71
+ keyPath: ['mcpServers', 'gitnexus'],
72
+ },
53
73
  ];
54
74
  const codex = {
55
75
  id: 'codex',
@@ -66,6 +86,10 @@ export function getEditorTargets(home = os.homedir()) {
66
86
  },
67
87
  { id: 'cursor', label: 'Cursor', dir: path.join(home, '.cursor', 'skills') },
68
88
  { id: 'opencode', label: 'OpenCode', dir: path.join(home, '.config', 'opencode', 'skills') },
89
+ { id: 'codebuddy', label: 'CodeBuddy', dir: path.join(home, '.codebuddy', 'skills') },
90
+ // Qoder skills live at ~/.qoder/skills/{name}/SKILL.md
91
+ // (https://docs.qoder.com/extensions/skills).
92
+ { id: 'qoder', label: 'Qoder', dir: path.join(home, '.qoder', 'skills') },
69
93
  // Codex reads skills from ~/.agents/skills (not ~/.codex).
70
94
  { id: 'codex', label: 'Codex', dir: path.join(home, '.agents', 'skills') },
71
95
  ];
@@ -78,6 +102,17 @@ export function getEditorTargets(home = os.homedir()) {
78
102
  needle: 'gitnexus-hook',
79
103
  scriptDir: path.join(home, '.claude', 'hooks', 'gitnexus'),
80
104
  },
105
+ {
106
+ id: 'codex',
107
+ label: 'Codex',
108
+ // Codex hooks use Claude Code's exact {hooks: {Event: [...]}} JSON shape
109
+ // and hookSpecificOutput response contract, in a dedicated hooks.json
110
+ // (https://developers.openai.com/codex/hooks).
111
+ settingsFile: path.join(home, '.codex', 'hooks.json'),
112
+ events: ['PreToolUse', 'PostToolUse'],
113
+ needle: 'gitnexus-hook',
114
+ scriptDir: path.join(home, '.codex', 'hooks', 'gitnexus'),
115
+ },
81
116
  {
82
117
  id: 'antigravity',
83
118
  label: 'Antigravity',
@@ -110,6 +145,15 @@ export function hookTarget(id, home) {
110
145
  throw new Error(`No hook target for editor "${id}"`);
111
146
  return t;
112
147
  }
148
+ /**
149
+ * True when err is a Node fs error with code ENOENT (file/dir absent).
150
+ * Shared by setup and uninstall: both must swallow ONLY absence when reading
151
+ * editor configs — any other read/stat failure (EACCES, EIO) is surfaced so an
152
+ * unreadable config is never treated as empty and rewritten gitnexus-only.
153
+ */
154
+ export function isEnoent(err) {
155
+ return err?.code === 'ENOENT';
156
+ }
113
157
  /**
114
158
  * Detect indentation style from file content so JSONC edits preserve the file's
115
159
  * existing formatting. Shared by setup (writes) and uninstall (removes).
@@ -108,7 +108,7 @@ export declare const en: {
108
108
  readonly 'help.command.help.description': "display help for command";
109
109
  readonly 'help.option.help': "display help for command";
110
110
  readonly 'help.option.version': "output the version number";
111
- readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
111
+ readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
112
112
  readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
113
113
  readonly 'help.command.analyze.description': "Index a repository (full analysis)";
114
114
  readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
@@ -108,7 +108,7 @@ export const en = {
108
108
  'help.command.help.description': 'display help for command',
109
109
  'help.option.help': 'display help for command',
110
110
  'help.option.version': 'output the version number',
111
- 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex',
111
+ 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex',
112
112
  'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
113
113
  'help.command.analyze.description': 'Index a repository (full analysis)',
114
114
  'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
@@ -109,7 +109,7 @@ export declare const cliResources: {
109
109
  readonly 'help.command.help.description': "display help for command";
110
110
  readonly 'help.option.help': "display help for command";
111
111
  readonly 'help.option.version': "output the version number";
112
- readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
112
+ readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
113
113
  readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
114
114
  readonly 'help.command.analyze.description': "Index a repository (full analysis)";
115
115
  readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
@@ -108,7 +108,7 @@ export const zhCN = {
108
108
  'help.command.help.description': '显示命令帮助',
109
109
  'help.option.help': '显示命令帮助',
110
110
  'help.option.version': '输出版本号',
111
- 'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、OpenCode、Codex 配置 MCP',
111
+ 'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、Antigravity、OpenCode、CodeBuddy、Qoder、Codex 配置 MCP',
112
112
  'help.command.uninstall.description': '撤销 `setup`:从所有检测到的编辑器中移除 GitNexus 的 MCP 配置、技能和钩子',
113
113
  'help.command.analyze.description': '索引仓库(完整分析)',
114
114
  'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)',
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ function collectCodingAgents(value, previous) {
17
17
  program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
18
18
  program
19
19
  .command('setup')
20
- .description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, Codex')
20
+ .description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex')
21
21
  .option('-c, --coding-agent <agents>', 'Configure only these coding agents (comma-separated or repeatable)', collectCodingAgents)
22
22
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
23
23
  program
package/dist/cli/setup.js CHANGED
@@ -14,7 +14,7 @@ import { promisify } from 'util';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
16
16
  import { getGlobalDir } from '../storage/repo-manager.js';
17
- import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, } from './editor-targets.js';
17
+ import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, isEnoent, } from './editor-targets.js';
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = path.dirname(__filename);
20
20
  const execFileAsync = promisify(execFile);
@@ -66,6 +66,8 @@ const CODING_AGENT_IDS = {
66
66
  claude: 'claude',
67
67
  antigravity: 'antigravity',
68
68
  opencode: 'opencode',
69
+ codebuddy: 'codebuddy',
70
+ qoder: 'qoder',
69
71
  codex: 'codex',
70
72
  };
71
73
  const SUPPORTED_CODING_AGENTS = Object.values(CODING_AGENT_IDS);
@@ -173,7 +175,13 @@ async function mergeJsoncFile(filePath, keyPath, value) {
173
175
  try {
174
176
  raw = await fs.readFile(filePath, 'utf-8');
175
177
  }
176
- catch {
178
+ catch (err) {
179
+ // Only an absent file means "start fresh". Any other read failure (EACCES,
180
+ // EIO, cloud-placeholder faults) must not be treated as empty — the write
181
+ // below would replace the user's existing config with a gitnexus-only
182
+ // document and report success. Rethrow into the per-editor catch instead.
183
+ if (!isEnoent(err))
184
+ throw err;
177
185
  raw = '';
178
186
  }
179
187
  if (raw.trim().length === 0) {
@@ -207,6 +215,35 @@ async function dirExists(dirPath) {
207
215
  return false;
208
216
  }
209
217
  }
218
+ /**
219
+ * Detection probe: is there a non-empty regular file at this path?
220
+ * Swallows ALL errors (like dirExists) — detection gates run outside the
221
+ * per-editor try blocks, so a rethrowing probe would abort setup for every
222
+ * remaining editor. Size > 0 keeps detection aligned with the config-chain
223
+ * resolver: an empty config file is not evidence of an install, and treating
224
+ * it as one would route the write to a fresh file whose mkdir manufactures
225
+ * the editor's directory.
226
+ */
227
+ async function isNonEmptyFile(filePath) {
228
+ try {
229
+ const stat = await fs.stat(filePath);
230
+ return stat.isFile() && stat.size > 0;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ /**
237
+ * Detection probe: does any file in the target's MCP config chain look like an
238
+ * install trace? Always walks [file, ...legacyFiles] so an editor gaining
239
+ * legacyFiles later is automatically covered (CodeBuddy and Qoder share this —
240
+ * a per-editor copy is how the root-config-only detection gap crept in, see
241
+ * PR #2368 review I4).
242
+ */
243
+ async function anyChainConfigFile(target) {
244
+ const hits = await Promise.all([target.file, ...(target.legacyFiles ?? [])].map(isNonEmptyFile));
245
+ return hits.includes(true);
246
+ }
210
247
  // ─── Editor-specific setup ─────────────────────────────────────────
211
248
  async function setupCursor(result) {
212
249
  const cursorDir = path.join(os.homedir(), '.cursor');
@@ -287,7 +324,12 @@ async function mergeHooksJsonc(filePath, entries) {
287
324
  try {
288
325
  raw = await fs.readFile(filePath, 'utf-8');
289
326
  }
290
- catch {
327
+ catch (err) {
328
+ // Same contract as mergeJsoncFile: an unreadable (non-ENOENT) settings
329
+ // file must not be rewritten as hooks-only — that would destroy every
330
+ // user setting in it. Rethrow into the hook installer's catch.
331
+ if (!isEnoent(err))
332
+ throw err;
291
333
  raw = '';
292
334
  }
293
335
  if (raw.trim().length === 0) {
@@ -365,20 +407,26 @@ export async function copyHookHelpers(srcDir, destDir, label, result) {
365
407
  return failedRequired;
366
408
  }
367
409
  /**
368
- * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
369
- * Merges hook config without overwriting existing hooks, preserving
370
- * comments and formatting in the JSONC file.
410
+ * Install GitNexus hooks for editors that use Claude Code's hooks schema.
411
+ *
412
+ * Claude Code registers hooks in ~/.claude/settings.json; Codex uses a
413
+ * dedicated ~/.codex/hooks.json with the identical {hooks: {Event: [...]}}
414
+ * JSON shape, stdin payload, and hookSpecificOutput response contract
415
+ * (https://developers.openai.com/codex/hooks), so both runtimes share this
416
+ * installer and the same bundled adapter script. Merges hook config without
417
+ * overwriting existing hooks, preserving comments and formatting.
371
418
  */
372
- async function installClaudeCodeHooks(result) {
373
- const claudeDir = path.join(os.homedir(), '.claude');
374
- if (!(await dirExists(claudeDir)))
419
+ async function installClaudeSchemaHooks(result, id) {
420
+ const hookCfg = hookTarget(id);
421
+ const settingsPath = hookCfg.settingsFile;
422
+ const label = `${hookCfg.label} hooks`;
423
+ // Gate on the editor's own config dir (~/.claude, ~/.codex) existing.
424
+ if (!(await dirExists(path.dirname(settingsPath))))
375
425
  return;
376
- const claudeHook = hookTarget('claude');
377
- const settingsPath = claudeHook.settingsFile;
378
426
  // Source hooks bundled within the gitnexus package (hooks/claude/)
379
427
  const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
380
- // Copy unified hook script to ~/.claude/hooks/gitnexus/
381
- const destHooksDir = claudeHook.scriptDir;
428
+ // Copy unified hook script to the editor's hooks/gitnexus/ dir
429
+ const destHooksDir = hookCfg.scriptDir;
382
430
  try {
383
431
  await fs.mkdir(destHooksDir, { recursive: true });
384
432
  const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
@@ -389,7 +437,7 @@ async function installClaudeCodeHooks(result) {
389
437
  const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
390
438
  const jsonCli = JSON.stringify(normalizedCli);
391
439
  if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
392
- result.errors.push('Claude Code hooks: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.');
440
+ result.errors.push(`${label}: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.`);
393
441
  }
394
442
  content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
395
443
  await fs.writeFile(dest, content, 'utf-8');
@@ -404,12 +452,12 @@ async function installClaudeCodeHooks(result) {
404
452
  await fs.access(dest);
405
453
  }
406
454
  catch {
407
- result.errors.push('Claude Code hooks: adapter script was not installed — skipping hook registration');
455
+ result.errors.push(`${label}: adapter script was not installed — skipping hook registration`);
408
456
  return;
409
457
  }
410
- const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, 'Claude Code hooks', result);
458
+ const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, label, result);
411
459
  if (failedRequired.length > 0) {
412
- result.errors.push(`Claude Code hooks: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
460
+ result.errors.push(`${label}: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
413
461
  return;
414
462
  }
415
463
  const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
@@ -425,9 +473,10 @@ async function installClaudeCodeHooks(result) {
425
473
  }
426
474
  })();
427
475
  const hookEntries = [];
428
- // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
429
- // Session context is delivered via CLAUDE.md / skills instead.
430
- if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', claudeHook.needle)) {
476
+ // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576),
477
+ // and Codex reads AGENTS.md natively. Session context is delivered via
478
+ // CLAUDE.md / AGENTS.md / skills instead.
479
+ if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', hookCfg.needle)) {
431
480
  hookEntries.push({
432
481
  eventName: 'PreToolUse',
433
482
  value: {
@@ -443,7 +492,7 @@ async function installClaudeCodeHooks(result) {
443
492
  },
444
493
  });
445
494
  }
446
- if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', claudeHook.needle)) {
495
+ if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', hookCfg.needle)) {
447
496
  hookEntries.push({
448
497
  eventName: 'PostToolUse',
449
498
  value: {
@@ -460,19 +509,19 @@ async function installClaudeCodeHooks(result) {
460
509
  });
461
510
  }
462
511
  if (hookEntries.length === 0) {
463
- result.configured.push('Claude Code hooks (already configured)');
512
+ result.configured.push(`${label} (already configured)`);
464
513
  return;
465
514
  }
466
515
  const ok = await mergeHooksJsonc(settingsPath, hookEntries);
467
516
  if (ok) {
468
- result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
517
+ result.configured.push(`${label} (PreToolUse, PostToolUse)`);
469
518
  }
470
519
  else {
471
- result.errors.push('Claude Code hooks: settings.json is corrupt — skipping to preserve existing content');
520
+ result.errors.push(`${label}: ${path.basename(settingsPath)} is corrupt — skipping to preserve existing content`);
472
521
  }
473
522
  }
474
523
  catch (err) {
475
- result.errors.push(`Claude Code hooks: ${err.message}`);
524
+ result.errors.push(`${label}: ${err.message}`);
476
525
  }
477
526
  }
478
527
  // ─── Antigravity (Google) ──────────────────────────────────────────
@@ -660,6 +709,118 @@ async function setupOpenCode(result) {
660
709
  result.errors.push(`OpenCode: ${err.message}`);
661
710
  }
662
711
  }
712
+ /**
713
+ * Resolve which config file in a target's [file, ...legacyFiles] priority
714
+ * chain setup should write into: the first that exists, else the recommended
715
+ * `file`. CodeBuddy reads only the first existing file in its chain, so
716
+ * creating the recommended file above a populated deprecated one would shadow
717
+ * the user's existing MCP servers.
718
+ */
719
+ async function resolveMcpConfigFile(target) {
720
+ for (const candidate of [target.file, ...(target.legacyFiles ?? [])]) {
721
+ try {
722
+ const stat = await fs.stat(candidate);
723
+ // Non-empty regular files only: a 0-byte recommended file must not
724
+ // shadow a populated deprecated one (mergeJsoncFile treats empty as a
725
+ // fresh document anyway), and directories are never config candidates.
726
+ if (stat.isFile() && stat.size > 0)
727
+ return candidate;
728
+ }
729
+ catch (err) {
730
+ // ENOENT = candidate absent — try the next one. Anything else (EACCES
731
+ // on the file or a parent) is surfaced: silently skipping could route
732
+ // the write to a lower-priority file the editor never reads.
733
+ if (!isEnoent(err))
734
+ throw err;
735
+ }
736
+ }
737
+ return target.file;
738
+ }
739
+ async function setupCodeBuddy(result) {
740
+ const codebuddyDir = path.join(os.homedir(), '.codebuddy');
741
+ const target = mcpTarget('codebuddy');
742
+ // Installed = the config dir exists OR any registered MCP config file does.
743
+ // A user whose only trace is a root-level config (e.g. a legacy
744
+ // ~/.codebuddy.json) still gets configured — uninstall already handles that
745
+ // shape, so setup skipping it was an asymmetry (PR #2368 review I4).
746
+ if (!(await dirExists(codebuddyDir)) && !(await anyChainConfigFile(target))) {
747
+ result.skipped.push('CodeBuddy (not installed)');
748
+ return;
749
+ }
750
+ try {
751
+ const configFile = await resolveMcpConfigFile(target);
752
+ const ok = await mergeJsoncFile(configFile, target.keyPath, getMcpEntry());
753
+ if (ok) {
754
+ result.configured.push('CodeBuddy');
755
+ }
756
+ else {
757
+ result.errors.push(`CodeBuddy: ${path.basename(configFile)} is corrupt — skipping to preserve existing content`);
758
+ }
759
+ }
760
+ catch (err) {
761
+ result.errors.push(`CodeBuddy: ${err instanceof Error ? err.message : String(err)}`);
762
+ }
763
+ }
764
+ async function setupQoder(result) {
765
+ const qoderDir = path.join(os.homedir(), '.qoder');
766
+ const target = mcpTarget('qoder');
767
+ const { file: mcpPath, keyPath } = target;
768
+ // Same chain-aware detection as CodeBuddy: ~/.qoder.json alone counts.
769
+ if (!(await dirExists(qoderDir)) && !(await anyChainConfigFile(target))) {
770
+ result.skipped.push('Qoder (not installed)');
771
+ return;
772
+ }
773
+ try {
774
+ const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
775
+ if (ok) {
776
+ result.configured.push('Qoder');
777
+ }
778
+ else {
779
+ result.errors.push('Qoder: .qoder.json is corrupt — skipping to preserve existing content');
780
+ }
781
+ }
782
+ catch (err) {
783
+ result.errors.push(`Qoder: ${err instanceof Error ? err.message : String(err)}`);
784
+ }
785
+ }
786
+ /**
787
+ * Install global CodeBuddy skills to ~/.codebuddy/skills/
788
+ * (https://www.codebuddy.ai/docs/cli/skills — same SKILL.md layout as Claude Code).
789
+ */
790
+ async function installCodeBuddySkills(result) {
791
+ const codebuddyDir = path.join(os.homedir(), '.codebuddy');
792
+ if (!(await dirExists(codebuddyDir)))
793
+ return;
794
+ const skillsDir = skillTarget('codebuddy').dir;
795
+ try {
796
+ const installed = await installSkillsTo(skillsDir);
797
+ if (installed.length > 0) {
798
+ result.configured.push(`CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`);
799
+ }
800
+ }
801
+ catch (err) {
802
+ result.errors.push(`CodeBuddy skills: ${err instanceof Error ? err.message : String(err)}`);
803
+ }
804
+ }
805
+ /**
806
+ * Install global Qoder skills to ~/.qoder/skills/
807
+ * (https://docs.qoder.com/extensions/skills — same SKILL.md layout as Claude Code).
808
+ */
809
+ async function installQoderSkills(result) {
810
+ const qoderDir = path.join(os.homedir(), '.qoder');
811
+ if (!(await dirExists(qoderDir)))
812
+ return;
813
+ const skillsDir = skillTarget('qoder').dir;
814
+ try {
815
+ const installed = await installSkillsTo(skillsDir);
816
+ if (installed.length > 0) {
817
+ result.configured.push(`Qoder skills (${installed.length} skills → ~/.qoder/skills/)`);
818
+ }
819
+ }
820
+ catch (err) {
821
+ result.errors.push(`Qoder skills: ${err instanceof Error ? err.message : String(err)}`);
822
+ }
823
+ }
663
824
  /**
664
825
  * Build a TOML section for Codex MCP config (~/.codex/config.toml).
665
826
  */
@@ -677,7 +838,12 @@ async function upsertCodexConfigToml(configPath) {
677
838
  try {
678
839
  existing = await fs.readFile(configPath, 'utf-8');
679
840
  }
680
- catch {
841
+ catch (err) {
842
+ // TOML variant of the mergeJsoncFile contract: treating a non-ENOENT read
843
+ // failure as an empty config would rewrite config.toml with only the
844
+ // gitnexus section. Rethrow into setupCodex's catch.
845
+ if (!isEnoent(err))
846
+ throw err;
681
847
  existing = '';
682
848
  }
683
849
  if (existing.includes(`[${getEditorTargets().codex.tomlSection}]`)) {
@@ -885,12 +1051,16 @@ export const setupCommand = async (options) => {
885
1051
  await setupAntigravity(result);
886
1052
  if (selected.has('opencode'))
887
1053
  await setupOpenCode(result);
1054
+ if (selected.has('codebuddy'))
1055
+ await setupCodeBuddy(result);
1056
+ if (selected.has('qoder'))
1057
+ await setupQoder(result);
888
1058
  if (selected.has('codex'))
889
1059
  await setupCodex(result);
890
1060
  // Install global skills for platforms that support them
891
1061
  if (selected.has('claude')) {
892
1062
  await installClaudeCodeSkills(result);
893
- await installClaudeCodeHooks(result);
1063
+ await installClaudeSchemaHooks(result, 'claude');
894
1064
  }
895
1065
  if (selected.has('antigravity')) {
896
1066
  await installAntigravitySkills(result);
@@ -900,8 +1070,14 @@ export const setupCommand = async (options) => {
900
1070
  await installCursorSkills(result);
901
1071
  if (selected.has('opencode'))
902
1072
  await installOpenCodeSkills(result);
903
- if (selected.has('codex'))
1073
+ if (selected.has('codebuddy'))
1074
+ await installCodeBuddySkills(result);
1075
+ if (selected.has('qoder'))
1076
+ await installQoderSkills(result);
1077
+ if (selected.has('codex')) {
904
1078
  await installCodexSkills(result);
1079
+ await installClaudeSchemaHooks(result, 'codex');
1080
+ }
905
1081
  // Print results
906
1082
  if (result.configured.length > 0) {
907
1083
  console.log(' Configured:');
@@ -33,7 +33,7 @@ import { execFile } from 'child_process';
33
33
  import { promisify } from 'util';
34
34
  import { fileURLToPath } from 'url';
35
35
  import { parseTree, modify, applyEdits, findNodeAtLocation, parse as parseJsonc, } from 'jsonc-parser';
36
- import { getEditorTargets, detectIndentation } from './editor-targets.js';
36
+ import { getEditorTargets, detectIndentation, isEnoent } from './editor-targets.js';
37
37
  const __filename = fileURLToPath(import.meta.url);
38
38
  const __dirname = path.dirname(__filename);
39
39
  const execFileAsync = promisify(execFile);
@@ -50,7 +50,13 @@ async function removeJsoncKey(filePath, keyPath, dryRun) {
50
50
  try {
51
51
  raw = await fs.readFile(filePath, 'utf-8');
52
52
  }
53
- catch {
53
+ catch (err) {
54
+ // ENOENT = genuinely not configured. Any other read failure (EACCES,
55
+ // locks) must not report 'missing' — the file may hold a real gitnexus
56
+ // entry the dry-run would then deny exists. Rethrow into the caller's
57
+ // per-file catch.
58
+ if (!isEnoent(err))
59
+ throw err;
54
60
  return 'missing';
55
61
  }
56
62
  if (raw.trim().length === 0)
@@ -88,7 +94,12 @@ async function removeHookEntries(filePath, eventNames, commandNeedle, dryRun) {
88
94
  try {
89
95
  raw = await fs.readFile(filePath, 'utf-8');
90
96
  }
91
- catch {
97
+ catch (err) {
98
+ // Masking a non-ENOENT read failure as 'missing' would also let the
99
+ // caller delete the hook scriptDir while the unreadable settings file
100
+ // still references it. Rethrow into the hook uninstaller's catch.
101
+ if (!isEnoent(err))
102
+ throw err;
92
103
  return { status: 'missing', count: 0 };
93
104
  }
94
105
  if (raw.trim().length === 0)
@@ -323,8 +334,15 @@ async function uninstallCodex(result, dryRun, configPath, tomlSection) {
323
334
  try {
324
335
  raw = await fs.readFile(configPath, 'utf-8');
325
336
  }
326
- catch {
327
- result.skipped.push('Codex MCP (not configured)');
337
+ catch (err) {
338
+ // Catch locally: this call site has no surrounding try, so a rethrow
339
+ // would abort the hooks/skills cleanup that runs after Codex.
340
+ if (isEnoent(err)) {
341
+ result.skipped.push('Codex MCP (not configured)');
342
+ }
343
+ else {
344
+ result.errors.push(`Codex: ${err instanceof Error ? err.message : String(err)}`);
345
+ }
328
346
  return;
329
347
  }
330
348
  if (!raw.includes(`[${tomlSection}]`)) {
@@ -369,20 +387,50 @@ export const uninstallCommand = async (options) => {
369
387
  console.log(' Dry run — nothing will be changed. Re-run with --force to apply.');
370
388
  console.log('');
371
389
  }
372
- const result = { removed: [], skipped: [], errors: [] };
390
+ const result = { removed: [], skipped: [], errors: [], corruptLegacy: false };
373
391
  // ─── MCP server entries (JSONC editors) ──────────────────────────
392
+ // Sweep legacyFiles too: setup writes into the first existing file of the
393
+ // editor's priority chain, so the gitnexus entry may live in a deprecated
394
+ // location (e.g. CodeBuddy's ~/.codebuddy/mcp.json).
374
395
  for (const target of targets.mcpJsonc) {
375
- try {
376
- const status = await removeJsoncKey(target.file, target.keyPath, dryRun);
377
- if (status === 'removed')
378
- result.removed.push(`${target.label} MCP server — ${target.keyPath.join('.')} in ${target.file}`);
379
- else if (status === 'corrupt')
380
- result.errors.push(`${target.label}: ${path.basename(target.file)} is corrupt left untouched`);
381
- else
382
- result.skipped.push(`${target.label} MCP (not configured)`);
396
+ let removedAny = false;
397
+ let erroredAny = false;
398
+ let corruptLegacyAny = false;
399
+ for (const file of [target.file, ...(target.legacyFiles ?? [])]) {
400
+ try {
401
+ const status = await removeJsoncKey(file, target.keyPath, dryRun);
402
+ if (status === 'removed') {
403
+ removedAny = true;
404
+ result.removed.push(`${target.label} MCP server — ${target.keyPath.join('.')} in ${file}`);
405
+ }
406
+ else if (status === 'corrupt') {
407
+ if (file === target.file) {
408
+ // The primary path is where gitnexus itself writes — corruption
409
+ // there is an error worth failing the command over.
410
+ erroredAny = true;
411
+ result.errors.push(`${target.label}: ${path.basename(file)} is corrupt — left untouched`);
412
+ }
413
+ else {
414
+ // Legacy chain files are vendor locations gitnexus may never have
415
+ // touched (e.g. a corrupt ~/.codebuddy.json from an old install).
416
+ // Report informationally without failing uninstall. Deliberate
417
+ // asymmetry: an UNREADABLE (non-ENOENT) legacy file still errors —
418
+ // that's an environmental problem worth surfacing, while corrupt-
419
+ // but-readable proves there is no removable gitnexus entry.
420
+ corruptLegacyAny = true;
421
+ result.corruptLegacy = true;
422
+ result.skipped.push(`${target.label} MCP (legacy ${path.basename(file)} is corrupt — left untouched)`);
423
+ }
424
+ }
425
+ }
426
+ catch (err) {
427
+ erroredAny = true;
428
+ result.errors.push(`${target.label}: ${err instanceof Error ? err.message : String(err)}`);
429
+ }
383
430
  }
384
- catch (err) {
385
- result.errors.push(`${target.label}: ${err.message}`);
431
+ // A corrupt legacy file makes "not configured" unknowable — suppress it.
432
+ if (!removedAny && !erroredAny && !corruptLegacyAny) {
433
+ result.skipped.push(`${target.label} MCP (not configured)`);
386
434
  }
387
435
  }
388
436
  await uninstallCodex(result, dryRun, targets.codex.configFile, targets.codex.tomlSection);
@@ -425,6 +473,12 @@ export const uninstallCommand = async (options) => {
425
473
  for (const name of result.removed)
426
474
  console.log(` - ${name}`);
427
475
  }
476
+ else if (result.errors.length > 0 || result.corruptLegacy) {
477
+ // Errors (corrupt primary files, unreadable configs) or corrupt legacy
478
+ // configs make "not configured" unknowable — claiming it right above an
479
+ // Errors block would be a contradiction users learn to distrust.
480
+ console.log(' Nothing removed.');
481
+ }
428
482
  else {
429
483
  console.log(' Nothing to remove — GitNexus is not configured in any detected editor.');
430
484
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.9",
3
+ "version": "1.6.10-rc.2",
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",
@@ -30,6 +30,7 @@ const PLATFORM_LOGIC = [
30
30
  'test/unit/setup-jsonc.test.ts',
31
31
  'test/unit/setup-codex.test.ts',
32
32
  'test/unit/setup-antigravity.test.ts',
33
+ 'test/integration/setup-uninstall-roundtrip.test.ts',
33
34
  'test/unit/resolve-invocation.test.ts',
34
35
  'test/unit/platform-capabilities.test.ts',
35
36
  'test/unit/worker-pool-windows-quarantine.test.ts',