limitless_extract_action_items
Extract action items and tasks from conversations with priority analysis and context recognition to create organized to-do lists from meetings.
Instructions
Intelligently extract action items and tasks from conversations with context, priority analysis, and smart pattern recognition. Perfect for getting your todo list from meetings.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| time_expression | No | Natural time expression like 'today', 'this week' (defaults to 'today'). | |
| timezone | No | IANA timezone for date calculations. | |
| assigned_to | No | Filter action items by assignee ('user' for items assigned to you). | |
| priority | No | Filter by priority level. |
Implementation Reference
- src/server.ts:851-912 (registration)Registration of the 'limitless_extract_action_items' tool with MCP server, including description, schema reference, and handler callback.server.tool("limitless_extract_action_items", "Intelligently extract action items and tasks from conversations with context, priority analysis, and smart pattern recognition. Perfect for getting your todo list from meetings.", ActionItemsArgsSchema, async (args, _extra) => { try { const timeExpression = args.time_expression || 'today'; const parser = new NaturalTimeParser({ timezone: args.timezone }); const timeRange = parser.parseTimeExpression(timeExpression); // Fetch all logs with pagination const allLogs: Lifelog[] = []; let cursor: string | undefined = undefined; while (true) { const result = await getLifelogsWithPagination(limitlessApiKey, { start: timeRange.start, end: timeRange.end, timezone: timeRange.timezone, includeMarkdown: true, includeHeadings: true, limit: MAX_API_LIMIT, direction: 'asc', cursor: cursor }); allLogs.push(...result.lifelogs); if (!result.pagination.nextCursor || result.lifelogs.length < MAX_API_LIMIT) { break; } cursor = result.pagination.nextCursor; } let allActionItems: ActionItem[] = []; for (const lifelog of allLogs) { if (lifelog.contents) { const items = ActionItemExtractor.extractFromNodes(lifelog.contents, lifelog.id); allActionItems.push(...items); } } // Apply filters if (args.assigned_to) { allActionItems = allActionItems.filter(item => item.assignee === args.assigned_to); } if (args.priority) { allActionItems = allActionItems.filter(item => item.priority === args.priority); } return createSafeResponse( allActionItems, allActionItems.length === 0 ? `No action items found for "${timeExpression}"` : `Found ${allActionItems.length} action item(s) for "${timeExpression}"` ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error extracting action items: ${errorMessage}` }], isError: true }; } } );
- src/server.ts:854-911 (handler)Handler function that orchestrates lifelog fetching via pagination, action item extraction using ActionItemExtractor, filtering by assignee and priority, and safe response formatting with truncation handling.async (args, _extra) => { try { const timeExpression = args.time_expression || 'today'; const parser = new NaturalTimeParser({ timezone: args.timezone }); const timeRange = parser.parseTimeExpression(timeExpression); // Fetch all logs with pagination const allLogs: Lifelog[] = []; let cursor: string | undefined = undefined; while (true) { const result = await getLifelogsWithPagination(limitlessApiKey, { start: timeRange.start, end: timeRange.end, timezone: timeRange.timezone, includeMarkdown: true, includeHeadings: true, limit: MAX_API_LIMIT, direction: 'asc', cursor: cursor }); allLogs.push(...result.lifelogs); if (!result.pagination.nextCursor || result.lifelogs.length < MAX_API_LIMIT) { break; } cursor = result.pagination.nextCursor; } let allActionItems: ActionItem[] = []; for (const lifelog of allLogs) { if (lifelog.contents) { const items = ActionItemExtractor.extractFromNodes(lifelog.contents, lifelog.id); allActionItems.push(...items); } } // Apply filters if (args.assigned_to) { allActionItems = allActionItems.filter(item => item.assignee === args.assigned_to); } if (args.priority) { allActionItems = allActionItems.filter(item => item.priority === args.priority); } return createSafeResponse( allActionItems, allActionItems.length === 0 ? `No action items found for "${timeExpression}"` : `Found ${allActionItems.length} action item(s) for "${timeExpression}"` ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error extracting action items: ${errorMessage}` }], isError: true }; } }
- src/server.ts:258-263 (schema)Zod schema defining input parameters for the tool: time_expression (optional, defaults today), timezone, assigned_to filter, and priority filter.const ActionItemsArgsSchema = { time_expression: z.string().optional().describe("Natural time expression like 'today', 'this week' (defaults to 'today')."), timezone: z.string().optional().describe("IANA timezone for date calculations."), assigned_to: z.string().optional().describe("Filter action items by assignee ('user' for items assigned to you)."), priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by priority level."), };
- src/advanced-features.ts:1042-1073 (helper)Primary helper method that scans LifelogContentNode contents using regex patterns to detect action items, extracts assignee from speaker info, surrounding context, infers priority, and deduplicates results.static extractFromNodes(nodes: LifelogContentNode[], lifelogId: string): ActionItem[] { const actionItems: ActionItem[] = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (!node.content) continue; for (const pattern of this.ACTION_PATTERNS) { const matches = Array.from(node.content.matchAll(pattern)); for (const match of matches) { const content = (match[2] || match[1]).trim(); if (content.length > 5) { // Filter out very short matches actionItems.push({ content, assignee: node.speakerIdentifier === "user" ? "user" : node.speakerName || undefined, context: this.getContext(nodes, i), timestamp: node.startTime || "", priority: this.inferPriority(content), source: { lifelogId, nodeIndex: i } }); } } } } return this.deduplicateActionItems(actionItems); }
- src/advanced-features.ts:1022-1117 (helper)ActionItemExtractor class containing regex patterns for action detection, context extraction, priority inference (inferPriority), and deduplication logic.export class ActionItemExtractor { private static readonly ACTION_PATTERNS = [ // Direct commitments with technical context /\b(I'll|I will|I need to|I should|I must)\s+([^.!?]{5,150})/gi, // Explicit action items with detailed context /\b(todo|to do|action item|task|deliverable):\s*([^.!?]{5,200})/gi, // Follow-up actions with specifics /\b(follow up|follow-up)\s+(on|with)\s+([^.!?]{5,150})/gi, // Specific actions with verbs and context /\b(need to|should|must|will|have to)\s+(send|call|email|schedule|review|update|create|finish|complete|implement|develop|test|deploy|analyze|research|investigate|prepare|draft|submit|approve)\s+([^.!?]{5,200})/gi, // Deadlines with detailed context /\b(by\s+(?:next\s+)?(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|week|month)|by\s+\d+|before\s+\d+|deadline|due)\s*:?\s*([^.!?]{10,200})/gi, // Technical implementation tasks /\b(implement|develop|build|code|write|design|architect|deploy|configure|setup|install|upgrade|migrate|optimize|refactor|debug|fix|patch)\s+([^.!?]{10,200})/gi, // Research and analysis tasks /\b(research|investigate|analyze|evaluate|assess|review|study|examine|explore|compare)\s+([^.!?]{10,200})/gi, // Decision-making commitments /\b(decide|determine|choose|select|finalize|confirm|approve|reject)\s+([^.!?]{10,200})/gi ]; static extractFromNodes(nodes: LifelogContentNode[], lifelogId: string): ActionItem[] { const actionItems: ActionItem[] = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (!node.content) continue; for (const pattern of this.ACTION_PATTERNS) { const matches = Array.from(node.content.matchAll(pattern)); for (const match of matches) { const content = (match[2] || match[1]).trim(); if (content.length > 5) { // Filter out very short matches actionItems.push({ content, assignee: node.speakerIdentifier === "user" ? "user" : node.speakerName || undefined, context: this.getContext(nodes, i), timestamp: node.startTime || "", priority: this.inferPriority(content), source: { lifelogId, nodeIndex: i } }); } } } } return this.deduplicateActionItems(actionItems); } private static getContext(nodes: LifelogContentNode[], currentIndex: number): string { const contextRange = 3; // Increased for more context const start = Math.max(0, currentIndex - contextRange); const end = Math.min(nodes.length, currentIndex + contextRange + 1); const contextNodes = nodes.slice(start, end); let context = ""; // Build rich context with speakers and technical details for (let i = 0; i < contextNodes.length; i++) { const node = contextNodes[i]; if (!node.content || !node.content.trim()) continue; const isCurrentNode = (start + i) === currentIndex; const speaker = node.speakerName ? `${node.speakerName}: ` : ""; const marker = isCurrentNode ? ">>> " : ""; context += `${marker}${speaker}${node.content.trim()} `; } // Preserve technical terms, numbers, and specific details in context return context.slice(0, 400).trim() + (context.length > 400 ? "..." : ""); } private static inferPriority(content: string): "high" | "medium" | "low" { const highPriorityKeywords = /\b(urgent|asap|immediately|critical|important|deadline|by tomorrow|by today)\b/i; const mediumPriorityKeywords = /\b(soon|this week|by friday|follow up|review)\b/i; if (highPriorityKeywords.test(content)) return "high"; if (mediumPriorityKeywords.test(content)) return "medium"; return "low"; } private static deduplicateActionItems(items: ActionItem[]): ActionItem[] { const seen = new Set<string>(); return items.filter(item => { const key = item.content.toLowerCase().trim(); if (seen.has(key)) return false; seen.add(key); return true; }); } }