extract-my-action-items

安装量: 140
排名: #6131

安装

npx skills add https://github.com/casper-studios/casper-marketplace --skill extract-my-action-items

Extract Action Items Extract action items from a Fireflies transcript using parallel subagents. Catches items automated summaries miss. Two modes: All attendees (default): No target specified — extract action items for every participant Single person: Target specified — extract action items for that person only Phase 1: Determine Mode Parse the user's invocation: If a target person is specified → single-person mode Otherwise → all-attendees mode Extract the search criteria (date, keyword, or transcript ID) from the invocation. Phase 2: Fetch & Preprocess (Subagent) The transcript API returns a JSON array (or an MCP wrapper containing one). Extract to plain text before chunking. You should inspect the user's local hooks config and avoid running commands that are blocked by the hooks. MCP based extraction mkdir -p .claude/scratchpad node -e " const fs = require('fs'); let data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); // Handle MCP wrapper: if top-level array has a .text field containing the real transcript, parse that if (data.length === 1 && typeof data[0]?.text === 'string') { // Extract speaker lines from the text content const lines = data[0].text.split(' \n ').filter(l => l.match(/^[A-Za-z].?:/)); fs.writeFileSync('.claude/scratchpad/transcript.txt', lines.join(' \n ')); const speakers = [...new Set(lines.map(l => l.split(':')[0].trim()))].sort(); console.log('Speakers:', JSON.stringify(speakers)); console.log('Total lines:', lines.length); } else { // Standard array of {speaker_name, text} objects const lines = data.map(e => (e.speaker_name || 'Unknown') + ': ' + (e.text || '')); fs.writeFileSync('.claude/scratchpad/transcript.txt', lines.join(' \n ')); const speakers = [...new Set(data.map(e => e.speaker_name).filter(Boolean))].sort(); console.log('Speakers:', JSON.stringify(speakers)); console.log('Total lines:', lines.length); } " [ TRANSCRIPT_JSON_FILE ] If the transcript JSON was saved to a tool-results file by the MCP client, pass that file path as the argument. API based extraction CRITICAL: The orchestrator MUST NOT call any Fireflies MCP tools directly. ALL Fireflies interaction happens inside this subagent. Launch a single general-purpose subagent with this prompt: Search Fireflies for a transcript matching: [SEARCH_CRITERIA] 1. Call mcp__fireflies__fireflies_get_transcripts to find the transcript (by date, keyword, or ID). 2. Call mcp__fireflies__fireflies_get_summary and mcp__fireflies__fireflies_get_transcript in parallel for the matched transcript. 3. The transcript API returns a JSON array. Extract to plain text: - With jq: jq -r '.[].text' < raw_transcript.json > .claude/scratchpad/transcript.txt - Fallback: python3 -c "import json,sys; print('\n'.join(e['text'] for e in json.load(sys.stdin)))" < raw_transcript.json > .claude/scratchpad/transcript.txt 4. Count lines: wc -l < .claude/scratchpad/transcript.txt 5. Extract the distinct speaker list from the transcript JSON: python3 -c "import json,sys; data=json.load(sys.stdin); print('\n'.join(sorted(set(e.get('speaker_name','') for e in data if e.get('speaker_name')))))" < raw_transcript.json Return EXACTLY this (no other text): - meeting_title: - meeting_date: <date> - transcript_id: <id> - transcript_path: .claude/scratchpad/transcript.txt - line_count: <number> - speakers: <comma-separated list> - summary: <the Fireflies summary text> Wait for the subagent to finish. Parse its returned values — these are the inputs for the remaining phases. Phase 3: Parallel Subagent Extraction Chunk sizing: ceil(total_lines / 5) lines per chunk, minimum 200. Adjust chunk count so no chunk is under 200 lines. Launch one general-purpose subagent per chunk. Single-Person Prompt Read lines [START] to [END] of [FILE_PATH]. Find ALL action items for [TARGET_PERSON]. Return each as: - </em><em>Item</em><em>: what they committed to - </em><em>Quote</em><em>: exact words from transcript - </em><em>Context</em><em>: who else involved, any deadline Beyond obvious commitments ("I'll do X"), catch these non-obvious patterns: - Self-notes: "I'll make a note to...", "let me jot down..." - Admissions implying catch-up: "I dropped the ball on X", "I still haven't read X" - Conditional offers that became commitments: "If we have time, I'm happy to..." - Volunteering: "I guess I'll volunteer to..." - Exploration tasks: "Let me spend a few hours with it" - Questions/topics for external parties: "I need to ask [person/firm] about X", "thing to discuss with [party]" All-Attendees Prompt Read lines [START] to [END] of [FILE_PATH]. The meeting attendees are: [SPEAKER_LIST] Find ALL action items for EVERY attendee. Group by person. For each item return: - </em><em>Person</em><em>: who owns the action item - </em><em>Item</em><em>: what they committed to - </em><em>Quote</em><em>: exact words from transcript - </em><em>Context</em>*: who else involved, any deadline Beyond obvious commitments ("I'll do X"), catch these non-obvious patterns: - Self-notes: "I'll make a note to...", "let me jot down..." - Admissions implying catch-up: "I dropped the ball on X", "I still haven't read X" - Conditional offers that became commitments: "If we have time, I'm happy to..." - Volunteering: "I guess I'll volunteer to..." - Exploration tasks: "Let me spend a few hours with it" - Questions/topics for external parties: "I need to ask [person/firm] about X", "thing to discuss with [party]" - Delegations: "[Person], can you handle X?", "I'll leave that to [person]" Phase 4: Consolidate Merge subagent results, deduplicate, and categorize. Only include categories that have items. Categories High Priority / Technical — Code changes, bug fixes, PR reviews, investigations Pairing / Collaboration — Scheduled syncs, joint work sessions Content / Research — Reading, writing, experiments, documentation Questions for External Parties — Topics to raise with specific people/firms outside the immediate team Exploration / Tooling — Tool evaluations, setup, environment tasks Catch-up — Things explicitly acknowledged as dropped or missed Output Single-person mode — Write to .claude/scratchpad/[name]-action-items-YYYY-MM-DD.md :</p> <h1 id="_1"></h1> <p>[Name] Action Items — [Meeting Title] ** Date: ** [Date] ** Fireflies Link: ** https://app.fireflies.ai/view/[TRANSCRIPT_ID]</p> <h2 id="_2"></h2> <h2 id="category-name">[Category Name]</h2> <p>[ ] ** Item title ** - Context and details -</p> <blockquote> <p>"Exact quote"</p> </blockquote> <h2 id="_3"></h2> <p>Quick Reference — Time-Sensitive 1. [Item with deadline or scheduled time] All-attendees mode — Write to .claude/scratchpad/action-items-YYYY-MM-DD.md :</p> <h1 id="_4"></h1> <p>Action Items — [Meeting Title] ** Date: ** [Date] ** Fireflies Link: ** https://app.fireflies.ai/view/[TRANSCRIPT_ID]</p> <h2 id="_5"></h2> <p>[Person Name]</p> <h3 id="_6"></h3> <h2 id="category-name_1">[Category Name]</h2> <p>[ ] ** Item title ** - Context and details -</p> <blockquote> <p>"Exact quote"</p> </blockquote> <h2 id="_7"></h2> <p>Quick Reference — Time-Sensitive 1. [Person] — [Item with deadline or scheduled time] Phase 5: Review & DM to Slack Use AskUserQuestion : "DM action items to each person on Slack?" — options: "Send DMs", "Skip — just keep the file" If approved, ensure .claude/slack-users.local.json exists in the project root: If missing , run node [SKILL_DIR]/scripts/fetch-slack-users.mjs (requires SLACK_BOT_TOKEN with users:read scope), present the output to the user for review, then save to .claude/slack-users.local.json (gitignored by <strong>/.claude/</strong>/*.local.json ) If present , proceed directly Run the bundled script with the output file path: node [ SKILL_DIR ] /scripts/slack-post.mjs [ OUTPUT_FILE_PATH ] The script sends Block Kit–formatted DMs to each person via conversations.open + chat.postMessage . Requires env var SLACK_BOT_TOKEN (with chat:write and im:write scopes). Behavior by mode: All-attendees: Each person matched in slack-users.local.json receives a DM with only their action items. Unresolvable names are skipped with a warning. Single-person: One DM to the target person. Name resolution supports exact match and fuzzy first-name match (e.g., "Jelvin" resolves to "Jelvin Base"). After the script runs, report any skipped names to the user. After posting (or skipping), delete all artifacts created during the run: transcript.txt , the action items markdown file, and any other temp files written to .claude/scratchpad/ during this workflow. Example Invocations /extract-my-action-items — all attendees, most recent meeting /extract-my-action-items standup — all attendees, search for "standup" /extract-my-action-items for Basti from yesterday — single person /extract-my-action-items 01KFY1RSEVVQW7MB1TKG4N2D20 — all attendees, specific transcript</p> </article> <a href="/" class="back-link">← <span data-i18n="detail.backToLeaderboard">返回排行榜</span></a> </div> <aside class="sidebar"> <section class="related-skills" id="relatedSkillsSection"> <h2 class="related-title" data-i18n="detail.relatedSkills">相关 Skills</h2> <div class="related-list" id="relatedSkillsList"> <div class="skeleton-card"></div> <div class="skeleton-card"></div> <div class="skeleton-card"></div> </div> </section> </aside> </div> </div> <script src="https://unpkg.com/i18next@23.11.5/i18next.min.js" defer></script> <script src="https://unpkg.com/i18next-browser-languagedetector@7.2.1/i18nextBrowserLanguageDetector.min.js" defer></script> <script defer> // Language resources - same pattern as index page const resources = { 'zh-CN': null, 'en': null, 'ja': null, 'ko': null, 'zh-TW': null, 'es': null, 'fr': null }; // Load language files (only current + fallback for performance) async function loadLanguageResources() { const savedLang = localStorage.getItem('i18nextLng') || 'en'; const langsToLoad = new Set([savedLang, 'en']); // current + fallback await Promise.all([...langsToLoad].map(async (lang) => { try { const response = await fetch(`/locales/${lang}.json`); if (response.ok) { resources[lang] = { translation: await response.json() }; } } catch (error) { console.warn(`Failed to load ${lang} language file:`, error); } })); } // Load a single language on demand (for language switching) async function loadLanguage(lang) { if (resources[lang]) return; try { const response = await fetch(`/locales/${lang}.json`); if (response.ok) { resources[lang] = { translation: await response.json() }; i18next.addResourceBundle(lang, 'translation', resources[lang].translation); } } catch (error) { console.warn(`Failed to load ${lang} language file:`, error); } } // Initialize i18next async function initI18n() { try { await loadLanguageResources(); // Filter out null values from resources const validResources = {}; for (const [lang, data] of Object.entries(resources)) { if (data !== null) { validResources[lang] = data; } } console.log('Loaded languages:', Object.keys(validResources)); console.log('zh-CN resource:', validResources['zh-CN']); console.log('detail.home in resource:', validResources['zh-CN']?.translation?.detail?.home); // 检查是否有保存的语言偏好 const savedLang = localStorage.getItem('i18nextLng'); // 如果没有保存的语言偏好,默认使用英文 const defaultLang = savedLang && ['zh-CN', 'en', 'ja', 'ko', 'zh-TW', 'es', 'fr'].includes(savedLang) ? savedLang : 'en'; await i18next .use(i18nextBrowserLanguageDetector) .init({ lng: defaultLang, // 强制设置初始语言 fallbackLng: 'en', supportedLngs: ['zh-CN', 'en', 'ja', 'ko', 'zh-TW', 'es', 'fr'], resources: validResources, detection: { order: ['localStorage'], // 只使用 localStorage,不检测浏览器语言 caches: ['localStorage'], lookupLocalStorage: 'i18nextLng' }, interpolation: { escapeValue: false } }); console.log('i18next initialized, language:', i18next.language); console.log('Test translation:', i18next.t('detail.home')); // Set initial language in selector const langSwitcher = document.getElementById('langSwitcher'); langSwitcher.value = i18next.language; // Update page language updatePageLanguage(); // Language switch event langSwitcher.addEventListener('change', async (e) => { await loadLanguage(e.target.value); // load on demand i18next.changeLanguage(e.target.value).then(() => { updatePageLanguage(); localStorage.setItem('i18nextLng', e.target.value); }); }); } catch (error) { console.error('i18next init failed:', error); } } // Translation helper function t(key, options = {}) { return i18next.t(key, options); } // Update all translatable elements function updatePageLanguage() { // Update HTML lang attribute document.documentElement.lang = i18next.language; // Update elements with data-i18n attribute document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); el.textContent = t(key); }); } // Copy command function function copyCommand() { const command = document.getElementById('installCommand').textContent; const btn = document.getElementById('copyBtn'); navigator.clipboard.writeText(command).then(() => { btn.textContent = t('copied'); btn.classList.add('copied'); setTimeout(() => { btn.textContent = t('copy'); btn.classList.remove('copied'); }, 2000); }).catch(() => { // Fallback for non-HTTPS const textArea = document.createElement('textarea'); textArea.value = command; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); btn.textContent = t('copied'); btn.classList.add('copied'); setTimeout(() => { btn.textContent = t('copy'); btn.classList.remove('copied'); }, 2000); }); } // Initialize document.getElementById('copyBtn').addEventListener('click', copyCommand); initI18n(); // 异步加载相关 Skills async function loadRelatedSkills() { const owner = 'casper-studios'; const skillName = 'extract-my-action-items'; const currentLang = 'en'; const listContainer = document.getElementById('relatedSkillsList'); const section = document.getElementById('relatedSkillsSection'); try { const response = await fetch(`/api/related-skills/${encodeURIComponent(owner)}/${encodeURIComponent(skillName)}?limit=6`); if (!response.ok) { throw new Error('Failed to load'); } const data = await response.json(); const relatedSkills = data.related_skills || []; if (relatedSkills.length === 0) { // 没有相关推荐时隐藏整个区域 section.style.display = 'none'; return; } // 渲染相关 Skills listContainer.innerHTML = relatedSkills.map(skill => { const desc = skill.description || ''; const truncatedDesc = desc.length > 60 ? desc.substring(0, 60) + '...' : desc; return ` <a href="${currentLang === 'en' ? '' : '/' + currentLang}/skill/${skill.owner}/${skill.repo}/${skill.skill_name}" class="related-card"> <div class="related-name">${escapeHtml(skill.skill_name)}</div> <div class="related-meta"> <span class="related-owner">${escapeHtml(skill.owner)}</span> <span class="related-installs">${skill.installs}</span> </div> <div class="related-desc">${escapeHtml(truncatedDesc)}</div> </a> `; }).join(''); } catch (error) { console.error('Failed to load related skills:', error); // 加载失败时显示提示或隐藏 listContainer.innerHTML = '<div class="related-empty">暂无相关推荐</div>'; } } // HTML 转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 页面加载完成后异步加载相关 Skills if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadRelatedSkills); } else { loadRelatedSkills(); } </script> </body> </html>