@fastmcp-me/mcp-sqlew
Version:
MCP server for efficient context sharing between Claude Code sub-agents with 96% token reduction via action-based tools
204 lines • 7.46 kB
JavaScript
/**
* File tracking tools for MCP Shared Context Server
* Provides file change tracking with layer integration and lock detection
*/
import { getDatabase, getOrCreateFile, getOrCreateAgent, getLayerId } from '../database.js';
import { STRING_TO_CHANGE_TYPE, CHANGE_TYPE_TO_STRING, STANDARD_LAYERS, DEFAULT_QUERY_LIMIT } from '../constants.js';
import { performAutoCleanup } from '../utils/cleanup.js';
/**
* Record a file change with optional layer assignment and description.
* Auto-registers the file and agent if they don't exist.
*
* @param params - File change parameters
* @returns Success response with change ID and timestamp
*/
export function recordFileChange(params) {
const db = getDatabase();
// Cleanup old file changes before inserting new one
performAutoCleanup(db);
try {
// Validate change_type
const changeTypeInt = STRING_TO_CHANGE_TYPE[params.change_type];
if (changeTypeInt === undefined) {
throw new Error(`Invalid change_type: ${params.change_type}. Must be one of: created, modified, deleted`);
}
// Validate layer if provided
let layerId = null;
if (params.layer) {
if (!STANDARD_LAYERS.includes(params.layer)) {
throw new Error(`Invalid layer: ${params.layer}. Must be one of: ${STANDARD_LAYERS.join(', ')}`);
}
layerId = getLayerId(db, params.layer);
if (layerId === null) {
throw new Error(`Layer not found: ${params.layer}`);
}
}
// Auto-register file and agent
const fileId = getOrCreateFile(db, params.file_path);
const agentId = getOrCreateAgent(db, params.agent_name);
// Insert file change record
const stmt = db.prepare(`
INSERT INTO t_file_changes (file_id, agent_id, layer_id, change_type, description)
VALUES (?, ?, ?, ?, ?)
`);
const result = stmt.run(fileId, agentId, layerId, changeTypeInt, params.description || null);
// Get timestamp
const tsResult = db.prepare('SELECT ts FROM t_file_changes WHERE id = ?')
.get(result.lastInsertRowid);
return {
success: true,
change_id: result.lastInsertRowid,
timestamp: tsResult ? new Date(tsResult.ts * 1000).toISOString() : new Date().toISOString(),
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to record file change: ${message}`);
}
}
/**
* Get file changes with advanced filtering.
* Uses token-efficient view when no specific filters are applied.
*
* @param params - Filter parameters
* @returns Array of file changes with metadata
*/
export function getFileChanges(params) {
const db = getDatabase();
try {
const limit = params.limit || DEFAULT_QUERY_LIMIT;
const conditions = [];
const values = [];
// Build WHERE clause based on filters
if (params.file_path) {
conditions.push('f.path = ?');
values.push(params.file_path);
}
if (params.agent_name) {
conditions.push('a.name = ?');
values.push(params.agent_name);
}
if (params.layer) {
// Validate layer
if (!STANDARD_LAYERS.includes(params.layer)) {
throw new Error(`Invalid layer: ${params.layer}. Must be one of: ${STANDARD_LAYERS.join(', ')}`);
}
conditions.push('l.name = ?');
values.push(params.layer);
}
if (params.change_type) {
const changeTypeInt = STRING_TO_CHANGE_TYPE[params.change_type];
if (changeTypeInt === undefined) {
throw new Error(`Invalid change_type: ${params.change_type}`);
}
conditions.push('fc.change_type = ?');
values.push(changeTypeInt);
}
if (params.since) {
// Convert ISO 8601 to Unix epoch
const sinceEpoch = Math.floor(new Date(params.since).getTime() / 1000);
conditions.push('fc.ts >= ?');
values.push(sinceEpoch);
}
// Use view if no specific filters (token efficient)
if (conditions.length === 0) {
const stmt = db.prepare(`
SELECT * FROM v_recent_file_changes
LIMIT ?
`);
const rows = stmt.all(limit);
return {
changes: rows,
count: rows.length,
};
}
// Otherwise, build custom query
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const stmt = db.prepare(`
SELECT
f.path,
a.name as changed_by,
l.name as layer,
CASE fc.change_type
WHEN 1 THEN 'created'
WHEN 2 THEN 'modified'
ELSE 'deleted'
END as change_type,
fc.description,
datetime(fc.ts, 'unixepoch') as changed_at
FROM t_file_changes fc
JOIN m_files f ON fc.file_id = f.id
JOIN m_agents a ON fc.agent_id = a.id
LEFT JOIN m_layers l ON fc.layer_id = l.id
${whereClause}
ORDER BY fc.ts DESC
LIMIT ?
`);
values.push(limit);
const rows = stmt.all(...values);
return {
changes: rows,
count: rows.length,
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get file changes: ${message}`);
}
}
/**
* Check if a file is "locked" (recently modified by another agent).
* Useful to prevent concurrent edits by multiple agents.
*
* @param params - File path and lock duration
* @returns Lock status with details
*/
export function checkFileLock(params) {
const db = getDatabase();
try {
const lockDuration = params.lock_duration || 300; // Default 5 minutes
const currentTime = Math.floor(Date.now() / 1000);
const lockThreshold = currentTime - lockDuration;
// Get the most recent change to this file
const stmt = db.prepare(`
SELECT
a.name as agent,
fc.change_type,
fc.ts
FROM t_file_changes fc
JOIN m_files f ON fc.file_id = f.id
JOIN m_agents a ON fc.agent_id = a.id
WHERE f.path = ?
ORDER BY fc.ts DESC
LIMIT 1
`);
const result = stmt.get(params.file_path);
if (!result) {
// File never changed
return {
locked: false,
};
}
// Check if within lock duration
if (result.ts >= lockThreshold) {
return {
locked: true,
last_agent: result.agent,
last_change: new Date(result.ts * 1000).toISOString(),
change_type: CHANGE_TYPE_TO_STRING[result.change_type],
};
}
// Not locked (too old)
return {
locked: false,
last_agent: result.agent,
last_change: new Date(result.ts * 1000).toISOString(),
change_type: CHANGE_TYPE_TO_STRING[result.change_type],
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to check file lock: ${message}`);
}
}
//# sourceMappingURL=files.js.map