@every-env/sparkle-mcp-server
Version:
MCP server for secure Sparkle folder file access with Claude AI, including clipboard history support
320 lines • 11.4 kB
JavaScript
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
export class ClipboardHistoryManager {
pasteboardPath;
constructor(sparklePath) {
this.pasteboardPath = path.join(this.expandPath(sparklePath), "Pasteboard");
}
expandPath(folderPath) {
if (folderPath.startsWith("~/")) {
return path.join(os.homedir(), folderPath.slice(2));
}
return folderPath;
}
/**
* Search clipboard history with various filters
*/
async searchClipboardHistory(options) {
const { query, startDate, endDate, type, limit = 50 } = options;
const results = [];
try {
// Get all date files
const dateFiles = await this.getDateFiles(startDate, endDate);
for (const dateFile of dateFiles) {
const dayResults = await this.searchDayClipboard(dateFile, query, type, limit);
if (dayResults.entries.length > 0) {
results.push(dayResults);
}
// Stop if we've reached the limit
const totalEntries = results.reduce((sum, r) => sum + r.entries.length, 0);
if (totalEntries >= limit) {
break;
}
}
return results;
}
catch (error) {
console.error("Error searching clipboard history:", error);
return [];
}
}
/**
* Get clipboard entries for a specific date
*/
async getClipboardByDate(date) {
const dateStr = this.formatDate(date);
try {
const entries = await this.readDateClipboard(dateStr);
return {
date: dateStr,
entries,
totalCount: entries.length
};
}
catch (error) {
return {
date: dateStr,
entries: [],
totalCount: 0
};
}
}
/**
* Get recent clipboard entries (last N days)
*/
async getRecentClipboard(days = 7, limit = 50) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
return this.searchClipboardHistory({
startDate,
endDate,
limit
});
}
/**
* Search for clipboard entries containing specific text
*/
async findClipboardText(searchText, limit = 30) {
const results = await this.searchClipboardHistory({
query: searchText,
limit
});
// Flatten results
const entries = [];
for (const result of results) {
entries.push(...result.entries);
if (entries.length >= limit) {
return entries.slice(0, limit);
}
}
return entries;
}
/**
* Get available date files
*/
async getDateFiles(startDate, endDate) {
try {
const entries = await fs.readdir(this.pasteboardPath, { withFileTypes: true });
let dateFiles = entries
.filter(entry => entry.isFile() && /^\d{4}-\d{2}-\d{2}_clipboard\.(txt|json)$/.test(entry.name))
.map(entry => entry.name.replace(/_clipboard\.(txt|json)$/, ''))
.sort()
.reverse(); // Most recent first
// Filter by date range if provided
if (startDate || endDate) {
dateFiles = dateFiles.filter(dateStr => {
const date = new Date(dateStr);
if (startDate && date < startDate)
return false;
if (endDate && date > endDate)
return false;
return true;
});
}
return dateFiles;
}
catch (error) {
console.error("Error reading pasteboard directory:", error);
return [];
}
}
/**
* Search clipboard entries for a specific day
*/
async searchDayClipboard(dateStr, query, type, limit) {
const entries = await this.readDateClipboard(dateStr);
let filtered = entries;
// Filter by query
if (query) {
const queryLower = query.toLowerCase();
filtered = filtered.filter(entry => entry.content.toLowerCase().includes(queryLower) ||
entry.metadata?.app?.toLowerCase().includes(queryLower));
}
// Filter by type
if (type) {
filtered = filtered.filter(entry => entry.type === type);
}
// Apply limit
if (limit && filtered.length > limit) {
filtered = filtered.slice(0, limit);
}
return {
date: dateStr,
entries: filtered,
totalCount: entries.length
};
}
/**
* Read clipboard entries from a date file
*/
async readDateClipboard(dateStr) {
const entries = [];
try {
// Try both .txt and .json formats
const possibleFiles = [
`${dateStr}_clipboard.json`, // JSON format: 2025-01-08_clipboard.json
`${dateStr}_clipboard.txt` // Text format: 2025-01-08_clipboard.txt
];
for (const filename of possibleFiles) {
const filePath = path.join(this.pasteboardPath, filename);
try {
const content = await fs.readFile(filePath, 'utf-8');
if (filename.endsWith('.json')) {
// Parse JSON format
const data = JSON.parse(content);
if (Array.isArray(data)) {
entries.push(...data.map((entry) => this.normalizeEntry(entry)));
}
else if (data.entries && Array.isArray(data.entries)) {
entries.push(...data.entries.map((entry) => this.normalizeEntry(entry)));
}
}
else {
// Parse text format - each line is a clipboard entry
// Format: "TIMESTAMP | TYPE | CONTENT"
const lines = content.split('\n').filter(line => line.trim());
for (const line of lines) {
const entry = this.parseTextEntry(line, dateStr);
if (entry.content.trim()) {
entries.push(entry);
}
}
}
break; // Found a file, stop looking
}
catch (error) {
// File doesn't exist or can't be read, try next format
continue;
}
}
// Sort by timestamp (most recent first)
return entries.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
catch (error) {
console.error(`Error reading clipboard for date ${dateStr}:`, error);
return [];
}
}
/**
* Normalize entry data from various formats
*/
normalizeEntry(data) {
return {
timestamp: data.timestamp ? new Date(data.timestamp) : new Date(),
content: data.content || data.text || data.data || '',
type: data.type || this.inferType(data.content || ''),
metadata: {
app: data.app || data.source || undefined,
size: data.size || data.content?.length || undefined,
format: data.format || undefined
}
};
}
/**
* Parse text entry (for simple text format)
*/
parseTextEntry(line, dateStr) {
// Try to parse structured format like: "14:30:00 | text | Content here"
// or "2024-01-15 14:30:00 | text | Content here"
const parts = line.split('|').map(p => p.trim());
if (parts.length >= 3) {
let timestampStr = parts[0];
// If timestamp doesn't include date, prepend the date
if (!timestampStr.includes('-') && timestampStr.includes(':')) {
timestampStr = `${dateStr} ${timestampStr}`;
}
return {
timestamp: new Date(timestampStr),
type: parts[1],
content: parts.slice(2).join('|').trim()
};
}
// If only 2 parts, assume: "timestamp | content"
if (parts.length === 2) {
let timestampStr = parts[0];
if (!timestampStr.includes('-') && timestampStr.includes(':')) {
timestampStr = `${dateStr} ${timestampStr}`;
}
return {
timestamp: new Date(timestampStr),
content: parts[1].trim(),
type: this.inferType(parts[1])
};
}
// Fall back to simple content with date from filename
return {
timestamp: new Date(dateStr),
content: line.trim(),
type: this.inferType(line)
};
}
/**
* Extract timestamp from filename
*/
extractTimestampFromFilename(filename) {
// Try to extract timestamp from filenames like "2024-01-15-143000.txt"
const match = filename.match(/(\d{4}-\d{2}-\d{2})[-_]?(\d{6})?/);
if (match) {
const dateStr = match[1];
const timeStr = match[2] || '000000';
return new Date(`${dateStr}T${timeStr.substr(0, 2)}:${timeStr.substr(2, 2)}:${timeStr.substr(4, 2)}`);
}
return null;
}
/**
* Infer content type
*/
inferType(content) {
if (content.startsWith('http://') || content.startsWith('https://')) {
return 'url';
}
if (content.startsWith('/') && content.includes('.')) {
return 'file-path';
}
if (content.match(/^data:image\//)) {
return 'image';
}
return 'text';
}
/**
* Format date as YYYY-MM-DD
*/
formatDate(date) {
return date.toISOString().split('T')[0];
}
/**
* Get date from date string
*/
getDateFromString(dateStr) {
return new Date(dateStr);
}
/**
* Get clipboard statistics
*/
async getClipboardStats() {
const dateFiles = await this.getDateFiles();
let totalEntries = 0;
const typeBreakdown = {};
const recentActivity = [];
for (const dateFile of dateFiles.slice(0, 30)) { // Last 30 days
const result = await this.searchDayClipboard(dateFile);
totalEntries += result.totalCount;
recentActivity.push({
date: dateFile,
count: result.totalCount
});
for (const entry of result.entries) {
typeBreakdown[entry.type] = (typeBreakdown[entry.type] || 0) + 1;
}
}
return {
totalDays: dateFiles.length,
totalEntries,
typeBreakdown,
recentActivity: recentActivity.slice(0, 7) // Last 7 days
};
}
}
//# sourceMappingURL=clipboard-history.js.map