@virsanghavi/axis-server 1.3.0 → 1.5.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-server.mjs +478 -26
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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 (
|
|
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;
|
|
@@ -1230,6 +1545,92 @@ var nerveCenter = new NerveCenter(manager, {
|
|
|
1230
1545
|
projectName: process.env.PROJECT_NAME || "default"
|
|
1231
1546
|
});
|
|
1232
1547
|
logger.info("=== Axis MCP Server Initialized ===");
|
|
1548
|
+
var RECHECK_INTERVAL_MS = 30 * 60 * 1e3;
|
|
1549
|
+
var GRACE_PERIOD_MS = 5 * 60 * 1e3;
|
|
1550
|
+
var subscription = {
|
|
1551
|
+
checked: false,
|
|
1552
|
+
valid: true,
|
|
1553
|
+
// Assume valid until proven otherwise (for startup)
|
|
1554
|
+
plan: "unknown",
|
|
1555
|
+
reason: "",
|
|
1556
|
+
checkedAt: 0
|
|
1557
|
+
};
|
|
1558
|
+
async function verifySubscription() {
|
|
1559
|
+
if (!apiSecret) {
|
|
1560
|
+
subscription = { checked: true, valid: true, plan: "local", reason: "No API key configured \u2014 local mode", checkedAt: Date.now() };
|
|
1561
|
+
logger.info("[subscription] No API key \u2014 running in local/dev mode, skipping verification");
|
|
1562
|
+
return subscription;
|
|
1563
|
+
}
|
|
1564
|
+
const verifyUrl = apiUrl.endsWith("/v1") ? `${apiUrl}/verify` : `${apiUrl}/v1/verify`;
|
|
1565
|
+
logger.info(`[subscription] Verifying subscription at ${verifyUrl}`);
|
|
1566
|
+
const controller = new AbortController();
|
|
1567
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1568
|
+
try {
|
|
1569
|
+
const response = await fetch(verifyUrl, {
|
|
1570
|
+
method: "GET",
|
|
1571
|
+
headers: {
|
|
1572
|
+
"Authorization": `Bearer ${apiSecret}`
|
|
1573
|
+
},
|
|
1574
|
+
signal: controller.signal
|
|
1575
|
+
});
|
|
1576
|
+
clearTimeout(timeout);
|
|
1577
|
+
const data = await response.json();
|
|
1578
|
+
logger.info(`[subscription] Verify response: ${JSON.stringify(data)}`);
|
|
1579
|
+
if (data.valid === true) {
|
|
1580
|
+
subscription = {
|
|
1581
|
+
checked: true,
|
|
1582
|
+
valid: true,
|
|
1583
|
+
plan: data.plan || "Pro",
|
|
1584
|
+
reason: "",
|
|
1585
|
+
checkedAt: Date.now(),
|
|
1586
|
+
validUntil: data.validUntil
|
|
1587
|
+
};
|
|
1588
|
+
} else {
|
|
1589
|
+
subscription = {
|
|
1590
|
+
checked: true,
|
|
1591
|
+
valid: false,
|
|
1592
|
+
plan: data.plan || "Free",
|
|
1593
|
+
reason: data.reason || "subscription_invalid",
|
|
1594
|
+
checkedAt: Date.now()
|
|
1595
|
+
};
|
|
1596
|
+
logger.warn(`[subscription] Subscription NOT valid: ${data.reason}`);
|
|
1597
|
+
}
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
clearTimeout(timeout);
|
|
1600
|
+
logger.warn(`[subscription] Verification failed (network): ${e.message}`);
|
|
1601
|
+
if (!subscription.checked) {
|
|
1602
|
+
subscription = {
|
|
1603
|
+
checked: true,
|
|
1604
|
+
valid: true,
|
|
1605
|
+
// Grace period
|
|
1606
|
+
plan: "unverified",
|
|
1607
|
+
reason: "Verification endpoint unreachable \u2014 grace period active",
|
|
1608
|
+
checkedAt: Date.now()
|
|
1609
|
+
};
|
|
1610
|
+
logger.warn("[subscription] First check failed \u2014 allowing grace period");
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return subscription;
|
|
1614
|
+
}
|
|
1615
|
+
function isSubscriptionStale() {
|
|
1616
|
+
return Date.now() - subscription.checkedAt > RECHECK_INTERVAL_MS;
|
|
1617
|
+
}
|
|
1618
|
+
function getSubscriptionBlockMessage() {
|
|
1619
|
+
return [
|
|
1620
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
1621
|
+
" Axis Pro subscription required",
|
|
1622
|
+
"",
|
|
1623
|
+
` Status: ${subscription.reason || "subscription_expired"}`,
|
|
1624
|
+
` Current plan: ${subscription.plan}`,
|
|
1625
|
+
"",
|
|
1626
|
+
" Your Axis Pro subscription has expired or is inactive.",
|
|
1627
|
+
" All Axis MCP tools are disabled until the subscription is renewed.",
|
|
1628
|
+
"",
|
|
1629
|
+
" \u2192 Renew at https://useaxis.dev/dashboard",
|
|
1630
|
+
" \u2192 After renewing, restart your IDE to re-verify.",
|
|
1631
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
|
|
1632
|
+
].join("\n");
|
|
1633
|
+
}
|
|
1233
1634
|
var ragEngine;
|
|
1234
1635
|
if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1235
1636
|
ragEngine = new RagEngine(
|
|
@@ -1241,21 +1642,21 @@ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUP
|
|
|
1241
1642
|
}
|
|
1242
1643
|
async function ensureFileSystem() {
|
|
1243
1644
|
try {
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1645
|
+
const fs5 = await import("fs/promises");
|
|
1646
|
+
const path5 = await import("path");
|
|
1246
1647
|
const fsSync2 = await import("fs");
|
|
1247
1648
|
const cwd = process.cwd();
|
|
1248
1649
|
logger.info(`Server CWD: ${cwd}`);
|
|
1249
|
-
const historyDir =
|
|
1250
|
-
await
|
|
1650
|
+
const historyDir = path5.join(cwd, "history");
|
|
1651
|
+
await fs5.mkdir(historyDir, { recursive: true }).catch(() => {
|
|
1251
1652
|
});
|
|
1252
|
-
const axisDir =
|
|
1253
|
-
const axisInstructions =
|
|
1254
|
-
const legacyInstructions =
|
|
1653
|
+
const axisDir = path5.join(cwd, ".axis");
|
|
1654
|
+
const axisInstructions = path5.join(axisDir, "instructions");
|
|
1655
|
+
const legacyInstructions = path5.join(cwd, "agent-instructions");
|
|
1255
1656
|
if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
|
|
1256
1657
|
logger.info("Using legacy agent-instructions directory");
|
|
1257
1658
|
} else {
|
|
1258
|
-
await
|
|
1659
|
+
await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
|
|
1259
1660
|
});
|
|
1260
1661
|
const defaults = [
|
|
1261
1662
|
["context.md", `# Project Context
|
|
@@ -1310,11 +1711,11 @@ force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed a
|
|
|
1310
1711
|
["activity.md", "# Activity Log\n\n"]
|
|
1311
1712
|
];
|
|
1312
1713
|
for (const [file, content] of defaults) {
|
|
1313
|
-
const p =
|
|
1714
|
+
const p = path5.join(axisInstructions, file);
|
|
1314
1715
|
try {
|
|
1315
|
-
await
|
|
1716
|
+
await fs5.access(p);
|
|
1316
1717
|
} catch {
|
|
1317
|
-
await
|
|
1718
|
+
await fs5.writeFile(p, content);
|
|
1318
1719
|
logger.info(`Created default context file: ${file}`);
|
|
1319
1720
|
}
|
|
1320
1721
|
}
|
|
@@ -1415,7 +1816,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1415
1816
|
},
|
|
1416
1817
|
{
|
|
1417
1818
|
name: SEARCH_CONTEXT_TOOL,
|
|
1418
|
-
description: "**
|
|
1819
|
+
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
1820
|
inputSchema: {
|
|
1420
1821
|
type: "object",
|
|
1421
1822
|
properties: {
|
|
@@ -1579,6 +1980,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1579
1980
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1580
1981
|
const { name, arguments: args } = request.params;
|
|
1581
1982
|
logger.info("Tool call", { name });
|
|
1983
|
+
if (isSubscriptionStale()) {
|
|
1984
|
+
await verifySubscription();
|
|
1985
|
+
}
|
|
1986
|
+
if (!subscription.valid) {
|
|
1987
|
+
logger.warn(`[subscription] Blocking tool call "${name}" \u2014 subscription invalid`);
|
|
1988
|
+
return {
|
|
1989
|
+
content: [{ type: "text", text: getSubscriptionBlockMessage() }],
|
|
1990
|
+
isError: true
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1582
1993
|
if (name === READ_CONTEXT_TOOL) {
|
|
1583
1994
|
const filename = String(args?.filename);
|
|
1584
1995
|
try {
|
|
@@ -1625,16 +2036,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1625
2036
|
}
|
|
1626
2037
|
if (name === SEARCH_CONTEXT_TOOL) {
|
|
1627
2038
|
const query = String(args?.query);
|
|
2039
|
+
let ragResults = null;
|
|
1628
2040
|
try {
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
if (ragEngine) {
|
|
1633
|
-
const results = await ragEngine.search(query);
|
|
1634
|
-
return { content: [{ type: "text", text: results.join("\n---\n") }] };
|
|
2041
|
+
const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
|
|
2042
|
+
if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
|
|
2043
|
+
ragResults = remote;
|
|
1635
2044
|
}
|
|
1636
|
-
|
|
2045
|
+
} catch {
|
|
2046
|
+
}
|
|
2047
|
+
if (!ragResults && ragEngine) {
|
|
2048
|
+
try {
|
|
2049
|
+
const localRag = await ragEngine.search(query);
|
|
2050
|
+
if (localRag.length > 0) {
|
|
2051
|
+
ragResults = localRag.join("\n---\n");
|
|
2052
|
+
}
|
|
2053
|
+
} catch {
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
let localResults = null;
|
|
2057
|
+
try {
|
|
2058
|
+
localResults = await localSearch(query);
|
|
2059
|
+
} catch (e) {
|
|
2060
|
+
logger.warn(`[search_codebase] Local search error: ${e}`);
|
|
2061
|
+
}
|
|
2062
|
+
const parts = [];
|
|
2063
|
+
if (ragResults) {
|
|
2064
|
+
parts.push("## Indexed Results (RAG)\n\n" + ragResults);
|
|
2065
|
+
}
|
|
2066
|
+
if (localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract")) {
|
|
2067
|
+
parts.push("## Local Codebase Search\n\n" + localResults);
|
|
2068
|
+
} else if (!ragResults) {
|
|
2069
|
+
return { content: [{ type: "text", text: localResults || "No results found." }] };
|
|
2070
|
+
}
|
|
2071
|
+
if (parts.length === 0) {
|
|
2072
|
+
return { content: [{ type: "text", text: "No results found for this query." }] };
|
|
1637
2073
|
}
|
|
2074
|
+
return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
|
|
1638
2075
|
}
|
|
1639
2076
|
if (name === "get_subscription_status") {
|
|
1640
2077
|
const email = String(args?.email);
|
|
@@ -1728,6 +2165,21 @@ async function main() {
|
|
|
1728
2165
|
ragEngine.setProjectId(nerveCenter.projectId);
|
|
1729
2166
|
logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
|
|
1730
2167
|
}
|
|
2168
|
+
await verifySubscription();
|
|
2169
|
+
if (!subscription.valid) {
|
|
2170
|
+
logger.error("[subscription] Subscription invalid at startup \u2014 all tools will be blocked");
|
|
2171
|
+
logger.error(`[subscription] Reason: ${subscription.reason} | Plan: ${subscription.plan}`);
|
|
2172
|
+
} else {
|
|
2173
|
+
logger.info(`[subscription] Subscription verified: ${subscription.plan} (valid until: ${subscription.validUntil || "N/A"})`);
|
|
2174
|
+
}
|
|
2175
|
+
setInterval(async () => {
|
|
2176
|
+
try {
|
|
2177
|
+
await verifySubscription();
|
|
2178
|
+
logger.info(`[subscription] Periodic re-check: valid=${subscription.valid}, plan=${subscription.plan}`);
|
|
2179
|
+
} catch (e) {
|
|
2180
|
+
logger.warn(`[subscription] Periodic re-check failed: ${e}`);
|
|
2181
|
+
}
|
|
2182
|
+
}, RECHECK_INTERVAL_MS);
|
|
1731
2183
|
logger.info("MCP server ready - all tools and resources registered");
|
|
1732
2184
|
const transport = new StdioServerTransport();
|
|
1733
2185
|
await server.connect(transport);
|