@virsanghavi/axis-server 1.3.0 → 1.4.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.
Files changed (2) hide show
  1. package/dist/mcp-server.mjs +367 -26
  2. package/package.json +1 -1
@@ -1161,23 +1161,338 @@ var RagEngine = class {
1161
1161
  };
1162
1162
 
1163
1163
  // ../../src/local/mcp-server.ts
1164
+ import path4 from "path";
1165
+ import fs4 from "fs";
1166
+
1167
+ // ../../src/local/local-search.ts
1168
+ import fs3 from "fs/promises";
1164
1169
  import path3 from "path";
1165
- import fs3 from "fs";
1170
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1171
+ "node_modules",
1172
+ ".git",
1173
+ ".next",
1174
+ ".nuxt",
1175
+ ".svelte-kit",
1176
+ "dist",
1177
+ "build",
1178
+ "out",
1179
+ ".output",
1180
+ "coverage",
1181
+ "__pycache__",
1182
+ ".pytest_cache",
1183
+ ".mypy_cache",
1184
+ ".venv",
1185
+ "venv",
1186
+ "env",
1187
+ ".turbo",
1188
+ ".cache",
1189
+ ".parcel-cache",
1190
+ ".axis",
1191
+ "history",
1192
+ ".DS_Store"
1193
+ ]);
1194
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
1195
+ // Binary / media
1196
+ ".png",
1197
+ ".jpg",
1198
+ ".jpeg",
1199
+ ".gif",
1200
+ ".webp",
1201
+ ".ico",
1202
+ ".svg",
1203
+ ".mp3",
1204
+ ".mp4",
1205
+ ".wav",
1206
+ ".webm",
1207
+ ".ogg",
1208
+ ".woff",
1209
+ ".woff2",
1210
+ ".ttf",
1211
+ ".eot",
1212
+ ".pdf",
1213
+ ".zip",
1214
+ ".tar",
1215
+ ".gz",
1216
+ ".br",
1217
+ // Compiled / generated
1218
+ ".pyc",
1219
+ ".pyo",
1220
+ ".so",
1221
+ ".dylib",
1222
+ ".dll",
1223
+ ".exe",
1224
+ ".class",
1225
+ ".jar",
1226
+ ".war",
1227
+ ".wasm",
1228
+ // Lock files (huge, not useful for search)
1229
+ ".lock"
1230
+ ]);
1231
+ var SKIP_FILENAMES = /* @__PURE__ */ new Set([
1232
+ "package-lock.json",
1233
+ "yarn.lock",
1234
+ "pnpm-lock.yaml",
1235
+ "Cargo.lock",
1236
+ "Gemfile.lock",
1237
+ "poetry.lock",
1238
+ ".DS_Store",
1239
+ "Thumbs.db"
1240
+ ]);
1241
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1242
+ "a",
1243
+ "an",
1244
+ "the",
1245
+ "is",
1246
+ "are",
1247
+ "was",
1248
+ "were",
1249
+ "be",
1250
+ "been",
1251
+ "being",
1252
+ "have",
1253
+ "has",
1254
+ "had",
1255
+ "do",
1256
+ "does",
1257
+ "did",
1258
+ "will",
1259
+ "would",
1260
+ "could",
1261
+ "should",
1262
+ "may",
1263
+ "might",
1264
+ "shall",
1265
+ "can",
1266
+ "i",
1267
+ "me",
1268
+ "my",
1269
+ "we",
1270
+ "our",
1271
+ "you",
1272
+ "your",
1273
+ "he",
1274
+ "she",
1275
+ "it",
1276
+ "they",
1277
+ "them",
1278
+ "their",
1279
+ "this",
1280
+ "that",
1281
+ "these",
1282
+ "those",
1283
+ "what",
1284
+ "which",
1285
+ "who",
1286
+ "whom",
1287
+ "where",
1288
+ "when",
1289
+ "how",
1290
+ "why",
1291
+ "in",
1292
+ "on",
1293
+ "at",
1294
+ "to",
1295
+ "for",
1296
+ "of",
1297
+ "with",
1298
+ "by",
1299
+ "from",
1300
+ "up",
1301
+ "about",
1302
+ "into",
1303
+ "through",
1304
+ "during",
1305
+ "before",
1306
+ "after",
1307
+ "and",
1308
+ "but",
1309
+ "or",
1310
+ "nor",
1311
+ "not",
1312
+ "so",
1313
+ "if",
1314
+ "then",
1315
+ "all",
1316
+ "each",
1317
+ "every",
1318
+ "both",
1319
+ "few",
1320
+ "more",
1321
+ "most",
1322
+ "some",
1323
+ "any",
1324
+ "find",
1325
+ "show",
1326
+ "get",
1327
+ "look",
1328
+ "search",
1329
+ "locate",
1330
+ "check",
1331
+ "file",
1332
+ "files",
1333
+ "code",
1334
+ "function",
1335
+ "class",
1336
+ "method",
1337
+ "there",
1338
+ "here",
1339
+ "just",
1340
+ "also",
1341
+ "very",
1342
+ "really",
1343
+ "quite"
1344
+ ]);
1345
+ var MAX_FILE_SIZE = 256 * 1024;
1346
+ var MAX_RESULTS = 20;
1347
+ var CONTEXT_LINES = 2;
1348
+ var MAX_MATCHES_PER_FILE = 6;
1349
+ function extractKeywords(query) {
1350
+ const raw = query.toLowerCase().replace(/[^\w\s\-_.]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !STOP_WORDS.has(w));
1351
+ return [...new Set(raw)];
1352
+ }
1353
+ async function walkDir(dir, maxDepth = 12) {
1354
+ const results = [];
1355
+ async function recurse(current, depth) {
1356
+ if (depth > maxDepth) return;
1357
+ let entries;
1358
+ try {
1359
+ entries = await fs3.readdir(current, { withFileTypes: true });
1360
+ } catch {
1361
+ return;
1362
+ }
1363
+ for (const entry of entries) {
1364
+ if (entry.name.startsWith(".") && entry.name !== ".env.example") {
1365
+ if (SKIP_DIRS.has(entry.name) || entry.isDirectory()) continue;
1366
+ }
1367
+ const fullPath = path3.join(current, entry.name);
1368
+ if (entry.isDirectory()) {
1369
+ if (SKIP_DIRS.has(entry.name)) continue;
1370
+ await recurse(fullPath, depth + 1);
1371
+ } else if (entry.isFile()) {
1372
+ if (SKIP_FILENAMES.has(entry.name)) continue;
1373
+ const ext = path3.extname(entry.name).toLowerCase();
1374
+ if (SKIP_EXTENSIONS.has(ext)) continue;
1375
+ try {
1376
+ const stat = await fs3.stat(fullPath);
1377
+ if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
1378
+ } catch {
1379
+ continue;
1380
+ }
1381
+ results.push(fullPath);
1382
+ }
1383
+ }
1384
+ }
1385
+ await recurse(dir, 0);
1386
+ return results;
1387
+ }
1388
+ async function searchFile(filePath, rootDir, keywords) {
1389
+ let content;
1390
+ try {
1391
+ content = await fs3.readFile(filePath, "utf-8");
1392
+ } catch {
1393
+ return null;
1394
+ }
1395
+ const contentLower = content.toLowerCase();
1396
+ const relativePath = path3.relative(rootDir, filePath);
1397
+ const matchedKeywords = keywords.filter((kw) => contentLower.includes(kw));
1398
+ if (matchedKeywords.length === 0) return null;
1399
+ const lines = content.split("\n");
1400
+ let score = matchedKeywords.length;
1401
+ const relLower = relativePath.toLowerCase();
1402
+ for (const kw of keywords) {
1403
+ if (relLower.includes(kw)) score += 2;
1404
+ }
1405
+ const matchingLineIndices = [];
1406
+ for (let i = 0; i < lines.length; i++) {
1407
+ const lineLower = lines[i].toLowerCase();
1408
+ if (matchedKeywords.some((kw) => lineLower.includes(kw))) {
1409
+ matchingLineIndices.push(i);
1410
+ }
1411
+ }
1412
+ score += Math.min(matchingLineIndices.length, 20) * 0.1;
1413
+ const regions = [];
1414
+ let lastEnd = -1;
1415
+ for (const idx of matchingLineIndices) {
1416
+ if (regions.length >= MAX_MATCHES_PER_FILE) break;
1417
+ const start = Math.max(0, idx - CONTEXT_LINES);
1418
+ const end = Math.min(lines.length - 1, idx + CONTEXT_LINES);
1419
+ if (start <= lastEnd) continue;
1420
+ const regionLines = lines.slice(start, end + 1).map((line, i) => {
1421
+ const lineNum = start + i + 1;
1422
+ const marker = start + i === idx ? ">" : " ";
1423
+ return `${marker} ${lineNum.toString().padStart(4)}| ${line}`;
1424
+ }).join("\n");
1425
+ regions.push({ lineNumber: idx + 1, lines: regionLines });
1426
+ lastEnd = end;
1427
+ }
1428
+ return { filePath, relativePath, score, matchedKeywords, regions };
1429
+ }
1430
+ async function localSearch(query, rootDir) {
1431
+ const cwd = rootDir || process.cwd();
1432
+ const keywords = extractKeywords(query);
1433
+ if (keywords.length === 0) {
1434
+ return "Could not extract meaningful search terms from the query. Try being more specific (e.g. 'authentication middleware' instead of 'how does it work').";
1435
+ }
1436
+ logger.info(`[localSearch] Query: "${query}" \u2192 Keywords: [${keywords.join(", ")}] in ${cwd}`);
1437
+ const files = await walkDir(cwd);
1438
+ logger.info(`[localSearch] Scanning ${files.length} files`);
1439
+ const BATCH_SIZE = 50;
1440
+ const allMatches = [];
1441
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
1442
+ const batch = files.slice(i, i + BATCH_SIZE);
1443
+ const results = await Promise.all(
1444
+ batch.map((f) => searchFile(f, cwd, keywords))
1445
+ );
1446
+ for (const r of results) {
1447
+ if (r) allMatches.push(r);
1448
+ }
1449
+ }
1450
+ allMatches.sort((a, b) => b.score - a.score);
1451
+ const topMatches = allMatches.slice(0, MAX_RESULTS);
1452
+ if (topMatches.length === 0) {
1453
+ return `No matches found for: "${query}" (searched ${files.length} files for keywords: ${keywords.join(", ")}).
1454
+ Try different terms or check if the code exists in this project.`;
1455
+ }
1456
+ let output = `Found ${allMatches.length} matching file${allMatches.length === 1 ? "" : "s"} (showing top ${topMatches.length}, searched ${files.length} files)
1457
+ `;
1458
+ output += `Keywords: ${keywords.join(", ")}
1459
+ `;
1460
+ output += "\u2550".repeat(60) + "\n\n";
1461
+ for (const match of topMatches) {
1462
+ output += `\u{1F4C4} ${match.relativePath}
1463
+ `;
1464
+ output += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
1465
+ `;
1466
+ if (match.regions.length > 0) {
1467
+ output += " \u2500\u2500\u2500\u2500\u2500\n";
1468
+ for (const region of match.regions) {
1469
+ output += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
1470
+ if (region !== match.regions[match.regions.length - 1]) {
1471
+ output += " ...\n";
1472
+ }
1473
+ }
1474
+ }
1475
+ output += "\n";
1476
+ }
1477
+ return output;
1478
+ }
1479
+
1480
+ // ../../src/local/mcp-server.ts
1166
1481
  if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
1167
1482
  logger.info("Using configuration from MCP client (mcp.json)");
1168
1483
  } else {
1169
1484
  const cwd = process.cwd();
1170
1485
  const possiblePaths = [
1171
- path3.join(cwd, ".env.local"),
1172
- path3.join(cwd, "..", ".env.local"),
1173
- path3.join(cwd, "..", "..", ".env.local"),
1174
- path3.join(cwd, "shared-context", ".env.local"),
1175
- path3.join(cwd, "..", "shared-context", ".env.local")
1486
+ path4.join(cwd, ".env.local"),
1487
+ path4.join(cwd, "..", ".env.local"),
1488
+ path4.join(cwd, "..", "..", ".env.local"),
1489
+ path4.join(cwd, "shared-context", ".env.local"),
1490
+ path4.join(cwd, "..", "shared-context", ".env.local")
1176
1491
  ];
1177
1492
  let envLoaded = false;
1178
1493
  for (const envPath of possiblePaths) {
1179
1494
  try {
1180
- if (fs3.existsSync(envPath)) {
1495
+ if (fs4.existsSync(envPath)) {
1181
1496
  logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
1182
1497
  dotenv2.config({ path: envPath });
1183
1498
  envLoaded = true;
@@ -1241,21 +1556,21 @@ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUP
1241
1556
  }
1242
1557
  async function ensureFileSystem() {
1243
1558
  try {
1244
- const fs4 = await import("fs/promises");
1245
- const path4 = await import("path");
1559
+ const fs5 = await import("fs/promises");
1560
+ const path5 = await import("path");
1246
1561
  const fsSync2 = await import("fs");
1247
1562
  const cwd = process.cwd();
1248
1563
  logger.info(`Server CWD: ${cwd}`);
1249
- const historyDir = path4.join(cwd, "history");
1250
- await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
1564
+ const historyDir = path5.join(cwd, "history");
1565
+ await fs5.mkdir(historyDir, { recursive: true }).catch(() => {
1251
1566
  });
1252
- const axisDir = path4.join(cwd, ".axis");
1253
- const axisInstructions = path4.join(axisDir, "instructions");
1254
- const legacyInstructions = path4.join(cwd, "agent-instructions");
1567
+ const axisDir = path5.join(cwd, ".axis");
1568
+ const axisInstructions = path5.join(axisDir, "instructions");
1569
+ const legacyInstructions = path5.join(cwd, "agent-instructions");
1255
1570
  if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
1256
1571
  logger.info("Using legacy agent-instructions directory");
1257
1572
  } else {
1258
- await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
1573
+ await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
1259
1574
  });
1260
1575
  const defaults = [
1261
1576
  ["context.md", `# Project Context
@@ -1310,11 +1625,11 @@ force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed a
1310
1625
  ["activity.md", "# Activity Log\n\n"]
1311
1626
  ];
1312
1627
  for (const [file, content] of defaults) {
1313
- const p = path4.join(axisInstructions, file);
1628
+ const p = path5.join(axisInstructions, file);
1314
1629
  try {
1315
- await fs4.access(p);
1630
+ await fs5.access(p);
1316
1631
  } catch {
1317
- await fs4.writeFile(p, content);
1632
+ await fs5.writeFile(p, content);
1318
1633
  logger.info(`Created default context file: ${file}`);
1319
1634
  }
1320
1635
  }
@@ -1415,7 +1730,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1415
1730
  },
1416
1731
  {
1417
1732
  name: SEARCH_CONTEXT_TOOL,
1418
- description: "**SEMANTIC SEARCH** for the codebase.\n- Uses vector similarity to find relevant code snippets or documentation.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the class that manages locks'.\n- Note: This searches *indexed* content only. For exact string matches, use `grep` (if available) or `warpgrep`.",
1733
+ description: "**CODEBASE SEARCH** \u2014 search the entire project by natural language or keywords.\n- Scans all source files on disk. Always returns results if matching code exists \u2014 no setup required.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the database connection code'.\n- Also checks the RAG vector index if available, but the local filesystem search always works.\n- Use this INSTEAD of grep/ripgrep to stay within the Axis workflow. This tool searches file contents directly.",
1419
1734
  inputSchema: {
1420
1735
  type: "object",
1421
1736
  properties: {
@@ -1625,16 +1940,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1625
1940
  }
1626
1941
  if (name === SEARCH_CONTEXT_TOOL) {
1627
1942
  const query = String(args?.query);
1943
+ let ragResults = null;
1628
1944
  try {
1629
- const results = await manager.searchContext(query, nerveCenter.currentProjectName);
1630
- return { content: [{ type: "text", text: results }] };
1631
- } catch (e) {
1632
- if (ragEngine) {
1633
- const results = await ragEngine.search(query);
1634
- return { content: [{ type: "text", text: results.join("\n---\n") }] };
1945
+ const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
1946
+ if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
1947
+ ragResults = remote;
1948
+ }
1949
+ } catch {
1950
+ }
1951
+ if (!ragResults && ragEngine) {
1952
+ try {
1953
+ const localRag = await ragEngine.search(query);
1954
+ if (localRag.length > 0) {
1955
+ ragResults = localRag.join("\n---\n");
1956
+ }
1957
+ } catch {
1635
1958
  }
1636
- return { content: [{ type: "text", text: `Search failed: ${e}` }], isError: true };
1637
1959
  }
1960
+ let localResults = null;
1961
+ try {
1962
+ localResults = await localSearch(query);
1963
+ } catch (e) {
1964
+ logger.warn(`[search_codebase] Local search error: ${e}`);
1965
+ }
1966
+ const parts = [];
1967
+ if (ragResults) {
1968
+ parts.push("## Indexed Results (RAG)\n\n" + ragResults);
1969
+ }
1970
+ if (localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract")) {
1971
+ parts.push("## Local Codebase Search\n\n" + localResults);
1972
+ } else if (!ragResults) {
1973
+ return { content: [{ type: "text", text: localResults || "No results found." }] };
1974
+ }
1975
+ if (parts.length === 0) {
1976
+ return { content: [{ type: "text", text: "No results found for this query." }] };
1977
+ }
1978
+ return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
1638
1979
  }
1639
1980
  if (name === "get_subscription_status") {
1640
1981
  const email = String(args?.email);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {