typegraph-mcp 0.9.43 → 0.9.45
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/check.ts +61 -0
- package/cli.ts +110 -3
- package/dist/benchmark.js +24 -0
- package/dist/check.js +41 -0
- package/dist/cli.js +173 -5
- package/dist/server.js +34 -3
- package/dist/smoke-test.js +24 -0
- package/dist/tsserver-client.js +24 -0
- package/engine-sync-test.ts +26 -0
- package/install-oxlint-test.ts +18 -4
- package/package.json +1 -1
- package/server.ts +12 -3
- package/tsserver-client.ts +29 -0
package/check.ts
CHANGED
|
@@ -170,6 +170,64 @@ function hasTrustedCodexProject(projectRoot: string): boolean | null {
|
|
|
170
170
|
return matchesTrustedProject();
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function hasCompleteJsonTypegraphRegistration(
|
|
174
|
+
config: unknown,
|
|
175
|
+
serverName: string,
|
|
176
|
+
projectRoot: string
|
|
177
|
+
): boolean {
|
|
178
|
+
if (typeof config !== "object" || config === null) return false;
|
|
179
|
+
const servers = (config as Record<string, unknown>)["mcpServers"];
|
|
180
|
+
if (typeof servers !== "object" || servers === null) return false;
|
|
181
|
+
const entry = (servers as Record<string, unknown>)[serverName];
|
|
182
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
183
|
+
|
|
184
|
+
const record = entry as Record<string, unknown>;
|
|
185
|
+
const command = record["command"];
|
|
186
|
+
const args = record["args"];
|
|
187
|
+
const env = record["env"];
|
|
188
|
+
const serializedArgs = JSON.stringify(args ?? []);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
typeof command === "string" &&
|
|
192
|
+
command.length > 0 &&
|
|
193
|
+
Array.isArray(args) &&
|
|
194
|
+
serializedArgs.includes("server.ts") &&
|
|
195
|
+
serializedArgs.includes("tsx") &&
|
|
196
|
+
typeof env === "object" &&
|
|
197
|
+
env !== null &&
|
|
198
|
+
(env as Record<string, unknown>)["TYPEGRAPH_PROJECT_ROOT"] === projectRoot &&
|
|
199
|
+
typeof (env as Record<string, unknown>)["TYPEGRAPH_TSCONFIG"] === "string"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readJsonConfig(configPath: string): unknown | null {
|
|
204
|
+
if (!fs.existsSync(configPath)) return null;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getAntigravityMcpConfigPaths(): string[] {
|
|
213
|
+
const home = process.env.HOME || "";
|
|
214
|
+
return [
|
|
215
|
+
path.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
216
|
+
path.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json"),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function findAntigravityRegistration(projectRoot: string): string | null {
|
|
221
|
+
const home = process.env.HOME || "";
|
|
222
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
223
|
+
const config = readJsonConfig(configPath);
|
|
224
|
+
if (hasCompleteJsonTypegraphRegistration(config, "typegraph-mcp", projectRoot)) {
|
|
225
|
+
return configPath.replace(home, "~");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
173
231
|
function readProjectPackageJson(projectRoot: string): Record<string, unknown> | null {
|
|
174
232
|
const packageJsonPath = path.resolve(projectRoot, "package.json");
|
|
175
233
|
if (!fs.existsSync(packageJsonPath)) return null;
|
|
@@ -346,6 +404,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
346
404
|
encoding: "utf-8",
|
|
347
405
|
});
|
|
348
406
|
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
407
|
+
const antigravityRegistrationPath = findAntigravityRegistration(projectRoot);
|
|
349
408
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
350
409
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
351
410
|
} else if (hasPluginMcp) {
|
|
@@ -382,6 +441,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
382
441
|
}
|
|
383
442
|
} else if (hasGlobalCodexRegistration) {
|
|
384
443
|
pass("MCP registered in global Codex CLI config");
|
|
444
|
+
} else if (antigravityRegistrationPath !== null) {
|
|
445
|
+
pass(`MCP registered in Antigravity config (${antigravityRegistrationPath})`);
|
|
385
446
|
} else {
|
|
386
447
|
const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
|
|
387
448
|
const mcpJsonPath = path.resolve(projectRoot, ".claude/mcp.json");
|
package/cli.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { resolveConfig } from "./config.js";
|
|
|
22
22
|
|
|
23
23
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
-
type AgentId = "claude-code" | "cursor" | "codex" | "gemini" | "copilot";
|
|
25
|
+
type AgentId = "claude-code" | "cursor" | "codex" | "gemini" | "copilot" | "antigravity";
|
|
26
26
|
|
|
27
27
|
interface AgentDef {
|
|
28
28
|
name: string;
|
|
@@ -80,7 +80,7 @@ const CLAUDE_NODE_PLACEHOLDER = "__TYPEGRAPH_NODE__";
|
|
|
80
80
|
|
|
81
81
|
const PLUGIN_DIR_NAME = "plugins/typegraph-mcp";
|
|
82
82
|
|
|
83
|
-
const AGENT_IDS: AgentId[] = ["claude-code", "cursor", "codex", "gemini", "copilot"];
|
|
83
|
+
const AGENT_IDS: AgentId[] = ["claude-code", "cursor", "codex", "gemini", "copilot", "antigravity"];
|
|
84
84
|
|
|
85
85
|
const AGENTS: Record<AgentId, AgentDef> = {
|
|
86
86
|
"claude-code": {
|
|
@@ -129,6 +129,13 @@ const AGENTS: Record<AgentId, AgentDef> = {
|
|
|
129
129
|
detect: (root) =>
|
|
130
130
|
fs.existsSync(path.join(root, ".github/copilot-instructions.md")),
|
|
131
131
|
},
|
|
132
|
+
antigravity: {
|
|
133
|
+
name: "Antigravity",
|
|
134
|
+
pluginFiles: [],
|
|
135
|
+
agentFile: "AGENTS.md",
|
|
136
|
+
needsAgentsSkills: true,
|
|
137
|
+
detect: (root) => fs.existsSync(path.join(root, ".gemini/antigravity")),
|
|
138
|
+
},
|
|
132
139
|
};
|
|
133
140
|
|
|
134
141
|
/** Core files always installed (server, modules, config, package manifest) */
|
|
@@ -250,6 +257,35 @@ function getCodexConfigPath(projectRoot: string): string {
|
|
|
250
257
|
return path.resolve(projectRoot, ".codex/config.toml");
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
function getAntigravityMcpConfigPaths(): string[] {
|
|
261
|
+
const home = process.env.HOME || "";
|
|
262
|
+
return [
|
|
263
|
+
path.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
264
|
+
path.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json"),
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ensureAntigravityCliPlugin(): void {
|
|
269
|
+
const home = process.env.HOME || "";
|
|
270
|
+
const pluginDir = path.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp");
|
|
271
|
+
const pluginJsonPath = path.join(pluginDir, "plugin.json");
|
|
272
|
+
if (!fs.existsSync(pluginJsonPath)) {
|
|
273
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
274
|
+
fs.writeFileSync(
|
|
275
|
+
pluginJsonPath,
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
{
|
|
278
|
+
name: "typegraph-mcp",
|
|
279
|
+
version: "1.0.0",
|
|
280
|
+
description: "TypeGraph MCP server for TypeScript navigation",
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2
|
|
284
|
+
) + "\n"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
253
289
|
function isTomlSectionGroup(sectionName: string | null, prefix: string): boolean {
|
|
254
290
|
return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
|
|
255
291
|
}
|
|
@@ -476,6 +512,9 @@ function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): voi
|
|
|
476
512
|
if (selectedAgents.includes("copilot")) {
|
|
477
513
|
registerJsonMcp(projectRoot, ".vscode/mcp.json", "servers");
|
|
478
514
|
}
|
|
515
|
+
if (selectedAgents.includes("antigravity")) {
|
|
516
|
+
registerAntigravityMcp(projectRoot);
|
|
517
|
+
}
|
|
479
518
|
}
|
|
480
519
|
|
|
481
520
|
/** Deregister the typegraph MCP server from all agent config files */
|
|
@@ -483,6 +522,7 @@ function deregisterMcpServers(projectRoot: string): void {
|
|
|
483
522
|
deregisterJsonMcp(projectRoot, ".cursor/mcp.json", "mcpServers");
|
|
484
523
|
deregisterCodexMcp(projectRoot);
|
|
485
524
|
deregisterJsonMcp(projectRoot, ".vscode/mcp.json", "servers");
|
|
525
|
+
deregisterAntigravityMcp(projectRoot);
|
|
486
526
|
}
|
|
487
527
|
|
|
488
528
|
/** Register MCP server in a JSON config file (Cursor or Copilot format) */
|
|
@@ -593,6 +633,73 @@ function deregisterCodexMcp(projectRoot: string): void {
|
|
|
593
633
|
}
|
|
594
634
|
}
|
|
595
635
|
|
|
636
|
+
/** Register MCP server in Antigravity's config files */
|
|
637
|
+
function registerAntigravityMcp(projectRoot: string): void {
|
|
638
|
+
const home = process.env.HOME || "";
|
|
639
|
+
const pluginDir = path.resolve(projectRoot, PLUGIN_DIR_NAME);
|
|
640
|
+
const tsConfigPath = path.resolve(projectRoot, "tsconfig.json");
|
|
641
|
+
ensureAntigravityCliPlugin();
|
|
642
|
+
|
|
643
|
+
const entry = {
|
|
644
|
+
command: process.execPath,
|
|
645
|
+
args: [
|
|
646
|
+
path.join(pluginDir, "node_modules/tsx/dist/cli.mjs"),
|
|
647
|
+
path.join(pluginDir, "server.ts"),
|
|
648
|
+
],
|
|
649
|
+
env: {
|
|
650
|
+
TYPEGRAPH_PROJECT_ROOT: projectRoot,
|
|
651
|
+
TYPEGRAPH_TSCONFIG: tsConfigPath,
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
656
|
+
let config: any = { mcpServers: {} };
|
|
657
|
+
if (fs.existsSync(configPath)) {
|
|
658
|
+
try {
|
|
659
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
660
|
+
} catch {
|
|
661
|
+
p.log.warn(`Could not parse ~${configPath.replace(home, "")} — skipping MCP registration`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
666
|
+
config.mcpServers["typegraph-mcp"] = entry;
|
|
667
|
+
|
|
668
|
+
const dir = path.dirname(configPath);
|
|
669
|
+
if (!fs.existsSync(dir)) {
|
|
670
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
671
|
+
}
|
|
672
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
673
|
+
p.log.success(`~${configPath.replace(home, "")}: registered typegraph-mcp server`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Deregister MCP server from Antigravity's config files */
|
|
678
|
+
function deregisterAntigravityMcp(projectRoot: string): void {
|
|
679
|
+
const home = process.env.HOME || "";
|
|
680
|
+
|
|
681
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
682
|
+
if (!fs.existsSync(configPath)) continue;
|
|
683
|
+
try {
|
|
684
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
685
|
+
if (config.mcpServers && config.mcpServers["typegraph-mcp"]) {
|
|
686
|
+
delete config.mcpServers["typegraph-mcp"];
|
|
687
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
688
|
+
delete config.mcpServers;
|
|
689
|
+
}
|
|
690
|
+
if (Object.keys(config).length === 0) {
|
|
691
|
+
fs.unlinkSync(configPath);
|
|
692
|
+
} else {
|
|
693
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
694
|
+
}
|
|
695
|
+
p.log.info(`~${configPath.replace(home, "")}: removed typegraph-mcp server`);
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
// Ignore
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
596
703
|
// ─── TSConfig Exclude ─────────────────────────────────────────────────────────
|
|
597
704
|
|
|
598
705
|
function ensureTsconfigExclude(projectRoot: string): void {
|
|
@@ -876,7 +983,7 @@ async function setup(yes: boolean): Promise<void> {
|
|
|
876
983
|
// 3. Agent selection
|
|
877
984
|
const selectedAgents = await selectAgents(projectRoot, yes);
|
|
878
985
|
|
|
879
|
-
const needsPluginSkills = selectedAgents.includes("claude-code") || selectedAgents.includes("cursor");
|
|
986
|
+
const needsPluginSkills = selectedAgents.includes("claude-code") || selectedAgents.includes("cursor") || selectedAgents.includes("antigravity");
|
|
880
987
|
const needsAgentsSkills = selectedAgents.some((id) => AGENTS[id].needsAgentsSkills);
|
|
881
988
|
|
|
882
989
|
p.log.step(`Installing to ${PLUGIN_DIR_NAME}/...`);
|
package/dist/benchmark.js
CHANGED
|
@@ -25,6 +25,7 @@ var TsServerClient = class {
|
|
|
25
25
|
shuttingDown = false;
|
|
26
26
|
restartCount = 0;
|
|
27
27
|
maxRestarts = 3;
|
|
28
|
+
projectsDirty = false;
|
|
28
29
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
29
30
|
resolvePath(file) {
|
|
30
31
|
return path.isAbsolute(file) ? file : path.resolve(this.projectRoot, file);
|
|
@@ -201,6 +202,24 @@ var TsServerClient = class {
|
|
|
201
202
|
this.sendNotification("open", { file: absPath });
|
|
202
203
|
await new Promise((r) => setTimeout(r, 50));
|
|
203
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
207
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
208
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
209
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
210
|
+
* projects, producing definition-only reference results). The next query
|
|
211
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
212
|
+
* from disk. Open files are unaffected by design — their content is
|
|
213
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
214
|
+
*/
|
|
215
|
+
markProjectsDirty() {
|
|
216
|
+
this.projectsDirty = true;
|
|
217
|
+
}
|
|
218
|
+
refreshIfDirty() {
|
|
219
|
+
if (!this.projectsDirty) return;
|
|
220
|
+
this.projectsDirty = false;
|
|
221
|
+
this.sendNotification("reloadProjects");
|
|
222
|
+
}
|
|
204
223
|
async reloadOpenFile(file) {
|
|
205
224
|
const absPath = this.resolvePath(file);
|
|
206
225
|
if (!this.openFiles.has(absPath)) return false;
|
|
@@ -218,6 +237,7 @@ var TsServerClient = class {
|
|
|
218
237
|
}
|
|
219
238
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
220
239
|
async definition(file, line, offset) {
|
|
240
|
+
this.refreshIfDirty();
|
|
221
241
|
const absPath = this.resolvePath(file);
|
|
222
242
|
await this.ensureOpen(absPath);
|
|
223
243
|
const body = await this.sendRequest("definition", {
|
|
@@ -232,6 +252,7 @@ var TsServerClient = class {
|
|
|
232
252
|
}));
|
|
233
253
|
}
|
|
234
254
|
async references(file, line, offset) {
|
|
255
|
+
this.refreshIfDirty();
|
|
235
256
|
const absPath = this.resolvePath(file);
|
|
236
257
|
await this.ensureOpen(absPath);
|
|
237
258
|
const body = await this.sendRequest("references", {
|
|
@@ -246,6 +267,7 @@ var TsServerClient = class {
|
|
|
246
267
|
}));
|
|
247
268
|
}
|
|
248
269
|
async quickinfo(file, line, offset) {
|
|
270
|
+
this.refreshIfDirty();
|
|
249
271
|
const absPath = this.resolvePath(file);
|
|
250
272
|
await this.ensureOpen(absPath);
|
|
251
273
|
try {
|
|
@@ -260,6 +282,7 @@ var TsServerClient = class {
|
|
|
260
282
|
}
|
|
261
283
|
}
|
|
262
284
|
async navto(searchValue, maxResults = 10, file) {
|
|
285
|
+
this.refreshIfDirty();
|
|
263
286
|
if (file) await this.ensureOpen(file);
|
|
264
287
|
const args = {
|
|
265
288
|
searchValue,
|
|
@@ -274,6 +297,7 @@ var TsServerClient = class {
|
|
|
274
297
|
}));
|
|
275
298
|
}
|
|
276
299
|
async navbar(file) {
|
|
300
|
+
this.refreshIfDirty();
|
|
277
301
|
const absPath = this.resolvePath(file);
|
|
278
302
|
await this.ensureOpen(absPath);
|
|
279
303
|
const body = await this.sendRequest("navbar", {
|
package/dist/check.js
CHANGED
|
@@ -478,6 +478,44 @@ function hasTrustedCodexProject(projectRoot) {
|
|
|
478
478
|
}
|
|
479
479
|
return matchesTrustedProject();
|
|
480
480
|
}
|
|
481
|
+
function hasCompleteJsonTypegraphRegistration(config, serverName, projectRoot) {
|
|
482
|
+
if (typeof config !== "object" || config === null) return false;
|
|
483
|
+
const servers = config["mcpServers"];
|
|
484
|
+
if (typeof servers !== "object" || servers === null) return false;
|
|
485
|
+
const entry = servers[serverName];
|
|
486
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
487
|
+
const record = entry;
|
|
488
|
+
const command = record["command"];
|
|
489
|
+
const args = record["args"];
|
|
490
|
+
const env = record["env"];
|
|
491
|
+
const serializedArgs = JSON.stringify(args ?? []);
|
|
492
|
+
return typeof command === "string" && command.length > 0 && Array.isArray(args) && serializedArgs.includes("server.ts") && serializedArgs.includes("tsx") && typeof env === "object" && env !== null && env["TYPEGRAPH_PROJECT_ROOT"] === projectRoot && typeof env["TYPEGRAPH_TSCONFIG"] === "string";
|
|
493
|
+
}
|
|
494
|
+
function readJsonConfig(configPath) {
|
|
495
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
496
|
+
try {
|
|
497
|
+
return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function getAntigravityMcpConfigPaths() {
|
|
503
|
+
const home = process.env.HOME || "";
|
|
504
|
+
return [
|
|
505
|
+
path3.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
506
|
+
path3.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json")
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
function findAntigravityRegistration(projectRoot) {
|
|
510
|
+
const home = process.env.HOME || "";
|
|
511
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
512
|
+
const config = readJsonConfig(configPath);
|
|
513
|
+
if (hasCompleteJsonTypegraphRegistration(config, "typegraph-mcp", projectRoot)) {
|
|
514
|
+
return configPath.replace(home, "~");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
481
519
|
function readProjectPackageJson(projectRoot) {
|
|
482
520
|
const packageJsonPath = path3.resolve(projectRoot, "package.json");
|
|
483
521
|
if (!fs2.existsSync(packageJsonPath)) return null;
|
|
@@ -611,6 +649,7 @@ async function main(configOverride) {
|
|
|
611
649
|
encoding: "utf-8"
|
|
612
650
|
});
|
|
613
651
|
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
652
|
+
const antigravityRegistrationPath = findAntigravityRegistration(projectRoot);
|
|
614
653
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
615
654
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
616
655
|
} else if (hasPluginMcp) {
|
|
@@ -647,6 +686,8 @@ async function main(configOverride) {
|
|
|
647
686
|
}
|
|
648
687
|
} else if (hasGlobalCodexRegistration) {
|
|
649
688
|
pass("MCP registered in global Codex CLI config");
|
|
689
|
+
} else if (antigravityRegistrationPath !== null) {
|
|
690
|
+
pass(`MCP registered in Antigravity config (${antigravityRegistrationPath})`);
|
|
650
691
|
} else {
|
|
651
692
|
const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
|
|
652
693
|
const mcpJsonPath = path3.resolve(projectRoot, ".claude/mcp.json");
|
package/dist/cli.js
CHANGED
|
@@ -485,6 +485,44 @@ function hasTrustedCodexProject(projectRoot3) {
|
|
|
485
485
|
}
|
|
486
486
|
return matchesTrustedProject();
|
|
487
487
|
}
|
|
488
|
+
function hasCompleteJsonTypegraphRegistration(config, serverName, projectRoot3) {
|
|
489
|
+
if (typeof config !== "object" || config === null) return false;
|
|
490
|
+
const servers = config["mcpServers"];
|
|
491
|
+
if (typeof servers !== "object" || servers === null) return false;
|
|
492
|
+
const entry = servers[serverName];
|
|
493
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
494
|
+
const record = entry;
|
|
495
|
+
const command2 = record["command"];
|
|
496
|
+
const args2 = record["args"];
|
|
497
|
+
const env = record["env"];
|
|
498
|
+
const serializedArgs = JSON.stringify(args2 ?? []);
|
|
499
|
+
return typeof command2 === "string" && command2.length > 0 && Array.isArray(args2) && serializedArgs.includes("server.ts") && serializedArgs.includes("tsx") && typeof env === "object" && env !== null && env["TYPEGRAPH_PROJECT_ROOT"] === projectRoot3 && typeof env["TYPEGRAPH_TSCONFIG"] === "string";
|
|
500
|
+
}
|
|
501
|
+
function readJsonConfig(configPath) {
|
|
502
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
503
|
+
try {
|
|
504
|
+
return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function getAntigravityMcpConfigPaths() {
|
|
510
|
+
const home = process.env.HOME || "";
|
|
511
|
+
return [
|
|
512
|
+
path3.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
513
|
+
path3.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json")
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
function findAntigravityRegistration(projectRoot3) {
|
|
517
|
+
const home = process.env.HOME || "";
|
|
518
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
519
|
+
const config = readJsonConfig(configPath);
|
|
520
|
+
if (hasCompleteJsonTypegraphRegistration(config, "typegraph-mcp", projectRoot3)) {
|
|
521
|
+
return configPath.replace(home, "~");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
488
526
|
function readProjectPackageJson(projectRoot3) {
|
|
489
527
|
const packageJsonPath = path3.resolve(projectRoot3, "package.json");
|
|
490
528
|
if (!fs2.existsSync(packageJsonPath)) return null;
|
|
@@ -605,6 +643,7 @@ async function main(configOverride) {
|
|
|
605
643
|
encoding: "utf-8"
|
|
606
644
|
});
|
|
607
645
|
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
646
|
+
const antigravityRegistrationPath = findAntigravityRegistration(projectRoot3);
|
|
608
647
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
609
648
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
610
649
|
} else if (hasPluginMcp) {
|
|
@@ -641,6 +680,8 @@ async function main(configOverride) {
|
|
|
641
680
|
}
|
|
642
681
|
} else if (hasGlobalCodexRegistration) {
|
|
643
682
|
pass("MCP registered in global Codex CLI config");
|
|
683
|
+
} else if (antigravityRegistrationPath !== null) {
|
|
684
|
+
pass(`MCP registered in Antigravity config (${antigravityRegistrationPath})`);
|
|
644
685
|
} else {
|
|
645
686
|
const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
|
|
646
687
|
const mcpJsonPath = path3.resolve(projectRoot3, ".claude/mcp.json");
|
|
@@ -913,6 +954,7 @@ var init_tsserver_client = __esm({
|
|
|
913
954
|
shuttingDown = false;
|
|
914
955
|
restartCount = 0;
|
|
915
956
|
maxRestarts = 3;
|
|
957
|
+
projectsDirty = false;
|
|
916
958
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
917
959
|
resolvePath(file) {
|
|
918
960
|
return path4.isAbsolute(file) ? file : path4.resolve(this.projectRoot, file);
|
|
@@ -1089,6 +1131,24 @@ var init_tsserver_client = __esm({
|
|
|
1089
1131
|
this.sendNotification("open", { file: absPath2 });
|
|
1090
1132
|
await new Promise((r) => setTimeout(r, 50));
|
|
1091
1133
|
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
1136
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
1137
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
1138
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
1139
|
+
* projects, producing definition-only reference results). The next query
|
|
1140
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
1141
|
+
* from disk. Open files are unaffected by design — their content is
|
|
1142
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
1143
|
+
*/
|
|
1144
|
+
markProjectsDirty() {
|
|
1145
|
+
this.projectsDirty = true;
|
|
1146
|
+
}
|
|
1147
|
+
refreshIfDirty() {
|
|
1148
|
+
if (!this.projectsDirty) return;
|
|
1149
|
+
this.projectsDirty = false;
|
|
1150
|
+
this.sendNotification("reloadProjects");
|
|
1151
|
+
}
|
|
1092
1152
|
async reloadOpenFile(file) {
|
|
1093
1153
|
const absPath2 = this.resolvePath(file);
|
|
1094
1154
|
if (!this.openFiles.has(absPath2)) return false;
|
|
@@ -1106,6 +1166,7 @@ var init_tsserver_client = __esm({
|
|
|
1106
1166
|
}
|
|
1107
1167
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
1108
1168
|
async definition(file, line, offset) {
|
|
1169
|
+
this.refreshIfDirty();
|
|
1109
1170
|
const absPath2 = this.resolvePath(file);
|
|
1110
1171
|
await this.ensureOpen(absPath2);
|
|
1111
1172
|
const body = await this.sendRequest("definition", {
|
|
@@ -1120,6 +1181,7 @@ var init_tsserver_client = __esm({
|
|
|
1120
1181
|
}));
|
|
1121
1182
|
}
|
|
1122
1183
|
async references(file, line, offset) {
|
|
1184
|
+
this.refreshIfDirty();
|
|
1123
1185
|
const absPath2 = this.resolvePath(file);
|
|
1124
1186
|
await this.ensureOpen(absPath2);
|
|
1125
1187
|
const body = await this.sendRequest("references", {
|
|
@@ -1134,6 +1196,7 @@ var init_tsserver_client = __esm({
|
|
|
1134
1196
|
}));
|
|
1135
1197
|
}
|
|
1136
1198
|
async quickinfo(file, line, offset) {
|
|
1199
|
+
this.refreshIfDirty();
|
|
1137
1200
|
const absPath2 = this.resolvePath(file);
|
|
1138
1201
|
await this.ensureOpen(absPath2);
|
|
1139
1202
|
try {
|
|
@@ -1148,6 +1211,7 @@ var init_tsserver_client = __esm({
|
|
|
1148
1211
|
}
|
|
1149
1212
|
}
|
|
1150
1213
|
async navto(searchValue, maxResults = 10, file) {
|
|
1214
|
+
this.refreshIfDirty();
|
|
1151
1215
|
if (file) await this.ensureOpen(file);
|
|
1152
1216
|
const args2 = {
|
|
1153
1217
|
searchValue,
|
|
@@ -1162,6 +1226,7 @@ var init_tsserver_client = __esm({
|
|
|
1162
1226
|
}));
|
|
1163
1227
|
}
|
|
1164
1228
|
async navbar(file) {
|
|
1229
|
+
this.refreshIfDirty();
|
|
1165
1230
|
const absPath2 = this.resolvePath(file);
|
|
1166
1231
|
await this.ensureOpen(absPath2);
|
|
1167
1232
|
const body = await this.sendRequest("navbar", {
|
|
@@ -2656,11 +2721,18 @@ async function main4() {
|
|
|
2656
2721
|
moduleGraph = graphResult.graph;
|
|
2657
2722
|
moduleResolver = graphResult.resolver;
|
|
2658
2723
|
startWatcher(projectRoot2, moduleGraph, graphResult.resolver, {
|
|
2659
|
-
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).
|
|
2660
|
-
|
|
2661
|
-
|
|
2724
|
+
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).then(
|
|
2725
|
+
(wasOpen) => {
|
|
2726
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
2727
|
+
},
|
|
2728
|
+
(err) => {
|
|
2729
|
+
client.markProjectsDirty();
|
|
2730
|
+
log3(`Failed to reload open file ${relPath2(filePath)}:`, err);
|
|
2731
|
+
}
|
|
2732
|
+
),
|
|
2662
2733
|
onFileDeleted: (filePath) => {
|
|
2663
2734
|
client.closeFile(filePath);
|
|
2735
|
+
client.markProjectsDirty();
|
|
2664
2736
|
}
|
|
2665
2737
|
});
|
|
2666
2738
|
const transport = new StdioServerTransport();
|
|
@@ -3217,7 +3289,7 @@ Practical rule:
|
|
|
3217
3289
|
var SNIPPET_MARKER = "## TypeScript Navigation (typegraph-mcp)";
|
|
3218
3290
|
var CLAUDE_NODE_PLACEHOLDER = "__TYPEGRAPH_NODE__";
|
|
3219
3291
|
var PLUGIN_DIR_NAME = "plugins/typegraph-mcp";
|
|
3220
|
-
var AGENT_IDS = ["claude-code", "cursor", "codex", "gemini", "copilot"];
|
|
3292
|
+
var AGENT_IDS = ["claude-code", "cursor", "codex", "gemini", "copilot", "antigravity"];
|
|
3221
3293
|
var AGENTS = {
|
|
3222
3294
|
"claude-code": {
|
|
3223
3295
|
name: "Claude Code",
|
|
@@ -3261,6 +3333,13 @@ var AGENTS = {
|
|
|
3261
3333
|
agentFile: ".github/copilot-instructions.md",
|
|
3262
3334
|
needsAgentsSkills: true,
|
|
3263
3335
|
detect: (root) => fs8.existsSync(path9.join(root, ".github/copilot-instructions.md"))
|
|
3336
|
+
},
|
|
3337
|
+
antigravity: {
|
|
3338
|
+
name: "Antigravity",
|
|
3339
|
+
pluginFiles: [],
|
|
3340
|
+
agentFile: "AGENTS.md",
|
|
3341
|
+
needsAgentsSkills: true,
|
|
3342
|
+
detect: (root) => fs8.existsSync(path9.join(root, ".gemini/antigravity"))
|
|
3264
3343
|
}
|
|
3265
3344
|
};
|
|
3266
3345
|
var CORE_FILES = [
|
|
@@ -3346,6 +3425,33 @@ function getCodexMcpServerEntry(projectRoot3) {
|
|
|
3346
3425
|
function getCodexConfigPath(projectRoot3) {
|
|
3347
3426
|
return path9.resolve(projectRoot3, ".codex/config.toml");
|
|
3348
3427
|
}
|
|
3428
|
+
function getAntigravityMcpConfigPaths2() {
|
|
3429
|
+
const home = process.env.HOME || "";
|
|
3430
|
+
return [
|
|
3431
|
+
path9.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
3432
|
+
path9.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json")
|
|
3433
|
+
];
|
|
3434
|
+
}
|
|
3435
|
+
function ensureAntigravityCliPlugin() {
|
|
3436
|
+
const home = process.env.HOME || "";
|
|
3437
|
+
const pluginDir = path9.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp");
|
|
3438
|
+
const pluginJsonPath = path9.join(pluginDir, "plugin.json");
|
|
3439
|
+
if (!fs8.existsSync(pluginJsonPath)) {
|
|
3440
|
+
fs8.mkdirSync(pluginDir, { recursive: true });
|
|
3441
|
+
fs8.writeFileSync(
|
|
3442
|
+
pluginJsonPath,
|
|
3443
|
+
JSON.stringify(
|
|
3444
|
+
{
|
|
3445
|
+
name: "typegraph-mcp",
|
|
3446
|
+
version: "1.0.0",
|
|
3447
|
+
description: "TypeGraph MCP server for TypeScript navigation"
|
|
3448
|
+
},
|
|
3449
|
+
null,
|
|
3450
|
+
2
|
|
3451
|
+
) + "\n"
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3349
3455
|
function isTomlSectionGroup(sectionName, prefix) {
|
|
3350
3456
|
return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
|
|
3351
3457
|
}
|
|
@@ -3519,11 +3625,15 @@ function registerMcpServers(projectRoot3, selectedAgents) {
|
|
|
3519
3625
|
if (selectedAgents.includes("copilot")) {
|
|
3520
3626
|
registerJsonMcp(projectRoot3, ".vscode/mcp.json", "servers");
|
|
3521
3627
|
}
|
|
3628
|
+
if (selectedAgents.includes("antigravity")) {
|
|
3629
|
+
registerAntigravityMcp(projectRoot3);
|
|
3630
|
+
}
|
|
3522
3631
|
}
|
|
3523
3632
|
function deregisterMcpServers(projectRoot3) {
|
|
3524
3633
|
deregisterJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
|
|
3525
3634
|
deregisterCodexMcp(projectRoot3);
|
|
3526
3635
|
deregisterJsonMcp(projectRoot3, ".vscode/mcp.json", "servers");
|
|
3636
|
+
deregisterAntigravityMcp(projectRoot3);
|
|
3527
3637
|
}
|
|
3528
3638
|
function registerJsonMcp(projectRoot3, configPath, rootKey) {
|
|
3529
3639
|
const fullPath = path9.resolve(projectRoot3, configPath);
|
|
@@ -3609,6 +3719,64 @@ function deregisterCodexMcp(projectRoot3) {
|
|
|
3609
3719
|
}
|
|
3610
3720
|
}
|
|
3611
3721
|
}
|
|
3722
|
+
function registerAntigravityMcp(projectRoot3) {
|
|
3723
|
+
const home = process.env.HOME || "";
|
|
3724
|
+
const pluginDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
|
|
3725
|
+
const tsConfigPath = path9.resolve(projectRoot3, "tsconfig.json");
|
|
3726
|
+
ensureAntigravityCliPlugin();
|
|
3727
|
+
const entry = {
|
|
3728
|
+
command: process.execPath,
|
|
3729
|
+
args: [
|
|
3730
|
+
path9.join(pluginDir, "node_modules/tsx/dist/cli.mjs"),
|
|
3731
|
+
path9.join(pluginDir, "server.ts")
|
|
3732
|
+
],
|
|
3733
|
+
env: {
|
|
3734
|
+
TYPEGRAPH_PROJECT_ROOT: projectRoot3,
|
|
3735
|
+
TYPEGRAPH_TSCONFIG: tsConfigPath
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
for (const configPath of getAntigravityMcpConfigPaths2()) {
|
|
3739
|
+
let config = { mcpServers: {} };
|
|
3740
|
+
if (fs8.existsSync(configPath)) {
|
|
3741
|
+
try {
|
|
3742
|
+
config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
3743
|
+
} catch {
|
|
3744
|
+
p.log.warn(`Could not parse ~${configPath.replace(home, "")} \u2014 skipping MCP registration`);
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
3749
|
+
config.mcpServers["typegraph-mcp"] = entry;
|
|
3750
|
+
const dir = path9.dirname(configPath);
|
|
3751
|
+
if (!fs8.existsSync(dir)) {
|
|
3752
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
3753
|
+
}
|
|
3754
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3755
|
+
p.log.success(`~${configPath.replace(home, "")}: registered typegraph-mcp server`);
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
function deregisterAntigravityMcp(projectRoot3) {
|
|
3759
|
+
const home = process.env.HOME || "";
|
|
3760
|
+
for (const configPath of getAntigravityMcpConfigPaths2()) {
|
|
3761
|
+
if (!fs8.existsSync(configPath)) continue;
|
|
3762
|
+
try {
|
|
3763
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
3764
|
+
if (config.mcpServers && config.mcpServers["typegraph-mcp"]) {
|
|
3765
|
+
delete config.mcpServers["typegraph-mcp"];
|
|
3766
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
3767
|
+
delete config.mcpServers;
|
|
3768
|
+
}
|
|
3769
|
+
if (Object.keys(config).length === 0) {
|
|
3770
|
+
fs8.unlinkSync(configPath);
|
|
3771
|
+
} else {
|
|
3772
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3773
|
+
}
|
|
3774
|
+
p.log.info(`~${configPath.replace(home, "")}: removed typegraph-mcp server`);
|
|
3775
|
+
}
|
|
3776
|
+
} catch {
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3612
3780
|
function ensureTsconfigExclude(projectRoot3) {
|
|
3613
3781
|
const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
|
|
3614
3782
|
if (!fs8.existsSync(tsconfigPath3)) return;
|
|
@@ -3831,7 +3999,7 @@ async function setup(yes2) {
|
|
|
3831
3999
|
}
|
|
3832
4000
|
}
|
|
3833
4001
|
const selectedAgents = await selectAgents(projectRoot3, yes2);
|
|
3834
|
-
const needsPluginSkills = selectedAgents.includes("claude-code") || selectedAgents.includes("cursor");
|
|
4002
|
+
const needsPluginSkills = selectedAgents.includes("claude-code") || selectedAgents.includes("cursor") || selectedAgents.includes("antigravity");
|
|
3835
4003
|
const needsAgentsSkills = selectedAgents.some((id) => AGENTS[id].needsAgentsSkills);
|
|
3836
4004
|
p.log.step(`Installing to ${PLUGIN_DIR_NAME}/...`);
|
|
3837
4005
|
const s = p.spinner();
|
package/dist/server.js
CHANGED
|
@@ -26,6 +26,7 @@ var TsServerClient = class {
|
|
|
26
26
|
shuttingDown = false;
|
|
27
27
|
restartCount = 0;
|
|
28
28
|
maxRestarts = 3;
|
|
29
|
+
projectsDirty = false;
|
|
29
30
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
30
31
|
resolvePath(file) {
|
|
31
32
|
return path.isAbsolute(file) ? file : path.resolve(this.projectRoot, file);
|
|
@@ -202,6 +203,24 @@ var TsServerClient = class {
|
|
|
202
203
|
this.sendNotification("open", { file: absPath2 });
|
|
203
204
|
await new Promise((r) => setTimeout(r, 50));
|
|
204
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
208
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
209
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
210
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
211
|
+
* projects, producing definition-only reference results). The next query
|
|
212
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
213
|
+
* from disk. Open files are unaffected by design — their content is
|
|
214
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
215
|
+
*/
|
|
216
|
+
markProjectsDirty() {
|
|
217
|
+
this.projectsDirty = true;
|
|
218
|
+
}
|
|
219
|
+
refreshIfDirty() {
|
|
220
|
+
if (!this.projectsDirty) return;
|
|
221
|
+
this.projectsDirty = false;
|
|
222
|
+
this.sendNotification("reloadProjects");
|
|
223
|
+
}
|
|
205
224
|
async reloadOpenFile(file) {
|
|
206
225
|
const absPath2 = this.resolvePath(file);
|
|
207
226
|
if (!this.openFiles.has(absPath2)) return false;
|
|
@@ -219,6 +238,7 @@ var TsServerClient = class {
|
|
|
219
238
|
}
|
|
220
239
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
221
240
|
async definition(file, line, offset) {
|
|
241
|
+
this.refreshIfDirty();
|
|
222
242
|
const absPath2 = this.resolvePath(file);
|
|
223
243
|
await this.ensureOpen(absPath2);
|
|
224
244
|
const body = await this.sendRequest("definition", {
|
|
@@ -233,6 +253,7 @@ var TsServerClient = class {
|
|
|
233
253
|
}));
|
|
234
254
|
}
|
|
235
255
|
async references(file, line, offset) {
|
|
256
|
+
this.refreshIfDirty();
|
|
236
257
|
const absPath2 = this.resolvePath(file);
|
|
237
258
|
await this.ensureOpen(absPath2);
|
|
238
259
|
const body = await this.sendRequest("references", {
|
|
@@ -247,6 +268,7 @@ var TsServerClient = class {
|
|
|
247
268
|
}));
|
|
248
269
|
}
|
|
249
270
|
async quickinfo(file, line, offset) {
|
|
271
|
+
this.refreshIfDirty();
|
|
250
272
|
const absPath2 = this.resolvePath(file);
|
|
251
273
|
await this.ensureOpen(absPath2);
|
|
252
274
|
try {
|
|
@@ -261,6 +283,7 @@ var TsServerClient = class {
|
|
|
261
283
|
}
|
|
262
284
|
}
|
|
263
285
|
async navto(searchValue, maxResults = 10, file) {
|
|
286
|
+
this.refreshIfDirty();
|
|
264
287
|
if (file) await this.ensureOpen(file);
|
|
265
288
|
const args = {
|
|
266
289
|
searchValue,
|
|
@@ -275,6 +298,7 @@ var TsServerClient = class {
|
|
|
275
298
|
}));
|
|
276
299
|
}
|
|
277
300
|
async navbar(file) {
|
|
301
|
+
this.refreshIfDirty();
|
|
278
302
|
const absPath2 = this.resolvePath(file);
|
|
279
303
|
await this.ensureOpen(absPath2);
|
|
280
304
|
const body = await this.sendRequest("navbar", {
|
|
@@ -1695,11 +1719,18 @@ async function main() {
|
|
|
1695
1719
|
moduleGraph = graphResult.graph;
|
|
1696
1720
|
moduleResolver = graphResult.resolver;
|
|
1697
1721
|
startWatcher(projectRoot, moduleGraph, graphResult.resolver, {
|
|
1698
|
-
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).
|
|
1699
|
-
|
|
1700
|
-
|
|
1722
|
+
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).then(
|
|
1723
|
+
(wasOpen) => {
|
|
1724
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
1725
|
+
},
|
|
1726
|
+
(err) => {
|
|
1727
|
+
client.markProjectsDirty();
|
|
1728
|
+
log3(`Failed to reload open file ${relPath(filePath)}:`, err);
|
|
1729
|
+
}
|
|
1730
|
+
),
|
|
1701
1731
|
onFileDeleted: (filePath) => {
|
|
1702
1732
|
client.closeFile(filePath);
|
|
1733
|
+
client.markProjectsDirty();
|
|
1703
1734
|
}
|
|
1704
1735
|
});
|
|
1705
1736
|
const transport = new StdioServerTransport();
|
package/dist/smoke-test.js
CHANGED
|
@@ -24,6 +24,7 @@ var TsServerClient = class {
|
|
|
24
24
|
shuttingDown = false;
|
|
25
25
|
restartCount = 0;
|
|
26
26
|
maxRestarts = 3;
|
|
27
|
+
projectsDirty = false;
|
|
27
28
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
28
29
|
resolvePath(file) {
|
|
29
30
|
return path.isAbsolute(file) ? file : path.resolve(this.projectRoot, file);
|
|
@@ -200,6 +201,24 @@ var TsServerClient = class {
|
|
|
200
201
|
this.sendNotification("open", { file: absPath });
|
|
201
202
|
await new Promise((r) => setTimeout(r, 50));
|
|
202
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
206
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
207
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
208
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
209
|
+
* projects, producing definition-only reference results). The next query
|
|
210
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
211
|
+
* from disk. Open files are unaffected by design — their content is
|
|
212
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
213
|
+
*/
|
|
214
|
+
markProjectsDirty() {
|
|
215
|
+
this.projectsDirty = true;
|
|
216
|
+
}
|
|
217
|
+
refreshIfDirty() {
|
|
218
|
+
if (!this.projectsDirty) return;
|
|
219
|
+
this.projectsDirty = false;
|
|
220
|
+
this.sendNotification("reloadProjects");
|
|
221
|
+
}
|
|
203
222
|
async reloadOpenFile(file) {
|
|
204
223
|
const absPath = this.resolvePath(file);
|
|
205
224
|
if (!this.openFiles.has(absPath)) return false;
|
|
@@ -217,6 +236,7 @@ var TsServerClient = class {
|
|
|
217
236
|
}
|
|
218
237
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
219
238
|
async definition(file, line, offset) {
|
|
239
|
+
this.refreshIfDirty();
|
|
220
240
|
const absPath = this.resolvePath(file);
|
|
221
241
|
await this.ensureOpen(absPath);
|
|
222
242
|
const body = await this.sendRequest("definition", {
|
|
@@ -231,6 +251,7 @@ var TsServerClient = class {
|
|
|
231
251
|
}));
|
|
232
252
|
}
|
|
233
253
|
async references(file, line, offset) {
|
|
254
|
+
this.refreshIfDirty();
|
|
234
255
|
const absPath = this.resolvePath(file);
|
|
235
256
|
await this.ensureOpen(absPath);
|
|
236
257
|
const body = await this.sendRequest("references", {
|
|
@@ -245,6 +266,7 @@ var TsServerClient = class {
|
|
|
245
266
|
}));
|
|
246
267
|
}
|
|
247
268
|
async quickinfo(file, line, offset) {
|
|
269
|
+
this.refreshIfDirty();
|
|
248
270
|
const absPath = this.resolvePath(file);
|
|
249
271
|
await this.ensureOpen(absPath);
|
|
250
272
|
try {
|
|
@@ -259,6 +281,7 @@ var TsServerClient = class {
|
|
|
259
281
|
}
|
|
260
282
|
}
|
|
261
283
|
async navto(searchValue, maxResults = 10, file) {
|
|
284
|
+
this.refreshIfDirty();
|
|
262
285
|
if (file) await this.ensureOpen(file);
|
|
263
286
|
const args = {
|
|
264
287
|
searchValue,
|
|
@@ -273,6 +296,7 @@ var TsServerClient = class {
|
|
|
273
296
|
}));
|
|
274
297
|
}
|
|
275
298
|
async navbar(file) {
|
|
299
|
+
this.refreshIfDirty();
|
|
276
300
|
const absPath = this.resolvePath(file);
|
|
277
301
|
await this.ensureOpen(absPath);
|
|
278
302
|
const body = await this.sendRequest("navbar", {
|
package/dist/tsserver-client.js
CHANGED
|
@@ -19,6 +19,7 @@ var TsServerClient = class {
|
|
|
19
19
|
shuttingDown = false;
|
|
20
20
|
restartCount = 0;
|
|
21
21
|
maxRestarts = 3;
|
|
22
|
+
projectsDirty = false;
|
|
22
23
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
23
24
|
resolvePath(file) {
|
|
24
25
|
return path.isAbsolute(file) ? file : path.resolve(this.projectRoot, file);
|
|
@@ -195,6 +196,24 @@ var TsServerClient = class {
|
|
|
195
196
|
this.sendNotification("open", { file: absPath });
|
|
196
197
|
await new Promise((r) => setTimeout(r, 50));
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
201
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
202
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
203
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
204
|
+
* projects, producing definition-only reference results). The next query
|
|
205
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
206
|
+
* from disk. Open files are unaffected by design — their content is
|
|
207
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
208
|
+
*/
|
|
209
|
+
markProjectsDirty() {
|
|
210
|
+
this.projectsDirty = true;
|
|
211
|
+
}
|
|
212
|
+
refreshIfDirty() {
|
|
213
|
+
if (!this.projectsDirty) return;
|
|
214
|
+
this.projectsDirty = false;
|
|
215
|
+
this.sendNotification("reloadProjects");
|
|
216
|
+
}
|
|
198
217
|
async reloadOpenFile(file) {
|
|
199
218
|
const absPath = this.resolvePath(file);
|
|
200
219
|
if (!this.openFiles.has(absPath)) return false;
|
|
@@ -212,6 +231,7 @@ var TsServerClient = class {
|
|
|
212
231
|
}
|
|
213
232
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
214
233
|
async definition(file, line, offset) {
|
|
234
|
+
this.refreshIfDirty();
|
|
215
235
|
const absPath = this.resolvePath(file);
|
|
216
236
|
await this.ensureOpen(absPath);
|
|
217
237
|
const body = await this.sendRequest("definition", {
|
|
@@ -226,6 +246,7 @@ var TsServerClient = class {
|
|
|
226
246
|
}));
|
|
227
247
|
}
|
|
228
248
|
async references(file, line, offset) {
|
|
249
|
+
this.refreshIfDirty();
|
|
229
250
|
const absPath = this.resolvePath(file);
|
|
230
251
|
await this.ensureOpen(absPath);
|
|
231
252
|
const body = await this.sendRequest("references", {
|
|
@@ -240,6 +261,7 @@ var TsServerClient = class {
|
|
|
240
261
|
}));
|
|
241
262
|
}
|
|
242
263
|
async quickinfo(file, line, offset) {
|
|
264
|
+
this.refreshIfDirty();
|
|
243
265
|
const absPath = this.resolvePath(file);
|
|
244
266
|
await this.ensureOpen(absPath);
|
|
245
267
|
try {
|
|
@@ -254,6 +276,7 @@ var TsServerClient = class {
|
|
|
254
276
|
}
|
|
255
277
|
}
|
|
256
278
|
async navto(searchValue, maxResults = 10, file) {
|
|
279
|
+
this.refreshIfDirty();
|
|
257
280
|
if (file) await this.ensureOpen(file);
|
|
258
281
|
const args = {
|
|
259
282
|
searchValue,
|
|
@@ -268,6 +291,7 @@ var TsServerClient = class {
|
|
|
268
291
|
}));
|
|
269
292
|
}
|
|
270
293
|
async navbar(file) {
|
|
294
|
+
this.refreshIfDirty();
|
|
271
295
|
const absPath = this.resolvePath(file);
|
|
272
296
|
await this.ensureOpen(absPath);
|
|
273
297
|
const body = await this.sendRequest("navbar", {
|
package/engine-sync-test.ts
CHANGED
|
@@ -114,6 +114,12 @@ async function main(): Promise<void> {
|
|
|
114
114
|
);
|
|
115
115
|
writeFile(projectRoot, "src/test.ts", "export const oldName = 1 as const;\n");
|
|
116
116
|
writeFile(projectRoot, "src/util.ts", "export const helper = 1 as const;\n");
|
|
117
|
+
writeFile(projectRoot, "src/closed.ts", "export const closedValue = 1 as const;\n");
|
|
118
|
+
writeFile(
|
|
119
|
+
projectRoot,
|
|
120
|
+
"src/closed-consumer.ts",
|
|
121
|
+
'import { closedValue } from "./closed";\nexport const observed = closedValue;\n'
|
|
122
|
+
);
|
|
117
123
|
|
|
118
124
|
fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
|
|
119
125
|
fs.symlinkSync(
|
|
@@ -196,6 +202,25 @@ async function main(): Promise<void> {
|
|
|
196
202
|
assert.ok(!exportsResult.exports.some((item) => item.symbol === "oldName"));
|
|
197
203
|
});
|
|
198
204
|
|
|
205
|
+
// Change a file that tsserver has not opened. The watcher should mark
|
|
206
|
+
// projects dirty so the next semantic query reloads closed-file contents.
|
|
207
|
+
writeFile(projectRoot, "src/closed.ts", "export const closedValue = 2 as const;\n");
|
|
208
|
+
|
|
209
|
+
await waitFor("closed file update to refresh semantic project state", async () => {
|
|
210
|
+
const deps = await callTool<DependencyTreeResult>("ts_dependency_tree", {
|
|
211
|
+
file: "src/closed-consumer.ts",
|
|
212
|
+
});
|
|
213
|
+
const normalizedDeps = deps.files.map(normalize);
|
|
214
|
+
assert.ok(normalizedDeps.includes("src/closed.ts"), `Expected src/closed.ts in ${normalizedDeps}`);
|
|
215
|
+
|
|
216
|
+
const typeInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
217
|
+
file: "src/closed-consumer.ts",
|
|
218
|
+
line: 2,
|
|
219
|
+
column: 14,
|
|
220
|
+
});
|
|
221
|
+
assert.match(typeInfo.type ?? "", /\b2\b/);
|
|
222
|
+
});
|
|
223
|
+
|
|
199
224
|
// Open util.ts directly so tsserver tracks it, then delete it from disk.
|
|
200
225
|
const utilInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
201
226
|
file: "src/util.ts",
|
|
@@ -223,6 +248,7 @@ async function main(): Promise<void> {
|
|
|
223
248
|
console.log("==============================");
|
|
224
249
|
console.log(" ✓ import swaps keep dependency_tree and type_info aligned");
|
|
225
250
|
console.log(" ✓ export renames refresh ts_module_exports semantic metadata");
|
|
251
|
+
console.log(" ✓ closed-file edits refresh tsserver project state");
|
|
226
252
|
console.log(" ✓ deleted open files do not survive as tsserver ghost snapshots");
|
|
227
253
|
} finally {
|
|
228
254
|
await transport.close().catch(() => {});
|
package/install-oxlint-test.ts
CHANGED
|
@@ -22,13 +22,14 @@ function copyDir(src: string, dest: string): void {
|
|
|
22
22
|
function runTsx(
|
|
23
23
|
toolRoot: string,
|
|
24
24
|
args: string[],
|
|
25
|
-
cwd: string
|
|
25
|
+
cwd: string,
|
|
26
|
+
env: NodeJS.ProcessEnv = process.env
|
|
26
27
|
): string {
|
|
27
28
|
return execFileSync(path.join(toolRoot, "node_modules/.bin/tsx"), args, {
|
|
28
29
|
cwd,
|
|
29
30
|
encoding: "utf-8",
|
|
30
31
|
maxBuffer: 10 * 1024 * 1024,
|
|
31
|
-
env
|
|
32
|
+
env,
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -44,6 +45,7 @@ async function main(): Promise<void> {
|
|
|
44
45
|
const fixtureRoot = path.join(repoRoot, ".fixtures/install-oxlint");
|
|
45
46
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-install-oxlint-"));
|
|
46
47
|
const projectRoot = path.join(tempRoot, "project");
|
|
48
|
+
const homeRoot = path.join(tempRoot, "home");
|
|
47
49
|
|
|
48
50
|
copyDir(fixtureRoot, projectRoot);
|
|
49
51
|
fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
|
|
@@ -54,7 +56,14 @@ async function main(): Promise<void> {
|
|
|
54
56
|
);
|
|
55
57
|
|
|
56
58
|
try {
|
|
57
|
-
|
|
59
|
+
fs.mkdirSync(homeRoot, { recursive: true });
|
|
60
|
+
const testEnv = { ...process.env, HOME: homeRoot };
|
|
61
|
+
const setupOutput = runTsx(
|
|
62
|
+
repoRoot,
|
|
63
|
+
[path.join(repoRoot, "cli.ts"), "setup", "--yes"],
|
|
64
|
+
projectRoot,
|
|
65
|
+
testEnv
|
|
66
|
+
);
|
|
58
67
|
const pluginRoot = path.join(projectRoot, "plugins/typegraph-mcp");
|
|
59
68
|
|
|
60
69
|
const tsconfig = fs.readFileSync(path.join(projectRoot, "tsconfig.json"), "utf-8");
|
|
@@ -70,7 +79,12 @@ async function main(): Promise<void> {
|
|
|
70
79
|
assertIncludes(setupOutput, 'Added "plugins/**" to .oxlintrc.json ignorePatterns');
|
|
71
80
|
assertIncludes(setupOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
|
|
72
81
|
|
|
73
|
-
const checkOutput = runTsx(
|
|
82
|
+
const checkOutput = runTsx(
|
|
83
|
+
pluginRoot,
|
|
84
|
+
[path.join(pluginRoot, "cli.ts"), "check"],
|
|
85
|
+
projectRoot,
|
|
86
|
+
testEnv
|
|
87
|
+
);
|
|
74
88
|
assertIncludes(checkOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
|
|
75
89
|
assert.ok(
|
|
76
90
|
!checkOutput.includes("Lint config check (no ESLint or Oxlint config found)"),
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -1134,11 +1134,20 @@ async function main() {
|
|
|
1134
1134
|
moduleResolver = graphResult.resolver;
|
|
1135
1135
|
startWatcher(projectRoot, moduleGraph, graphResult.resolver, {
|
|
1136
1136
|
onFileUpdated: (filePath) =>
|
|
1137
|
-
client.reloadOpenFile(filePath).
|
|
1138
|
-
|
|
1139
|
-
|
|
1137
|
+
client.reloadOpenFile(filePath).then(
|
|
1138
|
+
(wasOpen) => {
|
|
1139
|
+
// Closed files: tsserver's own disk watching decays in long-lived
|
|
1140
|
+
// instances; force a projects reload before the next query.
|
|
1141
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
1142
|
+
},
|
|
1143
|
+
(err) => {
|
|
1144
|
+
client.markProjectsDirty();
|
|
1145
|
+
log(`Failed to reload open file ${relPath(filePath)}:`, err);
|
|
1146
|
+
}
|
|
1147
|
+
),
|
|
1140
1148
|
onFileDeleted: (filePath) => {
|
|
1141
1149
|
client.closeFile(filePath);
|
|
1150
|
+
client.markProjectsDirty();
|
|
1142
1151
|
},
|
|
1143
1152
|
});
|
|
1144
1153
|
|
package/tsserver-client.ts
CHANGED
|
@@ -90,6 +90,7 @@ export class TsServerClient {
|
|
|
90
90
|
private shuttingDown = false;
|
|
91
91
|
private restartCount = 0;
|
|
92
92
|
private readonly maxRestarts = 3;
|
|
93
|
+
private projectsDirty = false;
|
|
93
94
|
|
|
94
95
|
constructor(
|
|
95
96
|
private readonly projectRoot: string,
|
|
@@ -323,6 +324,29 @@ export class TsServerClient {
|
|
|
323
324
|
await new Promise((r) => setTimeout(r, 50));
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
329
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
330
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
331
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
332
|
+
* projects, producing definition-only reference results). The next query
|
|
333
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
334
|
+
* from disk. Open files are unaffected by design — their content is
|
|
335
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
336
|
+
*/
|
|
337
|
+
markProjectsDirty(): void {
|
|
338
|
+
this.projectsDirty = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private refreshIfDirty(): void {
|
|
342
|
+
if (!this.projectsDirty) return;
|
|
343
|
+
this.projectsDirty = false;
|
|
344
|
+
// reloadProjects sends no response (notRequired), so fire it as a
|
|
345
|
+
// notification; the following request queues behind the reload on
|
|
346
|
+
// tsserver's single-threaded loop.
|
|
347
|
+
this.sendNotification("reloadProjects");
|
|
348
|
+
}
|
|
349
|
+
|
|
326
350
|
async reloadOpenFile(file: string): Promise<boolean> {
|
|
327
351
|
const absPath = this.resolvePath(file);
|
|
328
352
|
if (!this.openFiles.has(absPath)) return false;
|
|
@@ -343,6 +367,7 @@ export class TsServerClient {
|
|
|
343
367
|
// ─── Public API ────────────────────────────────────────────────────────
|
|
344
368
|
|
|
345
369
|
async definition(file: string, line: number, offset: number): Promise<DefinitionResult[]> {
|
|
370
|
+
this.refreshIfDirty();
|
|
346
371
|
const absPath = this.resolvePath(file);
|
|
347
372
|
await this.ensureOpen(absPath);
|
|
348
373
|
|
|
@@ -361,6 +386,7 @@ export class TsServerClient {
|
|
|
361
386
|
}
|
|
362
387
|
|
|
363
388
|
async references(file: string, line: number, offset: number): Promise<ReferenceEntry[]> {
|
|
389
|
+
this.refreshIfDirty();
|
|
364
390
|
const absPath = this.resolvePath(file);
|
|
365
391
|
await this.ensureOpen(absPath);
|
|
366
392
|
|
|
@@ -379,6 +405,7 @@ export class TsServerClient {
|
|
|
379
405
|
}
|
|
380
406
|
|
|
381
407
|
async quickinfo(file: string, line: number, offset: number): Promise<QuickInfoResult | null> {
|
|
408
|
+
this.refreshIfDirty();
|
|
382
409
|
const absPath = this.resolvePath(file);
|
|
383
410
|
await this.ensureOpen(absPath);
|
|
384
411
|
|
|
@@ -398,6 +425,7 @@ export class TsServerClient {
|
|
|
398
425
|
}
|
|
399
426
|
|
|
400
427
|
async navto(searchValue: string, maxResults = 10, file?: string): Promise<NavToItem[]> {
|
|
428
|
+
this.refreshIfDirty();
|
|
401
429
|
// If a file is specified, open it first so tsserver knows about it
|
|
402
430
|
if (file) await this.ensureOpen(file);
|
|
403
431
|
|
|
@@ -418,6 +446,7 @@ export class TsServerClient {
|
|
|
418
446
|
}
|
|
419
447
|
|
|
420
448
|
async navbar(file: string): Promise<NavBarItem[]> {
|
|
449
|
+
this.refreshIfDirty();
|
|
421
450
|
const absPath = this.resolvePath(file);
|
|
422
451
|
await this.ensureOpen(absPath);
|
|
423
452
|
|