@ttsc/graph 0.16.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeongho Nam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@ttsc/graph`
2
+
3
+ ![banner of @ttsc/graph](https://ttsc.dev/og.jpg)
4
+
5
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE) [![NPM Version](https://img.shields.io/npm/v/@ttsc/graph.svg)](https://www.npmjs.com/package/@ttsc/graph) [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/graph.svg)](https://www.npmjs.com/package/@ttsc/graph) [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs/graph) [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
6
+
7
+ Gives your AI coding assistant a **map of your codebase**. It can answer "how does this work?" without opening file after file.
8
+
9
+ When you ask an AI agent (Claude Code, Codex, and the like) about your project, it pokes around. It opens a file, follows an import, opens another, and on and on.
10
+
11
+ `@ttsc/graph` hands it the map up front: what calls what, what depends on what, and where each thing lives.
12
+
13
+ The map comes from the real TypeScript compiler. So it is exact — not a guess from skimming text.
14
+
15
+ ## What your agent gets
16
+
17
+ Say you ask: _"how does the editor draw a shape?"_
18
+
19
+ Your agent looks it up once instead of reading a dozen files, and gets back something like this:
20
+
21
+ ```
22
+ ShapeRenderer — src/render/shape.ts (line 18)
23
+ → calls rasterize(), new Canvas()
24
+ ← used by Editor
25
+ change it and 9 other things are affected
26
+ 18 export class ShapeRenderer {
27
+ 19 constructor(private canvas: Canvas) {}
28
+ ...
29
+ ```
30
+
31
+ One step shows what this thing calls, what uses it, and its source.
32
+
33
+ The answer comes from the compiler, so it is precise. When one file just re-exports something from another, the map still points at the file that really defines it.
34
+
35
+ Anything from `node_modules` is left out, because that is not your code.
36
+
37
+ On a public benchmark, a Claude agent answered reading zero files, cutting its token use by **77% to 86%** and its tool calls by **94% to 95%**.
38
+
39
+ See the [benchmark](https://ttsc.dev/docs/benchmark#code-graph-mcp) for the full numbers.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ npm install -D ttsc @ttsc/graph typescript@rc
45
+ ```
46
+
47
+ Install `ttsc` alongside it. `@ttsc/graph` runs through `ttsc`, so the two go together — the same pair as `@ttsc/lint`.
48
+
49
+ There is nothing else to set up: no separate compiler, no Go.
50
+
51
+ ## Connect it to your agent
52
+
53
+ Add this to your AI tool's config once. For Claude Code, that is a `.mcp.json` file in your project:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "ttsc-graph": {
59
+ "command": "npx",
60
+ "args": ["-y", "@ttsc/graph"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Start your agent from your project folder so it finds your `tsconfig.json`.
67
+
68
+ Your agent now has two new abilities:
69
+
70
+ | Ability | What it does |
71
+ | --- | --- |
72
+ | `graph_explore` | Look up a name or a file. Get back what it connects to — what it calls, what uses it, its types — and its source code. |
73
+ | `graph_diagnostics` | Get the errors in one file: TypeScript type errors, plus your project's `@ttsc/lint` and plugin findings, as `ttsc` reports them. |
74
+
75
+ You never call these yourself. Your agent uses them when it needs to.
76
+
77
+ Claude Code does this on its own. Codex is more cautious with third-party MCP tools and often explores with the shell instead, so tell it directly — add a line to your `AGENTS.md` asking it to call `graph_explore` first. See the [setup guide](https://ttsc.dev/docs/setup#codex-and-other-tool-conservative-agents).
78
+
79
+ ## How it works
80
+
81
+ `ttsc` already type-checks your project with the real TypeScript compiler and keeps the result in memory.
82
+
83
+ `@ttsc/graph` reads the map straight out of that. Every connection it shows is the compiler's own answer, not a guess.
84
+
85
+ That is why it is exact, and why it is fast: nothing is recompiled to answer a question.
86
+
87
+ ## References
88
+
89
+ `@ttsc/graph` is inspired by [codegraph](https://github.com/colbymchenry/codegraph), which gives agents a code graph over MCP. The [benchmark](https://ttsc.dev/docs/benchmark#code-graph-mcp) here is a faithful port of codegraph's.
90
+
91
+ The difference is how the map is built.
92
+
93
+ codegraph reads your code with tree-sitter, across many languages, and saves the result in a database it keeps in sync as files change. Its edges are a best guess from the syntax.
94
+
95
+ `@ttsc/graph` is TypeScript-only and keeps no database. It reads the graph live from the program `ttsc` already type-checked in memory.
96
+
97
+ So every edge is the compiler's own answer: a re-export is followed to the file that really defines the symbol, an external library is left out, and nothing is guessed.
package/lib/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/lib/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_1 = require("./index");
5
+ const code = (0, index_1.runGraph)(process.argv.slice(2));
6
+ if (typeof code === "number") {
7
+ process.exitCode = code;
8
+ }
9
+ //# sourceMappingURL=bin.js.map
package/lib/bin.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";;;AAEA,mCAAmC;AAEnC,MAAM,IAAI,GAAG,IAAA,gBAAQ,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7C,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;IAC7B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Background worker that computes the project's plugin diagnostics and writes
3
+ * them where the ttscgraph server reads them.
4
+ *
5
+ * The graph binary runs only the TypeScript semantic pass. Everything a plugin
6
+ * adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
7
+ * findings — comes from `ttsc`'s own check, which runs whatever plugins the
8
+ * project configured. This worker invokes that check through the public
9
+ * `TtscCompiler`, so nothing here is specific to any plugin: it forwards
10
+ * whatever diagnostics ttsc produces.
11
+ *
12
+ * A native plugin reports its findings with a string `code` (tsc uses numeric
13
+ * codes), so the string-coded diagnostics are exactly the plugin/lint set the
14
+ * graph does not already have. They are serialized with code 0 and the rule
15
+ * folded into the message, the shape ttscgraph's injected-diagnostics provider
16
+ * consumes.
17
+ *
18
+ * Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
19
+ * build a plugin, a project that does not compile — any of these simply leaves
20
+ * no file, and the graph shows its tsc diagnostics alone. The worker must never
21
+ * be able to break the server it feeds.
22
+ */
23
+ export declare function runDiagnosticsWorker(argv?: readonly string[]): void;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDiagnosticsWorker = runDiagnosticsWorker;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_module_1 = require("node:module");
9
+ /**
10
+ * Background worker that computes the project's plugin diagnostics and writes
11
+ * them where the ttscgraph server reads them.
12
+ *
13
+ * The graph binary runs only the TypeScript semantic pass. Everything a plugin
14
+ * adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
15
+ * findings — comes from `ttsc`'s own check, which runs whatever plugins the
16
+ * project configured. This worker invokes that check through the public
17
+ * `TtscCompiler`, so nothing here is specific to any plugin: it forwards
18
+ * whatever diagnostics ttsc produces.
19
+ *
20
+ * A native plugin reports its findings with a string `code` (tsc uses numeric
21
+ * codes), so the string-coded diagnostics are exactly the plugin/lint set the
22
+ * graph does not already have. They are serialized with code 0 and the rule
23
+ * folded into the message, the shape ttscgraph's injected-diagnostics provider
24
+ * consumes.
25
+ *
26
+ * Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
27
+ * build a plugin, a project that does not compile — any of these simply leaves
28
+ * no file, and the graph shows its tsc diagnostics alone. The worker must never
29
+ * be able to break the server it feeds.
30
+ */
31
+ function runDiagnosticsWorker(argv = process.argv.slice(2)) {
32
+ const [cwd, tsconfig, outPath] = argv;
33
+ if (!cwd || !outPath) {
34
+ return;
35
+ }
36
+ try {
37
+ const ttscPackageJson = require.resolve("ttsc/package.json", {
38
+ paths: [cwd],
39
+ });
40
+ const fromProject = (0, node_module_1.createRequire)(ttscPackageJson);
41
+ const { TtscCompiler } = fromProject("ttsc");
42
+ const result = new TtscCompiler({
43
+ cwd,
44
+ tsconfig: tsconfig || undefined,
45
+ }).compile();
46
+ const raw = result &&
47
+ typeof result === "object" &&
48
+ Array.isArray(result.diagnostics)
49
+ ? (result.diagnostics)
50
+ : [];
51
+ const out = raw
52
+ .filter((d) => typeof d.file === "string" &&
53
+ (typeof d.line === "number" || typeof d.start === "number"))
54
+ .map((d) => ({
55
+ file: d.file,
56
+ // A byte offset when the structured lane gives one; otherwise null and
57
+ // the server attributes by line. @ttsc/lint and transform plugins reach
58
+ // the result through ttsc's text banner, which carries a line but no
59
+ // offset.
60
+ start: typeof d.start === "number" ? d.start : null,
61
+ line: typeof d.line === "number" ? d.line : 1,
62
+ column: typeof d.character === "number" ? d.character : 1,
63
+ // tsc diagnostics use numeric codes; @ttsc/lint and native plugins hash
64
+ // their rule to a code >= 9000. A rare string id is marked non-tsc (the
65
+ // server then drops the "TS" prefix); the rule name travels in the
66
+ // message regardless.
67
+ code: typeof d.code === "number" ? d.code : 9000,
68
+ message: String(d.messageText ?? ""),
69
+ }));
70
+ // Atomic publish: write to a sibling temp file and rename, so the server
71
+ // never reads a half-written file (a partial read would drop every finding
72
+ // for that query).
73
+ const tmp = `${outPath}.${process.pid}.tmp`;
74
+ node_fs_1.default.writeFileSync(tmp, JSON.stringify(out));
75
+ node_fs_1.default.renameSync(tmp, outPath);
76
+ }
77
+ catch {
78
+ // Resilient by contract: no file means the graph shows tsc-only diagnostics.
79
+ }
80
+ }
81
+ if (require.main === module) {
82
+ runDiagnosticsWorker();
83
+ }
84
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":";;;;;;AAAA,sDAAyB;AACzB,6CAA4C;AAE5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8BACE,IAAI,GAAsB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/C,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAC3D,KAAK,EAAE,CAAC,GAAG,CAAC;SACb,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,IAAA,2BAAa,EAAC,eAAe,CAAC,CAAC;QACnD,MAAM,EAAE,YAAY,EAAE,GAAG,WAAW,CAAC,MAAM,CAK1C,CAAC;QAEF,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;YAC9B,GAAG;YACH,QAAQ,EAAE,QAAQ,IAAI,SAAS;SAChC,CAAC,CAAC,OAAO,EAAE,CAAC;QAEb,MAAM,GAAG,GACP,MAAM;YACN,OAAO,MAAM,KAAK,QAAQ;YAC1B,KAAK,CAAC,OAAO,CAAE,MAAoC,CAAC,WAAW,CAAC;YAC9D,CAAC,CAAC,CAAE,MAAqD,CAAC,WAAW,CAAC;YACtE,CAAC,CAAC,EAAE,CAAC;QAET,MAAM,GAAG,GAAG,GAAG;aACZ,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;YAC1B,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAC9D;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,IAAc;YACtB,uEAAuE;YACvE,wEAAwE;YACxE,qEAAqE;YACrE,UAAU;YACV,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,KAAgB,CAAC,CAAC,CAAC,IAAI;YAC/D,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAe,CAAC,CAAC,CAAC,CAAC;YACzD,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,SAAoB,CAAC,CAAC,CAAC,CAAC;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,mEAAmE;YACnE,sBAAsB;YACtB,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAe,CAAC,CAAC,CAAC,IAAI;YAC5D,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;SACrC,CAAC,CAAC,CAAC;QAEN,yEAAyE;QACzE,2EAA2E;QAC3E,mBAAmB;QACnB,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;QAC5C,iBAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,iBAAE,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,CAAC;AACzB,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
3
+ * cannot be located.
4
+ *
5
+ * `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
6
+ * of this launcher), so resolution starts from the user's project, not from
7
+ * this package's own tree.
8
+ *
9
+ * Resolution order:
10
+ *
11
+ * 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
12
+ * 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
13
+ * That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
14
+ * an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
15
+ * location — found from `process.cwd()` (the project where the agent ran the
16
+ * server).
17
+ */
18
+ export declare function resolveGraphBinary(env?: NodeJS.ProcessEnv, cwd?: string): string | null;
19
+ /**
20
+ * Spawn the resident MCP server, inheriting stdio so the agent's MCP client
21
+ * speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
22
+ * child's exit code.
23
+ *
24
+ * Before spawning, it kicks off the background diagnostics worker (except in
25
+ * `--connect` proxy mode) and points the server at its output file, so a
26
+ * plugin-using project's lint and transform-plugin diagnostics fuse onto the
27
+ * graph without blocking startup.
28
+ */
29
+ export declare function runGraph(argv?: readonly string[]): number;
package/lib/index.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveGraphBinary = resolveGraphBinary;
7
+ exports.runGraph = runGraph;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_module_1 = require("node:module");
11
+ const node_os_1 = __importDefault(require("node:os"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ /**
14
+ * Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
15
+ * cannot be located.
16
+ *
17
+ * `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
18
+ * of this launcher), so resolution starts from the user's project, not from
19
+ * this package's own tree.
20
+ *
21
+ * Resolution order:
22
+ *
23
+ * 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
24
+ * 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
25
+ * That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
26
+ * an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
27
+ * location — found from `process.cwd()` (the project where the agent ran the
28
+ * server).
29
+ */
30
+ function resolveGraphBinary(env = process.env, cwd = process.cwd()) {
31
+ if (env.TTSC_GRAPH_BINARY && node_path_1.default.isAbsolute(env.TTSC_GRAPH_BINARY)) {
32
+ return env.TTSC_GRAPH_BINARY;
33
+ }
34
+ const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
35
+ try {
36
+ const ttscPackageJson = require.resolve("ttsc/package.json", {
37
+ paths: [cwd],
38
+ });
39
+ const fromTtsc = (0, node_module_1.createRequire)(ttscPackageJson);
40
+ return fromTtsc.resolve(`@ttsc/${process.platform}-${process.arch}/bin/${exe}`);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** The project root and tsconfig the server was pointed at, mirroring the
47
+ * `--cwd` / `--tsconfig` flags ttscgraph itself parses, so the background
48
+ * diagnostics worker checks the same project. */
49
+ function parseProjectArgs(argv) {
50
+ let cwd = process.cwd();
51
+ let tsconfig = "tsconfig.json";
52
+ for (let i = 0; i < argv.length; i++) {
53
+ const arg = argv[i];
54
+ if (arg === "--cwd" && i + 1 < argv.length)
55
+ cwd = argv[++i];
56
+ else if (arg.startsWith("--cwd="))
57
+ cwd = arg.slice("--cwd=".length);
58
+ else if (arg === "--tsconfig" && i + 1 < argv.length)
59
+ tsconfig = argv[++i];
60
+ else if (arg.startsWith("--tsconfig="))
61
+ tsconfig = arg.slice("--tsconfig=".length);
62
+ }
63
+ return { cwd: node_path_1.default.resolve(cwd), tsconfig };
64
+ }
65
+ /** A `--connect` proxy pipes stdio to a running daemon and serves no graph of
66
+ * its own, so it needs no diagnostics computed locally. */
67
+ function isConnectProxy(argv) {
68
+ return argv.some((a) => a === "--connect" || a.startsWith("--connect="));
69
+ }
70
+ /**
71
+ * Start the background worker that computes the project's plugin diagnostics
72
+ * (`@ttsc/lint` and transform-plugin findings) and writes them to
73
+ * `diagnosticsFile`, where the server picks them up. Returns the child handle
74
+ * for cleanup, or `null` when it could not be spawned.
75
+ *
76
+ * It runs in parallel with the server so the MCP handshake is never blocked on
77
+ * a compile; the server shows its tsc diagnostics immediately and fuses the
78
+ * plugin findings once the file lands. Any failure is non-fatal — the graph
79
+ * simply stays tsc-only.
80
+ */
81
+ function startDiagnosticsWorker(argv, diagnosticsFile) {
82
+ try {
83
+ const { cwd, tsconfig } = parseProjectArgs(argv);
84
+ const worker = (0, node_child_process_1.spawn)(process.execPath, [require.resolve("./diagnostics.js"), cwd, tsconfig, diagnosticsFile], { stdio: "ignore", windowsHide: true });
85
+ worker.on("error", () => {
86
+ /* resilient: a worker that cannot start leaves no file */
87
+ });
88
+ worker.unref();
89
+ return worker;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Spawn the resident MCP server, inheriting stdio so the agent's MCP client
97
+ * speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
98
+ * child's exit code.
99
+ *
100
+ * Before spawning, it kicks off the background diagnostics worker (except in
101
+ * `--connect` proxy mode) and points the server at its output file, so a
102
+ * plugin-using project's lint and transform-plugin diagnostics fuse onto the
103
+ * graph without blocking startup.
104
+ */
105
+ function runGraph(argv = process.argv.slice(2)) {
106
+ const binary = resolveGraphBinary();
107
+ if (binary === null) {
108
+ process.stderr.write("@ttsc/graph: could not resolve the ttscgraph binary. " +
109
+ "Install `ttsc` so its platform package is present, " +
110
+ "or set TTSC_GRAPH_BINARY to an absolute path.\n");
111
+ return 1;
112
+ }
113
+ const env = { ...process.env };
114
+ let diagnosticsFile = null;
115
+ let worker = null;
116
+ if (!isConnectProxy(argv)) {
117
+ diagnosticsFile = node_path_1.default.join(node_os_1.default.tmpdir(), `ttsc-graph-diagnostics-${process.pid}.json`);
118
+ env.TTSC_GRAPH_DIAGNOSTICS_FILE = diagnosticsFile;
119
+ worker = startDiagnosticsWorker(argv, diagnosticsFile);
120
+ }
121
+ const result = (0, node_child_process_1.spawnSync)(binary, [...argv], {
122
+ stdio: "inherit",
123
+ env,
124
+ windowsHide: true,
125
+ });
126
+ const workerPid = worker?.pid;
127
+ try {
128
+ worker?.kill();
129
+ }
130
+ catch {
131
+ /* ignore */
132
+ }
133
+ if (diagnosticsFile) {
134
+ try {
135
+ node_fs_1.default.rmSync(diagnosticsFile, { force: true });
136
+ }
137
+ catch {
138
+ /* ignore */
139
+ }
140
+ // The worker writes atomically through `<file>.<pid>.tmp`; remove a leftover
141
+ // if it was killed mid-write, so a churning daemon does not litter tmpdir.
142
+ if (workerPid !== undefined) {
143
+ try {
144
+ node_fs_1.default.rmSync(`${diagnosticsFile}.${workerPid}.tmp`, { force: true });
145
+ }
146
+ catch {
147
+ /* ignore */
148
+ }
149
+ }
150
+ }
151
+ if (result.error) {
152
+ process.stderr.write(`@ttsc/graph: ${result.error.message}\n`);
153
+ return 1;
154
+ }
155
+ return result.status ?? 1;
156
+ }
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@ttsc/graph",
3
+ "version": "0.16.4",
4
+ "description": "Checker-resolved code graph and diagnostics over MCP for coding agents, backed by ttsc's in-process TypeScript-Go compiler.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "bin": {
8
+ "ttsc-graph": "lib/bin.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./lib/index.d.ts",
13
+ "default": "./lib/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "keywords": [
18
+ "ttsc",
19
+ "mcp",
20
+ "code-graph",
21
+ "typescript",
22
+ "tsgo"
23
+ ],
24
+ "files": [
25
+ "README.md",
26
+ "lib",
27
+ "src"
28
+ ],
29
+ "devDependencies": {
30
+ "@types/node": "^25.3.0",
31
+ "rimraf": "^6.1.2",
32
+ "typescript": "7.0.1-rc",
33
+ "ttsc": "0.16.4"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/samchon/ttsc"
38
+ },
39
+ "author": "Jeongho Nam",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/samchon/ttsc/issues"
43
+ },
44
+ "homepage": "https://ttsc.dev",
45
+ "scripts": {
46
+ "build": "rimraf lib && tsc"
47
+ }
48
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runGraph } from "./index";
4
+
5
+ const code = runGraph(process.argv.slice(2));
6
+ if (typeof code === "number") {
7
+ process.exitCode = code;
8
+ }
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+
4
+ /**
5
+ * Background worker that computes the project's plugin diagnostics and writes
6
+ * them where the ttscgraph server reads them.
7
+ *
8
+ * The graph binary runs only the TypeScript semantic pass. Everything a plugin
9
+ * adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
10
+ * findings — comes from `ttsc`'s own check, which runs whatever plugins the
11
+ * project configured. This worker invokes that check through the public
12
+ * `TtscCompiler`, so nothing here is specific to any plugin: it forwards
13
+ * whatever diagnostics ttsc produces.
14
+ *
15
+ * A native plugin reports its findings with a string `code` (tsc uses numeric
16
+ * codes), so the string-coded diagnostics are exactly the plugin/lint set the
17
+ * graph does not already have. They are serialized with code 0 and the rule
18
+ * folded into the message, the shape ttscgraph's injected-diagnostics provider
19
+ * consumes.
20
+ *
21
+ * Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
22
+ * build a plugin, a project that does not compile — any of these simply leaves
23
+ * no file, and the graph shows its tsc diagnostics alone. The worker must never
24
+ * be able to break the server it feeds.
25
+ */
26
+ export function runDiagnosticsWorker(
27
+ argv: readonly string[] = process.argv.slice(2),
28
+ ): void {
29
+ const [cwd, tsconfig, outPath] = argv;
30
+ if (!cwd || !outPath) {
31
+ return;
32
+ }
33
+ try {
34
+ const ttscPackageJson = require.resolve("ttsc/package.json", {
35
+ paths: [cwd],
36
+ });
37
+ const fromProject = createRequire(ttscPackageJson);
38
+ const { TtscCompiler } = fromProject("ttsc") as {
39
+ TtscCompiler: new (context: {
40
+ cwd: string;
41
+ tsconfig?: string;
42
+ }) => { compile: () => unknown };
43
+ };
44
+
45
+ const result = new TtscCompiler({
46
+ cwd,
47
+ tsconfig: tsconfig || undefined,
48
+ }).compile();
49
+
50
+ const raw: ReadonlyArray<Record<string, unknown>> =
51
+ result &&
52
+ typeof result === "object" &&
53
+ Array.isArray((result as { diagnostics?: unknown }).diagnostics)
54
+ ? ((result as { diagnostics: Record<string, unknown>[] }).diagnostics)
55
+ : [];
56
+
57
+ const out = raw
58
+ .filter(
59
+ (d) =>
60
+ typeof d.file === "string" &&
61
+ (typeof d.line === "number" || typeof d.start === "number"),
62
+ )
63
+ .map((d) => ({
64
+ file: d.file as string,
65
+ // A byte offset when the structured lane gives one; otherwise null and
66
+ // the server attributes by line. @ttsc/lint and transform plugins reach
67
+ // the result through ttsc's text banner, which carries a line but no
68
+ // offset.
69
+ start: typeof d.start === "number" ? (d.start as number) : null,
70
+ line: typeof d.line === "number" ? (d.line as number) : 1,
71
+ column: typeof d.character === "number" ? (d.character as number) : 1,
72
+ // tsc diagnostics use numeric codes; @ttsc/lint and native plugins hash
73
+ // their rule to a code >= 9000. A rare string id is marked non-tsc (the
74
+ // server then drops the "TS" prefix); the rule name travels in the
75
+ // message regardless.
76
+ code: typeof d.code === "number" ? (d.code as number) : 9000,
77
+ message: String(d.messageText ?? ""),
78
+ }));
79
+
80
+ // Atomic publish: write to a sibling temp file and rename, so the server
81
+ // never reads a half-written file (a partial read would drop every finding
82
+ // for that query).
83
+ const tmp = `${outPath}.${process.pid}.tmp`;
84
+ fs.writeFileSync(tmp, JSON.stringify(out));
85
+ fs.renameSync(tmp, outPath);
86
+ } catch {
87
+ // Resilient by contract: no file means the graph shows tsc-only diagnostics.
88
+ }
89
+ }
90
+
91
+ if (require.main === module) {
92
+ runDiagnosticsWorker();
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,171 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ /**
8
+ * Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
9
+ * cannot be located.
10
+ *
11
+ * `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
12
+ * of this launcher), so resolution starts from the user's project, not from
13
+ * this package's own tree.
14
+ *
15
+ * Resolution order:
16
+ *
17
+ * 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
18
+ * 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
19
+ * That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
20
+ * an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
21
+ * location — found from `process.cwd()` (the project where the agent ran the
22
+ * server).
23
+ */
24
+ export function resolveGraphBinary(
25
+ env: NodeJS.ProcessEnv = process.env,
26
+ cwd: string = process.cwd(),
27
+ ): string | null {
28
+ if (env.TTSC_GRAPH_BINARY && path.isAbsolute(env.TTSC_GRAPH_BINARY)) {
29
+ return env.TTSC_GRAPH_BINARY;
30
+ }
31
+ const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
32
+ try {
33
+ const ttscPackageJson = require.resolve("ttsc/package.json", {
34
+ paths: [cwd],
35
+ });
36
+ const fromTtsc = createRequire(ttscPackageJson);
37
+ return fromTtsc.resolve(
38
+ `@ttsc/${process.platform}-${process.arch}/bin/${exe}`,
39
+ );
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** The project root and tsconfig the server was pointed at, mirroring the
46
+ * `--cwd` / `--tsconfig` flags ttscgraph itself parses, so the background
47
+ * diagnostics worker checks the same project. */
48
+ function parseProjectArgs(argv: readonly string[]): {
49
+ cwd: string;
50
+ tsconfig: string;
51
+ } {
52
+ let cwd = process.cwd();
53
+ let tsconfig = "tsconfig.json";
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const arg = argv[i]!;
56
+ if (arg === "--cwd" && i + 1 < argv.length) cwd = argv[++i]!;
57
+ else if (arg.startsWith("--cwd=")) cwd = arg.slice("--cwd=".length);
58
+ else if (arg === "--tsconfig" && i + 1 < argv.length) tsconfig = argv[++i]!;
59
+ else if (arg.startsWith("--tsconfig=")) tsconfig = arg.slice("--tsconfig=".length);
60
+ }
61
+ return { cwd: path.resolve(cwd), tsconfig };
62
+ }
63
+
64
+ /** A `--connect` proxy pipes stdio to a running daemon and serves no graph of
65
+ * its own, so it needs no diagnostics computed locally. */
66
+ function isConnectProxy(argv: readonly string[]): boolean {
67
+ return argv.some((a) => a === "--connect" || a.startsWith("--connect="));
68
+ }
69
+
70
+ /**
71
+ * Start the background worker that computes the project's plugin diagnostics
72
+ * (`@ttsc/lint` and transform-plugin findings) and writes them to
73
+ * `diagnosticsFile`, where the server picks them up. Returns the child handle
74
+ * for cleanup, or `null` when it could not be spawned.
75
+ *
76
+ * It runs in parallel with the server so the MCP handshake is never blocked on
77
+ * a compile; the server shows its tsc diagnostics immediately and fuses the
78
+ * plugin findings once the file lands. Any failure is non-fatal — the graph
79
+ * simply stays tsc-only.
80
+ */
81
+ function startDiagnosticsWorker(
82
+ argv: readonly string[],
83
+ diagnosticsFile: string,
84
+ ): ReturnType<typeof spawn> | null {
85
+ try {
86
+ const { cwd, tsconfig } = parseProjectArgs(argv);
87
+ const worker = spawn(
88
+ process.execPath,
89
+ [require.resolve("./diagnostics.js"), cwd, tsconfig, diagnosticsFile],
90
+ { stdio: "ignore", windowsHide: true },
91
+ );
92
+ worker.on("error", () => {
93
+ /* resilient: a worker that cannot start leaves no file */
94
+ });
95
+ worker.unref();
96
+ return worker;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Spawn the resident MCP server, inheriting stdio so the agent's MCP client
104
+ * speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
105
+ * child's exit code.
106
+ *
107
+ * Before spawning, it kicks off the background diagnostics worker (except in
108
+ * `--connect` proxy mode) and points the server at its output file, so a
109
+ * plugin-using project's lint and transform-plugin diagnostics fuse onto the
110
+ * graph without blocking startup.
111
+ */
112
+ export function runGraph(
113
+ argv: readonly string[] = process.argv.slice(2),
114
+ ): number {
115
+ const binary = resolveGraphBinary();
116
+ if (binary === null) {
117
+ process.stderr.write(
118
+ "@ttsc/graph: could not resolve the ttscgraph binary. " +
119
+ "Install `ttsc` so its platform package is present, " +
120
+ "or set TTSC_GRAPH_BINARY to an absolute path.\n",
121
+ );
122
+ return 1;
123
+ }
124
+
125
+ const env: NodeJS.ProcessEnv = { ...process.env };
126
+ let diagnosticsFile: string | null = null;
127
+ let worker: ReturnType<typeof spawn> | null = null;
128
+ if (!isConnectProxy(argv)) {
129
+ diagnosticsFile = path.join(
130
+ os.tmpdir(),
131
+ `ttsc-graph-diagnostics-${process.pid}.json`,
132
+ );
133
+ env.TTSC_GRAPH_DIAGNOSTICS_FILE = diagnosticsFile;
134
+ worker = startDiagnosticsWorker(argv, diagnosticsFile);
135
+ }
136
+
137
+ const result = spawnSync(binary, [...argv], {
138
+ stdio: "inherit",
139
+ env,
140
+ windowsHide: true,
141
+ });
142
+
143
+ const workerPid = worker?.pid;
144
+ try {
145
+ worker?.kill();
146
+ } catch {
147
+ /* ignore */
148
+ }
149
+ if (diagnosticsFile) {
150
+ try {
151
+ fs.rmSync(diagnosticsFile, { force: true });
152
+ } catch {
153
+ /* ignore */
154
+ }
155
+ // The worker writes atomically through `<file>.<pid>.tmp`; remove a leftover
156
+ // if it was killed mid-write, so a churning daemon does not litter tmpdir.
157
+ if (workerPid !== undefined) {
158
+ try {
159
+ fs.rmSync(`${diagnosticsFile}.${workerPid}.tmp`, { force: true });
160
+ } catch {
161
+ /* ignore */
162
+ }
163
+ }
164
+ }
165
+
166
+ if (result.error) {
167
+ process.stderr.write(`@ttsc/graph: ${result.error.message}\n`);
168
+ return 1;
169
+ }
170
+ return result.status ?? 1;
171
+ }