@virsanghavi/axis-server 1.8.1 → 1.9.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-server.mjs +75 -93
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -827,7 +827,7 @@ var NerveCenter = class _NerveCenter {
|
|
|
827
827
|
delete this.state.locks[filePath];
|
|
828
828
|
await this.saveState();
|
|
829
829
|
}
|
|
830
|
-
this.logLockEvent("FORCE_UNLOCKED", filePath, "admin", void 0, reason);
|
|
830
|
+
await this.logLockEvent("FORCE_UNLOCKED", filePath, "admin", void 0, reason);
|
|
831
831
|
await this.appendToNotepad(`
|
|
832
832
|
- [FORCE UNLOCK] ${filePath} unlocked by admin. Reason: ${reason}`);
|
|
833
833
|
return `File ${filePath} has been forcibly unlocked.`;
|
|
@@ -854,6 +854,7 @@ ${notepad}`;
|
|
|
854
854
|
async logLockEvent(eventType, filePath, requestingAgent, blockingAgent, intent) {
|
|
855
855
|
try {
|
|
856
856
|
if (this.contextManager.apiUrl) {
|
|
857
|
+
logger.info(`[logLockEvent] Logging ${eventType} event via API for ${filePath} (agent: ${requestingAgent}, blocker: ${blockingAgent || "none"})`);
|
|
857
858
|
await this.callCoordination("lock-events", "POST", {
|
|
858
859
|
eventType,
|
|
859
860
|
filePath,
|
|
@@ -861,7 +862,9 @@ ${notepad}`;
|
|
|
861
862
|
blockingAgent: blockingAgent || null,
|
|
862
863
|
intent: intent || null
|
|
863
864
|
});
|
|
865
|
+
logger.info(`[logLockEvent] Successfully logged ${eventType} event`);
|
|
864
866
|
} else if (this.useSupabase && this.supabase && this._projectId) {
|
|
867
|
+
logger.info(`[logLockEvent] Logging ${eventType} event via Supabase for ${filePath}`);
|
|
865
868
|
await this.supabase.from("lock_events").insert({
|
|
866
869
|
project_id: this._projectId,
|
|
867
870
|
event_type: eventType,
|
|
@@ -870,20 +873,21 @@ ${notepad}`;
|
|
|
870
873
|
blocking_agent: blockingAgent || null,
|
|
871
874
|
intent: intent || null
|
|
872
875
|
});
|
|
876
|
+
logger.info(`[logLockEvent] Successfully logged ${eventType} event`);
|
|
877
|
+
} else {
|
|
878
|
+
logger.warn(`[logLockEvent] No persistence backend available \u2014 ${eventType} event for ${filePath} will not be recorded`);
|
|
873
879
|
}
|
|
874
880
|
} catch (e) {
|
|
875
|
-
logger.
|
|
881
|
+
logger.error(`[logLockEvent] Failed to log ${eventType} event for ${filePath}: ${e.message}`);
|
|
876
882
|
}
|
|
877
883
|
}
|
|
878
884
|
// --- Decision & Orchestration ---
|
|
879
885
|
/**
|
|
880
886
|
* Normalize a lock path to be relative to the project root.
|
|
881
887
|
* 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)
|
|
888
|
+
* paths resolve to the same key. This ensures that:
|
|
889
|
+
* "/Users/vir/Projects/MyApp/src/api/route.ts" and "src/api/route.ts"
|
|
890
|
+
* are treated as the same lock.
|
|
887
891
|
*/
|
|
888
892
|
static normalizeLockPath(filePath) {
|
|
889
893
|
let normalized = filePath.replace(/\/+$/, "");
|
|
@@ -897,59 +901,54 @@ ${notepad}`;
|
|
|
897
901
|
return normalized;
|
|
898
902
|
}
|
|
899
903
|
/**
|
|
900
|
-
* Validate that a lock
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
904
|
+
* Validate that a lock targets an individual file, not a directory.
|
|
905
|
+
* Agents must lock specific files — directory locks are rejected because
|
|
906
|
+
* they block all other agents from working on ANY file in that tree,
|
|
907
|
+
* even for completely unrelated features.
|
|
908
|
+
*
|
|
909
|
+
* Detection strategy:
|
|
910
|
+
* 1. If the path exists on disk, use fs.stat to check (handles extensionless files like Makefile)
|
|
911
|
+
* 2. If the path doesn't exist, use file extension heuristic
|
|
905
912
|
*/
|
|
906
|
-
static
|
|
907
|
-
|
|
908
|
-
if (!
|
|
909
|
-
return { valid: false, reason: "Cannot lock the
|
|
910
|
-
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
913
|
+
static async validateFileOnly(filePath) {
|
|
914
|
+
const normalized = _NerveCenter.normalizeLockPath(filePath);
|
|
915
|
+
if (!normalized || normalized === "." || normalized === "/") {
|
|
916
|
+
return { valid: false, reason: "Cannot lock the project root. Lock individual files instead." };
|
|
917
|
+
}
|
|
918
|
+
const absolutePath = path2.isAbsolute(filePath) ? filePath : path2.join(process.cwd(), filePath);
|
|
919
|
+
try {
|
|
920
|
+
const stat = await fs2.stat(absolutePath);
|
|
921
|
+
if (stat.isDirectory()) {
|
|
922
|
+
return {
|
|
923
|
+
valid: false,
|
|
924
|
+
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.`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return { valid: true };
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
const lastSegment = normalized.split("/").filter(Boolean).pop() || "";
|
|
931
|
+
if (!lastSegment.includes(".")) {
|
|
915
932
|
return {
|
|
916
933
|
valid: false,
|
|
917
|
-
reason: `
|
|
934
|
+
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
935
|
};
|
|
919
936
|
}
|
|
920
937
|
return { valid: true };
|
|
921
938
|
}
|
|
922
939
|
/**
|
|
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)
|
|
931
|
-
*/
|
|
932
|
-
static pathsConflict(pathA, pathB) {
|
|
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.
|
|
940
|
+
* Find an existing lock that conflicts with the requested path (exact match).
|
|
941
|
+
* Paths are normalized before comparison so absolute and relative paths
|
|
942
|
+
* targeting the same file are correctly detected as conflicts.
|
|
944
943
|
*/
|
|
945
|
-
|
|
944
|
+
findExactConflict(requestedPath, requestingAgent, locks) {
|
|
946
945
|
const normalizedRequested = _NerveCenter.normalizeLockPath(requestedPath);
|
|
947
946
|
for (const lock of locks) {
|
|
948
947
|
if (lock.agentId === requestingAgent) continue;
|
|
949
948
|
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
950
949
|
if (isStale) continue;
|
|
951
950
|
const normalizedLock = _NerveCenter.normalizeLockPath(lock.filePath);
|
|
952
|
-
if (
|
|
951
|
+
if (normalizedRequested === normalizedLock) {
|
|
953
952
|
return lock;
|
|
954
953
|
}
|
|
955
954
|
}
|
|
@@ -960,38 +959,42 @@ ${notepad}`;
|
|
|
960
959
|
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
961
960
|
const normalizedPath = _NerveCenter.normalizeLockPath(filePath);
|
|
962
961
|
logger.info(`[proposeFileAccess] Normalized path: '${normalizedPath}' (from '${filePath}')`);
|
|
963
|
-
const
|
|
964
|
-
if (!
|
|
965
|
-
logger.warn(`[proposeFileAccess] REJECTED \u2014
|
|
962
|
+
const fileCheck = await _NerveCenter.validateFileOnly(filePath);
|
|
963
|
+
if (!fileCheck.valid) {
|
|
964
|
+
logger.warn(`[proposeFileAccess] REJECTED \u2014 not a file: ${fileCheck.reason}`);
|
|
966
965
|
return {
|
|
967
966
|
status: "REJECTED",
|
|
968
|
-
message:
|
|
967
|
+
message: fileCheck.reason
|
|
969
968
|
};
|
|
970
969
|
}
|
|
971
970
|
if (this.contextManager.apiUrl) {
|
|
972
971
|
try {
|
|
973
972
|
const result = await this.callCoordination("locks", "POST", {
|
|
974
973
|
action: "lock",
|
|
975
|
-
filePath,
|
|
974
|
+
filePath: normalizedPath,
|
|
976
975
|
agentId,
|
|
977
976
|
intent,
|
|
978
977
|
userPrompt
|
|
979
978
|
});
|
|
980
979
|
if (result.status === "DENIED") {
|
|
981
980
|
logger.info(`[proposeFileAccess] DENIED by server: ${result.message}`);
|
|
982
|
-
this.logLockEvent("BLOCKED",
|
|
981
|
+
await this.logLockEvent("BLOCKED", normalizedPath, agentId, result.current_lock?.agent_id, intent);
|
|
983
982
|
return {
|
|
984
983
|
status: "REQUIRES_ORCHESTRATION",
|
|
985
|
-
message: result.message || `File '${
|
|
984
|
+
message: result.message || `File '${normalizedPath}' is locked by another agent`,
|
|
986
985
|
currentLock: result.current_lock
|
|
987
986
|
};
|
|
988
987
|
}
|
|
988
|
+
if (result.status === "REJECTED") {
|
|
989
|
+
logger.warn(`[proposeFileAccess] REJECTED by server: ${result.message}`);
|
|
990
|
+
return { status: "REJECTED", message: result.message };
|
|
991
|
+
}
|
|
989
992
|
logger.info(`[proposeFileAccess] GRANTED by server`);
|
|
990
|
-
this.logLockEvent("GRANTED",
|
|
993
|
+
await this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
991
994
|
await this.appendToNotepad(`
|
|
992
|
-
- [LOCK] ${agentId} locked ${
|
|
995
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
993
996
|
Intent: ${intent}`);
|
|
994
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
997
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
995
998
|
} catch (e) {
|
|
996
999
|
if (e.message && e.message.includes("409")) {
|
|
997
1000
|
logger.info(`[proposeFileAccess] Lock conflict (409)`);
|
|
@@ -1004,10 +1007,10 @@ ${notepad}`;
|
|
|
1004
1007
|
}
|
|
1005
1008
|
} catch {
|
|
1006
1009
|
}
|
|
1007
|
-
this.logLockEvent("BLOCKED",
|
|
1010
|
+
await this.logLockEvent("BLOCKED", normalizedPath, agentId, blockingAgent, intent);
|
|
1008
1011
|
return {
|
|
1009
1012
|
status: "REQUIRES_ORCHESTRATION",
|
|
1010
|
-
message: `File '${
|
|
1013
|
+
message: `File '${normalizedPath}' is locked by another agent`
|
|
1011
1014
|
};
|
|
1012
1015
|
}
|
|
1013
1016
|
logger.error(`[proposeFileAccess] API lock failed: ${e.message}`, e);
|
|
@@ -1016,29 +1019,9 @@ ${notepad}`;
|
|
|
1016
1019
|
}
|
|
1017
1020
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
1018
1021
|
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
1022
|
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
1040
1023
|
p_project_id: this._projectId,
|
|
1041
|
-
p_file_path:
|
|
1024
|
+
p_file_path: normalizedPath,
|
|
1042
1025
|
p_agent_id: agentId,
|
|
1043
1026
|
p_intent: intent,
|
|
1044
1027
|
p_user_prompt: userPrompt,
|
|
@@ -1047,45 +1030,44 @@ ${notepad}`;
|
|
|
1047
1030
|
if (error) throw error;
|
|
1048
1031
|
const row = Array.isArray(data) ? data[0] : data;
|
|
1049
1032
|
if (row && row.status === "DENIED") {
|
|
1050
|
-
this.logLockEvent("BLOCKED",
|
|
1033
|
+
await this.logLockEvent("BLOCKED", normalizedPath, agentId, row.owner_id, intent);
|
|
1051
1034
|
return {
|
|
1052
1035
|
status: "REQUIRES_ORCHESTRATION",
|
|
1053
|
-
message: `Conflict: File '${
|
|
1036
|
+
message: `Conflict: File '${normalizedPath}' is locked by '${row.owner_id}'`,
|
|
1054
1037
|
currentLock: {
|
|
1055
1038
|
agentId: row.owner_id,
|
|
1056
|
-
filePath,
|
|
1039
|
+
filePath: normalizedPath,
|
|
1057
1040
|
intent: row.intent,
|
|
1058
1041
|
timestamp: row.updated_at ? Date.parse(row.updated_at) : Date.now()
|
|
1059
1042
|
}
|
|
1060
1043
|
};
|
|
1061
1044
|
}
|
|
1062
|
-
this.logLockEvent("GRANTED",
|
|
1045
|
+
await this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
1063
1046
|
await this.appendToNotepad(`
|
|
1064
|
-
- [LOCK] ${agentId} locked ${
|
|
1047
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
1065
1048
|
Intent: ${intent}`);
|
|
1066
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
1049
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
1067
1050
|
} catch (e) {
|
|
1068
1051
|
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
1069
1052
|
}
|
|
1070
1053
|
}
|
|
1071
1054
|
const allLocks = Object.values(this.state.locks);
|
|
1072
|
-
const conflict = this.
|
|
1055
|
+
const conflict = this.findExactConflict(filePath, agentId, allLocks);
|
|
1073
1056
|
if (conflict) {
|
|
1074
|
-
|
|
1075
|
-
this.logLockEvent("BLOCKED", filePath, agentId, conflict.agentId, intent);
|
|
1057
|
+
await this.logLockEvent("BLOCKED", normalizedPath, agentId, conflict.agentId, intent);
|
|
1076
1058
|
return {
|
|
1077
1059
|
status: "REQUIRES_ORCHESTRATION",
|
|
1078
|
-
message: `Conflict: '${
|
|
1060
|
+
message: `Conflict: File '${normalizedPath}' is locked by '${conflict.agentId}'`,
|
|
1079
1061
|
currentLock: conflict
|
|
1080
1062
|
};
|
|
1081
1063
|
}
|
|
1082
|
-
this.state.locks[
|
|
1064
|
+
this.state.locks[normalizedPath] = { agentId, filePath: normalizedPath, intent, userPrompt, timestamp: Date.now() };
|
|
1083
1065
|
await this.saveState();
|
|
1084
|
-
this.logLockEvent("GRANTED",
|
|
1066
|
+
await this.logLockEvent("GRANTED", normalizedPath, agentId, void 0, intent);
|
|
1085
1067
|
await this.appendToNotepad(`
|
|
1086
|
-
- [LOCK] ${agentId} locked ${
|
|
1068
|
+
- [LOCK] ${agentId} locked ${normalizedPath}
|
|
1087
1069
|
Intent: ${intent}`);
|
|
1088
|
-
return { status: "GRANTED", message: `Access granted for ${
|
|
1070
|
+
return { status: "GRANTED", message: `Access granted for ${normalizedPath}` };
|
|
1089
1071
|
});
|
|
1090
1072
|
}
|
|
1091
1073
|
async updateSharedContext(text, agentId) {
|
|
@@ -2069,7 +2051,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2069
2051
|
// --- Decision & Orchestration ---
|
|
2070
2052
|
{
|
|
2071
2053
|
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
|
|
2054
|
+
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
2055
|
inputSchema: {
|
|
2074
2056
|
type: "object",
|
|
2075
2057
|
properties: {
|