typegraph-mcp 0.9.33 → 0.9.35

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
@@ -128,9 +128,13 @@ typegraph-mcp <command> [options]
128
128
  bench Run benchmarks (token, latency, accuracy)
129
129
  start Start the MCP server (stdin/stdout)
130
130
 
131
- --yes Skip prompts --help Show help
131
+ --yes Skip prompts
132
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
133
+ --help Show help
132
134
  ```
133
135
 
136
+ `remove` always cleans up project-local config. If it detects a legacy global `~/.codex/config.toml` entry that points at the current project, it will ask before removing it in interactive mode. In non-interactive mode, pass `--clean-global-codex` to allow that global cleanup.
137
+
134
138
  ## Troubleshooting
135
139
 
136
140
  Run the health check first — it catches most issues:
@@ -156,11 +160,13 @@ Add this to your project's `.codex/config.toml`:
156
160
 
157
161
  ```toml
158
162
  [mcp_servers.typegraph]
159
- command = "npx"
160
- args = ["tsx", "/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
163
+ command = "/absolute/path/to/your-project/plugins/typegraph-mcp/node_modules/.bin/tsx"
164
+ args = ["/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
161
165
  env = { TYPEGRAPH_PROJECT_ROOT = "/absolute/path/to/your-project", TYPEGRAPH_TSCONFIG = "/absolute/path/to/your-project/tsconfig.json" }
162
166
  ```
163
167
 
168
+ Using the plugin-local `tsx` binary avoids relying on `npx tsx` being resolvable when Codex launches the MCP server.
169
+
164
170
  Codex only loads project `.codex/config.toml` files for trusted projects. If needed, add this to `~/.codex/config.toml`:
165
171
 
166
172
  ```toml
package/check.ts CHANGED
@@ -112,11 +112,19 @@ function hasCodexTypegraphRegistration(content: string): boolean {
112
112
  return /\[mcp_servers\.typegraph\]/.test(content);
113
113
  }
114
114
 
115
+ function hasCodexTsxLauncher(content: string): boolean {
116
+ return (
117
+ /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) ||
118
+ /args\s*=\s*\[[\s\S]*"tsx"/.test(content)
119
+ );
120
+ }
121
+
115
122
  function hasCompleteCodexTypegraphRegistration(content: string): boolean {
116
123
  return (
117
124
  hasCodexTypegraphRegistration(content) &&
118
125
  /command\s*=\s*"[^"]+"/.test(content) &&
119
- /args\s*=\s*\[[\s\S]*"tsx"/.test(content) &&
126
+ /args\s*=\s*\[[\s\S]*\]/.test(content) &&
127
+ hasCodexTsxLauncher(content) &&
120
128
  /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) &&
121
129
  /TYPEGRAPH_TSCONFIG\s*=/.test(content)
122
130
  );
@@ -308,7 +316,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
308
316
  const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
309
317
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
310
318
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
311
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
319
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
320
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
312
321
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
313
322
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
314
323
  if (hasProjectCodexRegistration) {
@@ -324,7 +333,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
324
333
  const issues: string[] = [];
325
334
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
326
335
  if (!hasCommand) issues.push("command is missing");
327
- if (!hasArgs) issues.push("args should include 'tsx'");
336
+ if (!hasArgs) issues.push("args are missing");
337
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
328
338
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
329
339
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
330
340
  fail(
package/cli.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Options:
12
12
  * --yes Skip confirmation prompts (accept all defaults)
13
+ * --clean-global-codex Also remove a stale global Codex MCP entry for this project
13
14
  * --help Show help
14
15
  */
15
16
 
@@ -35,15 +36,35 @@ interface AgentDef {
35
36
  detect: (projectRoot: string) => boolean;
36
37
  }
37
38
 
39
+ interface LegacyGlobalCodexCleanup {
40
+ globalConfigPath: string;
41
+ nextContent: string;
42
+ }
43
+
38
44
  // ─── Constants ───────────────────────────────────────────────────────────────
39
45
 
40
46
  const AGENT_SNIPPET = `
41
47
  ## TypeScript Navigation (typegraph-mcp)
42
48
 
43
- Use the \`ts_*\` MCP tools instead of grep/glob for navigating TypeScript code. They resolve through barrel files, re-exports, and project references returning precise results, not string matches.
49
+ Where suitable, use the \`ts_*\` MCP tools instead of grep/glob for navigating TypeScript code. They resolve through barrel files, re-exports, and project references and return semantic results instead of string matches.
50
+
51
+ - Point queries: \`ts_find_symbol\`, \`ts_definition\`, \`ts_references\`, \`ts_type_info\`, \`ts_navigate_to\`, \`ts_trace_chain\`, \`ts_blast_radius\`, \`ts_module_exports\`
52
+ - Graph queries: \`ts_dependency_tree\`, \`ts_dependents\`, \`ts_import_cycles\`, \`ts_shortest_path\`, \`ts_subgraph\`, \`ts_module_boundary\`
53
+
54
+ Start with the navigation tools before reading entire files. Use direct file reads only after the MCP tools identify the exact symbols or lines that matter.
55
+
56
+ Use \`rg\` or \`grep\` when semantic symbol navigation is not the right tool, especially for:
57
+
58
+ - docs, config, SQL, migrations, JSON, env vars, route strings, and other non-TypeScript assets
59
+ - broad text discovery when you do not yet know the symbol name
60
+ - exact string matching across the repo
61
+ - validating wording or finding repeated plan/document references
62
+
63
+ Practical rule:
44
64
 
45
- - **Point queries** (tsserver): \`ts_find_symbol\`, \`ts_definition\`, \`ts_references\`, \`ts_type_info\`, \`ts_navigate_to\`, \`ts_trace_chain\`, \`ts_blast_radius\`, \`ts_module_exports\`
46
- - **Graph queries** (import graph): \`ts_dependency_tree\`, \`ts_dependents\`, \`ts_import_cycles\`, \`ts_shortest_path\`, \`ts_subgraph\`, \`ts_module_boundary\`
65
+ - use \`ts_*\` first for TypeScript symbol definition, references, types, and dependency analysis
66
+ - use \`rg\`/\`grep\` for text search and non-TypeScript exploration
67
+ - combine both when a task spans TypeScript code and surrounding docs/config
47
68
  `.trimStart();
48
69
 
49
70
  const SNIPPET_MARKER = "## TypeScript Navigation (typegraph-mcp)";
@@ -148,8 +169,9 @@ Commands:
148
169
  start Start the MCP server (stdin/stdout)
149
170
 
150
171
  Options:
151
- --yes Skip confirmation prompts (accept all defaults)
152
- --help Show this help
172
+ --yes Skip confirmation prompts (accept all defaults)
173
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
174
+ --help Show this help
153
175
  `.trim();
154
176
 
155
177
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -192,12 +214,78 @@ function getAbsoluteMcpServerEntry(projectRoot: string): {
192
214
  };
193
215
  }
194
216
 
217
+ function getCodexMcpServerEntry(projectRoot: string): {
218
+ command: string;
219
+ args: string[];
220
+ env: Record<string, string>;
221
+ } {
222
+ return {
223
+ command: path.resolve(projectRoot, PLUGIN_DIR_NAME, "node_modules/.bin/tsx"),
224
+ args: [path.resolve(projectRoot, PLUGIN_DIR_NAME, "server.ts")],
225
+ env: {
226
+ TYPEGRAPH_PROJECT_ROOT: projectRoot,
227
+ TYPEGRAPH_TSCONFIG: path.resolve(projectRoot, "tsconfig.json"),
228
+ },
229
+ };
230
+ }
231
+
195
232
  function getCodexConfigPath(projectRoot: string): string {
196
233
  return path.resolve(projectRoot, ".codex/config.toml");
197
234
  }
198
235
 
199
- function hasCodexMcpSection(content: string): boolean {
200
- return content.includes("[mcp_servers.typegraph]");
236
+ function isTomlSectionGroup(sectionName: string | null, prefix: string): boolean {
237
+ return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
238
+ }
239
+
240
+ function splitTomlBlocks(content: string): Array<{ sectionName: string | null; raw: string }> {
241
+ const lines = content.split(/\r?\n/);
242
+ const blocks: Array<{ sectionName: string | null; raw: string }> = [];
243
+ let sectionName: string | null = null;
244
+ let currentLines: string[] = [];
245
+
246
+ for (const line of lines) {
247
+ const match = line.match(/^\[([^\]]+)\]\s*$/);
248
+ if (match) {
249
+ if (currentLines.length > 0 || sectionName !== null) {
250
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
251
+ }
252
+ sectionName = match[1]!;
253
+ currentLines = [line];
254
+ continue;
255
+ }
256
+
257
+ currentLines.push(line);
258
+ }
259
+
260
+ if (currentLines.length > 0 || sectionName !== null) {
261
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
262
+ }
263
+
264
+ return blocks;
265
+ }
266
+
267
+ function removeTomlSectionGroup(
268
+ content: string,
269
+ prefix: string
270
+ ): { content: string; removed: boolean; removedContent: string } {
271
+ const blocks = splitTomlBlocks(content);
272
+ const removedBlocks = blocks.filter((block) => isTomlSectionGroup(block.sectionName, prefix));
273
+ if (removedBlocks.length === 0) {
274
+ return { content, removed: false, removedContent: "" };
275
+ }
276
+
277
+ const keptBlocks = blocks.filter((block) => !isTomlSectionGroup(block.sectionName, prefix));
278
+ const nextContent = keptBlocks
279
+ .map((block) => block.raw)
280
+ .join("\n")
281
+ .replace(/\n{3,}/g, "\n\n")
282
+ .trimEnd();
283
+
284
+ return {
285
+ content: nextContent ? `${nextContent}\n` : "",
286
+ removed: true,
287
+ removedContent: removedBlocks.map((block) => block.raw).join("\n").trim(),
288
+ };
201
289
  }
202
290
 
203
291
  function upsertCodexMcpSection(content: string, block: string): { content: string; changed: boolean } {
@@ -221,12 +309,13 @@ function upsertCodexMcpSection(content: string, block: string): { content: strin
221
309
  }
222
310
 
223
311
  function makeCodexMcpBlock(projectRoot: string): string {
224
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
312
+ const absoluteEntry = getCodexMcpServerEntry(projectRoot);
313
+ const args = absoluteEntry.args.map((arg) => `"${arg}"`).join(", ");
225
314
  return [
226
315
  "",
227
316
  "[mcp_servers.typegraph]",
228
317
  `command = "${absoluteEntry.command}"`,
229
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
318
+ `args = [${args}]`,
230
319
  `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
231
320
  "",
232
321
  ].join("\n");
@@ -273,6 +362,57 @@ function isCodexProjectTrusted(projectRoot: string): boolean {
273
362
  return matchesTrustedProject();
274
363
  }
275
364
 
365
+ function pathEqualsOrContains(candidatePath: string, targetPath: string): boolean {
366
+ const resolvedCandidate = path.resolve(candidatePath);
367
+ const resolvedTarget = path.resolve(targetPath);
368
+ if (resolvedCandidate === resolvedTarget || resolvedCandidate.startsWith(`${resolvedTarget}${path.sep}`)) {
369
+ return true;
370
+ }
371
+
372
+ try {
373
+ const realCandidate = fs.realpathSync(candidatePath);
374
+ const realTarget = fs.realpathSync(targetPath);
375
+ return realCandidate === realTarget || realCandidate.startsWith(`${realTarget}${path.sep}`);
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+
381
+ function findLegacyGlobalCodexCleanup(projectRoot: string): LegacyGlobalCodexCleanup | null {
382
+ const home = process.env.HOME;
383
+ if (!home) return null;
384
+
385
+ const globalConfigPath = path.join(home, ".codex/config.toml");
386
+ if (!fs.existsSync(globalConfigPath)) return null;
387
+
388
+ const content = fs.readFileSync(globalConfigPath, "utf-8");
389
+ const { content: nextContent, removed, removedContent } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
390
+ if (!removed) return null;
391
+
392
+ const pluginRoot = path.resolve(projectRoot, PLUGIN_DIR_NAME);
393
+ const quotedPaths = Array.from(removedContent.matchAll(/"([^"\n]+)"/g), (match) => match[1]!);
394
+ const looksProjectSpecific = quotedPaths.some((quotedPath) =>
395
+ pathEqualsOrContains(quotedPath, projectRoot) ||
396
+ pathEqualsOrContains(quotedPath, pluginRoot)
397
+ );
398
+
399
+ if (!looksProjectSpecific) {
400
+ return null;
401
+ }
402
+
403
+ return { globalConfigPath, nextContent };
404
+ }
405
+
406
+ function removeLegacyGlobalCodexMcp(cleanup: LegacyGlobalCodexCleanup): void {
407
+ if (cleanup.nextContent === "") {
408
+ fs.unlinkSync(cleanup.globalConfigPath);
409
+ } else {
410
+ fs.writeFileSync(cleanup.globalConfigPath, cleanup.nextContent);
411
+ }
412
+
413
+ p.log.info("~/.codex/config.toml: removed stale global typegraph MCP server entry for this project");
414
+ }
415
+
276
416
  /** Register the typegraph MCP server in agent-specific config files */
277
417
  function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): void {
278
418
  if (selectedAgents.includes("cursor")) {
@@ -386,26 +526,19 @@ function registerCodexMcp(projectRoot: string): void {
386
526
  function deregisterCodexMcp(projectRoot: string): void {
387
527
  const configPath = ".codex/config.toml";
388
528
  const fullPath = getCodexConfigPath(projectRoot);
389
- if (!fs.existsSync(fullPath)) return;
390
-
391
- let content = fs.readFileSync(fullPath, "utf-8");
392
- if (!hasCodexMcpSection(content)) return;
393
-
394
- // Remove the [mcp_servers.typegraph] section (stops at next section header or end of file)
395
- content = content.replace(
396
- /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
397
- ""
398
- );
399
-
400
- // Clean up multiple trailing newlines
401
- content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
402
-
403
- if (content.trim() === "") {
404
- fs.unlinkSync(fullPath);
405
- } else {
406
- fs.writeFileSync(fullPath, content);
529
+ if (fs.existsSync(fullPath)) {
530
+ const content = fs.readFileSync(fullPath, "utf-8");
531
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
532
+
533
+ if (removed) {
534
+ if (nextContent === "") {
535
+ fs.unlinkSync(fullPath);
536
+ } else {
537
+ fs.writeFileSync(fullPath, nextContent);
538
+ }
539
+ p.log.info(`${configPath}: removed typegraph MCP server`);
540
+ }
407
541
  }
408
- p.log.info(`${configPath}: removed typegraph MCP server`);
409
542
  }
410
543
 
411
544
  // ─── TSConfig Exclude ─────────────────────────────────────────────────────────
@@ -733,16 +866,26 @@ async function setup(yes: boolean): Promise<void> {
733
866
 
734
867
  // ─── Remove Command ──────────────────────────────────────────────────────────
735
868
 
736
- async function removePlugin(projectRoot: string, pluginDir: string): Promise<void> {
869
+ async function removePlugin(
870
+ projectRoot: string,
871
+ pluginDir: string,
872
+ options: { removeGlobalCodex: boolean; legacyGlobalCodexCleanup: LegacyGlobalCodexCleanup | null }
873
+ ): Promise<void> {
737
874
  const s = p.spinner();
738
875
  s.start("Removing typegraph-mcp...");
739
876
 
740
- // 1. Remove plugin directory
877
+ // 1. Deregister MCP server from agent config files while project paths still exist
878
+ deregisterMcpServers(projectRoot);
879
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
880
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
881
+ }
882
+
883
+ // 2. Remove plugin directory
741
884
  if (fs.existsSync(pluginDir)) {
742
885
  fs.rmSync(pluginDir, { recursive: true });
743
886
  }
744
887
 
745
- // 2. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
888
+ // 3. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
746
889
  const agentsSkillsDir = path.resolve(projectRoot, ".agents/skills");
747
890
  for (const skill of SKILL_NAMES) {
748
891
  const skillDir = path.join(agentsSkillsDir, skill);
@@ -759,7 +902,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
759
902
  }
760
903
  }
761
904
 
762
- // 3. Remove agent instruction snippet from all known agent files
905
+ // 4. Remove agent instruction snippet from all known agent files
763
906
  const allAgentFiles = AGENT_IDS
764
907
  .map((id) => AGENTS[id].agentFile)
765
908
  .filter((f): f is string => f !== null);
@@ -782,7 +925,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
782
925
  }
783
926
  }
784
927
 
785
- // 4. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
928
+ // 5. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
786
929
  const claudeMdPath = path.resolve(projectRoot, "CLAUDE.md");
787
930
  if (fs.existsSync(claudeMdPath)) {
788
931
  let content = fs.readFileSync(claudeMdPath, "utf-8");
@@ -792,9 +935,6 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
792
935
 
793
936
  s.stop("Removed typegraph-mcp");
794
937
 
795
- // 5. Deregister MCP server from agent config files
796
- deregisterMcpServers(projectRoot);
797
-
798
938
  p.outro("typegraph-mcp has been uninstalled from this project.");
799
939
  }
800
940
 
@@ -808,39 +948,35 @@ async function setupAgentInstructions(projectRoot: string, selectedAgents: Agent
808
948
  return; // No agents with instruction files selected (e.g. Cursor only)
809
949
  }
810
950
 
811
- // Find existing files, resolve symlinks to deduplicate
951
+ // Ensure each selected agent file exists and has the snippet once. Resolve
952
+ // symlinks to avoid writing duplicate content through multiple aliases.
812
953
  const seenRealPaths = new Map<string, string>(); // realPath -> first agentFile name
813
- const existingFiles: { file: string; realPath: string; hasSnippet: boolean }[] = [];
814
954
  for (const agentFile of agentFiles) {
815
955
  const filePath = path.resolve(projectRoot, agentFile);
816
- if (!fs.existsSync(filePath)) continue;
817
- const realPath = fs.realpathSync(filePath);
956
+ if (!fs.existsSync(filePath)) {
957
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
958
+ fs.writeFileSync(filePath, AGENT_SNIPPET + "\n");
959
+ p.log.success(`${agentFile}: created with typegraph-mcp instructions`);
960
+ continue;
961
+ }
818
962
 
963
+ const realPath = fs.realpathSync(filePath);
819
964
  const previousFile = seenRealPaths.get(realPath);
820
965
  if (previousFile) {
821
966
  p.log.info(`${agentFile}: same file as ${previousFile} (skipped)`);
822
967
  continue;
823
968
  }
824
- seenRealPaths.set(realPath, agentFile);
825
- const content = fs.readFileSync(filePath, "utf-8");
826
- existingFiles.push({ file: agentFile, realPath, hasSnippet: content.includes(SNIPPET_MARKER) });
827
- }
828
969
 
829
- if (existingFiles.length === 0) {
830
- p.log.warn(`No agent instruction files found (${agentFiles.join(", ")})`);
831
- p.note(AGENT_SNIPPET, "Add this snippet to your agent instructions file");
832
- } else if (existingFiles.some((f) => f.hasSnippet)) {
833
- for (const f of existingFiles) {
834
- if (f.hasSnippet) {
835
- p.log.info(`${f.file}: already has typegraph-mcp instructions`);
836
- }
970
+ seenRealPaths.set(realPath, agentFile);
971
+ const content = fs.readFileSync(realPath, "utf-8");
972
+ if (content.includes(SNIPPET_MARKER)) {
973
+ p.log.info(`${agentFile}: already has typegraph-mcp instructions`);
974
+ continue;
837
975
  }
838
- } else {
839
- const target = existingFiles[0]!;
840
- const content = fs.readFileSync(target.realPath, "utf-8");
976
+
841
977
  const appendContent = (content.endsWith("\n") ? "" : "\n") + "\n" + AGENT_SNIPPET;
842
- fs.appendFileSync(target.realPath, appendContent);
843
- p.log.success(`${target.file}: appended typegraph-mcp instructions`);
978
+ fs.appendFileSync(realPath, appendContent);
979
+ p.log.success(`${agentFile}: appended typegraph-mcp instructions`);
844
980
  }
845
981
 
846
982
  // Update --plugin-dir line in CLAUDE.md if Claude Code is selected
@@ -902,6 +1038,7 @@ async function runVerification(pluginDir: string, selectedAgents: AgentId[]): Pr
902
1038
  async function remove(yes: boolean): Promise<void> {
903
1039
  const projectRoot = process.cwd();
904
1040
  const pluginDir = path.resolve(projectRoot, PLUGIN_DIR_NAME);
1041
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
905
1042
 
906
1043
  process.stdout.write("\x1Bc");
907
1044
  p.intro("TypeGraph MCP Remove");
@@ -919,7 +1056,33 @@ async function remove(yes: boolean): Promise<void> {
919
1056
  }
920
1057
  }
921
1058
 
922
- await removePlugin(projectRoot, pluginDir);
1059
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot);
1060
+ let removeGlobalCodex = cleanGlobalCodex;
1061
+
1062
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes) {
1063
+ const shouldRemoveGlobal = await p.confirm({
1064
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
1065
+ initialValue: false,
1066
+ });
1067
+ if (p.isCancel(shouldRemoveGlobal)) {
1068
+ p.cancel("Removal cancelled.");
1069
+ process.exit(0);
1070
+ }
1071
+ removeGlobalCodex = shouldRemoveGlobal;
1072
+ }
1073
+
1074
+ await removePlugin(projectRoot, pluginDir, {
1075
+ removeGlobalCodex,
1076
+ legacyGlobalCodexCleanup,
1077
+ });
1078
+
1079
+ if (legacyGlobalCodexCleanup && !removeGlobalCodex) {
1080
+ p.log.warn(
1081
+ "Left a stale global Codex MCP entry for this project in ~/.codex/config.toml. " +
1082
+ "Codex may show MCP startup warnings or errors until you remove it. " +
1083
+ "Re-run `typegraph-mcp remove --clean-global-codex` or remove the `typegraph` block manually."
1084
+ );
1085
+ }
923
1086
  }
924
1087
 
925
1088
  // ─── Check Command ───────────────────────────────────────────────────────────
package/dist/check.js CHANGED
@@ -440,8 +440,11 @@ function readProjectCodexConfig(projectRoot) {
440
440
  function hasCodexTypegraphRegistration(content) {
441
441
  return /\[mcp_servers\.typegraph\]/.test(content);
442
442
  }
443
+ function hasCodexTsxLauncher(content) {
444
+ return /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) || /args\s*=\s*\[[\s\S]*"tsx"/.test(content);
445
+ }
443
446
  function hasCompleteCodexTypegraphRegistration(content) {
444
- return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*"tsx"/.test(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
447
+ return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*\]/.test(content) && hasCodexTsxLauncher(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
445
448
  }
446
449
  function hasTrustedCodexProject(projectRoot) {
447
450
  const home = process.env.HOME;
@@ -584,7 +587,8 @@ async function main(configOverride) {
584
587
  const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
585
588
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
586
589
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
587
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
590
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
591
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
588
592
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
589
593
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
590
594
  if (hasProjectCodexRegistration) {
@@ -600,7 +604,8 @@ async function main(configOverride) {
600
604
  const issues = [];
601
605
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
602
606
  if (!hasCommand) issues.push("command is missing");
603
- if (!hasArgs) issues.push("args should include 'tsx'");
607
+ if (!hasArgs) issues.push("args are missing");
608
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
604
609
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
605
610
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
606
611
  fail(
package/dist/cli.js CHANGED
@@ -447,8 +447,11 @@ function readProjectCodexConfig(projectRoot3) {
447
447
  function hasCodexTypegraphRegistration(content) {
448
448
  return /\[mcp_servers\.typegraph\]/.test(content);
449
449
  }
450
+ function hasCodexTsxLauncher(content) {
451
+ return /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) || /args\s*=\s*\[[\s\S]*"tsx"/.test(content);
452
+ }
450
453
  function hasCompleteCodexTypegraphRegistration(content) {
451
- return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*"tsx"/.test(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
454
+ return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*\]/.test(content) && hasCodexTsxLauncher(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
452
455
  }
453
456
  function hasTrustedCodexProject(projectRoot3) {
454
457
  const home = process.env.HOME;
@@ -526,9 +529,9 @@ async function main(configOverride) {
526
529
  console.log(` Fix: ${fix}`);
527
530
  failed++;
528
531
  }
529
- function warn(msg, note2) {
532
+ function warn(msg, note) {
530
533
  console.log(` ! ${msg}`);
531
- console.log(` ${note2}`);
534
+ console.log(` ${note}`);
532
535
  warned++;
533
536
  }
534
537
  function skip(msg) {
@@ -591,7 +594,8 @@ async function main(configOverride) {
591
594
  const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
592
595
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
593
596
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
594
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
597
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
598
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
595
599
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
596
600
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
597
601
  if (hasProjectCodexRegistration) {
@@ -607,7 +611,8 @@ async function main(configOverride) {
607
611
  const issues = [];
608
612
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
609
613
  if (!hasCommand) issues.push("command is missing");
610
- if (!hasArgs) issues.push("args should include 'tsx'");
614
+ if (!hasArgs) issues.push("args are missing");
615
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
611
616
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
612
617
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
613
618
  fail(
@@ -2915,10 +2920,25 @@ import * as p from "@clack/prompts";
2915
2920
  var AGENT_SNIPPET = `
2916
2921
  ## TypeScript Navigation (typegraph-mcp)
2917
2922
 
2918
- Use the \`ts_*\` MCP tools instead of grep/glob for navigating TypeScript code. They resolve through barrel files, re-exports, and project references \u2014 returning precise results, not string matches.
2923
+ Where suitable, use the \`ts_*\` MCP tools instead of grep/glob for navigating TypeScript code. They resolve through barrel files, re-exports, and project references and return semantic results instead of string matches.
2924
+
2925
+ - Point queries: \`ts_find_symbol\`, \`ts_definition\`, \`ts_references\`, \`ts_type_info\`, \`ts_navigate_to\`, \`ts_trace_chain\`, \`ts_blast_radius\`, \`ts_module_exports\`
2926
+ - Graph queries: \`ts_dependency_tree\`, \`ts_dependents\`, \`ts_import_cycles\`, \`ts_shortest_path\`, \`ts_subgraph\`, \`ts_module_boundary\`
2927
+
2928
+ Start with the navigation tools before reading entire files. Use direct file reads only after the MCP tools identify the exact symbols or lines that matter.
2929
+
2930
+ Use \`rg\` or \`grep\` when semantic symbol navigation is not the right tool, especially for:
2919
2931
 
2920
- - **Point queries** (tsserver): \`ts_find_symbol\`, \`ts_definition\`, \`ts_references\`, \`ts_type_info\`, \`ts_navigate_to\`, \`ts_trace_chain\`, \`ts_blast_radius\`, \`ts_module_exports\`
2921
- - **Graph queries** (import graph): \`ts_dependency_tree\`, \`ts_dependents\`, \`ts_import_cycles\`, \`ts_shortest_path\`, \`ts_subgraph\`, \`ts_module_boundary\`
2932
+ - docs, config, SQL, migrations, JSON, env vars, route strings, and other non-TypeScript assets
2933
+ - broad text discovery when you do not yet know the symbol name
2934
+ - exact string matching across the repo
2935
+ - validating wording or finding repeated plan/document references
2936
+
2937
+ Practical rule:
2938
+
2939
+ - use \`ts_*\` first for TypeScript symbol definition, references, types, and dependency analysis
2940
+ - use \`rg\`/\`grep\` for text search and non-TypeScript exploration
2941
+ - combine both when a task spans TypeScript code and surrounding docs/config
2922
2942
  `.trimStart();
2923
2943
  var SNIPPET_MARKER = "## TypeScript Navigation (typegraph-mcp)";
2924
2944
  var PLUGIN_DIR_NAME = "plugins/typegraph-mcp";
@@ -3009,8 +3029,9 @@ Commands:
3009
3029
  start Start the MCP server (stdin/stdout)
3010
3030
 
3011
3031
  Options:
3012
- --yes Skip confirmation prompts (accept all defaults)
3013
- --help Show this help
3032
+ --yes Skip confirmation prompts (accept all defaults)
3033
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
3034
+ --help Show this help
3014
3035
  `.trim();
3015
3036
  function copyFile(src, dest) {
3016
3037
  const destDir = path9.dirname(dest);
@@ -3030,10 +3051,10 @@ var MCP_SERVER_ENTRY = {
3030
3051
  TYPEGRAPH_TSCONFIG: "./tsconfig.json"
3031
3052
  }
3032
3053
  };
3033
- function getAbsoluteMcpServerEntry(projectRoot3) {
3054
+ function getCodexMcpServerEntry(projectRoot3) {
3034
3055
  return {
3035
- command: "npx",
3036
- args: ["tsx", path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "server.ts")],
3056
+ command: path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "node_modules/.bin/tsx"),
3057
+ args: [path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "server.ts")],
3037
3058
  env: {
3038
3059
  TYPEGRAPH_PROJECT_ROOT: projectRoot3,
3039
3060
  TYPEGRAPH_TSCONFIG: path9.resolve(projectRoot3, "tsconfig.json")
@@ -3043,8 +3064,45 @@ function getAbsoluteMcpServerEntry(projectRoot3) {
3043
3064
  function getCodexConfigPath(projectRoot3) {
3044
3065
  return path9.resolve(projectRoot3, ".codex/config.toml");
3045
3066
  }
3046
- function hasCodexMcpSection(content) {
3047
- return content.includes("[mcp_servers.typegraph]");
3067
+ function isTomlSectionGroup(sectionName, prefix) {
3068
+ return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
3069
+ }
3070
+ function splitTomlBlocks(content) {
3071
+ const lines = content.split(/\r?\n/);
3072
+ const blocks = [];
3073
+ let sectionName = null;
3074
+ let currentLines = [];
3075
+ for (const line of lines) {
3076
+ const match = line.match(/^\[([^\]]+)\]\s*$/);
3077
+ if (match) {
3078
+ if (currentLines.length > 0 || sectionName !== null) {
3079
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
3080
+ }
3081
+ sectionName = match[1];
3082
+ currentLines = [line];
3083
+ continue;
3084
+ }
3085
+ currentLines.push(line);
3086
+ }
3087
+ if (currentLines.length > 0 || sectionName !== null) {
3088
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
3089
+ }
3090
+ return blocks;
3091
+ }
3092
+ function removeTomlSectionGroup(content, prefix) {
3093
+ const blocks = splitTomlBlocks(content);
3094
+ const removedBlocks = blocks.filter((block) => isTomlSectionGroup(block.sectionName, prefix));
3095
+ if (removedBlocks.length === 0) {
3096
+ return { content, removed: false, removedContent: "" };
3097
+ }
3098
+ const keptBlocks = blocks.filter((block) => !isTomlSectionGroup(block.sectionName, prefix));
3099
+ const nextContent = keptBlocks.map((block) => block.raw).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
3100
+ return {
3101
+ content: nextContent ? `${nextContent}
3102
+ ` : "",
3103
+ removed: true,
3104
+ removedContent: removedBlocks.map((block) => block.raw).join("\n").trim()
3105
+ };
3048
3106
  }
3049
3107
  function upsertCodexMcpSection(content, block) {
3050
3108
  const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
@@ -3063,12 +3121,13 @@ ${normalizedBlock}
3063
3121
  return { content: nextContent, changed: true };
3064
3122
  }
3065
3123
  function makeCodexMcpBlock(projectRoot3) {
3066
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
3124
+ const absoluteEntry = getCodexMcpServerEntry(projectRoot3);
3125
+ const args2 = absoluteEntry.args.map((arg) => `"${arg}"`).join(", ");
3067
3126
  return [
3068
3127
  "",
3069
3128
  "[mcp_servers.typegraph]",
3070
3129
  `command = "${absoluteEntry.command}"`,
3071
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
3130
+ `args = [${args2}]`,
3072
3131
  `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
3073
3132
  ""
3074
3133
  ].join("\n");
@@ -3103,6 +3162,46 @@ function isCodexProjectTrusted(projectRoot3) {
3103
3162
  }
3104
3163
  return matchesTrustedProject();
3105
3164
  }
3165
+ function pathEqualsOrContains(candidatePath, targetPath) {
3166
+ const resolvedCandidate = path9.resolve(candidatePath);
3167
+ const resolvedTarget = path9.resolve(targetPath);
3168
+ if (resolvedCandidate === resolvedTarget || resolvedCandidate.startsWith(`${resolvedTarget}${path9.sep}`)) {
3169
+ return true;
3170
+ }
3171
+ try {
3172
+ const realCandidate = fs8.realpathSync(candidatePath);
3173
+ const realTarget = fs8.realpathSync(targetPath);
3174
+ return realCandidate === realTarget || realCandidate.startsWith(`${realTarget}${path9.sep}`);
3175
+ } catch {
3176
+ return false;
3177
+ }
3178
+ }
3179
+ function findLegacyGlobalCodexCleanup(projectRoot3) {
3180
+ const home = process.env.HOME;
3181
+ if (!home) return null;
3182
+ const globalConfigPath = path9.join(home, ".codex/config.toml");
3183
+ if (!fs8.existsSync(globalConfigPath)) return null;
3184
+ const content = fs8.readFileSync(globalConfigPath, "utf-8");
3185
+ const { content: nextContent, removed, removedContent } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
3186
+ if (!removed) return null;
3187
+ const pluginRoot = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3188
+ const quotedPaths = Array.from(removedContent.matchAll(/"([^"\n]+)"/g), (match) => match[1]);
3189
+ const looksProjectSpecific = quotedPaths.some(
3190
+ (quotedPath) => pathEqualsOrContains(quotedPath, projectRoot3) || pathEqualsOrContains(quotedPath, pluginRoot)
3191
+ );
3192
+ if (!looksProjectSpecific) {
3193
+ return null;
3194
+ }
3195
+ return { globalConfigPath, nextContent };
3196
+ }
3197
+ function removeLegacyGlobalCodexMcp(cleanup) {
3198
+ if (cleanup.nextContent === "") {
3199
+ fs8.unlinkSync(cleanup.globalConfigPath);
3200
+ } else {
3201
+ fs8.writeFileSync(cleanup.globalConfigPath, cleanup.nextContent);
3202
+ }
3203
+ p.log.info("~/.codex/config.toml: removed stale global typegraph MCP server entry for this project");
3204
+ }
3106
3205
  function registerMcpServers(projectRoot3, selectedAgents) {
3107
3206
  if (selectedAgents.includes("cursor")) {
3108
3207
  registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
@@ -3190,20 +3289,18 @@ function registerCodexMcp(projectRoot3) {
3190
3289
  function deregisterCodexMcp(projectRoot3) {
3191
3290
  const configPath = ".codex/config.toml";
3192
3291
  const fullPath = getCodexConfigPath(projectRoot3);
3193
- if (!fs8.existsSync(fullPath)) return;
3194
- let content = fs8.readFileSync(fullPath, "utf-8");
3195
- if (!hasCodexMcpSection(content)) return;
3196
- content = content.replace(
3197
- /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
3198
- ""
3199
- );
3200
- content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
3201
- if (content.trim() === "") {
3202
- fs8.unlinkSync(fullPath);
3203
- } else {
3204
- fs8.writeFileSync(fullPath, content);
3292
+ if (fs8.existsSync(fullPath)) {
3293
+ const content = fs8.readFileSync(fullPath, "utf-8");
3294
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
3295
+ if (removed) {
3296
+ if (nextContent === "") {
3297
+ fs8.unlinkSync(fullPath);
3298
+ } else {
3299
+ fs8.writeFileSync(fullPath, nextContent);
3300
+ }
3301
+ p.log.info(`${configPath}: removed typegraph MCP server`);
3302
+ }
3205
3303
  }
3206
- p.log.info(`${configPath}: removed typegraph MCP server`);
3207
3304
  }
3208
3305
  function ensureTsconfigExclude(projectRoot3) {
3209
3306
  const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
@@ -3445,9 +3542,13 @@ async function setup(yes2) {
3445
3542
  ensureEslintIgnore(projectRoot3);
3446
3543
  await runVerification(targetDir, selectedAgents);
3447
3544
  }
3448
- async function removePlugin(projectRoot3, pluginDir) {
3545
+ async function removePlugin(projectRoot3, pluginDir, options) {
3449
3546
  const s = p.spinner();
3450
3547
  s.start("Removing typegraph-mcp...");
3548
+ deregisterMcpServers(projectRoot3);
3549
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
3550
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
3551
+ }
3451
3552
  if (fs8.existsSync(pluginDir)) {
3452
3553
  fs8.rmSync(pluginDir, { recursive: true });
3453
3554
  }
@@ -3487,7 +3588,6 @@ async function removePlugin(projectRoot3, pluginDir) {
3487
3588
  fs8.writeFileSync(claudeMdPath, content);
3488
3589
  }
3489
3590
  s.stop("Removed typegraph-mcp");
3490
- deregisterMcpServers(projectRoot3);
3491
3591
  p.outro("typegraph-mcp has been uninstalled from this project.");
3492
3592
  }
3493
3593
  async function setupAgentInstructions(projectRoot3, selectedAgents) {
@@ -3496,10 +3596,14 @@ async function setupAgentInstructions(projectRoot3, selectedAgents) {
3496
3596
  return;
3497
3597
  }
3498
3598
  const seenRealPaths = /* @__PURE__ */ new Map();
3499
- const existingFiles = [];
3500
3599
  for (const agentFile of agentFiles) {
3501
3600
  const filePath = path9.resolve(projectRoot3, agentFile);
3502
- if (!fs8.existsSync(filePath)) continue;
3601
+ if (!fs8.existsSync(filePath)) {
3602
+ fs8.mkdirSync(path9.dirname(filePath), { recursive: true });
3603
+ fs8.writeFileSync(filePath, AGENT_SNIPPET + "\n");
3604
+ p.log.success(`${agentFile}: created with typegraph-mcp instructions`);
3605
+ continue;
3606
+ }
3503
3607
  const realPath = fs8.realpathSync(filePath);
3504
3608
  const previousFile = seenRealPaths.get(realPath);
3505
3609
  if (previousFile) {
@@ -3507,24 +3611,14 @@ async function setupAgentInstructions(projectRoot3, selectedAgents) {
3507
3611
  continue;
3508
3612
  }
3509
3613
  seenRealPaths.set(realPath, agentFile);
3510
- const content = fs8.readFileSync(filePath, "utf-8");
3511
- existingFiles.push({ file: agentFile, realPath, hasSnippet: content.includes(SNIPPET_MARKER) });
3512
- }
3513
- if (existingFiles.length === 0) {
3514
- p.log.warn(`No agent instruction files found (${agentFiles.join(", ")})`);
3515
- p.note(AGENT_SNIPPET, "Add this snippet to your agent instructions file");
3516
- } else if (existingFiles.some((f) => f.hasSnippet)) {
3517
- for (const f of existingFiles) {
3518
- if (f.hasSnippet) {
3519
- p.log.info(`${f.file}: already has typegraph-mcp instructions`);
3520
- }
3614
+ const content = fs8.readFileSync(realPath, "utf-8");
3615
+ if (content.includes(SNIPPET_MARKER)) {
3616
+ p.log.info(`${agentFile}: already has typegraph-mcp instructions`);
3617
+ continue;
3521
3618
  }
3522
- } else {
3523
- const target = existingFiles[0];
3524
- const content = fs8.readFileSync(target.realPath, "utf-8");
3525
3619
  const appendContent = (content.endsWith("\n") ? "" : "\n") + "\n" + AGENT_SNIPPET;
3526
- fs8.appendFileSync(target.realPath, appendContent);
3527
- p.log.success(`${target.file}: appended typegraph-mcp instructions`);
3620
+ fs8.appendFileSync(realPath, appendContent);
3621
+ p.log.success(`${agentFile}: appended typegraph-mcp instructions`);
3528
3622
  }
3529
3623
  if (selectedAgents.includes("claude-code")) {
3530
3624
  const claudeMdPath = path9.resolve(projectRoot3, "CLAUDE.md");
@@ -3573,6 +3667,7 @@ async function runVerification(pluginDir, selectedAgents) {
3573
3667
  async function remove(yes2) {
3574
3668
  const projectRoot3 = process.cwd();
3575
3669
  const pluginDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3670
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
3576
3671
  process.stdout.write("\x1Bc");
3577
3672
  p.intro("TypeGraph MCP Remove");
3578
3673
  if (!fs8.existsSync(pluginDir)) {
@@ -3586,7 +3681,28 @@ async function remove(yes2) {
3586
3681
  process.exit(0);
3587
3682
  }
3588
3683
  }
3589
- await removePlugin(projectRoot3, pluginDir);
3684
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot3);
3685
+ let removeGlobalCodex = cleanGlobalCodex;
3686
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes2) {
3687
+ const shouldRemoveGlobal = await p.confirm({
3688
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
3689
+ initialValue: false
3690
+ });
3691
+ if (p.isCancel(shouldRemoveGlobal)) {
3692
+ p.cancel("Removal cancelled.");
3693
+ process.exit(0);
3694
+ }
3695
+ removeGlobalCodex = shouldRemoveGlobal;
3696
+ }
3697
+ await removePlugin(projectRoot3, pluginDir, {
3698
+ removeGlobalCodex,
3699
+ legacyGlobalCodexCleanup
3700
+ });
3701
+ if (legacyGlobalCodexCleanup && !removeGlobalCodex) {
3702
+ p.log.warn(
3703
+ "Left a stale global Codex MCP entry for this project in ~/.codex/config.toml. Codex may show MCP startup warnings or errors until you remove it. Re-run `typegraph-mcp remove --clean-global-codex` or remove the `typegraph` block manually."
3704
+ );
3705
+ }
3590
3706
  }
3591
3707
  function resolvePluginDir() {
3592
3708
  const installed = path9.resolve(process.cwd(), PLUGIN_DIR_NAME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typegraph-mcp",
3
- "version": "0.9.33",
3
+ "version": "0.9.35",
4
4
  "description": "Type-aware codebase navigation for AI coding agents — 14 MCP tools powered by tsserver + oxc",
5
5
  "license": "MIT",
6
6
  "type": "module",