@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.cjs CHANGED
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var fs4 = require('fs/promises');
5
- var path = require('path');
4
+ var fs5 = require('fs/promises');
5
+ var path2 = require('path');
6
6
  var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
7
7
  var crypto = require('crypto');
8
8
  var envPaths = require('env-paths');
9
9
  var pino = require('pino');
10
+ var os = require('os');
10
11
  var Database = require('better-sqlite3');
11
12
  var qulite = require('@coderule/qulite');
12
13
  var clients = require('@coderule/clients');
13
- var fs2 = require('fs');
14
+ var fs3 = require('fs');
14
15
  var worker_threads = require('worker_threads');
15
16
  var chokidar = require('chokidar');
16
17
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
@@ -18,12 +19,13 @@ var zod = require('zod');
18
19
 
19
20
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
21
 
21
- var fs4__default = /*#__PURE__*/_interopDefault(fs4);
22
- var path__default = /*#__PURE__*/_interopDefault(path);
22
+ var fs5__default = /*#__PURE__*/_interopDefault(fs5);
23
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
23
24
  var envPaths__default = /*#__PURE__*/_interopDefault(envPaths);
24
25
  var pino__default = /*#__PURE__*/_interopDefault(pino);
26
+ var os__default = /*#__PURE__*/_interopDefault(os);
25
27
  var Database__default = /*#__PURE__*/_interopDefault(Database);
26
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
28
+ var fs3__default = /*#__PURE__*/_interopDefault(fs3);
27
29
  var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
28
30
 
29
31
  // node_modules/tsup/assets/cjs_shims.js
@@ -50,6 +52,166 @@ var DEFAULT_MAX_SNAPSHOT_ATTEMPTS = 5;
50
52
  var DEFAULT_HTTP_TIMEOUT_MS = 12e4;
51
53
  var DEFAULT_UPLOAD_CHUNK_SIZE = 1;
52
54
  var DEFAULT_MAX_QUERY_WAIT_MS = 5e4;
55
+ function collectCandidateSources(options) {
56
+ const sources = [];
57
+ if (options?.cliRoot) {
58
+ sources.push({
59
+ value: options.cliRoot,
60
+ source: "cli-arg",
61
+ baseConfidence: 1
62
+ });
63
+ }
64
+ if (process.env.CODERULE_ROOT) {
65
+ sources.push({
66
+ value: process.env.CODERULE_ROOT,
67
+ source: "env-coderule-root",
68
+ baseConfidence: 0.9
69
+ });
70
+ }
71
+ if (process.env.WORKSPACE_FOLDER_PATHS) {
72
+ sources.push({
73
+ value: process.env.WORKSPACE_FOLDER_PATHS,
74
+ source: "env-workspace",
75
+ baseConfidence: 0.8
76
+ });
77
+ }
78
+ sources.push({
79
+ value: process.cwd(),
80
+ source: "process-cwd",
81
+ baseConfidence: 0.6
82
+ });
83
+ return sources;
84
+ }
85
+ async function pathExists(targetPath) {
86
+ try {
87
+ await fs5__default.default.access(targetPath);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ async function isDirectory(targetPath) {
94
+ try {
95
+ const stat = await fs5__default.default.stat(targetPath);
96
+ return stat.isDirectory();
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ async function hasGitDirectory(rootPath) {
102
+ const gitPath = path2__default.default.join(rootPath, ".git");
103
+ return pathExists(gitPath);
104
+ }
105
+ function isShallowPath(absolutePath) {
106
+ const normalized = absolutePath.replace(/\\/g, "/");
107
+ const separatorCount = (normalized.match(/\//g) || []).length;
108
+ return separatorCount <= 1;
109
+ }
110
+ function isHomeDirectory(candidatePath) {
111
+ const home = os__default.default.homedir();
112
+ const normalizedCandidate = path2__default.default.normalize(candidatePath);
113
+ const normalizedHome = path2__default.default.normalize(home);
114
+ return normalizedCandidate === normalizedHome;
115
+ }
116
+ async function applyModifiers(candidate) {
117
+ const modifiers = [];
118
+ if (await hasGitDirectory(candidate.path)) {
119
+ modifiers.push({ reason: "has .git directory", delta: 0.1 });
120
+ }
121
+ if (isShallowPath(candidate.path)) {
122
+ modifiers.push({ reason: "shallow path (likely root)", delta: -0.2 });
123
+ }
124
+ if (isHomeDirectory(candidate.path)) {
125
+ modifiers.push({ reason: "is home directory", delta: -0.3 });
126
+ }
127
+ const totalModifier = modifiers.reduce((sum, m) => sum + m.delta, 0);
128
+ const finalConfidence = candidate.baseConfidence + totalModifier;
129
+ return {
130
+ ...candidate,
131
+ modifiers,
132
+ finalConfidence
133
+ };
134
+ }
135
+ async function createCandidate(source) {
136
+ const absolutePath = path2__default.default.resolve(source.value);
137
+ const exists = await pathExists(absolutePath);
138
+ const isDir = exists ? await isDirectory(absolutePath) : false;
139
+ const candidate = {
140
+ path: absolutePath,
141
+ source: source.source,
142
+ baseConfidence: source.baseConfidence,
143
+ modifiers: [],
144
+ finalConfidence: source.baseConfidence,
145
+ exists,
146
+ isDirectory: isDir
147
+ };
148
+ if (exists && isDir) {
149
+ return applyModifiers(candidate);
150
+ }
151
+ return candidate;
152
+ }
153
+ async function resolveRootPath(options) {
154
+ const logger2 = options?.logger;
155
+ const sources = collectCandidateSources(options);
156
+ const candidates = await Promise.all(sources.map(createCandidate));
157
+ logger2?.debug(
158
+ {
159
+ candidates: candidates.map((c) => ({
160
+ path: c.path,
161
+ source: c.source,
162
+ baseConfidence: c.baseConfidence,
163
+ finalConfidence: c.finalConfidence,
164
+ exists: c.exists,
165
+ isDirectory: c.isDirectory,
166
+ modifiers: c.modifiers
167
+ }))
168
+ },
169
+ "Collected root path candidates"
170
+ );
171
+ const validCandidates = candidates.filter((c) => c.exists && c.isDirectory);
172
+ if (validCandidates.length === 0) {
173
+ const attempted = candidates.map((c) => c.path).join(", ");
174
+ throw new Error(
175
+ `No valid root directory found. Attempted: ${attempted}. Ensure the directory exists and is accessible.`
176
+ );
177
+ }
178
+ const sourcePriority = {
179
+ "cli-arg": 1,
180
+ "env-coderule-root": 2,
181
+ "env-workspace": 3,
182
+ "process-cwd": 4
183
+ };
184
+ validCandidates.sort((a, b) => {
185
+ const confidenceDiff = b.finalConfidence - a.finalConfidence;
186
+ if (confidenceDiff !== 0) {
187
+ return confidenceDiff;
188
+ }
189
+ return sourcePriority[a.source] - sourcePriority[b.source];
190
+ });
191
+ const best = validCandidates[0];
192
+ if (best.finalConfidence < 0.7) {
193
+ logger2?.warn(
194
+ {
195
+ path: best.path,
196
+ source: best.source,
197
+ confidence: best.finalConfidence,
198
+ modifiers: best.modifiers
199
+ },
200
+ "Selected root path has low confidence"
201
+ );
202
+ } else {
203
+ logger2?.info(
204
+ {
205
+ path: best.path,
206
+ source: best.source,
207
+ confidence: best.finalConfidence,
208
+ modifiers: best.modifiers
209
+ },
210
+ "Resolved root path"
211
+ );
212
+ }
213
+ return best;
214
+ }
53
215
 
54
216
  // src/config/Configurator.ts
55
217
  var DEFAULT_RETRIEVAL_FORMATTER = "standard";
@@ -62,9 +224,9 @@ var DEFAULTS = {
62
224
  maxSnapshotAttempts: DEFAULT_MAX_SNAPSHOT_ATTEMPTS
63
225
  };
64
226
  function normalizeRoot(root) {
65
- const resolved = path__default.default.resolve(root);
66
- const normalized = path__default.default.normalize(resolved);
67
- return normalized.split(path__default.default.sep).join("/");
227
+ const resolved = path2__default.default.resolve(root);
228
+ const normalized = path2__default.default.normalize(resolved);
229
+ return normalized.split(path2__default.default.sep).join("/");
68
230
  }
69
231
  function sha256(input) {
70
232
  return crypto.createHash("sha256").update(input).digest("hex");
@@ -96,7 +258,8 @@ function parseFormatter(value) {
96
258
  );
97
259
  }
98
260
  async function resolveConfig({
99
- token
261
+ token,
262
+ rootPath: cliRoot
100
263
  }) {
101
264
  const resolvedToken = token ?? process.env.CODERULE_TOKEN;
102
265
  if (!resolvedToken) {
@@ -104,14 +267,17 @@ async function resolveConfig({
104
267
  "Missing token: provide params.token or CODERULE_TOKEN env"
105
268
  );
106
269
  }
107
- const rootCandidate = process.env.CODERULE_ROOT || process.cwd();
108
- const rootPath = path__default.default.resolve(rootCandidate);
270
+ const rootCandidate = await resolveRootPath({
271
+ cliRoot,
272
+ logger: logger.child({ scope: "root-resolver" })
273
+ });
274
+ const rootPath = rootCandidate.path;
109
275
  const normalized = normalizeRoot(rootPath);
110
276
  const rootId = sha256(normalized);
111
277
  const dataDir = process.env.CODERULE_DATA_DIR || envPaths__default.default("coderule").data;
112
- const watchDir = path__default.default.join(dataDir, "watch");
113
- await fs4__default.default.mkdir(watchDir, { recursive: true });
114
- const dbPath = path__default.default.join(watchDir, `${rootId}.sqlite`);
278
+ const watchDir = path2__default.default.join(dataDir, "watch");
279
+ await fs5__default.default.mkdir(watchDir, { recursive: true });
280
+ const dbPath = path2__default.default.join(watchDir, `${rootId}.sqlite`);
115
281
  const baseConfig = {
116
282
  token: resolvedToken,
117
283
  rootPath,
@@ -188,6 +354,9 @@ async function resolveConfig({
188
354
  logger.debug(
189
355
  {
190
356
  rootPath,
357
+ rootSource: rootCandidate.source,
358
+ rootConfidence: rootCandidate.finalConfidence,
359
+ rootModifiers: rootCandidate.modifiers,
191
360
  dbPath,
192
361
  dataDir,
193
362
  authBaseUrl: baseConfig.authBaseUrl,
@@ -688,7 +857,7 @@ async function fetchVisitorRules(clients, logger2) {
688
857
  return rules;
689
858
  }
690
859
  function toPosix(input) {
691
- return input.split(path__default.default.sep).join("/");
860
+ return input.split(path2__default.default.sep).join("/");
692
861
  }
693
862
  function getLowerBasename(input) {
694
863
  const base = input.split("/").pop();
@@ -707,7 +876,7 @@ function compileRulesBundle(rules) {
707
876
  if (!info) {
708
877
  logger.debug({ path: fullPath }, "Predicate fallback lstat");
709
878
  try {
710
- info = fs2__default.default.lstatSync(fullPath);
879
+ info = fs3__default.default.lstatSync(fullPath);
711
880
  } catch (error) {
712
881
  logger.warn(
713
882
  { err: error, path: fullPath },
@@ -856,14 +1025,14 @@ var Hasher = class {
856
1025
  await new Promise((resolve) => setImmediate(resolve));
857
1026
  }
858
1027
  resolveAbsolutePath(record) {
859
- if (path__default.default.isAbsolute(record.display_path)) {
1028
+ if (path2__default.default.isAbsolute(record.display_path)) {
860
1029
  return record.display_path;
861
1030
  }
862
- return path__default.default.join(this.options.rootPath, record.rel_path);
1031
+ return path2__default.default.join(this.options.rootPath, record.rel_path);
863
1032
  }
864
1033
  async ensureExists(absPath, record) {
865
1034
  try {
866
- await fs4__default.default.access(absPath);
1035
+ await fs5__default.default.access(absPath);
867
1036
  return true;
868
1037
  } catch (error) {
869
1038
  this.log.warn(
@@ -886,7 +1055,7 @@ var Hasher = class {
886
1055
  const service = crypto.createHash("sha256");
887
1056
  service.update(relPath);
888
1057
  service.update("\n");
889
- const stream = fs2__default.default.createReadStream(absPath);
1058
+ const stream = fs3__default.default.createReadStream(absPath);
890
1059
  stream.on("data", (chunk) => {
891
1060
  content.update(chunk);
892
1061
  service.update(chunk);
@@ -1001,13 +1170,13 @@ async function bootstrap(params) {
1001
1170
  return runtime;
1002
1171
  }
1003
1172
  function toPosixRelative(root, target) {
1004
- const rel = path__default.default.relative(root, target);
1173
+ const rel = path2__default.default.relative(root, target);
1005
1174
  if (!rel || rel === "") return "";
1006
- return rel.split(path__default.default.sep).join("/");
1175
+ return rel.split(path2__default.default.sep).join("/");
1007
1176
  }
1008
1177
  function isInsideRoot(root, target) {
1009
- const rel = path__default.default.relative(root, target);
1010
- return rel === "" || !rel.startsWith("..") && !path__default.default.isAbsolute(rel);
1178
+ const rel = path2__default.default.relative(root, target);
1179
+ return rel === "" || !rel.startsWith("..") && !path2__default.default.isAbsolute(rel);
1011
1180
  }
1012
1181
 
1013
1182
  // src/fs/Walker.ts
@@ -1022,7 +1191,7 @@ function cloneStats(stats) {
1022
1191
  }
1023
1192
  async function readSymlinkTarget(absPath, log) {
1024
1193
  try {
1025
- return await fs4__default.default.readlink(absPath);
1194
+ return await fs5__default.default.readlink(absPath);
1026
1195
  } catch (error) {
1027
1196
  log.warn({ err: error, path: absPath }, "Failed to read symlink target");
1028
1197
  return null;
@@ -1032,13 +1201,13 @@ async function walkDirectory(current, opts, stats) {
1032
1201
  const dirLogger = opts.logger;
1033
1202
  let dirents;
1034
1203
  try {
1035
- dirents = await fs4__default.default.readdir(current, { withFileTypes: true });
1204
+ dirents = await fs5__default.default.readdir(current, { withFileTypes: true });
1036
1205
  } catch (error) {
1037
1206
  dirLogger.warn({ err: error, path: current }, "Failed to read directory");
1038
1207
  return;
1039
1208
  }
1040
1209
  for (const dirent of dirents) {
1041
- const absPath = path__default.default.join(current, dirent.name);
1210
+ const absPath = path2__default.default.join(current, dirent.name);
1042
1211
  const relPath = toPosixRelative(opts.rootPath, absPath);
1043
1212
  if (dirent.isDirectory()) {
1044
1213
  if (shouldPruneDirectory(relPath, opts.bundle)) {
@@ -1051,7 +1220,7 @@ async function walkDirectory(current, opts, stats) {
1051
1220
  if (dirent.isSymbolicLink() || dirent.isFile()) {
1052
1221
  let stat;
1053
1222
  try {
1054
- stat = await fs4__default.default.lstat(absPath);
1223
+ stat = await fs5__default.default.lstat(absPath);
1055
1224
  } catch (error) {
1056
1225
  dirLogger.warn({ err: error, path: absPath }, "Failed to stat file");
1057
1226
  continue;
@@ -1143,9 +1312,9 @@ async function uploadMissing(rootPath, missing, syncClient, logger2, maxAttempts
1143
1312
  const list = chunks[idx];
1144
1313
  const map = /* @__PURE__ */ new Map();
1145
1314
  for (const missingFile of list) {
1146
- const absPath = path__default.default.join(rootPath, missingFile.file_path);
1315
+ const absPath = path2__default.default.join(rootPath, missingFile.file_path);
1147
1316
  try {
1148
- const buffer = await fs4__default.default.readFile(absPath);
1317
+ const buffer = await fs5__default.default.readFile(absPath);
1149
1318
  map.set(missingFile.file_hash, {
1150
1319
  path: missingFile.file_path,
1151
1320
  content: buffer
@@ -1272,6 +1441,21 @@ async function runInitialSyncPipeline(runtime, options) {
1272
1441
  hashLogger.debug("Hasher processed batch");
1273
1442
  }
1274
1443
  }
1444
+ if (options?.blockUntilReady !== false) {
1445
+ const syncLogger = runtime.logger.child({ scope: "snapshot" });
1446
+ const result = await publishSnapshot(
1447
+ runtime.config.rootPath,
1448
+ runtime.filesRepo,
1449
+ runtime.snapshotsRepo,
1450
+ runtime.clients.sync,
1451
+ syncLogger,
1452
+ {
1453
+ maxAttempts: runtime.config.maxSnapshotAttempts,
1454
+ uploadChunkSize: runtime.config.uploadChunkSize
1455
+ }
1456
+ );
1457
+ return result;
1458
+ }
1275
1459
  const computation = computeSnapshot(runtime.filesRepo);
1276
1460
  const createdAt = Date.now();
1277
1461
  runtime.snapshotsRepo.insert(
@@ -1410,7 +1594,7 @@ function computeBackoff(attempts) {
1410
1594
  }
1411
1595
  async function readSymlinkTarget2(absPath) {
1412
1596
  try {
1413
- return await fs4__default.default.readlink(absPath);
1597
+ return await fs5__default.default.readlink(absPath);
1414
1598
  } catch {
1415
1599
  return null;
1416
1600
  }
@@ -1551,7 +1735,7 @@ var ServiceRunner = class {
1551
1735
  async handleEvent(event, absPath, stats) {
1552
1736
  if (!this.running) return;
1553
1737
  const root = this.runtime.config.rootPath;
1554
- const absolute = path__default.default.isAbsolute(absPath) ? absPath : path__default.default.join(root, absPath);
1738
+ const absolute = path2__default.default.isAbsolute(absPath) ? absPath : path2__default.default.join(root, absPath);
1555
1739
  if (!isInsideRoot(root, absolute)) {
1556
1740
  return;
1557
1741
  }
@@ -1571,7 +1755,7 @@ var ServiceRunner = class {
1571
1755
  async handleAddChange(absPath, _stats) {
1572
1756
  let fileStats;
1573
1757
  try {
1574
- fileStats = await fs4__default.default.lstat(absPath);
1758
+ fileStats = await fs5__default.default.lstat(absPath);
1575
1759
  } catch (error) {
1576
1760
  this.runtime.logger.warn(
1577
1761
  { err: error, path: absPath },
@@ -2072,7 +2256,6 @@ ${statusText}`;
2072
2256
 
2073
2257
  // src/mcp-cli.ts
2074
2258
  var ENV_FLAG_MAP = {
2075
- root: "CODERULE_ROOT",
2076
2259
  "data-dir": "CODERULE_DATA_DIR",
2077
2260
  "auth-url": "CODERULE_AUTH_URL",
2078
2261
  "sync-url": "CODERULE_SYNC_URL",
@@ -2098,6 +2281,9 @@ function printUsage() {
2098
2281
  console.log(
2099
2282
  " --clean, --reindex Remove existing local state before running"
2100
2283
  );
2284
+ console.log(
2285
+ " --exit Exit after reindex (use with --reindex)"
2286
+ );
2101
2287
  console.log(
2102
2288
  " --inline-hasher Force inline hashing (debug only)"
2103
2289
  );
@@ -2143,7 +2329,9 @@ function printUsage() {
2143
2329
  }
2144
2330
  function parseArgs(argv) {
2145
2331
  let token = process.env.CODERULE_TOKEN;
2332
+ let rootPath;
2146
2333
  let clean = false;
2334
+ let exit = false;
2147
2335
  let inlineHasher = false;
2148
2336
  const env = {};
2149
2337
  const args = [...argv];
@@ -2157,6 +2345,10 @@ function parseArgs(argv) {
2157
2345
  clean = true;
2158
2346
  continue;
2159
2347
  }
2348
+ if (arg === "--exit") {
2349
+ exit = true;
2350
+ continue;
2351
+ }
2160
2352
  if (arg === "--inline-hasher") {
2161
2353
  inlineHasher = true;
2162
2354
  continue;
@@ -2173,6 +2365,14 @@ function parseArgs(argv) {
2173
2365
  token = arg.slice("--token=".length);
2174
2366
  continue;
2175
2367
  }
2368
+ if (arg === "--root") {
2369
+ const value = args.shift();
2370
+ if (!value) {
2371
+ throw new Error("Missing value for --root");
2372
+ }
2373
+ rootPath = value;
2374
+ continue;
2375
+ }
2176
2376
  if (arg.startsWith("--")) {
2177
2377
  const flag = arg.slice(2);
2178
2378
  const envKey = ENV_FLAG_MAP[flag];
@@ -2205,22 +2405,22 @@ function parseArgs(argv) {
2205
2405
  "Missing token. Provide via argument or CODERULE_TOKEN environment variable."
2206
2406
  );
2207
2407
  }
2208
- return { token, clean, inlineHasher, env };
2408
+ return { token, rootPath, clean, exit, inlineHasher, env };
2209
2409
  }
2210
- async function ensureClean(configToken) {
2211
- const config = await resolveConfig({ token: configToken });
2410
+ async function ensureClean(configToken, rootPath) {
2411
+ const config = await resolveConfig({ token: configToken, rootPath });
2212
2412
  const targets = [
2213
2413
  config.dbPath,
2214
2414
  `${config.dbPath}-shm`,
2215
2415
  `${config.dbPath}-wal`
2216
2416
  ];
2217
- await Promise.all(targets.map((target) => fs4__default.default.rm(target, { force: true })));
2218
- await fs4__default.default.rm(path__default.default.join(config.dataDir, "watch", `${config.rootId}.sqlite-shm`), {
2417
+ await Promise.all(targets.map((target) => fs5__default.default.rm(target, { force: true })));
2418
+ await fs5__default.default.rm(path2__default.default.join(config.dataDir, "watch", `${config.rootId}.sqlite-shm`), {
2219
2419
  force: true
2220
2420
  }).catch(() => {
2221
2421
  });
2222
- const dir = path__default.default.dirname(config.dbPath);
2223
- await fs4__default.default.mkdir(dir, { recursive: true });
2422
+ const dir = path2__default.default.dirname(config.dbPath);
2423
+ await fs5__default.default.mkdir(dir, { recursive: true });
2224
2424
  console.log(`Removed scanner database at ${config.dbPath}`);
2225
2425
  }
2226
2426
  function awaitShutdownSignals() {
@@ -2251,10 +2451,30 @@ async function main() {
2251
2451
  process.env[key] = value;
2252
2452
  }
2253
2453
  if (options.clean) {
2254
- await ensureClean(options.token);
2454
+ await ensureClean(options.token, options.rootPath);
2255
2455
  }
2256
- const runtime = await bootstrap({ token: options.token });
2456
+ const runtime = await bootstrap({
2457
+ token: options.token,
2458
+ rootPath: options.rootPath
2459
+ });
2257
2460
  const runner = new ServiceRunner(runtime);
2461
+ if (options.exit && options.clean) {
2462
+ try {
2463
+ const initial = await runInitialSyncPipeline(runtime, {
2464
+ blockUntilReady: true
2465
+ });
2466
+ runtime.logger.info(
2467
+ {
2468
+ snapshotHash: initial.snapshotHash,
2469
+ filesCount: initial.filesCount
2470
+ },
2471
+ "Reindex completed; exiting"
2472
+ );
2473
+ } finally {
2474
+ await runner.stop();
2475
+ }
2476
+ return;
2477
+ }
2258
2478
  try {
2259
2479
  await runner.prepareWatcher(true);
2260
2480
  const server = createMcpServer({ runtime, runner });