@coderule/mcp 1.8.0 → 2.0.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/dist/mcp-cli.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import fs4 from 'fs/promises';
3
- import path from 'path';
2
+ import fs5 from 'fs/promises';
3
+ import path2 from 'path';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { createHash } from 'crypto';
6
6
  import envPaths from 'env-paths';
7
7
  import pino from 'pino';
8
+ import os from 'os';
8
9
  import Database from 'better-sqlite3';
9
10
  import { Qulite, JobStatus, enqueueFsEvent } from '@coderule/qulite';
10
11
  import { CoderuleClients, ASTHttpClient, SyncHttpClient } from '@coderule/clients';
11
- import fs2 from 'fs';
12
+ import fs3 from 'fs';
12
13
  import { Worker } from 'worker_threads';
13
14
  import chokidar from 'chokidar';
14
15
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -35,6 +36,166 @@ var DEFAULT_MAX_SNAPSHOT_ATTEMPTS = 5;
35
36
  var DEFAULT_HTTP_TIMEOUT_MS = 12e4;
36
37
  var DEFAULT_UPLOAD_CHUNK_SIZE = 1;
37
38
  var DEFAULT_MAX_QUERY_WAIT_MS = 5e4;
39
+ function collectCandidateSources(options) {
40
+ const sources = [];
41
+ if (options?.cliRoot) {
42
+ sources.push({
43
+ value: options.cliRoot,
44
+ source: "cli-arg",
45
+ baseConfidence: 1
46
+ });
47
+ }
48
+ if (process.env.CODERULE_ROOT) {
49
+ sources.push({
50
+ value: process.env.CODERULE_ROOT,
51
+ source: "env-coderule-root",
52
+ baseConfidence: 0.9
53
+ });
54
+ }
55
+ if (process.env.WORKSPACE_FOLDER_PATHS) {
56
+ sources.push({
57
+ value: process.env.WORKSPACE_FOLDER_PATHS,
58
+ source: "env-workspace",
59
+ baseConfidence: 0.8
60
+ });
61
+ }
62
+ sources.push({
63
+ value: process.cwd(),
64
+ source: "process-cwd",
65
+ baseConfidence: 0.6
66
+ });
67
+ return sources;
68
+ }
69
+ async function pathExists(targetPath) {
70
+ try {
71
+ await fs5.access(targetPath);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ async function isDirectory(targetPath) {
78
+ try {
79
+ const stat = await fs5.stat(targetPath);
80
+ return stat.isDirectory();
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ async function hasGitDirectory(rootPath) {
86
+ const gitPath = path2.join(rootPath, ".git");
87
+ return pathExists(gitPath);
88
+ }
89
+ function isShallowPath(absolutePath) {
90
+ const normalized = absolutePath.replace(/\\/g, "/");
91
+ const separatorCount = (normalized.match(/\//g) || []).length;
92
+ return separatorCount <= 1;
93
+ }
94
+ function isHomeDirectory(candidatePath) {
95
+ const home = os.homedir();
96
+ const normalizedCandidate = path2.normalize(candidatePath);
97
+ const normalizedHome = path2.normalize(home);
98
+ return normalizedCandidate === normalizedHome;
99
+ }
100
+ async function applyModifiers(candidate) {
101
+ const modifiers = [];
102
+ if (await hasGitDirectory(candidate.path)) {
103
+ modifiers.push({ reason: "has .git directory", delta: 0.1 });
104
+ }
105
+ if (isShallowPath(candidate.path)) {
106
+ modifiers.push({ reason: "shallow path (likely root)", delta: -0.2 });
107
+ }
108
+ if (isHomeDirectory(candidate.path)) {
109
+ modifiers.push({ reason: "is home directory", delta: -0.3 });
110
+ }
111
+ const totalModifier = modifiers.reduce((sum, m) => sum + m.delta, 0);
112
+ const finalConfidence = candidate.baseConfidence + totalModifier;
113
+ return {
114
+ ...candidate,
115
+ modifiers,
116
+ finalConfidence
117
+ };
118
+ }
119
+ async function createCandidate(source) {
120
+ const absolutePath = path2.resolve(source.value);
121
+ const exists = await pathExists(absolutePath);
122
+ const isDir = exists ? await isDirectory(absolutePath) : false;
123
+ const candidate = {
124
+ path: absolutePath,
125
+ source: source.source,
126
+ baseConfidence: source.baseConfidence,
127
+ modifiers: [],
128
+ finalConfidence: source.baseConfidence,
129
+ exists,
130
+ isDirectory: isDir
131
+ };
132
+ if (exists && isDir) {
133
+ return applyModifiers(candidate);
134
+ }
135
+ return candidate;
136
+ }
137
+ async function resolveRootPath(options) {
138
+ const logger2 = options?.logger;
139
+ const sources = collectCandidateSources(options);
140
+ const candidates = await Promise.all(sources.map(createCandidate));
141
+ logger2?.debug(
142
+ {
143
+ candidates: candidates.map((c) => ({
144
+ path: c.path,
145
+ source: c.source,
146
+ baseConfidence: c.baseConfidence,
147
+ finalConfidence: c.finalConfidence,
148
+ exists: c.exists,
149
+ isDirectory: c.isDirectory,
150
+ modifiers: c.modifiers
151
+ }))
152
+ },
153
+ "Collected root path candidates"
154
+ );
155
+ const validCandidates = candidates.filter((c) => c.exists && c.isDirectory);
156
+ if (validCandidates.length === 0) {
157
+ const attempted = candidates.map((c) => c.path).join(", ");
158
+ throw new Error(
159
+ `No valid root directory found. Attempted: ${attempted}. Ensure the directory exists and is accessible.`
160
+ );
161
+ }
162
+ const sourcePriority = {
163
+ "cli-arg": 1,
164
+ "env-coderule-root": 2,
165
+ "env-workspace": 3,
166
+ "process-cwd": 4
167
+ };
168
+ validCandidates.sort((a, b) => {
169
+ const confidenceDiff = b.finalConfidence - a.finalConfidence;
170
+ if (confidenceDiff !== 0) {
171
+ return confidenceDiff;
172
+ }
173
+ return sourcePriority[a.source] - sourcePriority[b.source];
174
+ });
175
+ const best = validCandidates[0];
176
+ if (best.finalConfidence < 0.7) {
177
+ logger2?.warn(
178
+ {
179
+ path: best.path,
180
+ source: best.source,
181
+ confidence: best.finalConfidence,
182
+ modifiers: best.modifiers
183
+ },
184
+ "Selected root path has low confidence"
185
+ );
186
+ } else {
187
+ logger2?.info(
188
+ {
189
+ path: best.path,
190
+ source: best.source,
191
+ confidence: best.finalConfidence,
192
+ modifiers: best.modifiers
193
+ },
194
+ "Resolved root path"
195
+ );
196
+ }
197
+ return best;
198
+ }
38
199
 
39
200
  // src/config/Configurator.ts
40
201
  var DEFAULT_RETRIEVAL_FORMATTER = "standard";
@@ -47,9 +208,9 @@ var DEFAULTS = {
47
208
  maxSnapshotAttempts: DEFAULT_MAX_SNAPSHOT_ATTEMPTS
48
209
  };
49
210
  function normalizeRoot(root) {
50
- const resolved = path.resolve(root);
51
- const normalized = path.normalize(resolved);
52
- return normalized.split(path.sep).join("/");
211
+ const resolved = path2.resolve(root);
212
+ const normalized = path2.normalize(resolved);
213
+ return normalized.split(path2.sep).join("/");
53
214
  }
54
215
  function sha256(input) {
55
216
  return createHash("sha256").update(input).digest("hex");
@@ -81,7 +242,8 @@ function parseFormatter(value) {
81
242
  );
82
243
  }
83
244
  async function resolveConfig({
84
- token
245
+ token,
246
+ rootPath: cliRoot
85
247
  }) {
86
248
  const resolvedToken = token ?? process.env.CODERULE_TOKEN;
87
249
  if (!resolvedToken) {
@@ -89,14 +251,17 @@ async function resolveConfig({
89
251
  "Missing token: provide params.token or CODERULE_TOKEN env"
90
252
  );
91
253
  }
92
- const rootCandidate = process.env.CODERULE_ROOT || process.cwd();
93
- const rootPath = path.resolve(rootCandidate);
254
+ const rootCandidate = await resolveRootPath({
255
+ cliRoot,
256
+ logger: logger.child({ scope: "root-resolver" })
257
+ });
258
+ const rootPath = rootCandidate.path;
94
259
  const normalized = normalizeRoot(rootPath);
95
260
  const rootId = sha256(normalized);
96
261
  const dataDir = process.env.CODERULE_DATA_DIR || envPaths("coderule").data;
97
- const watchDir = path.join(dataDir, "watch");
98
- await fs4.mkdir(watchDir, { recursive: true });
99
- const dbPath = path.join(watchDir, `${rootId}.sqlite`);
262
+ const watchDir = path2.join(dataDir, "watch");
263
+ await fs5.mkdir(watchDir, { recursive: true });
264
+ const dbPath = path2.join(watchDir, `${rootId}.sqlite`);
100
265
  const baseConfig = {
101
266
  token: resolvedToken,
102
267
  rootPath,
@@ -173,6 +338,9 @@ async function resolveConfig({
173
338
  logger.debug(
174
339
  {
175
340
  rootPath,
341
+ rootSource: rootCandidate.source,
342
+ rootConfidence: rootCandidate.finalConfidence,
343
+ rootModifiers: rootCandidate.modifiers,
176
344
  dbPath,
177
345
  dataDir,
178
346
  authBaseUrl: baseConfig.authBaseUrl,
@@ -673,7 +841,7 @@ async function fetchVisitorRules(clients, logger2) {
673
841
  return rules;
674
842
  }
675
843
  function toPosix(input) {
676
- return input.split(path.sep).join("/");
844
+ return input.split(path2.sep).join("/");
677
845
  }
678
846
  function getLowerBasename(input) {
679
847
  const base = input.split("/").pop();
@@ -692,7 +860,7 @@ function compileRulesBundle(rules) {
692
860
  if (!info) {
693
861
  logger.debug({ path: fullPath }, "Predicate fallback lstat");
694
862
  try {
695
- info = fs2.lstatSync(fullPath);
863
+ info = fs3.lstatSync(fullPath);
696
864
  } catch (error) {
697
865
  logger.warn(
698
866
  { err: error, path: fullPath },
@@ -841,14 +1009,14 @@ var Hasher = class {
841
1009
  await new Promise((resolve) => setImmediate(resolve));
842
1010
  }
843
1011
  resolveAbsolutePath(record) {
844
- if (path.isAbsolute(record.display_path)) {
1012
+ if (path2.isAbsolute(record.display_path)) {
845
1013
  return record.display_path;
846
1014
  }
847
- return path.join(this.options.rootPath, record.rel_path);
1015
+ return path2.join(this.options.rootPath, record.rel_path);
848
1016
  }
849
1017
  async ensureExists(absPath, record) {
850
1018
  try {
851
- await fs4.access(absPath);
1019
+ await fs5.access(absPath);
852
1020
  return true;
853
1021
  } catch (error) {
854
1022
  this.log.warn(
@@ -871,7 +1039,7 @@ var Hasher = class {
871
1039
  const service = createHash("sha256");
872
1040
  service.update(relPath);
873
1041
  service.update("\n");
874
- const stream = fs2.createReadStream(absPath);
1042
+ const stream = fs3.createReadStream(absPath);
875
1043
  stream.on("data", (chunk) => {
876
1044
  content.update(chunk);
877
1045
  service.update(chunk);
@@ -986,13 +1154,13 @@ async function bootstrap(params) {
986
1154
  return runtime;
987
1155
  }
988
1156
  function toPosixRelative(root, target) {
989
- const rel = path.relative(root, target);
1157
+ const rel = path2.relative(root, target);
990
1158
  if (!rel || rel === "") return "";
991
- return rel.split(path.sep).join("/");
1159
+ return rel.split(path2.sep).join("/");
992
1160
  }
993
1161
  function isInsideRoot(root, target) {
994
- const rel = path.relative(root, target);
995
- return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
1162
+ const rel = path2.relative(root, target);
1163
+ return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
996
1164
  }
997
1165
 
998
1166
  // src/fs/Walker.ts
@@ -1007,7 +1175,7 @@ function cloneStats(stats) {
1007
1175
  }
1008
1176
  async function readSymlinkTarget(absPath, log) {
1009
1177
  try {
1010
- return await fs4.readlink(absPath);
1178
+ return await fs5.readlink(absPath);
1011
1179
  } catch (error) {
1012
1180
  log.warn({ err: error, path: absPath }, "Failed to read symlink target");
1013
1181
  return null;
@@ -1017,13 +1185,13 @@ async function walkDirectory(current, opts, stats) {
1017
1185
  const dirLogger = opts.logger;
1018
1186
  let dirents;
1019
1187
  try {
1020
- dirents = await fs4.readdir(current, { withFileTypes: true });
1188
+ dirents = await fs5.readdir(current, { withFileTypes: true });
1021
1189
  } catch (error) {
1022
1190
  dirLogger.warn({ err: error, path: current }, "Failed to read directory");
1023
1191
  return;
1024
1192
  }
1025
1193
  for (const dirent of dirents) {
1026
- const absPath = path.join(current, dirent.name);
1194
+ const absPath = path2.join(current, dirent.name);
1027
1195
  const relPath = toPosixRelative(opts.rootPath, absPath);
1028
1196
  if (dirent.isDirectory()) {
1029
1197
  if (shouldPruneDirectory(relPath, opts.bundle)) {
@@ -1036,7 +1204,7 @@ async function walkDirectory(current, opts, stats) {
1036
1204
  if (dirent.isSymbolicLink() || dirent.isFile()) {
1037
1205
  let stat;
1038
1206
  try {
1039
- stat = await fs4.lstat(absPath);
1207
+ stat = await fs5.lstat(absPath);
1040
1208
  } catch (error) {
1041
1209
  dirLogger.warn({ err: error, path: absPath }, "Failed to stat file");
1042
1210
  continue;
@@ -1128,9 +1296,9 @@ async function uploadMissing(rootPath, missing, syncClient, logger2, maxAttempts
1128
1296
  const list = chunks[idx];
1129
1297
  const map = /* @__PURE__ */ new Map();
1130
1298
  for (const missingFile of list) {
1131
- const absPath = path.join(rootPath, missingFile.file_path);
1299
+ const absPath = path2.join(rootPath, missingFile.file_path);
1132
1300
  try {
1133
- const buffer = await fs4.readFile(absPath);
1301
+ const buffer = await fs5.readFile(absPath);
1134
1302
  map.set(missingFile.file_hash, {
1135
1303
  path: missingFile.file_path,
1136
1304
  content: buffer
@@ -1395,7 +1563,7 @@ function computeBackoff(attempts) {
1395
1563
  }
1396
1564
  async function readSymlinkTarget2(absPath) {
1397
1565
  try {
1398
- return await fs4.readlink(absPath);
1566
+ return await fs5.readlink(absPath);
1399
1567
  } catch {
1400
1568
  return null;
1401
1569
  }
@@ -1536,7 +1704,7 @@ var ServiceRunner = class {
1536
1704
  async handleEvent(event, absPath, stats) {
1537
1705
  if (!this.running) return;
1538
1706
  const root = this.runtime.config.rootPath;
1539
- const absolute = path.isAbsolute(absPath) ? absPath : path.join(root, absPath);
1707
+ const absolute = path2.isAbsolute(absPath) ? absPath : path2.join(root, absPath);
1540
1708
  if (!isInsideRoot(root, absolute)) {
1541
1709
  return;
1542
1710
  }
@@ -1556,7 +1724,7 @@ var ServiceRunner = class {
1556
1724
  async handleAddChange(absPath, _stats) {
1557
1725
  let fileStats;
1558
1726
  try {
1559
- fileStats = await fs4.lstat(absPath);
1727
+ fileStats = await fs5.lstat(absPath);
1560
1728
  } catch (error) {
1561
1729
  this.runtime.logger.warn(
1562
1730
  { err: error, path: absPath },
@@ -2057,7 +2225,6 @@ ${statusText}`;
2057
2225
 
2058
2226
  // src/mcp-cli.ts
2059
2227
  var ENV_FLAG_MAP = {
2060
- root: "CODERULE_ROOT",
2061
2228
  "data-dir": "CODERULE_DATA_DIR",
2062
2229
  "auth-url": "CODERULE_AUTH_URL",
2063
2230
  "sync-url": "CODERULE_SYNC_URL",
@@ -2128,6 +2295,7 @@ function printUsage() {
2128
2295
  }
2129
2296
  function parseArgs(argv) {
2130
2297
  let token = process.env.CODERULE_TOKEN;
2298
+ let rootPath;
2131
2299
  let clean = false;
2132
2300
  let inlineHasher = false;
2133
2301
  const env = {};
@@ -2158,6 +2326,14 @@ function parseArgs(argv) {
2158
2326
  token = arg.slice("--token=".length);
2159
2327
  continue;
2160
2328
  }
2329
+ if (arg === "--root") {
2330
+ const value = args.shift();
2331
+ if (!value) {
2332
+ throw new Error("Missing value for --root");
2333
+ }
2334
+ rootPath = value;
2335
+ continue;
2336
+ }
2161
2337
  if (arg.startsWith("--")) {
2162
2338
  const flag = arg.slice(2);
2163
2339
  const envKey = ENV_FLAG_MAP[flag];
@@ -2190,22 +2366,22 @@ function parseArgs(argv) {
2190
2366
  "Missing token. Provide via argument or CODERULE_TOKEN environment variable."
2191
2367
  );
2192
2368
  }
2193
- return { token, clean, inlineHasher, env };
2369
+ return { token, rootPath, clean, inlineHasher, env };
2194
2370
  }
2195
- async function ensureClean(configToken) {
2196
- const config = await resolveConfig({ token: configToken });
2371
+ async function ensureClean(configToken, rootPath) {
2372
+ const config = await resolveConfig({ token: configToken, rootPath });
2197
2373
  const targets = [
2198
2374
  config.dbPath,
2199
2375
  `${config.dbPath}-shm`,
2200
2376
  `${config.dbPath}-wal`
2201
2377
  ];
2202
- await Promise.all(targets.map((target) => fs4.rm(target, { force: true })));
2203
- await fs4.rm(path.join(config.dataDir, "watch", `${config.rootId}.sqlite-shm`), {
2378
+ await Promise.all(targets.map((target) => fs5.rm(target, { force: true })));
2379
+ await fs5.rm(path2.join(config.dataDir, "watch", `${config.rootId}.sqlite-shm`), {
2204
2380
  force: true
2205
2381
  }).catch(() => {
2206
2382
  });
2207
- const dir = path.dirname(config.dbPath);
2208
- await fs4.mkdir(dir, { recursive: true });
2383
+ const dir = path2.dirname(config.dbPath);
2384
+ await fs5.mkdir(dir, { recursive: true });
2209
2385
  console.log(`Removed scanner database at ${config.dbPath}`);
2210
2386
  }
2211
2387
  function awaitShutdownSignals() {
@@ -2236,9 +2412,12 @@ async function main() {
2236
2412
  process.env[key] = value;
2237
2413
  }
2238
2414
  if (options.clean) {
2239
- await ensureClean(options.token);
2415
+ await ensureClean(options.token, options.rootPath);
2240
2416
  }
2241
- const runtime = await bootstrap({ token: options.token });
2417
+ const runtime = await bootstrap({
2418
+ token: options.token,
2419
+ rootPath: options.rootPath
2420
+ });
2242
2421
  const runner = new ServiceRunner(runtime);
2243
2422
  try {
2244
2423
  await runner.prepareWatcher(true);