first commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
||||
import { BaseBrowserToolExecutor } from '../base-browser';
|
||||
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
||||
import {
|
||||
parseISO,
|
||||
subDays,
|
||||
subWeeks,
|
||||
subMonths,
|
||||
subYears,
|
||||
startOfToday,
|
||||
startOfYesterday,
|
||||
isValid,
|
||||
format,
|
||||
} from 'date-fns';
|
||||
|
||||
interface HistoryToolParams {
|
||||
text?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
maxResults?: number;
|
||||
excludeCurrentTabs?: boolean;
|
||||
}
|
||||
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
lastVisitTime?: number; // Timestamp in milliseconds
|
||||
visitCount?: number;
|
||||
typedCount?: number;
|
||||
}
|
||||
|
||||
interface HistoryResult {
|
||||
items: HistoryItem[];
|
||||
totalCount: number;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
startTimeFormatted: string;
|
||||
endTimeFormatted: string;
|
||||
};
|
||||
query?: string;
|
||||
}
|
||||
|
||||
class HistoryTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.HISTORY;
|
||||
private static readonly ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Parse a date string into milliseconds since epoch.
|
||||
* Returns null if the date string is invalid.
|
||||
* Supports:
|
||||
* - ISO date strings (e.g., "2023-10-31", "2023-10-31T14:30:00.000Z")
|
||||
* - Relative times: "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"
|
||||
* - Special keywords: "now", "today", "yesterday"
|
||||
*/
|
||||
private parseDateString(dateStr: string | undefined | null): number | null {
|
||||
if (!dateStr) {
|
||||
// If an empty or null string is passed, it might mean "no specific date",
|
||||
// depending on how you want to treat it. Returning null is safer.
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const lowerDateStr = dateStr.toLowerCase().trim();
|
||||
|
||||
if (lowerDateStr === 'now') return now.getTime();
|
||||
if (lowerDateStr === 'today') return startOfToday().getTime();
|
||||
if (lowerDateStr === 'yesterday') return startOfYesterday().getTime();
|
||||
|
||||
const relativeMatch = lowerDateStr.match(
|
||||
/^(\d+)\s+(day|days|week|weeks|month|months|year|years)\s+ago$/,
|
||||
);
|
||||
if (relativeMatch) {
|
||||
const amount = parseInt(relativeMatch[1], 10);
|
||||
const unit = relativeMatch[2];
|
||||
let resultDate: Date;
|
||||
if (unit.startsWith('day')) resultDate = subDays(now, amount);
|
||||
else if (unit.startsWith('week')) resultDate = subWeeks(now, amount);
|
||||
else if (unit.startsWith('month')) resultDate = subMonths(now, amount);
|
||||
else if (unit.startsWith('year')) resultDate = subYears(now, amount);
|
||||
else return null; // Should not happen with the regex
|
||||
return resultDate.getTime();
|
||||
}
|
||||
|
||||
// Try parsing as ISO or other common date string formats
|
||||
// Native Date constructor can be unreliable for non-standard formats.
|
||||
// date-fns' parseISO is good for ISO 8601.
|
||||
// For other formats, date-fns' parse function is more flexible.
|
||||
let parsedDate = parseISO(dateStr); // Handles "2023-10-31" or "2023-10-31T10:00:00"
|
||||
if (isValid(parsedDate)) {
|
||||
return parsedDate.getTime();
|
||||
}
|
||||
|
||||
// Fallback to new Date() for other potential formats, but with caution
|
||||
parsedDate = new Date(dateStr);
|
||||
if (isValid(parsedDate) && dateStr.includes(parsedDate.getFullYear().toString())) {
|
||||
return parsedDate.getTime();
|
||||
}
|
||||
|
||||
console.warn(`Could not parse date string: ${dateStr}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp as a human-readable date string
|
||||
*/
|
||||
private formatDate(timestamp: number): string {
|
||||
// Using date-fns for consistent and potentially localized formatting
|
||||
return format(timestamp, 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
|
||||
async execute(args: HistoryToolParams): Promise<ToolResult> {
|
||||
try {
|
||||
console.log('Executing HistoryTool with args:', args);
|
||||
|
||||
const {
|
||||
text = '',
|
||||
maxResults = 100, // Default to 100 results
|
||||
excludeCurrentTabs = false,
|
||||
} = args;
|
||||
|
||||
const now = Date.now();
|
||||
let startTimeMs: number;
|
||||
let endTimeMs: number;
|
||||
|
||||
// Parse startTime
|
||||
if (args.startTime) {
|
||||
const parsedStart = this.parseDateString(args.startTime);
|
||||
if (parsedStart === null) {
|
||||
return createErrorResponse(
|
||||
`Invalid format for start time: "${args.startTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
|
||||
);
|
||||
}
|
||||
startTimeMs = parsedStart;
|
||||
} else {
|
||||
// Default to 24 hours ago if startTime is not provided
|
||||
startTimeMs = now - HistoryTool.ONE_DAY_MS;
|
||||
}
|
||||
|
||||
// Parse endTime
|
||||
if (args.endTime) {
|
||||
const parsedEnd = this.parseDateString(args.endTime);
|
||||
if (parsedEnd === null) {
|
||||
return createErrorResponse(
|
||||
`Invalid format for end time: "${args.endTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
|
||||
);
|
||||
}
|
||||
endTimeMs = parsedEnd;
|
||||
} else {
|
||||
// Default to current time if endTime is not provided
|
||||
endTimeMs = now;
|
||||
}
|
||||
|
||||
// Validate time range
|
||||
if (startTimeMs > endTimeMs) {
|
||||
return createErrorResponse('Start time cannot be after end time.');
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Searching history from ${this.formatDate(startTimeMs)} to ${this.formatDate(endTimeMs)} for query "${text}"`,
|
||||
);
|
||||
|
||||
const historyItems = await chrome.history.search({
|
||||
text,
|
||||
startTime: startTimeMs,
|
||||
endTime: endTimeMs,
|
||||
maxResults,
|
||||
});
|
||||
|
||||
console.log(`Found ${historyItems.length} history items before filtering current tabs.`);
|
||||
|
||||
let filteredItems = historyItems;
|
||||
if (excludeCurrentTabs && historyItems.length > 0) {
|
||||
const currentTabs = await chrome.tabs.query({});
|
||||
const openUrls = new Set<string>();
|
||||
|
||||
currentTabs.forEach((tab) => {
|
||||
if (tab.url) {
|
||||
openUrls.add(tab.url);
|
||||
}
|
||||
});
|
||||
|
||||
if (openUrls.size > 0) {
|
||||
filteredItems = historyItems.filter((item) => !(item.url && openUrls.has(item.url)));
|
||||
console.log(
|
||||
`Filtered out ${historyItems.length - filteredItems.length} items that are currently open. ${filteredItems.length} items remaining.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result: HistoryResult = {
|
||||
items: filteredItems.map((item) => ({
|
||||
id: item.id,
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
lastVisitTime: item.lastVisitTime,
|
||||
visitCount: item.visitCount,
|
||||
typedCount: item.typedCount,
|
||||
})),
|
||||
totalCount: filteredItems.length,
|
||||
timeRange: {
|
||||
startTime: startTimeMs,
|
||||
endTime: endTimeMs,
|
||||
startTimeFormatted: this.formatDate(startTimeMs),
|
||||
endTimeFormatted: this.formatDate(endTimeMs),
|
||||
},
|
||||
};
|
||||
|
||||
if (text) {
|
||||
result.query = text;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in HistoryTool.execute:', error);
|
||||
return createErrorResponse(
|
||||
`Error retrieving browsing history: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const historyTool = new HistoryTool();
|
Reference in New Issue
Block a user