webnav-ts-mcp 0.1.0
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 +21 -0
- package/README.md +257 -0
- package/dist/cli.js +34 -0
- package/dist/glob.js +65 -0
- package/dist/langCommand.js +71 -0
- package/dist/outlineImports.js +46 -0
- package/dist/server.js +165 -0
- package/dist/shared/errors.js +80 -0
- package/dist/shared/exclude.js +32 -0
- package/dist/shared/format.js +598 -0
- package/dist/shared/log.js +11 -0
- package/dist/shared/lspClient.js +653 -0
- package/dist/shared/lspTypes.js +2 -0
- package/dist/shared/notices.js +83 -0
- package/dist/shared/params.js +33 -0
- package/dist/shared/paths.js +37 -0
- package/dist/shared/resolve.js +398 -0
- package/dist/shared/text.js +29 -0
- package/dist/shared/workspace.js +139 -0
- package/dist/webIndex.js +773 -0
- package/dist/webnav.js +586 -0
- package/package.json +61 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the workspace root for the server.
|
|
3
|
+
*
|
|
4
|
+
* Hosts may spawn stdio MCP processes with a cwd that is not the repo (e.g.
|
|
5
|
+
* Cursor using $HOME). Prefer an explicit override, then Claude Code's
|
|
6
|
+
* injected project dir, then the process's working directory: never a path
|
|
7
|
+
* baked into this package's install location, which would silently point every
|
|
8
|
+
* un-configured host at wherever the server happens to be installed from.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { debug } from "./log.js";
|
|
15
|
+
function realpath(p) {
|
|
16
|
+
try {
|
|
17
|
+
return fs.realpathSync(p);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return path.resolve(p);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function expandUser(p) {
|
|
24
|
+
return p === "~" || p.startsWith("~/") ? path.join(process.env.HOME ?? "", p.slice(1)) : p;
|
|
25
|
+
}
|
|
26
|
+
/** The statically configured workspace root and where it came from (`explicitEnv`, `CLAUDE_PROJECT_DIR` or `cwd`). */
|
|
27
|
+
export function describeWorkspaceRoot(explicitEnv) {
|
|
28
|
+
for (const key of [explicitEnv, "CLAUDE_PROJECT_DIR"]) {
|
|
29
|
+
const raw = process.env[key];
|
|
30
|
+
if (raw) {
|
|
31
|
+
return { root: realpath(expandUser(raw)), source: key };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { root: realpath(process.cwd()), source: "cwd" };
|
|
35
|
+
}
|
|
36
|
+
const gitCommonDirCache = new Map();
|
|
37
|
+
/** The repository's shared `.git` directory for `p` (identical for a checkout and all its linked worktrees), or undefined outside a git repo. */
|
|
38
|
+
function gitCommonDir(p) {
|
|
39
|
+
if (gitCommonDirCache.has(p)) {
|
|
40
|
+
return gitCommonDirCache.get(p);
|
|
41
|
+
}
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
const out = execFileSync("git", ["-C", p, "rev-parse", "--git-common-dir"], {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: 5000,
|
|
47
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
48
|
+
}).trim();
|
|
49
|
+
if (out) {
|
|
50
|
+
result = realpath(path.isAbsolute(out) ? out : path.join(p, out));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
result = undefined;
|
|
55
|
+
}
|
|
56
|
+
gitCommonDirCache.set(p, result);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* True when `a` and `b` are the same directory, or two checkouts/worktrees of
|
|
61
|
+
* one git repository. This is what makes a client-reported root safe to adopt:
|
|
62
|
+
* a session in some unrelated project must never redirect the server.
|
|
63
|
+
*/
|
|
64
|
+
export function sameRepository(a, b) {
|
|
65
|
+
if (a === b) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const commonA = gitCommonDir(a);
|
|
69
|
+
return commonA !== undefined && commonA === gitCommonDir(b);
|
|
70
|
+
}
|
|
71
|
+
function rootPaths(uris) {
|
|
72
|
+
const paths = [];
|
|
73
|
+
for (const uri of uris) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(uri);
|
|
76
|
+
if (parsed.protocol === "file:") {
|
|
77
|
+
paths.push(realpath(fileURLToPath(parsed)));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
return paths;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Decides which directory a navigation server operates on.
|
|
86
|
+
*
|
|
87
|
+
* Hosts start a project's MCP servers once, with the *main* checkout as
|
|
88
|
+
* `CLAUDE_PROJECT_DIR`/cwd, even when the session works in a git worktree, so
|
|
89
|
+
* a fixed root silently answers from the wrong tree. Order of authority:
|
|
90
|
+
*
|
|
91
|
+
* 1. `explicitEnv` set: pinned, never overridden.
|
|
92
|
+
* 2. The client's MCP roots (`roots/list`), first one that is the configured
|
|
93
|
+
* base or another worktree/checkout of the same git repository.
|
|
94
|
+
* 3. The configured base (`CLAUDE_PROJECT_DIR`, else cwd).
|
|
95
|
+
*/
|
|
96
|
+
export class WorkspaceSelector {
|
|
97
|
+
explicitEnv;
|
|
98
|
+
base;
|
|
99
|
+
baseSource;
|
|
100
|
+
pinned;
|
|
101
|
+
constructor(explicitEnv) {
|
|
102
|
+
this.explicitEnv = explicitEnv;
|
|
103
|
+
({ root: this.base, source: this.baseSource } = describeWorkspaceRoot(explicitEnv));
|
|
104
|
+
this.pinned = Boolean(process.env[explicitEnv]);
|
|
105
|
+
}
|
|
106
|
+
/** One sentence an agent can act on for a `Selection.source` value. */
|
|
107
|
+
explain(source) {
|
|
108
|
+
if (source === "client roots") {
|
|
109
|
+
return `client roots (the MCP client reported this checkout/worktree of the same repository as the configured base ${this.base})`;
|
|
110
|
+
}
|
|
111
|
+
if (source === this.explicitEnv) {
|
|
112
|
+
return `pinned by $${this.explicitEnv}; client roots are ignored`;
|
|
113
|
+
}
|
|
114
|
+
if (source === "CLAUDE_PROJECT_DIR") {
|
|
115
|
+
return "$CLAUDE_PROJECT_DIR (default; the client has not reported another checkout/worktree of this repository)";
|
|
116
|
+
}
|
|
117
|
+
return `server working directory ($${this.explicitEnv} and $CLAUDE_PROJECT_DIR are unset)`;
|
|
118
|
+
}
|
|
119
|
+
async select(rootsProvider) {
|
|
120
|
+
if (this.pinned) {
|
|
121
|
+
return { root: this.base, source: this.baseSource };
|
|
122
|
+
}
|
|
123
|
+
for (const root of await this.clientRoots(rootsProvider)) {
|
|
124
|
+
if (sameRepository(root, this.base)) {
|
|
125
|
+
return { root, source: "client roots" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { root: this.base, source: this.baseSource };
|
|
129
|
+
}
|
|
130
|
+
async clientRoots(rootsProvider) {
|
|
131
|
+
try {
|
|
132
|
+
return rootPaths((await rootsProvider()) ?? []);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
debug("roots/list failed; keeping the configured workspace", error);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|