@coderule/mcp 1.8.0 → 2.0.1

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
@@ -1257,6 +1425,21 @@ async function runInitialSyncPipeline(runtime, options) {
1257
1425
  hashLogger.debug("Hasher processed batch");
1258
1426
  }
1259
1427
  }
1428
+ if (options?.blockUntilReady !== false) {
1429
+ const syncLogger = runtime.logger.child({ scope: "snapshot" });
1430
+ const result = await publishSnapshot(
1431
+ runtime.config.rootPath,
1432
+ runtime.filesRepo,
1433
+ runtime.snapshotsRepo,
1434
+ runtime.clients.sync,
1435
+ syncLogger,
1436
+ {
1437
+ maxAttempts: runtime.config.maxSnapshotAttempts,
1438
+ uploadChunkSize: runtime.config.uploadChunkSize
1439
+ }
1440
+ );
1441
+ return result;
1442
+ }
1260
1443
  const computation = computeSnapshot(runtime.filesRepo);
1261
1444
  const createdAt = Date.now();
1262
1445
  runtime.snapshotsRepo.insert(
@@ -1395,7 +1578,7 @@ function computeBackoff(attempts) {
1395
1578
  }
1396
1579
  async function readSymlinkTarget2(absPath) {
1397
1580
  try {
1398
- return await fs4.readlink(absPath);
1581
+ return await fs5.readlink(absPath);
1399
1582
  } catch {
1400
1583
  return null;
1401
1584
  }
@@ -1536,7 +1719,7 @@ var ServiceRunner = class {
1536
1719
  async handleEvent(event, absPath, stats) {
1537
1720
  if (!this.running) return;
1538
1721
  const root = this.runtime.config.rootPath;
1539
- const absolute = path.isAbsolute(absPath) ? absPath : path.join(root, absPath);
1722
+ const absolute = path2.isAbsolute(absPath) ? absPath : path2.join(root, absPath);
1540
1723
  if (!isInsideRoot(root, absolute)) {
1541
1724
  return;
1542
1725
  }
@@ -1556,7 +1739,7 @@ var ServiceRunner = class {
1556
1739
  async handleAddChange(absPath, _stats) {
1557
1740
  let fileStats;
1558
1741
  try {
1559
- fileStats = await fs4.lstat(absPath);
1742
+ fileStats = await fs5.lstat(absPath);
1560
1743
  } catch (error) {
1561
1744
  this.runtime.logger.warn(
1562
1745
  { err: error, path: absPath },
@@ -2057,7 +2240,6 @@ ${statusText}`;
2057
2240
 
2058
2241
  // src/mcp-cli.ts
2059
2242
  var ENV_FLAG_MAP = {
2060
- root: "CODERULE_ROOT",
2061
2243
  "data-dir": "CODERULE_DATA_DIR",
2062
2244
  "auth-url": "CODERULE_AUTH_URL",
2063
2245
  "sync-url": "CODERULE_SYNC_URL",
@@ -2083,6 +2265,9 @@ function printUsage() {
2083
2265
  console.log(
2084
2266
  " --clean, --reindex Remove existing local state before running"
2085
2267
  );
2268
+ console.log(
2269
+ " --exit Exit after reindex (use with --reindex)"
2270
+ );
2086
2271
  console.log(
2087
2272
  " --inline-hasher Force inline hashing (debug only)"
2088
2273
  );
@@ -2128,7 +2313,9 @@ function printUsage() {
2128
2313
  }
2129
2314
  function parseArgs(argv) {
2130
2315
  let token = process.env.CODERULE_TOKEN;
2316
+ let rootPath;
2131
2317
  let clean = false;
2318
+ let exit = false;
2132
2319
  let inlineHasher = false;
2133
2320
  const env = {};
2134
2321
  const args = [...argv];
@@ -2142,6 +2329,10 @@ function parseArgs(argv) {
2142
2329
  clean = true;
2143
2330
  continue;
2144
2331
  }
2332
+ if (arg === "--exit") {
2333
+ exit = true;
2334
+ continue;
2335
+ }
2145
2336
  if (arg === "--inline-hasher") {
2146
2337
  inlineHasher = true;
2147
2338
  continue;
@@ -2158,6 +2349,14 @@ function parseArgs(argv) {
2158
2349
  token = arg.slice("--token=".length);
2159
2350
  continue;
2160
2351
  }
2352
+ if (arg === "--root") {
2353
+ const value = args.shift();
2354
+ if (!value) {
2355
+ throw new Error("Missing value for --root");
2356
+ }
2357
+ rootPath = value;
2358
+ continue;
2359
+ }
2161
2360
  if (arg.startsWith("--")) {
2162
2361
  const flag = arg.slice(2);
2163
2362
  const envKey = ENV_FLAG_MAP[flag];
@@ -2190,22 +2389,22 @@ function parseArgs(argv) {
2190
2389
  "Missing token. Provide via argument or CODERULE_TOKEN environment variable."
2191
2390
  );
2192
2391
  }
2193
- return { token, clean, inlineHasher, env };
2392
+ return { token, rootPath, clean, exit, inlineHasher, env };
2194
2393
  }
2195
- async function ensureClean(configToken) {
2196
- const config = await resolveConfig({ token: configToken });
2394
+ async function ensureClean(configToken, rootPath) {
2395
+ const config = await resolveConfig({ token: configToken, rootPath });
2197
2396
  const targets = [
2198
2397
  config.dbPath,
2199
2398
  `${config.dbPath}-shm`,
2200
2399
  `${config.dbPath}-wal`
2201
2400
  ];
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`), {
2401
+ await Promise.all(targets.map((target) => fs5.rm(target, { force: true })));
2402
+ await fs5.rm(path2.join(config.dataDir, "watch", `${config.rootId}.sqlite-shm`), {
2204
2403
  force: true
2205
2404
  }).catch(() => {
2206
2405
  });
2207
- const dir = path.dirname(config.dbPath);
2208
- await fs4.mkdir(dir, { recursive: true });
2406
+ const dir = path2.dirname(config.dbPath);
2407
+ await fs5.mkdir(dir, { recursive: true });
2209
2408
  console.log(`Removed scanner database at ${config.dbPath}`);
2210
2409
  }
2211
2410
  function awaitShutdownSignals() {
@@ -2236,10 +2435,30 @@ async function main() {
2236
2435
  process.env[key] = value;
2237
2436
  }
2238
2437
  if (options.clean) {
2239
- await ensureClean(options.token);
2438
+ await ensureClean(options.token, options.rootPath);
2240
2439
  }
2241
- const runtime = await bootstrap({ token: options.token });
2440
+ const runtime = await bootstrap({
2441
+ token: options.token,
2442
+ rootPath: options.rootPath
2443
+ });
2242
2444
  const runner = new ServiceRunner(runtime);
2445
+ if (options.exit && options.clean) {
2446
+ try {
2447
+ const initial = await runInitialSyncPipeline(runtime, {
2448
+ blockUntilReady: true
2449
+ });
2450
+ runtime.logger.info(
2451
+ {
2452
+ snapshotHash: initial.snapshotHash,
2453
+ filesCount: initial.filesCount
2454
+ },
2455
+ "Reindex completed; exiting"
2456
+ );
2457
+ } finally {
2458
+ await runner.stop();
2459
+ }
2460
+ return;
2461
+ }
2243
2462
  try {
2244
2463
  await runner.prepareWatcher(true);
2245
2464
  const server = createMcpServer({ runtime, runner });