Meeting Notes for Claude, Cursor, ChatGPT & agents

AI meeting notetaker — a free, agent-native alternative to Granola / Otter / Fireflies that lives inside your AI agent instead of a standalone app

Install in one command.
curl -fsSL https://syncorelabs.ai/install.sh | sh

macOS / Linux. Windows + manual install.

What it does

AI meeting notetaker — a free, agent-native alternative to Granola / Otter / Fireflies that lives inside your AI agent instead of a standalone app. Record and transcribe meetings, calls, lectures, voice memos, and any other audio with live speaker-diarized transcription via Deepgram, then read transcripts back for summarization, meeting minutes, action items, follow-up emails, or translation. Use this skill whenever the user wants to: take meeting notes, write meeting minutes, record a meeting / call / interview / standup / 1-on-1, capture a voice recording or voice memo, transcribe audio, summarize what was discussed, pull action items out of a conversation, or recall what was said in a past meeting (会议纪要, 语音录音, 会议记录, 录音转写, 复盘). Sessions live under ~/.syncore/data/note-taking/ and stay searchable across past meetings. KICKOFF FLOW (when user says 'start meeting' / '开始会议' / 'record this' WITHOUT giving an explicit title): BEFORE calling start_session, look up the user's calendar for an event happening NOW. ONE-CALL FAST PATH: 1. Call the FIRST available `*__list_current_meeting` tool. Both `google-calendar` and `apple-calendar` ship one (v1.2.0+ / v1.1.0+); each takes the local machine clock, computes a now ± 10-min window, skips all-day events, and returns 0 / 1 / many candidates. Preference order: `google-calendar__list_current_meeting` first (hits Google's API directly — most accurate for invited meetings + attendee emails), then `apple-calendar__list_current_meeting` (zero-config on macOS; transparently returns Google events too if the user added their Google account to System Settings → Internet Accounts), then `outlook__list_events` (Microsoft 365 — no list_current_meeting helper there yet, fall back to a manual now ± 10 min `list_events` call). SKIP any tool whose name ends in `authenticate` / `complete_authentication` — those are OAuth bootstrap stubs (e.g. `mcp__claude_ai_Google_Calendar__authenticate`), NOT list-events tools. 2. Read `agent_directive` from the response — it spells out the right action for the agent based on `match_count`: 0 → ask the user 'What should I call this meeting?', 1 → propose-and-confirm with the matched event, N+ → list options and ask which one. Treat `agent_directive` as authoritative; don't rewrite the logic. 3. Discovery fallback — only if NO list_current_meeting / list_events tool is reachable in the active set, call `syncore__discover(query='current calendar events')`, then `syncore__use(skill_id=<top hit>)` once, then retry step 1. If discover still surfaces nothing, fall through to the ASK path below — do NOT spin further (no repeated `syncore__use` attempts on the same id). The candidate-handling rules in `agent_directive` are mandatory; reproducing them here for clarity: • `match_count == 0` — DO NOT fabricate an event title. Ask the user 'What should I call this meeting?' in their chat language and use their literal answer. • `match_count == 1` — propose the title + attendees in the user's chat language ('Looks like you have "<title>" with <attendees> right now — start with that, or call it something else?') and WAIT for explicit confirmation. On confirm: call start_session(title=<event title>, participants=[<attendees>], language=<dominant spoken/meeting language>, ui_language=<chat language>). • `match_count >= 2` — render a numbered picker ('1. <title> · <HH:MM>', '2. ...') and ask 'Which meeting are you in? (1 / 2 / something else)'. Wait for the pick. If the user types a free-form name instead, use that literally. Carry attendees over from the chosen event even when the user renames the meeting. On any failure (tool throws, not connected, deferred, empty result, calendar lookup exceeds 40 seconds), fall through SILENTLY to the ASK path: just say 'What should I call this meeting, and who's joining?' — that's it, full stop. Anti-hallucination guard: DO NOT explain the calendar lookup at all. The user asked to start a meeting, not for a status report on the calendar tool. Forbidden phrasings include but are NOT limited to: 'Calendar lookup failed', 'known issue', 'I'll bypass calendar', 'skipping calendar check', 'calendar is acting up', 'bypassing constraints', 'starting recording directly'. These read as either a bug confession or a fabricated excuse — neither helps the user. 'Silent fall-through' means SILENT — no prefix, no apology, no excuse, no rationale. Just the ask. Calendar lookup is bounded at 40s by the skill's timeout config; if a longer hang ever happens, treat it identically to any other failure mode and fall through silently. Treat the calendar as a CONTEXT HINT, not an authoritative source. The window won't always have a perfect match — your job is to *reason about what's plausibly happening*, not to require an exact event lookup. Real cases you'll see: • Single tight match (event running now, in window) → propose verbatim, wait for confirm. • Multiple overlapping events → pick the most likely one (factor: whose calendar is it, who's the organizer, recency, attendee count) and propose THAT one, mention the alternatives so the user can switch. • Event starting/ended at the edge of the window (e.g. ended 10 min ago, starting in 20 min) → still useful — propose 'looks like you might be wrapping up <title> / heading into <title> — should I use that name, or call it something else?'. • Vague title ('Sync', '1on1', 'Standup') → use it but pull a more descriptive flavor from attendees ('1on1 with Bob' rather than just '1on1'). • No events at all → don't push the lookup any further, just ASK 'What should I call this meeting?'. • Event title in a different language than the user's chat → keep the calendar's language for the meeting title (the meeting note will read native in Obsidian) but propose in the chat's language. Whatever you propose, the user always has the final say. Their override wins — if they reject your guess and say 'just call it 周会', call start_session(title='周会', participants=..., language=<dominant spoken/meeting language>, ui_language=<chat language>) without re-querying the calendar. attendees can usually still be carried over from the calendar event even when the user renames the meeting. Format the proposal IN THE USER'S CHAT LANGUAGE: '看起来你现在有 "<title>" (与 <attendees>),用这个名字吗?' / 'Looks like you have "<title>" with <attendees> right now — start with that, or call it something else?'. Wait for explicit confirmation before calling start_session. On confirm: call start_session(title=<event title>, participants=[<attendee display names>], language=<dominant spoken/meeting language>, ui_language=<chat language>). On user-supplied title: use that; carry attendees from the event when relevant. Skip the calendar lookup entirely when the user already supplied a title in the trigger phrase ('start meeting about Q2', '记录一下产品反馈这个会议') — pass that title directly. The 'don't surprise the user' rule is firm: never silently pull a name from calendar without confirmation. start_session returns a `rendered_card` markdown block (the post-start confirmation card with title / attendees / status) — ECHO IT VERBATIM. Same pattern as finalize_meeting / open_meeting / past_meetings. Do NOT paraphrase or rewrap the table; chat renderers strip / mangle markdown that's been re-formatted. PERIODIC PROGRESS UPDATES (during a recording): if your client supports background / scheduled tool calls (Claude Code's /loop, agent SDK schedulers, etc.), call check_progress(session_id='latest') roughly every 2 minutes while the session is open. Each call returns ONLY the new utterances since the previous check, plus state flags. Do TWO things with the response: (a) If `has_new_content` is true, render a PROGRESS CARD in the meeting language: bullets summarising the new_text (as many as the content warrants — don't pad to fit a count) plus a meaningful direct quote. Use participants + speaker_names to label diarised speakers (e.g. 'Speaker 1' → 'Bob' when known). If the new_text reveals a speaker's identity ('Hi I'm Bob' / 'this is Alice from PM'), call update_speaker_name(speaker=<id>, name='<inferred>') to persist the mapping — subsequent checks will return the labels and the post-meeting summary inherits them. (b) If `should_suggest_end` is true (= 2 consecutive empty checks ≈ 4 minutes silent), ask the user 'Looks like the meeting wrapped up — should I close it now? (yes/no)' in their language. On confirm, run the WRAP-UP FLOW below. Don't auto-close without asking — false positives during quiet stretches (someone reading aloud, a long pause) shouldn't end the recording. In clients that DON'T support periodic tool calls, treat check_progress as an on-demand 'how's it going?' tool: call it when the user asks for an update mid-meeting. Same return shape, same rendering contract. MID-MEETING Q&A FLOW (when user asks a question DURING a recording — anything that isn't 'end meeting' or a status check): your answer MUST draw from BOTH sources, not just one: SOURCE 1 — the transcript (Deepgram-captured spoken audio). Get it via get_session(session_id='latest', tail_minutes=N). SOURCE 2 — this conversation's prior chat history with the user. The user has been TYPING things to you alongside the audio recording: corrections of speaker names, clarifications of jargon ('that 'platto' I just said is spelled p-l-a-t-t-o'), explicit context the audio didn't carry ('this meeting is about Q2 roadmap'), and earlier answers you've given that are now reference material. The transcript captures voice; chat history captures intent and metadata that voice often missed. Neither source alone is enough. The transcript is what was *said*; chat history is what was *typed* — both belong to the same meeting from the user's perspective. Examples of the combination paying off: • User asks 'remind me what platto is about' → transcript has 'platto' mentioned 4 times; chat history has the user typing 'platto = our internal CMS for marketing assets' 10 minutes ago. Answer combines both. • User asks 'who's Alice' → transcript has Speaker 1 talking; chat history has user telling you 'Speaker 1 is Alice from PM' an hour ago. Use both. • User asks 'did we agree on the deadline?' → transcript shows tentative discussion; chat history has user typing 'we just nodded yes to Friday' off-mic. Both belong in the answer. Calling get_session for the transcript portion — picking N: - Recent context only ('what did Bob just say', 'what was the last decision'): tail_minutes=5, sometimes 10. Cheap, focused. - Whole-meeting question ('did anyone mention budget at all today', 'has the deadline come up'): omit tail_minutes (full transcript). Costs more tokens but the user is explicitly asking about the whole arc. - User's own previous turn referenced something specific ('like we said earlier'): combine — chat history for the prior turn, transcript for what the meeting just said. The response carries `transcript` (markdown with `## Speaker N _(MM:SS – MM:SS)_` headers + utterance text), `speaker_stats`, `participants`, `speaker_names`, `last_utterance_at`. Speaker labelling — REASON, don't just relay raw IDs. When `speaker_names` already maps a speaker, use that name. When it DOESN'T, work out the likely identity from: the participants list, prior chat history (the user often types 'Speaker 1 is Alice' or '我是 Speaker 0' early on), the meeting setup ('1on1 with Bob' + mic-only Mac → Speaker 0 = user, Speaker 1 = Bob), and the content of utterances (a self-introduction, direct address, role-specific phrasing). If your inference is high-confidence (≥80%), call update_speaker_name() to persist before answering, so the label flows forward. If medium-confidence, use the name in your answer with a soft hedge ('looks like Alice said...') without persisting yet. If low-confidence, fall back to 'Speaker 1' but explicitly note the ambiguity ('Speaker 1, I'm not sure who that is yet — possibly Alice based on context'). When citing, include the timestamp in MM:SS form so the user can scrub the recording if they want exact audio. If the user is asking about something they themselves said, find their voice in the transcript by speaker (typically Speaker 0 on mono mic-only sessions). Direct quote + a follow-up answer (as long as it needs to be — a sentence for a simple fact-check, a paragraph if the question is genuinely about reasoning across the meeting) beats a paraphrase here — the user is often fact-checking against their own memory. Freshness: if the user's question is about something said in the LAST 1-2 seconds (e.g. 'what did they just say?' fired immediately after a sentence ended), the Deepgram final may not have landed in transcript.md yet. Heuristic: if last_utterance_at is more than ~3 seconds in the past, you have everything; if it's right against the question's timestamp, briefly wait (pass tail_minutes=1) and re-call get_session once before answering. Don't loop — one re-call max. During this flow, NEVER call end_session or finalize_meeting unless the user explicitly says so. Q&A is read-only. ABANDONED-MEETING FLOW: list_sessions / past_meetings / get_session / open_meeting all return a `needs_summary: bool` field per session. True means the recording was closed (typically by the daemon's silence watchdog when no agent was around to run WRAP-UP) AND has a wiki snapshot AND that snapshot has no summary block. When you see `needs_summary: true` on a session the user is asking about (or referencing one in a list), proactively offer to summarise NOW: 'I noticed "<title>" auto-closed without a summary — want me to draft one now? (yes/no)'. On confirm: get_session(id) → compose summary + action_items + follow_ups in the meeting's `language` from the inline transcript → finalize_meeting(id, summary, action_items, follow_ups). The same WRAP-UP rendering contract applies (ECHO the rendered_card verbatim). Don't auto-summarise without asking. Sessions where `ended_reason` starts with `auto_silence_` are the canonical case but the same flow works for any closed-but-unsummarised recording (e.g., daemon crash mid-finalize, helper killed by power loss). WRAP-UP FLOW (triggered by 'end meeting' / 'end session' / '结束会议' / '整理会议笔记' / agent's own check_progress detecting silence + user confirming): the SAME response turn must contain end_session AND finalize_meeting (plus, optionally, any update_speaker_name calls in between when new high-confidence speaker mappings emerge — see below). The pair end_session → finalize_meeting is non-negotiable; skipping finalize_meeting is the most common WRAP-UP regression, leaving the meeting on disk WITHOUT a summary block in the wiki, so cross-meeting search returns nothing useful for that recording. The skill's `needs_summary` flag catches the orphan later, but it costs the user a follow-up turn they shouldn't have to take. CALL 1 — end_session(session_id='latest' or explicit). Stops the audio pipeline, writes session.json status=closed, triggers the wiki snapshot. The response now bundles EVERYTHING the next call needs: `transcript` (inline markdown text), `language` (meeting language, the form summary/action_items/follow_ups MUST be in), `participants`, `speaker_names`, plus a `next_step` string spelling out the exact second-call signature. Read those fields off the response — do NOT call get_session as a separate hop. CALL 2 — finalize_meeting(session_id, summary, action_items, follow_ups). MUST happen in the same response turn, BEFORE you reply to the user. Compose all three fields in the `language` end_session returned. action_items = 'who does what by when' (Alice ships X by Friday); follow_ups = 'where this goes next' (next meeting time, parked agenda, decisions to revisit). Before composing, RESOLVE SPEAKER IDENTITIES one more time. The end_session response carries `speaker_names` (anything you already persisted) + `participants` (the agent-supplied attendee list from start_session) + the inline `transcript`. The chat history with the user from the whole session is also in your context. Combine all four to map each Speaker N → real name with the highest confidence you can muster — '1on1 with Bob' + mic-only setup means Speaker 0 = the user, Speaker 1 = Bob even if no one introduced themselves. A two-attendee call with participants=['Alice', 'Bob'] + mid-meeting chat 'Alice is asking the questions, Bob is answering' is enough to map both. Persist any new high-confidence mappings via update_speaker_name BEFORE finalize_meeting so the post-meeting card and the wiki snapshot use real names. The summary / action_items / follow_ups themselves should reference attendees by their REAL NAMES, never by 'Speaker N' — 'Speaker 1 提到 X' in a wiki note 6 months from now is useless; 'Bob 提到 X' is searchable. finalize_meeting returns a `rendered_card` markdown block — ECHO IT VERBATIM as your final reply. Do not paraphrase or rewrap; chat renderers strip non-http URLs that get wrapped in markdown links and the table layout is tested. Anti-patterns to avoid: - Calling end_session and replying to the user before finalize_meeting. Wrong: 'OK, ended.' Right: silently chain end_session → finalize_meeting → ECHO rendered_card. - Calling get_session between end_session and finalize_meeting. The transcript is already inline in end_session's response. Extra call = extra latency. - Translating action_items/follow_ups to the user's chat language. They go in the MEETING's language so the wiki snapshot reads natively in Obsidian. - Skipping action_items because the meeting was 'just a chat'. Pass an empty list `[]` if there really aren't any — finalize_meeting handles that, the snapshot just omits the section. - Passing `null` for action_items / follow_ups when empty. Some chat renderers (Claude.ai web tool-call panel) crash on null array params with 'Cannot convert undefined or null to object', and the user sees a red Error block instead of the success card. Use `[]` (empty array) — never `null`. If you realise mid-conversation that you called end_session WITHOUT finalize_meeting (e.g. you replied to the user already), your recovery path is the ABANDONED-MEETING FLOW: get_session(id) → compose three fields → finalize_meeting. Same effort, better not to land here. CROSS-MEETING SEARCH: when user asks 'what did we discuss about X' / '上周聊到的Y' / 'recall the decision about Z' / questions that span multiple meetings, call wiki__search(query=..., scope='raw', type='meeting') — meeting transcripts and finalised summaries are indexed by the wiki skill's BM25 FTS; scope='raw' targets non-derived content and type='meeting' filters to just the meeting snapshots (excludes papers, web articles, etc.). Synthesize the answer across hits, citing each source meeting by title + date. On first interaction with this skill in a conversation, call list_sessions(limit=3) once and surface to the user: (a) the meetings_root link (open in Obsidian when available, otherwise the data folder in their file manager) so they can browse all past meetings; and (b) up to 3 most-recent meetings with their titles, dates, and clickable open URLs from each session's `open` field. start_session also returns `recent_past_meetings` and `meetings_root` so a freshly-recorded meeting still puts prior context one click away.

The Meeting Notes skill is one of 75+ pre-wired Syncore integrations that any MCP-compatible client — Claude Desktop, Claude Code, Cursor, Windsurf, Codex CLI, and ChatGPT desktop — can call out of the box. You don't configure MCP servers or copy API keys into a JSON file; the Syncore daemon discovers the skill at startup and exposes its tools to whichever AI client is running.

Sample prompts

Once Syncore is installed and Meeting Notes is connected, ask your agent something like:

Tools available (14)

How to connect

Meeting Notes needs a provider API key to talk to deepgram. Connect it from the Syncore dashboard:

  1. Run curl -fsSL https://syncorelabs.ai/install.sh | sh on macOS / Linux (or the PowerShell command on Windows).
  2. Run syncore login to create your account and a device-bound encryption key.
  3. Visit syncorelabs.ai/connect and authorize Meeting Notes.
  4. Run syncore update to sync the skill + credentials to every installed AI client.

Pricing

Free at every Syncore tier. Connect your account once at /connect; the OAuth or API-key credential stays encrypted on your machine and syncs to your other devices via the Syncore vault.

See the full Syncore pricing breakdown — Free $0/mo, Pro $29/mo, Ultra $99/mo — on the homepage.

Related

Looking at Meeting Notes? You may also want:

  • Airtable list bases, read / query records with filter formulas, create new rows, and update fields on existing records via Airtable REST API
  • Apple Calendar list calendars, today's events, upcoming events, search by free-text, create / update / delete events with attendees, location, all-day flag
  • Apple Reminders list reminder lists with incomplete counts, list reminders inside a list, surface today's due + overdue items, create / update / complete / del...
  • Asana list workspaces and projects, list / create tasks, set due dates, and mark tasks complete
  • Cal.com list and manage bookings, event types, availability schedules; create / cancel / reschedule meetings on the user's Cal.com account

Or browse all 75 skills in the Syncore catalog.

Loading skill details…