@virsanghavi/axis-server 1.8.1 → 1.9.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 +67 -91
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -879,11 +879,9 @@ ${notepad}`;
|
|
|
879
879
|
/**
|
|
880
880
|
* Normalize a lock path to be relative to the project root.
|
|
881
881
|
* Strips the project root prefix (process.cwd()) so that absolute and relative
|
|
882
|
-
* paths resolve to the same key.
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* "src/api/v1/route.ts" => "src/api/v1/route.ts"
|
|
886
|
-
* "/Users/vir/Projects/MyApp/" => "" (project root)
|
|
882
|
+
* paths resolve to the same key. This ensures that:
|
|
883
|
+
* "/Users/vir/Projects/MyApp/src/api/route.ts" and "src/api/route.ts"
|
|
884
|
+
* are treated as the same lock.
|
|
887
885
|
*/
|
|
888
886
|
static normalizeLockPath(filePath) {
|
|
889
887
|
let normalized = filePath.replace(/\/+$/, "");
|
|
@@ -897,59 +895,54 @@ ${notepad}`;
|
|
|
897
895
|
return normalized;
|
|
898
896
|
}
|
|
899
897
|
/**
|
|
900
|
-
* Validate that a lock
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
898
|
+
* Validate that a lock targets an individual file, not a directory.
|
|
899
|
+
* Agents must lock specific files — directory locks are rejected because
|
|
900
|
+
* they block all other agents from working on ANY file in that tree,
|
|
901
|
+
* even for completely unrelated features.
|
|
902
|
+
*
|
|
903
|
+
* Detection strategy:
|
|
904
|
+
* 1. If the path exists on disk, use fs.stat to check (handles extensionless files like Makefile)
|
|
905
|
+
* 2. If the path doesn't exist, use file extension heuristic
|
|
905
906
|
*/
|
|
906
|
-
static
|
|
907
|
-
|
|
908
|
-
if (!
|
|
909
|
-
return { valid: false, reason: "Cannot lock the
|
|
910
|
-
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
907
|
+
static async validateFileOnly(filePath) {
|
|
908
|
+
const normalized = _NerveCenter.normalizeLockPath(filePath);
|
|
909
|
+
if (!normalized || normalized === "." || normalized === "/") {
|
|
910
|
+
return { valid: false, reason: "Cannot lock the project root. Lock individual files instead." };
|
|
911
|
+
}
|
|
912
|
+
const absolutePath = path2.isAbsolute(filePath) ? filePath : path2.join(process.cwd(), filePath);
|
|
913
|
+
try {
|
|
914
|
+
const stat = await fs2.stat(absolutePath);
|
|
915
|
+
if (stat.isDirectory()) {
|
|
916
|
+
return {
|
|
917
|
+
valid: false,
|
|
918
|
+
reason: `'${normalized}' is a directory. Lock individual files instead \u2014 directory locks block all agents from the entire tree, preventing parallel work on different features.`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return { valid: true };
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
const lastSegment = normalized.split("/").filter(Boolean).pop() || "";
|
|
925
|
+
if (!lastSegment.includes(".")) {
|
|
915
926
|
return {
|
|
916
927
|
valid: false,
|
|
917
|
-
reason: `
|
|
928
|
+
reason: `'${normalized}' looks like a directory (no file extension). Lock individual files instead \u2014 directory locks block all agents from the entire tree, preventing parallel work on different features.`
|
|
918
929
|
};
|
|
919
930
|
}
|
|
920
931
|
return { valid: true };
|
|
921
932
|
}
|
|
922
933
|
/**
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
* Examples:
|
|
927
|
-
* pathsConflict("src/api", "src/api/route.ts") => true (parent blocks child)
|
|
928
|
-
* pathsConflict("src/api/route.ts", "src/api") => true (child blocks parent)
|
|
929
|
-
* pathsConflict("src/api", "src/api") => true (exact match)
|
|
930
|
-
* pathsConflict("src/api", "src/lib") => false (siblings)
|
|
934
|
+
* Find an existing lock that conflicts with the requested path (exact match).
|
|
935
|
+
* Paths are normalized before comparison so absolute and relative paths
|
|
936
|
+
* targeting the same file are correctly detected as conflicts.
|
|
931
937
|
*/
|
|
932
|
-
|
|
933
|
-
const a = pathA.replace(/\/+$/, "");
|
|
934
|
-
const b = pathB.replace(/\/+$/, "");
|
|
935
|
-
if (a === b) return true;
|
|
936
|
-
if (b.startsWith(a + "/")) return true;
|
|
937
|
-
if (a.startsWith(b + "/")) return true;
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Find any existing lock that conflicts hierarchically with the requested path.
|
|
942
|
-
* Skips locks owned by the same agent and stale locks.
|
|
943
|
-
* All paths are normalized before comparison.
|
|
944
|
-
*/
|
|
945
|
-
findHierarchicalConflict(requestedPath, requestingAgent, locks) {
|
|
938
|
+
findExactConflict(requestedPath, requestingAgent, locks) {
|
|
946
939
|
const normalizedRequested = _NerveCenter.normalizeLockPath(requestedPath);
|
|
947
940
|
for (const lock of locks) {
|
|
948
941
|
if (lock.agentId === requestingAgent) continue;
|
|
949
942
|
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
950
943
|
if (isStale) continue;
|
|
951
944
|
const normalizedLock = _NerveCenter.normalizeLockPath(lock.filePath);
|
|
952
|
-
if (
|
|
945
|
+
if (normalizedRequested === normalizedLock) {
|
|
953
946
|
return lock;
|
|
954
947
|
}
|
|
955
948
|
}
|
|
@@ -960,38 +953,42 @@ ${notepad}`;
|
|
|
960
953
|
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
961
954
|
const normalizedPath = _NerveCenter.normalizeLockPath(filePath);
|
|
962
955
|
logger.info(`[proposeFileAccess] Normalized path: '${normalizedPath}' (from '${filePath}')`);
|
|
963
|
-
const
|
|
964
|
-
if (!
|
|
965
|
-
logger.warn(`[proposeFileAccess] REJECTED \u2014
|
|
956
|
+
const fileCheck = await _NerveCenter.validateFileOnly(filePath);
|
|
957
|
+
if (!fileCheck.valid) {
|
|
958
|
+
logger.warn(`[proposeFileAccess] REJECTED \u2014 not a file: ${fileCheck.reason}`);
|
|
966
959
|
return {
|
|
967
960
|
status: "REJECTED",
|
|
968
|
-
message:
|
|
961
|
+
message: fileCheck.reason
|
|
969
962
|
};
|
|
970
963
|
}
|
|
971
964
|
if (this.contextManager.apiUrl) {
|
|
972
965
|
try {
|
|
973
966
|
const result = await this.callCoordination("locks", "POST", {
|
|
974
967
|
action: "lock",
|
|
975
|
-
filePath,
|
|
968
|
+
filePath: normalizedPath,
|
|
976
969
|
agentId,
|
|
977
970
|
intent,
|
|
978
971
|
userPrompt
|
|
979
972
|
});
|
|
980
973
|
if (result.status === "DENIED") {
|
|
981
974
|
logger.info(`[proposeFileAccess] DENIED by server: ${result.message}`);
|
|
982
|
-
this.logLockEvent("BLOCKED",
|
|
975
|
+
this.logLockEvent("BLOCKED", normalizedPath, agentId, result.current_lock?.agent_id, intent);
|
|
983
976
|
return {
|
|
984
977
|
status: "REQUIRES_ORCHESTRATION",
|
|
985
|
-
message: result.message || `File '${
|
|
978
|
+
message: result.message || `File '${normalizedPath}' is locked by another agent`,
|
|
986
979
|
currentLock: result.current_lock
|
|
987
980
|
};
|
|
988
981
|
}
|
|
982
|
+
if (result.status === "REJECTED") {
|
|
983
|
+
logger.warn(`[proposeFileAccess] REJECTED by server: ${result.message}`);
|
|
984
|
+
return { status: "REJECTED", message: result.message };
|
|
985
|
+
}
|
|
989
986
|
logger.info(`[proposeFileAccess] GRANTED by server`);
|
|
990
|
-
this.logLockEvent("GRANTED",
|
|
987
|
+
this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
991
988
|
await this.appendToNotepad(`
|
|
992
|
-
- [LOCK] ${agentId} locked ${
|
|
989
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
993
990
|
Intent: ${intent}`);
|
|
994
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
991
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
995
992
|
} catch (e) {
|
|
996
993
|
if (e.message && e.message.includes("409")) {
|
|
997
994
|
logger.info(`[proposeFileAccess] Lock conflict (409)`);
|
|
@@ -1004,10 +1001,10 @@ ${notepad}`;
|
|
|
1004
1001
|
}
|
|
1005
1002
|
} catch {
|
|
1006
1003
|
}
|
|
1007
|
-
this.logLockEvent("BLOCKED",
|
|
1004
|
+
this.logLockEvent("BLOCKED", normalizedPath, agentId, blockingAgent, intent);
|
|
1008
1005
|
return {
|
|
1009
1006
|
status: "REQUIRES_ORCHESTRATION",
|
|
1010
|
-
message: `File '${
|
|
1007
|
+
message: `File '${normalizedPath}' is locked by another agent`
|
|
1011
1008
|
};
|
|
1012
1009
|
}
|
|
1013
1010
|
logger.error(`[proposeFileAccess] API lock failed: ${e.message}`, e);
|
|
@@ -1016,29 +1013,9 @@ ${notepad}`;
|
|
|
1016
1013
|
}
|
|
1017
1014
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
1018
1015
|
try {
|
|
1019
|
-
const { data: existingLocks } = await this.supabase.from("locks").select("agent_id, file_path, intent, updated_at").eq("project_id", this._projectId);
|
|
1020
|
-
if (existingLocks && existingLocks.length > 0) {
|
|
1021
|
-
const asFileLocks = existingLocks.map((row2) => ({
|
|
1022
|
-
agentId: row2.agent_id,
|
|
1023
|
-
filePath: row2.file_path,
|
|
1024
|
-
intent: row2.intent,
|
|
1025
|
-
userPrompt: "",
|
|
1026
|
-
timestamp: row2.updated_at ? Date.parse(row2.updated_at) : Date.now()
|
|
1027
|
-
}));
|
|
1028
|
-
const conflict2 = this.findHierarchicalConflict(filePath, agentId, asFileLocks);
|
|
1029
|
-
if (conflict2) {
|
|
1030
|
-
logger.info(`[proposeFileAccess] Hierarchical conflict: '${filePath}' overlaps with locked '${conflict2.filePath}' (owner: ${conflict2.agentId})`);
|
|
1031
|
-
this.logLockEvent("BLOCKED", filePath, agentId, conflict2.agentId, intent);
|
|
1032
|
-
return {
|
|
1033
|
-
status: "REQUIRES_ORCHESTRATION",
|
|
1034
|
-
message: `Conflict: '${filePath}' overlaps with '${conflict2.filePath}' locked by '${conflict2.agentId}'`,
|
|
1035
|
-
currentLock: conflict2
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
1016
|
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
1040
1017
|
p_project_id: this._projectId,
|
|
1041
|
-
p_file_path:
|
|
1018
|
+
p_file_path: normalizedPath,
|
|
1042
1019
|
p_agent_id: agentId,
|
|
1043
1020
|
p_intent: intent,
|
|
1044
1021
|
p_user_prompt: userPrompt,
|
|
@@ -1047,45 +1024,44 @@ ${notepad}`;
|
|
|
1047
1024
|
if (error) throw error;
|
|
1048
1025
|
const row = Array.isArray(data) ? data[0] : data;
|
|
1049
1026
|
if (row && row.status === "DENIED") {
|
|
1050
|
-
this.logLockEvent("BLOCKED",
|
|
1027
|
+
this.logLockEvent("BLOCKED", normalizedPath, agentId, row.owner_id, intent);
|
|
1051
1028
|
return {
|
|
1052
1029
|
status: "REQUIRES_ORCHESTRATION",
|
|
1053
|
-
message: `Conflict: File '${
|
|
1030
|
+
message: `Conflict: File '${normalizedPath}' is locked by '${row.owner_id}'`,
|
|
1054
1031
|
currentLock: {
|
|
1055
1032
|
agentId: row.owner_id,
|
|
1056
|
-
filePath,
|
|
1033
|
+
filePath: normalizedPath,
|
|
1057
1034
|
intent: row.intent,
|
|
1058
1035
|
timestamp: row.updated_at ? Date.parse(row.updated_at) : Date.now()
|
|
1059
1036
|
}
|
|
1060
1037
|
};
|
|
1061
1038
|
}
|
|
1062
|
-
this.logLockEvent("GRANTED",
|
|
1039
|
+
this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
1063
1040
|
await this.appendToNotepad(`
|
|
1064
|
-
- [LOCK] ${agentId} locked ${
|
|
1041
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
1065
1042
|
Intent: ${intent}`);
|
|
1066
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
1043
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
1067
1044
|
} catch (e) {
|
|
1068
1045
|
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
1069
1046
|
}
|
|
1070
1047
|
}
|
|
1071
1048
|
const allLocks = Object.values(this.state.locks);
|
|
1072
|
-
const conflict = this.
|
|
1049
|
+
const conflict = this.findExactConflict(filePath, agentId, allLocks);
|
|
1073
1050
|
if (conflict) {
|
|
1074
|
-
|
|
1075
|
-
this.logLockEvent("BLOCKED", filePath, agentId, conflict.agentId, intent);
|
|
1051
|
+
this.logLockEvent("BLOCKED", normalizedPath, agentId, conflict.agentId, intent);
|
|
1076
1052
|
return {
|
|
1077
1053
|
status: "REQUIRES_ORCHESTRATION",
|
|
1078
|
-
message: `Conflict: '${
|
|
1054
|
+
message: `Conflict: File '${normalizedPath}' is locked by '${conflict.agentId}'`,
|
|
1079
1055
|
currentLock: conflict
|
|
1080
1056
|
};
|
|
1081
1057
|
}
|
|
1082
|
-
this.state.locks[
|
|
1058
|
+
this.state.locks[normalizedPath] = { agentId, filePath: normalizedPath, intent, userPrompt, timestamp: Date.now() };
|
|
1083
1059
|
await this.saveState();
|
|
1084
|
-
this.logLockEvent("GRANTED",
|
|
1060
|
+
this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
1085
1061
|
await this.appendToNotepad(`
|
|
1086
|
-
- [LOCK] ${agentId} locked ${
|
|
1062
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
1087
1063
|
Intent: ${intent}`);
|
|
1088
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
1064
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
1089
1065
|
});
|
|
1090
1066
|
}
|
|
1091
1067
|
async updateSharedContext(text, agentId) {
|
|
@@ -2069,7 +2045,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2069
2045
|
// --- Decision & Orchestration ---
|
|
2070
2046
|
{
|
|
2071
2047
|
name: "propose_file_access",
|
|
2072
|
-
description: "**CRITICAL: REQUEST FILE LOCK** \u2014 call this before EVERY file edit, no exceptions.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, `REQUIRES_ORCHESTRATION` if
|
|
2048
|
+
description: "**CRITICAL: REQUEST FILE LOCK** \u2014 call this before EVERY file edit, no exceptions.\n- Checks if another agent currently holds a lock on the same file.\n- Returns `GRANTED` if safe to proceed, `REQUIRES_ORCHESTRATION` if another agent has the file locked, or `REJECTED` if you tried to lock a directory.\n- **File-only locks**: You MUST lock individual files, not directories. Directory locks are rejected because they block all agents from the entire tree, preventing parallel work on different features. Lock each file you edit separately.\n- Paths are normalized relative to the project root, so absolute and relative paths are treated equivalently.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute or relative to a specific file), and `intent` (descriptive \u2014 e.g. 'Refactor auth to use JWT', NOT 'editing file').\n- Locks expire after 30 minutes. Use `force_unlock` only as a last resort for crashed agents.\n- **IMPORTANT**: Every lock you acquire MUST be released. Call `complete_job` when done with each task, and `finalize_session` before ending your session. Dangling locks block all other agents.",
|
|
2073
2049
|
inputSchema: {
|
|
2074
2050
|
type: "object",
|
|
2075
2051
|
properties: {
|