Files
broswer-automation/app/chrome-extension/entrypoints/background/tools/browser/history.ts
nasir@endelospay.com d97cad1736 first commit
2025-08-12 02:54:17 +05:00

233 lines
7.1 KiB
TypeScript

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();