typegraph-mcp 0.9.31 → 0.9.33

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
@@ -80,13 +80,13 @@ This gives you 14 MCP tools, 5 workflow skills that teach Claude *when* and *how
80
80
 
81
81
  **Other agents** (Cursor, Codex CLI, Gemini CLI, GitHub Copilot) — restart your agent session. The MCP server and skills are already configured.
82
82
 
83
- For **Codex CLI**, setup now registers the server with `codex mcp add` using absolute paths so the tools work even when Codex launches from outside your project root.
83
+ For **Codex CLI**, setup now writes a project-local `.codex/config.toml` entry using absolute paths so the tools stay scoped to the project and still work when Codex launches from a subdirectory.
84
84
 
85
85
  First query takes ~2s (tsserver warmup). Subsequent queries: 1-60ms.
86
86
 
87
87
  ## Requirements
88
88
 
89
- - **Node.js** >= 18
89
+ - **Node.js** >= 22
90
90
  - **TypeScript** >= 5.0 in the target project
91
91
  - **npm** for dependency installation
92
92
 
@@ -141,30 +141,31 @@ npx typegraph-mcp check
141
141
 
142
142
  | Symptom | Fix |
143
143
  |---|---|
144
- | Server won't start | `cd plugins/typegraph-mcp && npm install` |
145
- | "TypeScript not found" | Add `typescript` to devDependencies |
144
+ | Server won't start | `cd plugins/typegraph-mcp && npm install --include=optional` |
145
+ | "TypeScript not found" | Run `pnpm install` or `npm install`; if TypeScript is not declared, add it to devDependencies first |
146
146
  | Tools return empty results | Check `TYPEGRAPH_TSCONFIG` points to the right tsconfig |
147
147
  | Build errors from plugins/ | Add `"plugins/**"` to tsconfig.json `exclude` array |
148
+ | `@esbuild/*` or `@rollup/*` package missing | Reinstall with Node 22: `npm install --include=optional` |
148
149
  | "npm warn Unknown project config" | Safe to ignore — caused by pnpm settings in your `.npmrc` that npm doesn't recognize |
149
150
 
150
151
  ## Manual MCP configuration
151
152
 
152
153
  ### Codex CLI
153
154
 
154
- Register the server with absolute paths:
155
+ Add this to your project's `.codex/config.toml`:
155
156
 
156
- ```bash
157
- codex mcp add typegraph \
158
- --env TYPEGRAPH_PROJECT_ROOT=/absolute/path/to/your-project \
159
- --env TYPEGRAPH_TSCONFIG=/absolute/path/to/your-project/tsconfig.json \
160
- -- npx tsx /absolute/path/to/your-project/plugins/typegraph-mcp/server.ts
157
+ ```toml
158
+ [mcp_servers.typegraph]
159
+ command = "npx"
160
+ args = ["tsx", "/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
161
+ env = { TYPEGRAPH_PROJECT_ROOT = "/absolute/path/to/your-project", TYPEGRAPH_TSCONFIG = "/absolute/path/to/your-project/tsconfig.json" }
161
162
  ```
162
163
 
163
- Verify with:
164
+ Codex only loads project `.codex/config.toml` files for trusted projects. If needed, add this to `~/.codex/config.toml`:
164
165
 
165
- ```bash
166
- codex mcp get typegraph
167
- codex mcp list
166
+ ```toml
167
+ [projects."/absolute/path/to/your-project"]
168
+ trust_level = "trusted"
168
169
  ```
169
170
 
170
171
  ### JSON-based MCP clients
@@ -213,7 +214,8 @@ Two subsystems start concurrently:
213
214
  ```bash
214
215
  git clone https://github.com/guyowen/typegraph-mcp.git
215
216
  cd typegraph-mcp
216
- npm install
217
+ nvm use
218
+ npm install --include=optional
217
219
  ```
218
220
 
219
221
  ### Run locally against a project
package/check.ts CHANGED
@@ -102,6 +102,105 @@ function testTsserver(projectRoot: string): Promise<boolean> {
102
102
  });
103
103
  }
104
104
 
105
+ function readProjectCodexConfig(projectRoot: string): string | null {
106
+ const configPath = path.resolve(projectRoot, ".codex/config.toml");
107
+ if (!fs.existsSync(configPath)) return null;
108
+ return fs.readFileSync(configPath, "utf-8");
109
+ }
110
+
111
+ function hasCodexTypegraphRegistration(content: string): boolean {
112
+ return /\[mcp_servers\.typegraph\]/.test(content);
113
+ }
114
+
115
+ function hasCompleteCodexTypegraphRegistration(content: string): boolean {
116
+ return (
117
+ hasCodexTypegraphRegistration(content) &&
118
+ /command\s*=\s*"[^"]+"/.test(content) &&
119
+ /args\s*=\s*\[[\s\S]*"tsx"/.test(content) &&
120
+ /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) &&
121
+ /TYPEGRAPH_TSCONFIG\s*=/.test(content)
122
+ );
123
+ }
124
+
125
+ function hasTrustedCodexProject(projectRoot: string): boolean | null {
126
+ const home = process.env.HOME;
127
+ if (!home) return null;
128
+
129
+ const globalConfigPath = path.join(home, ".codex/config.toml");
130
+ if (!fs.existsSync(globalConfigPath)) return null;
131
+
132
+ const lines = fs.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
133
+ let currentProject: string | null = null;
134
+ let currentTrusted = false;
135
+
136
+ const matchesTrustedProject = (): boolean =>
137
+ currentProject !== null &&
138
+ currentTrusted &&
139
+ (projectRoot === currentProject || projectRoot.startsWith(currentProject + path.sep));
140
+
141
+ for (const line of lines) {
142
+ const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
143
+ if (sectionMatch) {
144
+ if (matchesTrustedProject()) return true;
145
+ currentProject = path.resolve(sectionMatch[1]!);
146
+ currentTrusted = false;
147
+ continue;
148
+ }
149
+
150
+ if (line.startsWith("[")) {
151
+ if (matchesTrustedProject()) return true;
152
+ currentProject = null;
153
+ currentTrusted = false;
154
+ continue;
155
+ }
156
+
157
+ if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
158
+ currentTrusted = true;
159
+ }
160
+ }
161
+
162
+ return matchesTrustedProject();
163
+ }
164
+
165
+ function readProjectPackageJson(projectRoot: string): Record<string, unknown> | null {
166
+ const packageJsonPath = path.resolve(projectRoot, "package.json");
167
+ if (!fs.existsSync(packageJsonPath)) return null;
168
+
169
+ try {
170
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as Record<string, unknown>;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ function getProjectInstallCommand(projectRoot: string, packageJson: Record<string, unknown> | null): string {
177
+ const packageManager = typeof packageJson?.["packageManager"] === "string"
178
+ ? packageJson["packageManager"]
179
+ : "";
180
+
181
+ if (packageManager.startsWith("pnpm@") || fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
182
+ return "pnpm install";
183
+ }
184
+ if (packageManager.startsWith("yarn@") || fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
185
+ return "yarn install";
186
+ }
187
+ return "npm install";
188
+ }
189
+
190
+ function hasDeclaredDependency(packageJson: Record<string, unknown> | null, packageName: string): boolean {
191
+ const depKeys = [
192
+ "dependencies",
193
+ "devDependencies",
194
+ "peerDependencies",
195
+ "optionalDependencies",
196
+ ] as const;
197
+
198
+ return depKeys.some((key) => {
199
+ const deps = packageJson?.[key];
200
+ return typeof deps === "object" && deps !== null && packageName in deps;
201
+ });
202
+ }
203
+
105
204
  // ─── Main ────────────────────────────────────────────────────────────────────
106
205
 
107
206
  export async function main(configOverride?: TypegraphConfig): Promise<CheckResult> {
@@ -111,6 +210,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
111
210
  let passed = 0;
112
211
  let failed = 0;
113
212
  let warned = 0;
213
+ const projectPackageJson = readProjectPackageJson(projectRoot);
214
+ const installCommand = getProjectInstallCommand(projectRoot, projectPackageJson);
114
215
 
115
216
  function pass(msg: string): void {
116
217
  console.log(` \u2713 ${msg}`);
@@ -142,10 +243,10 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
142
243
  // 1. Node.js version
143
244
  const nodeVersion = process.version;
144
245
  const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]!, 10);
145
- if (nodeMajor >= 18) {
146
- pass(`Node.js ${nodeVersion} (>= 18 required)`);
246
+ if (nodeMajor >= 22) {
247
+ pass(`Node.js ${nodeVersion} (>= 22 required)`);
147
248
  } else {
148
- fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
249
+ fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
149
250
  }
150
251
 
151
252
  // 2. tsx availability (if we're running, tsx works — but check it's in the project)
@@ -168,9 +269,14 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
168
269
  tsVersion = tsPkg.version;
169
270
  pass(`TypeScript found (v${tsVersion})`);
170
271
  } catch {
272
+ const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
171
273
  fail(
172
- "TypeScript not found in project",
173
- "Add `typescript` to devDependencies and run `npm install`"
274
+ hasDeclaredTs
275
+ ? "TypeScript is declared but not installed in project"
276
+ : "TypeScript not found in project",
277
+ hasDeclaredTs
278
+ ? `Run \`${installCommand}\` to install project dependencies`
279
+ : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
174
280
  );
175
281
  }
176
282
 
@@ -186,18 +292,50 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
186
292
  // Check for plugin .mcp.json in the tool directory (embedded plugin install)
187
293
  const pluginMcpPath = path.join(toolDir, ".mcp.json");
188
294
  const hasPluginMcp = fs.existsSync(pluginMcpPath) && fs.existsSync(path.join(toolDir, ".claude-plugin/plugin.json"));
295
+ const projectCodexConfig = readProjectCodexConfig(projectRoot);
296
+ const hasProjectCodexRegistration =
297
+ projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
189
298
  const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
190
299
  stdio: "pipe",
191
300
  encoding: "utf-8",
192
301
  });
193
- const hasCodexRegistration = codexGet.status === 0;
302
+ const hasGlobalCodexRegistration = codexGet.status === 0;
194
303
  if (process.env.CLAUDE_PLUGIN_ROOT) {
195
304
  pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
196
305
  } else if (hasPluginMcp) {
197
306
  pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
198
- } else if (hasCodexRegistration) {
199
- pass("MCP registered in Codex CLI");
307
+ } else if (projectCodexConfig !== null) {
308
+ const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
309
+ const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
310
+ const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
311
+ const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
312
+ const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
313
+ const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
314
+ if (hasProjectCodexRegistration) {
315
+ pass("MCP registered in project .codex/config.toml");
316
+ const trusted = hasTrustedCodexProject(projectRoot);
317
+ if (trusted === false) {
318
+ warn(
319
+ "Project Codex config may be ignored",
320
+ "Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = \"trusted\""
321
+ );
322
+ }
323
+ } else {
324
+ const issues: string[] = [];
325
+ if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
326
+ if (!hasCommand) issues.push("command is missing");
327
+ if (!hasArgs) issues.push("args should include 'tsx'");
328
+ if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
329
+ if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
330
+ fail(
331
+ `Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
332
+ `Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
333
+ );
334
+ }
335
+ } else if (hasGlobalCodexRegistration) {
336
+ pass("MCP registered in global Codex CLI config");
200
337
  } else {
338
+ const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
201
339
  const mcpJsonPath = path.resolve(projectRoot, ".claude/mcp.json");
202
340
  if (fs.existsSync(mcpJsonPath)) {
203
341
  try {
@@ -244,7 +382,10 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
244
382
  );
245
383
  }
246
384
  } else {
247
- fail(".claude/mcp.json not found", `Create .claude/mcp.json with typegraph server registration`);
385
+ fail(
386
+ "No MCP registration found",
387
+ `Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
388
+ );
248
389
  }
249
390
  }
250
391
 
package/cli.ts CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
- import { execSync, spawnSync } from "node:child_process";
18
+ import { execSync } from "node:child_process";
19
19
  import * as p from "@clack/prompts";
20
20
  import { resolveConfig } from "./config.js";
21
21
 
@@ -192,6 +192,87 @@ function getAbsoluteMcpServerEntry(projectRoot: string): {
192
192
  };
193
193
  }
194
194
 
195
+ function getCodexConfigPath(projectRoot: string): string {
196
+ return path.resolve(projectRoot, ".codex/config.toml");
197
+ }
198
+
199
+ function hasCodexMcpSection(content: string): boolean {
200
+ return content.includes("[mcp_servers.typegraph]");
201
+ }
202
+
203
+ function upsertCodexMcpSection(content: string, block: string): { content: string; changed: boolean } {
204
+ const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
205
+ const normalizedBlock = block.trim();
206
+
207
+ if (sectionRe.test(content)) {
208
+ const existingSection = (content.match(sectionRe)?.[0] ?? "").trim();
209
+ if (existingSection === normalizedBlock) {
210
+ return { content, changed: false };
211
+ }
212
+
213
+ const nextContent = content.replace(sectionRe, `\n${normalizedBlock}\n`);
214
+ return { content: nextContent.trimEnd() + "\n", changed: true };
215
+ }
216
+
217
+ const nextContent = content
218
+ ? content.trimEnd() + "\n\n" + normalizedBlock + "\n"
219
+ : normalizedBlock + "\n";
220
+ return { content: nextContent, changed: true };
221
+ }
222
+
223
+ function makeCodexMcpBlock(projectRoot: string): string {
224
+ const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
225
+ return [
226
+ "",
227
+ "[mcp_servers.typegraph]",
228
+ `command = "${absoluteEntry.command}"`,
229
+ `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
230
+ `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
231
+ "",
232
+ ].join("\n");
233
+ }
234
+
235
+ function isCodexProjectTrusted(projectRoot: string): boolean {
236
+ const home = process.env.HOME;
237
+ if (!home) return false;
238
+
239
+ const globalConfigPath = path.join(home, ".codex/config.toml");
240
+ if (!fs.existsSync(globalConfigPath)) return false;
241
+
242
+ const content = fs.readFileSync(globalConfigPath, "utf-8");
243
+ const lines = content.split(/\r?\n/);
244
+ let currentProject: string | null = null;
245
+ let currentTrusted = false;
246
+
247
+ const matchesTrustedProject = (): boolean =>
248
+ currentProject !== null &&
249
+ currentTrusted &&
250
+ (projectRoot === currentProject || projectRoot.startsWith(currentProject + path.sep));
251
+
252
+ for (const line of lines) {
253
+ const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
254
+ if (sectionMatch) {
255
+ if (matchesTrustedProject()) return true;
256
+ currentProject = path.resolve(sectionMatch[1]!);
257
+ currentTrusted = false;
258
+ continue;
259
+ }
260
+
261
+ if (line.startsWith("[")) {
262
+ if (matchesTrustedProject()) return true;
263
+ currentProject = null;
264
+ currentTrusted = false;
265
+ continue;
266
+ }
267
+
268
+ if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
269
+ currentTrusted = true;
270
+ }
271
+ }
272
+
273
+ return matchesTrustedProject();
274
+ }
275
+
195
276
  /** Register the typegraph MCP server in agent-specific config files */
196
277
  function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): void {
197
278
  if (selectedAgents.includes("cursor")) {
@@ -274,104 +355,41 @@ function deregisterJsonMcp(projectRoot: string, configPath: string, rootKey: str
274
355
 
275
356
  /** Register MCP server in Codex CLI's TOML config */
276
357
  function registerCodexMcp(projectRoot: string): void {
277
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
278
-
279
- const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
280
- stdio: "pipe",
281
- encoding: "utf-8",
282
- });
283
-
284
- if (codexGet.status === 0) {
285
- const output = `${codexGet.stdout ?? ""}${codexGet.stderr ?? ""}`;
286
- const hasServerPath = output.includes(absoluteEntry.args[1]!);
287
- const hasProjectRoot = output.includes("TYPEGRAPH_PROJECT_ROOT=*****") || output.includes(projectRoot);
288
- const hasTsconfig = output.includes("TYPEGRAPH_TSCONFIG=*****") || output.includes(path.resolve(projectRoot, "tsconfig.json"));
289
- if (hasServerPath && hasProjectRoot && hasTsconfig) {
290
- p.log.info("Codex CLI: typegraph MCP server already registered");
291
- return;
292
- }
293
- spawnSync("codex", ["mcp", "remove", "typegraph"], {
294
- stdio: "pipe",
295
- encoding: "utf-8",
296
- });
297
- }
298
-
299
- const codexAdd = spawnSync(
300
- "codex",
301
- [
302
- "mcp",
303
- "add",
304
- "typegraph",
305
- "--env",
306
- `TYPEGRAPH_PROJECT_ROOT=${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}`,
307
- "--env",
308
- `TYPEGRAPH_TSCONFIG=${absoluteEntry.env.TYPEGRAPH_TSCONFIG}`,
309
- "--",
310
- absoluteEntry.command,
311
- ...absoluteEntry.args,
312
- ],
313
- {
314
- stdio: "pipe",
315
- encoding: "utf-8",
316
- }
317
- );
318
-
319
- if (codexAdd.status === 0) {
320
- p.log.success("Codex CLI: registered typegraph MCP server");
321
- return;
322
- }
323
-
324
- p.log.warn(
325
- `Codex CLI registration failed — falling back to ${".codex/config.toml"}`
326
- );
327
-
328
358
  const configPath = ".codex/config.toml";
329
- const fullPath = path.resolve(projectRoot, configPath);
359
+ const fullPath = getCodexConfigPath(projectRoot);
360
+ const block = makeCodexMcpBlock(projectRoot);
330
361
  let content = "";
331
362
 
332
363
  if (fs.existsSync(fullPath)) {
333
364
  content = fs.readFileSync(fullPath, "utf-8");
334
- // Already registered?
335
- if (content.includes("[mcp_servers.typegraph]")) {
336
- p.log.info(`${configPath}: typegraph MCP server already registered`);
337
- return;
338
- }
339
365
  }
340
366
 
341
- const block = [
342
- "",
343
- "[mcp_servers.typegraph]",
344
- `command = "${absoluteEntry.command}"`,
345
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
346
- `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
347
- "",
348
- ].join("\n");
349
-
350
367
  const dir = path.dirname(fullPath);
351
368
  if (!fs.existsSync(dir)) {
352
369
  fs.mkdirSync(dir, { recursive: true });
353
370
  }
354
- const newContent = content ? content.trimEnd() + "\n" + block : block.trimStart();
355
- fs.writeFileSync(fullPath, newContent);
356
- p.log.success(`${configPath}: registered typegraph MCP server`);
371
+
372
+ const { content: nextContent, changed } = upsertCodexMcpSection(content, block);
373
+ if (changed) {
374
+ fs.writeFileSync(fullPath, nextContent);
375
+ p.log.success(`${configPath}: registered typegraph MCP server`);
376
+ } else {
377
+ p.log.info(`${configPath}: typegraph MCP server already registered`);
378
+ }
379
+
380
+ if (!isCodexProjectTrusted(projectRoot)) {
381
+ p.log.info(`Codex CLI: trust ${projectRoot} in ~/.codex/config.toml to load project MCP settings`);
382
+ }
357
383
  }
358
384
 
359
385
  /** Deregister MCP server from Codex CLI's TOML config */
360
386
  function deregisterCodexMcp(projectRoot: string): void {
361
- const codexRemove = spawnSync("codex", ["mcp", "remove", "typegraph"], {
362
- stdio: "pipe",
363
- encoding: "utf-8",
364
- });
365
- if (codexRemove.status === 0) {
366
- p.log.info("Codex CLI: removed typegraph MCP server");
367
- }
368
-
369
387
  const configPath = ".codex/config.toml";
370
- const fullPath = path.resolve(projectRoot, configPath);
388
+ const fullPath = getCodexConfigPath(projectRoot);
371
389
  if (!fs.existsSync(fullPath)) return;
372
390
 
373
391
  let content = fs.readFileSync(fullPath, "utf-8");
374
- if (!content.includes("[mcp_servers.typegraph]")) return;
392
+ if (!hasCodexMcpSection(content)) return;
375
393
 
376
394
  // Remove the [mcp_servers.typegraph] section (stops at next section header or end of file)
377
395
  content = content.replace(
@@ -643,12 +661,12 @@ async function setup(yes: boolean): Promise<void> {
643
661
 
644
662
  s.message("Installing dependencies...");
645
663
  try {
646
- execSync("npm install", { cwd: targetDir, stdio: "pipe" });
664
+ execSync("npm install --include=optional", { cwd: targetDir, stdio: "pipe" });
647
665
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files with dependencies`);
648
666
  } catch (err) {
649
667
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files`);
650
668
  p.log.warn(`Dependency install failed: ${err instanceof Error ? err.message : String(err)}`);
651
- p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install`);
669
+ p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install --include=optional`);
652
670
  }
653
671
 
654
672
  // 4. Copy skills to .agents/skills/ for cross-platform discovery
package/dist/check.js CHANGED
@@ -432,11 +432,84 @@ function testTsserver(projectRoot) {
432
432
  child.stdin.write(request + "\n");
433
433
  });
434
434
  }
435
+ function readProjectCodexConfig(projectRoot) {
436
+ const configPath = path3.resolve(projectRoot, ".codex/config.toml");
437
+ if (!fs2.existsSync(configPath)) return null;
438
+ return fs2.readFileSync(configPath, "utf-8");
439
+ }
440
+ function hasCodexTypegraphRegistration(content) {
441
+ return /\[mcp_servers\.typegraph\]/.test(content);
442
+ }
443
+ 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);
445
+ }
446
+ function hasTrustedCodexProject(projectRoot) {
447
+ const home = process.env.HOME;
448
+ if (!home) return null;
449
+ const globalConfigPath = path3.join(home, ".codex/config.toml");
450
+ if (!fs2.existsSync(globalConfigPath)) return null;
451
+ const lines = fs2.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
452
+ let currentProject = null;
453
+ let currentTrusted = false;
454
+ const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot === currentProject || projectRoot.startsWith(currentProject + path3.sep));
455
+ for (const line of lines) {
456
+ const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
457
+ if (sectionMatch) {
458
+ if (matchesTrustedProject()) return true;
459
+ currentProject = path3.resolve(sectionMatch[1]);
460
+ currentTrusted = false;
461
+ continue;
462
+ }
463
+ if (line.startsWith("[")) {
464
+ if (matchesTrustedProject()) return true;
465
+ currentProject = null;
466
+ currentTrusted = false;
467
+ continue;
468
+ }
469
+ if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
470
+ currentTrusted = true;
471
+ }
472
+ }
473
+ return matchesTrustedProject();
474
+ }
475
+ function readProjectPackageJson(projectRoot) {
476
+ const packageJsonPath = path3.resolve(projectRoot, "package.json");
477
+ if (!fs2.existsSync(packageJsonPath)) return null;
478
+ try {
479
+ return JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
480
+ } catch {
481
+ return null;
482
+ }
483
+ }
484
+ function getProjectInstallCommand(projectRoot, packageJson) {
485
+ const packageManager = typeof packageJson?.["packageManager"] === "string" ? packageJson["packageManager"] : "";
486
+ if (packageManager.startsWith("pnpm@") || fs2.existsSync(path3.join(projectRoot, "pnpm-lock.yaml"))) {
487
+ return "pnpm install";
488
+ }
489
+ if (packageManager.startsWith("yarn@") || fs2.existsSync(path3.join(projectRoot, "yarn.lock"))) {
490
+ return "yarn install";
491
+ }
492
+ return "npm install";
493
+ }
494
+ function hasDeclaredDependency(packageJson, packageName) {
495
+ const depKeys = [
496
+ "dependencies",
497
+ "devDependencies",
498
+ "peerDependencies",
499
+ "optionalDependencies"
500
+ ];
501
+ return depKeys.some((key) => {
502
+ const deps = packageJson?.[key];
503
+ return typeof deps === "object" && deps !== null && packageName in deps;
504
+ });
505
+ }
435
506
  async function main(configOverride) {
436
507
  const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
437
508
  let passed = 0;
438
509
  let failed = 0;
439
510
  let warned = 0;
511
+ const projectPackageJson = readProjectPackageJson(projectRoot);
512
+ const installCommand = getProjectInstallCommand(projectRoot, projectPackageJson);
440
513
  function pass(msg) {
441
514
  console.log(` \u2713 ${msg}`);
442
515
  passed++;
@@ -461,10 +534,10 @@ async function main(configOverride) {
461
534
  console.log("");
462
535
  const nodeVersion = process.version;
463
536
  const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
464
- if (nodeMajor >= 18) {
465
- pass(`Node.js ${nodeVersion} (>= 18 required)`);
537
+ if (nodeMajor >= 22) {
538
+ pass(`Node.js ${nodeVersion} (>= 22 required)`);
466
539
  } else {
467
- fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
540
+ fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
468
541
  }
469
542
  const tsxInRoot = fs2.existsSync(path3.join(projectRoot, "node_modules/.bin/tsx"));
470
543
  const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
@@ -482,9 +555,10 @@ async function main(configOverride) {
482
555
  tsVersion = tsPkg.version;
483
556
  pass(`TypeScript found (v${tsVersion})`);
484
557
  } catch {
558
+ const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
485
559
  fail(
486
- "TypeScript not found in project",
487
- "Add `typescript` to devDependencies and run `npm install`"
560
+ hasDeclaredTs ? "TypeScript is declared but not installed in project" : "TypeScript not found in project",
561
+ hasDeclaredTs ? `Run \`${installCommand}\` to install project dependencies` : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
488
562
  );
489
563
  }
490
564
  const tsconfigAbs = path3.resolve(projectRoot, tsconfigPath);
@@ -495,18 +569,49 @@ async function main(configOverride) {
495
569
  }
496
570
  const pluginMcpPath = path3.join(toolDir, ".mcp.json");
497
571
  const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
572
+ const projectCodexConfig = readProjectCodexConfig(projectRoot);
573
+ const hasProjectCodexRegistration = projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
498
574
  const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
499
575
  stdio: "pipe",
500
576
  encoding: "utf-8"
501
577
  });
502
- const hasCodexRegistration = codexGet.status === 0;
578
+ const hasGlobalCodexRegistration = codexGet.status === 0;
503
579
  if (process.env.CLAUDE_PLUGIN_ROOT) {
504
580
  pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
505
581
  } else if (hasPluginMcp) {
506
582
  pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
507
- } else if (hasCodexRegistration) {
508
- pass("MCP registered in Codex CLI");
583
+ } else if (projectCodexConfig !== null) {
584
+ const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
585
+ const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
586
+ const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
587
+ const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
588
+ const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
589
+ const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
590
+ if (hasProjectCodexRegistration) {
591
+ pass("MCP registered in project .codex/config.toml");
592
+ const trusted = hasTrustedCodexProject(projectRoot);
593
+ if (trusted === false) {
594
+ warn(
595
+ "Project Codex config may be ignored",
596
+ 'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
597
+ );
598
+ }
599
+ } else {
600
+ const issues = [];
601
+ if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
602
+ if (!hasCommand) issues.push("command is missing");
603
+ if (!hasArgs) issues.push("args should include 'tsx'");
604
+ if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
605
+ if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
606
+ fail(
607
+ `Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
608
+ `Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
609
+ );
610
+ }
611
+ } else if (hasGlobalCodexRegistration) {
612
+ pass("MCP registered in global Codex CLI config");
509
613
  } else {
614
+ const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
510
615
  const mcpJsonPath = path3.resolve(projectRoot, ".claude/mcp.json");
511
616
  if (fs2.existsSync(mcpJsonPath)) {
512
617
  try {
@@ -551,7 +656,10 @@ async function main(configOverride) {
551
656
  );
552
657
  }
553
658
  } else {
554
- fail(".claude/mcp.json not found", `Create .claude/mcp.json with typegraph server registration`);
659
+ fail(
660
+ "No MCP registration found",
661
+ `Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
662
+ );
555
663
  }
556
664
  }
557
665
  const toolNodeModules = path3.join(toolDir, "node_modules");
package/dist/cli.js CHANGED
@@ -439,11 +439,84 @@ function testTsserver(projectRoot3) {
439
439
  child.stdin.write(request + "\n");
440
440
  });
441
441
  }
442
+ function readProjectCodexConfig(projectRoot3) {
443
+ const configPath = path3.resolve(projectRoot3, ".codex/config.toml");
444
+ if (!fs2.existsSync(configPath)) return null;
445
+ return fs2.readFileSync(configPath, "utf-8");
446
+ }
447
+ function hasCodexTypegraphRegistration(content) {
448
+ return /\[mcp_servers\.typegraph\]/.test(content);
449
+ }
450
+ 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);
452
+ }
453
+ function hasTrustedCodexProject(projectRoot3) {
454
+ const home = process.env.HOME;
455
+ if (!home) return null;
456
+ const globalConfigPath = path3.join(home, ".codex/config.toml");
457
+ if (!fs2.existsSync(globalConfigPath)) return null;
458
+ const lines = fs2.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
459
+ let currentProject = null;
460
+ let currentTrusted = false;
461
+ const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot3 === currentProject || projectRoot3.startsWith(currentProject + path3.sep));
462
+ for (const line of lines) {
463
+ const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
464
+ if (sectionMatch) {
465
+ if (matchesTrustedProject()) return true;
466
+ currentProject = path3.resolve(sectionMatch[1]);
467
+ currentTrusted = false;
468
+ continue;
469
+ }
470
+ if (line.startsWith("[")) {
471
+ if (matchesTrustedProject()) return true;
472
+ currentProject = null;
473
+ currentTrusted = false;
474
+ continue;
475
+ }
476
+ if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
477
+ currentTrusted = true;
478
+ }
479
+ }
480
+ return matchesTrustedProject();
481
+ }
482
+ function readProjectPackageJson(projectRoot3) {
483
+ const packageJsonPath = path3.resolve(projectRoot3, "package.json");
484
+ if (!fs2.existsSync(packageJsonPath)) return null;
485
+ try {
486
+ return JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ function getProjectInstallCommand(projectRoot3, packageJson) {
492
+ const packageManager = typeof packageJson?.["packageManager"] === "string" ? packageJson["packageManager"] : "";
493
+ if (packageManager.startsWith("pnpm@") || fs2.existsSync(path3.join(projectRoot3, "pnpm-lock.yaml"))) {
494
+ return "pnpm install";
495
+ }
496
+ if (packageManager.startsWith("yarn@") || fs2.existsSync(path3.join(projectRoot3, "yarn.lock"))) {
497
+ return "yarn install";
498
+ }
499
+ return "npm install";
500
+ }
501
+ function hasDeclaredDependency(packageJson, packageName) {
502
+ const depKeys = [
503
+ "dependencies",
504
+ "devDependencies",
505
+ "peerDependencies",
506
+ "optionalDependencies"
507
+ ];
508
+ return depKeys.some((key) => {
509
+ const deps = packageJson?.[key];
510
+ return typeof deps === "object" && deps !== null && packageName in deps;
511
+ });
512
+ }
442
513
  async function main(configOverride) {
443
514
  const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
444
515
  let passed = 0;
445
516
  let failed = 0;
446
517
  let warned = 0;
518
+ const projectPackageJson = readProjectPackageJson(projectRoot3);
519
+ const installCommand = getProjectInstallCommand(projectRoot3, projectPackageJson);
447
520
  function pass(msg) {
448
521
  console.log(` \u2713 ${msg}`);
449
522
  passed++;
@@ -468,10 +541,10 @@ async function main(configOverride) {
468
541
  console.log("");
469
542
  const nodeVersion = process.version;
470
543
  const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
471
- if (nodeMajor >= 18) {
472
- pass(`Node.js ${nodeVersion} (>= 18 required)`);
544
+ if (nodeMajor >= 22) {
545
+ pass(`Node.js ${nodeVersion} (>= 22 required)`);
473
546
  } else {
474
- fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
547
+ fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
475
548
  }
476
549
  const tsxInRoot = fs2.existsSync(path3.join(projectRoot3, "node_modules/.bin/tsx"));
477
550
  const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
@@ -489,9 +562,10 @@ async function main(configOverride) {
489
562
  tsVersion = tsPkg.version;
490
563
  pass(`TypeScript found (v${tsVersion})`);
491
564
  } catch {
565
+ const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
492
566
  fail(
493
- "TypeScript not found in project",
494
- "Add `typescript` to devDependencies and run `npm install`"
567
+ hasDeclaredTs ? "TypeScript is declared but not installed in project" : "TypeScript not found in project",
568
+ hasDeclaredTs ? `Run \`${installCommand}\` to install project dependencies` : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
495
569
  );
496
570
  }
497
571
  const tsconfigAbs = path3.resolve(projectRoot3, tsconfigPath3);
@@ -502,18 +576,49 @@ async function main(configOverride) {
502
576
  }
503
577
  const pluginMcpPath = path3.join(toolDir, ".mcp.json");
504
578
  const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
579
+ const projectCodexConfig = readProjectCodexConfig(projectRoot3);
580
+ const hasProjectCodexRegistration = projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
505
581
  const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
506
582
  stdio: "pipe",
507
583
  encoding: "utf-8"
508
584
  });
509
- const hasCodexRegistration = codexGet.status === 0;
585
+ const hasGlobalCodexRegistration = codexGet.status === 0;
510
586
  if (process.env.CLAUDE_PLUGIN_ROOT) {
511
587
  pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
512
588
  } else if (hasPluginMcp) {
513
589
  pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
514
- } else if (hasCodexRegistration) {
515
- pass("MCP registered in Codex CLI");
590
+ } else if (projectCodexConfig !== null) {
591
+ const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
592
+ const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
593
+ const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
594
+ const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
595
+ const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
596
+ const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
597
+ if (hasProjectCodexRegistration) {
598
+ pass("MCP registered in project .codex/config.toml");
599
+ const trusted = hasTrustedCodexProject(projectRoot3);
600
+ if (trusted === false) {
601
+ warn(
602
+ "Project Codex config may be ignored",
603
+ 'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
604
+ );
605
+ }
606
+ } else {
607
+ const issues = [];
608
+ if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
609
+ if (!hasCommand) issues.push("command is missing");
610
+ if (!hasArgs) issues.push("args should include 'tsx'");
611
+ if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
612
+ if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
613
+ fail(
614
+ `Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
615
+ `Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
616
+ );
617
+ }
618
+ } else if (hasGlobalCodexRegistration) {
619
+ pass("MCP registered in global Codex CLI config");
516
620
  } else {
621
+ const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
517
622
  const mcpJsonPath = path3.resolve(projectRoot3, ".claude/mcp.json");
518
623
  if (fs2.existsSync(mcpJsonPath)) {
519
624
  try {
@@ -558,7 +663,10 @@ async function main(configOverride) {
558
663
  );
559
664
  }
560
665
  } else {
561
- fail(".claude/mcp.json not found", `Create .claude/mcp.json with typegraph server registration`);
666
+ fail(
667
+ "No MCP registration found",
668
+ `Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
669
+ );
562
670
  }
563
671
  }
564
672
  const toolNodeModules = path3.join(toolDir, "node_modules");
@@ -2802,7 +2910,7 @@ var init_server = __esm({
2802
2910
  init_config();
2803
2911
  import * as fs8 from "fs";
2804
2912
  import * as path9 from "path";
2805
- import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
2913
+ import { execSync as execSync2 } from "child_process";
2806
2914
  import * as p from "@clack/prompts";
2807
2915
  var AGENT_SNIPPET = `
2808
2916
  ## TypeScript Navigation (typegraph-mcp)
@@ -2932,6 +3040,69 @@ function getAbsoluteMcpServerEntry(projectRoot3) {
2932
3040
  }
2933
3041
  };
2934
3042
  }
3043
+ function getCodexConfigPath(projectRoot3) {
3044
+ return path9.resolve(projectRoot3, ".codex/config.toml");
3045
+ }
3046
+ function hasCodexMcpSection(content) {
3047
+ return content.includes("[mcp_servers.typegraph]");
3048
+ }
3049
+ function upsertCodexMcpSection(content, block) {
3050
+ const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
3051
+ const normalizedBlock = block.trim();
3052
+ if (sectionRe.test(content)) {
3053
+ const existingSection = (content.match(sectionRe)?.[0] ?? "").trim();
3054
+ if (existingSection === normalizedBlock) {
3055
+ return { content, changed: false };
3056
+ }
3057
+ const nextContent2 = content.replace(sectionRe, `
3058
+ ${normalizedBlock}
3059
+ `);
3060
+ return { content: nextContent2.trimEnd() + "\n", changed: true };
3061
+ }
3062
+ const nextContent = content ? content.trimEnd() + "\n\n" + normalizedBlock + "\n" : normalizedBlock + "\n";
3063
+ return { content: nextContent, changed: true };
3064
+ }
3065
+ function makeCodexMcpBlock(projectRoot3) {
3066
+ const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
3067
+ return [
3068
+ "",
3069
+ "[mcp_servers.typegraph]",
3070
+ `command = "${absoluteEntry.command}"`,
3071
+ `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
3072
+ `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
3073
+ ""
3074
+ ].join("\n");
3075
+ }
3076
+ function isCodexProjectTrusted(projectRoot3) {
3077
+ const home = process.env.HOME;
3078
+ if (!home) return false;
3079
+ const globalConfigPath = path9.join(home, ".codex/config.toml");
3080
+ if (!fs8.existsSync(globalConfigPath)) return false;
3081
+ const content = fs8.readFileSync(globalConfigPath, "utf-8");
3082
+ const lines = content.split(/\r?\n/);
3083
+ let currentProject = null;
3084
+ let currentTrusted = false;
3085
+ const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot3 === currentProject || projectRoot3.startsWith(currentProject + path9.sep));
3086
+ for (const line of lines) {
3087
+ const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
3088
+ if (sectionMatch) {
3089
+ if (matchesTrustedProject()) return true;
3090
+ currentProject = path9.resolve(sectionMatch[1]);
3091
+ currentTrusted = false;
3092
+ continue;
3093
+ }
3094
+ if (line.startsWith("[")) {
3095
+ if (matchesTrustedProject()) return true;
3096
+ currentProject = null;
3097
+ currentTrusted = false;
3098
+ continue;
3099
+ }
3100
+ if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
3101
+ currentTrusted = true;
3102
+ }
3103
+ }
3104
+ return matchesTrustedProject();
3105
+ }
2935
3106
  function registerMcpServers(projectRoot3, selectedAgents) {
2936
3107
  if (selectedAgents.includes("cursor")) {
2937
3108
  registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
@@ -2994,90 +3165,34 @@ function deregisterJsonMcp(projectRoot3, configPath, rootKey) {
2994
3165
  }
2995
3166
  }
2996
3167
  function registerCodexMcp(projectRoot3) {
2997
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
2998
- const codexGet = spawnSync2("codex", ["mcp", "get", "typegraph"], {
2999
- stdio: "pipe",
3000
- encoding: "utf-8"
3001
- });
3002
- if (codexGet.status === 0) {
3003
- const output = `${codexGet.stdout ?? ""}${codexGet.stderr ?? ""}`;
3004
- const hasServerPath = output.includes(absoluteEntry.args[1]);
3005
- const hasProjectRoot = output.includes("TYPEGRAPH_PROJECT_ROOT=*****") || output.includes(projectRoot3);
3006
- const hasTsconfig = output.includes("TYPEGRAPH_TSCONFIG=*****") || output.includes(path9.resolve(projectRoot3, "tsconfig.json"));
3007
- if (hasServerPath && hasProjectRoot && hasTsconfig) {
3008
- p.log.info("Codex CLI: typegraph MCP server already registered");
3009
- return;
3010
- }
3011
- spawnSync2("codex", ["mcp", "remove", "typegraph"], {
3012
- stdio: "pipe",
3013
- encoding: "utf-8"
3014
- });
3015
- }
3016
- const codexAdd = spawnSync2(
3017
- "codex",
3018
- [
3019
- "mcp",
3020
- "add",
3021
- "typegraph",
3022
- "--env",
3023
- `TYPEGRAPH_PROJECT_ROOT=${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}`,
3024
- "--env",
3025
- `TYPEGRAPH_TSCONFIG=${absoluteEntry.env.TYPEGRAPH_TSCONFIG}`,
3026
- "--",
3027
- absoluteEntry.command,
3028
- ...absoluteEntry.args
3029
- ],
3030
- {
3031
- stdio: "pipe",
3032
- encoding: "utf-8"
3033
- }
3034
- );
3035
- if (codexAdd.status === 0) {
3036
- p.log.success("Codex CLI: registered typegraph MCP server");
3037
- return;
3038
- }
3039
- p.log.warn(
3040
- `Codex CLI registration failed \u2014 falling back to ${".codex/config.toml"}`
3041
- );
3042
3168
  const configPath = ".codex/config.toml";
3043
- const fullPath = path9.resolve(projectRoot3, configPath);
3169
+ const fullPath = getCodexConfigPath(projectRoot3);
3170
+ const block = makeCodexMcpBlock(projectRoot3);
3044
3171
  let content = "";
3045
3172
  if (fs8.existsSync(fullPath)) {
3046
3173
  content = fs8.readFileSync(fullPath, "utf-8");
3047
- if (content.includes("[mcp_servers.typegraph]")) {
3048
- p.log.info(`${configPath}: typegraph MCP server already registered`);
3049
- return;
3050
- }
3051
3174
  }
3052
- const block = [
3053
- "",
3054
- "[mcp_servers.typegraph]",
3055
- `command = "${absoluteEntry.command}"`,
3056
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
3057
- `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
3058
- ""
3059
- ].join("\n");
3060
3175
  const dir = path9.dirname(fullPath);
3061
3176
  if (!fs8.existsSync(dir)) {
3062
3177
  fs8.mkdirSync(dir, { recursive: true });
3063
3178
  }
3064
- const newContent = content ? content.trimEnd() + "\n" + block : block.trimStart();
3065
- fs8.writeFileSync(fullPath, newContent);
3066
- p.log.success(`${configPath}: registered typegraph MCP server`);
3179
+ const { content: nextContent, changed } = upsertCodexMcpSection(content, block);
3180
+ if (changed) {
3181
+ fs8.writeFileSync(fullPath, nextContent);
3182
+ p.log.success(`${configPath}: registered typegraph MCP server`);
3183
+ } else {
3184
+ p.log.info(`${configPath}: typegraph MCP server already registered`);
3185
+ }
3186
+ if (!isCodexProjectTrusted(projectRoot3)) {
3187
+ p.log.info(`Codex CLI: trust ${projectRoot3} in ~/.codex/config.toml to load project MCP settings`);
3188
+ }
3067
3189
  }
3068
3190
  function deregisterCodexMcp(projectRoot3) {
3069
- const codexRemove = spawnSync2("codex", ["mcp", "remove", "typegraph"], {
3070
- stdio: "pipe",
3071
- encoding: "utf-8"
3072
- });
3073
- if (codexRemove.status === 0) {
3074
- p.log.info("Codex CLI: removed typegraph MCP server");
3075
- }
3076
3191
  const configPath = ".codex/config.toml";
3077
- const fullPath = path9.resolve(projectRoot3, configPath);
3192
+ const fullPath = getCodexConfigPath(projectRoot3);
3078
3193
  if (!fs8.existsSync(fullPath)) return;
3079
3194
  let content = fs8.readFileSync(fullPath, "utf-8");
3080
- if (!content.includes("[mcp_servers.typegraph]")) return;
3195
+ if (!hasCodexMcpSection(content)) return;
3081
3196
  content = content.replace(
3082
3197
  /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
3083
3198
  ""
@@ -3279,12 +3394,12 @@ async function setup(yes2) {
3279
3394
  }
3280
3395
  s.message("Installing dependencies...");
3281
3396
  try {
3282
- execSync2("npm install", { cwd: targetDir, stdio: "pipe" });
3397
+ execSync2("npm install --include=optional", { cwd: targetDir, stdio: "pipe" });
3283
3398
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files with dependencies`);
3284
3399
  } catch (err) {
3285
3400
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files`);
3286
3401
  p.log.warn(`Dependency install failed: ${err instanceof Error ? err.message : String(err)}`);
3287
- p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install`);
3402
+ p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install --include=optional`);
3288
3403
  }
3289
3404
  if (needsAgentsSkills) {
3290
3405
  const agentsNames = selectedAgents.filter((id) => AGENTS[id].needsAgentsSkills).map((id) => AGENTS[id].name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typegraph-mcp",
3
- "version": "0.9.31",
3
+ "version": "0.9.33",
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",
@@ -8,6 +8,10 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/guyowen/typegraph-mcp.git"
10
10
  },
11
+ "packageManager": "npm@11.5.2",
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
11
15
  "bin": {
12
16
  "typegraph-mcp": "./dist/cli.js",
13
17
  "typegraph": "./dist/cli.js"
@@ -9,6 +9,14 @@ set -e
9
9
 
10
10
  PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
11
11
 
12
+ if command -v node &> /dev/null; then
13
+ NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
14
+ if [ "$NODE_MAJOR" -lt 22 ]; then
15
+ echo "Warning: typegraph-mcp requires Node.js >= 22. Current: $(node -v)"
16
+ exit 1
17
+ fi
18
+ fi
19
+
12
20
  # Check if node_modules exist with required packages
13
21
  if [ -d "$PLUGIN_DIR/node_modules/@modelcontextprotocol" ] && \
14
22
  [ -d "$PLUGIN_DIR/node_modules/oxc-parser" ] && \
@@ -21,7 +29,7 @@ echo "Installing typegraph-mcp dependencies..."
21
29
  cd "$PLUGIN_DIR"
22
30
 
23
31
  if command -v npm &> /dev/null; then
24
- npm install
32
+ npm install --include=optional
25
33
  else
26
34
  echo "Warning: npm not found. Run 'npm install' in $PLUGIN_DIR manually."
27
35
  exit 1