@virsanghavi/axis-server 1.7.2 → 1.8.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 +64 -13
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -284,7 +284,7 @@ var CircuitOpenError = class extends Error {
|
|
|
284
284
|
this.name = "CircuitOpenError";
|
|
285
285
|
}
|
|
286
286
|
};
|
|
287
|
-
var NerveCenter = class {
|
|
287
|
+
var NerveCenter = class _NerveCenter {
|
|
288
288
|
mutex;
|
|
289
289
|
state;
|
|
290
290
|
contextManager;
|
|
@@ -876,6 +876,38 @@ ${notepad}`;
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
// --- Decision & Orchestration ---
|
|
879
|
+
/**
|
|
880
|
+
* Check if two file paths conflict hierarchically.
|
|
881
|
+
* A lock on a directory blocks locks on any file within it, and vice versa.
|
|
882
|
+
* Examples:
|
|
883
|
+
* pathsConflict("/foo/bar", "/foo/bar/baz.ts") => true (parent blocks child)
|
|
884
|
+
* pathsConflict("/foo/bar/baz.ts", "/foo/bar") => true (child blocks parent)
|
|
885
|
+
* pathsConflict("/foo/bar", "/foo/bar") => true (exact match)
|
|
886
|
+
* pathsConflict("/foo/bar", "/foo/baz") => false (siblings)
|
|
887
|
+
*/
|
|
888
|
+
static pathsConflict(pathA, pathB) {
|
|
889
|
+
const a = pathA.replace(/\/+$/, "");
|
|
890
|
+
const b = pathB.replace(/\/+$/, "");
|
|
891
|
+
if (a === b) return true;
|
|
892
|
+
if (b.startsWith(a + "/")) return true;
|
|
893
|
+
if (a.startsWith(b + "/")) return true;
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Find any existing lock that conflicts hierarchically with the requested path.
|
|
898
|
+
* Skips locks owned by the same agent and stale locks.
|
|
899
|
+
*/
|
|
900
|
+
findHierarchicalConflict(requestedPath, requestingAgent, locks) {
|
|
901
|
+
for (const lock of locks) {
|
|
902
|
+
if (lock.agentId === requestingAgent) continue;
|
|
903
|
+
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
904
|
+
if (isStale) continue;
|
|
905
|
+
if (_NerveCenter.pathsConflict(requestedPath, lock.filePath)) {
|
|
906
|
+
return lock;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
879
911
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
880
912
|
return await this.mutex.runExclusive(async () => {
|
|
881
913
|
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
@@ -927,6 +959,26 @@ ${notepad}`;
|
|
|
927
959
|
}
|
|
928
960
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
929
961
|
try {
|
|
962
|
+
const { data: existingLocks } = await this.supabase.from("locks").select("agent_id, file_path, intent, updated_at").eq("project_id", this._projectId);
|
|
963
|
+
if (existingLocks && existingLocks.length > 0) {
|
|
964
|
+
const asFileLocks = existingLocks.map((row2) => ({
|
|
965
|
+
agentId: row2.agent_id,
|
|
966
|
+
filePath: row2.file_path,
|
|
967
|
+
intent: row2.intent,
|
|
968
|
+
userPrompt: "",
|
|
969
|
+
timestamp: row2.updated_at ? Date.parse(row2.updated_at) : Date.now()
|
|
970
|
+
}));
|
|
971
|
+
const conflict2 = this.findHierarchicalConflict(filePath, agentId, asFileLocks);
|
|
972
|
+
if (conflict2) {
|
|
973
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict: '${filePath}' overlaps with locked '${conflict2.filePath}' (owner: ${conflict2.agentId})`);
|
|
974
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict2.agentId, intent);
|
|
975
|
+
return {
|
|
976
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
977
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict2.filePath}' locked by '${conflict2.agentId}'`,
|
|
978
|
+
currentLock: conflict2
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
930
982
|
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
931
983
|
p_project_id: this._projectId,
|
|
932
984
|
p_file_path: filePath,
|
|
@@ -959,17 +1011,16 @@ ${notepad}`;
|
|
|
959
1011
|
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
960
1012
|
}
|
|
961
1013
|
}
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
1014
|
+
const allLocks = Object.values(this.state.locks);
|
|
1015
|
+
const conflict = this.findHierarchicalConflict(filePath, agentId, allLocks);
|
|
1016
|
+
if (conflict) {
|
|
1017
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict (local): '${filePath}' overlaps with locked '${conflict.filePath}' (owner: ${conflict.agentId})`);
|
|
1018
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict.agentId, intent);
|
|
1019
|
+
return {
|
|
1020
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
1021
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict.filePath}' locked by '${conflict.agentId}'`,
|
|
1022
|
+
currentLock: conflict
|
|
1023
|
+
};
|
|
973
1024
|
}
|
|
974
1025
|
this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
|
|
975
1026
|
await this.saveState();
|
|
@@ -1961,7 +2012,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1961
2012
|
// --- Decision & Orchestration ---
|
|
1962
2013
|
{
|
|
1963
2014
|
name: "propose_file_access",
|
|
1964
|
-
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, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), 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.",
|
|
2015
|
+
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, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- **Hierarchical matching**: Locking a directory also blocks locks on files within it, and vice versa. E.g. locking `src/api/` blocks `src/api/auth/login.ts`.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), 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.",
|
|
1965
2016
|
inputSchema: {
|
|
1966
2017
|
type: "object",
|
|
1967
2018
|
properties: {
|