') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Add link.sources[] for cross-connector bundling + Granola connector by KrisBraun · Pull Request #138 · plotday/plot · GitHub
Skip to content

Add link.sources[] for cross-connector bundling + Granola connector - #138

Merged
KrisBraun merged 2 commits into
mainfrom
feat/link-sources-array-and-granola
May 18, 2026
Merged

Add link.sources[] for cross-connector bundling + Granola connector#138
KrisBraun merged 2 commits into
mainfrom
feat/link-sources-array-and-granola

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

  • Replace link.source + link.relatedSource (single text fields) with link.sources: string[]. Two links whose sources arrays overlap share a thread (sources && new.sources). Lets any connector bundle into an existing thread via a canonical alias rather than depending on another connector's exact source format.
  • Calendar connectors (Google, Outlook, Apple) now emit canonical aliases on every event link — most importantly icaluid:<iCalUID> — alongside their existing connector-native sources.
  • Fellow migrated from the deprecated relatedSource to the new sources shape.
  • New Granola connector that uses these canonical aliases to attach AI meeting notes onto the existing calendar event thread. Works for Google/Outlook/Apple events, including recurring meetings (all instances share the iCalUID and collapse onto the master thread). Standalone Granola thread is created when no calendar match exists.

NewLink.source and NewLink.relatedSource remain as @deprecated fields — the runtime normalizes legacy values into sources so existing connectors keep compiling unchanged.

Pairs with the runtime/schema change in plot.git (private monorepo) which adds the link.sources text[] column + GIN index, rewrites the link.ts upsert to use array overlap, migrates the note.ts source lookup, and updates the privacy redaction filters.

What's new

ConnectorAliases on each event link
google-calendargoogle-calendar:<iCalUID??id>, icaluid:<iCalUID>, google-event:<id>
outlook-calendaroutlook-calendar:<calendarId>:<eventId>, icaluid:<iCalUId>, outlook-event:<eventId>
apple-calendarapple-calendar:<UID>, icaluid:<UID>
granola (new)granola:note:<id> + canonical aliases derived from calendar_event.calendar_event_id

Test plan

  • pnpm build in twister/, connectors/granola/, connectors/{google,outlook,apple}-calendar/, connectors/fellow/
  • pnpm validate-changesets passes (already verified locally — link-sources-array valid)
  • Sync a Google Calendar with a recurring meeting; confirm link.sources contains icaluid:<UID>
  • Connect Granola with an API key, sync; confirm Granola notes attach onto the matching calendar event thread (not a sibling) and use the granola-summary note key so re-syncs replace in place
  • Confirm a Granola note with no matching calendar event creates a standalone thread
  • Outlook + Apple equivalents work via the same icaluid: alias

🤖 Generated with Claude Code

KrisBraunand others added 2 commits May 18, 2026 13:38
Replace the single `link.source` + `link.relatedSource` bundling key
with a `sources: string[]` array. Two links whose `sources` arrays
overlap share a thread (`sources && new.sources`). Each connector can
emit multiple canonical aliases so any other connector that knows one
of them — most importantly `icaluid:<iCalUID>` — bundles in without
having to know the original connector's exact source format.
Calendar connectors now emit:
- google-calendar: `google-calendar:<iCalUID??id>`, `icaluid:<iCalUID>`,
`google-event:<id>`
- outlook-calendar: `outlook-calendar:<calendarId>:<eventId>`,
`icaluid:<iCalUId>`, `outlook-event:<eventId>`
- apple-calendar: `apple-calendar:<UID>`, `icaluid:<UID>`
Fellow migrated from `relatedSource` to the same `sources` shape.
New Granola connector (`@plotday/connector-granola`): syncs AI meeting
notes via Granola's public API (Bearer API key), emits canonical
calendar aliases so notes attach onto the existing calendar event
thread regardless of which calendar connector created it. Recurring
meetings collapse onto the master thread for free (all instances share
the iCalUID). When no calendar event matches, a standalone Granola
thread is created.
`NewLink.source` and `NewLink.relatedSource` remain as deprecated
fields — the runtime normalizes them into `sources` so existing
connectors keep compiling unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 9d44ee6 into mainMay 18, 2026
1 check passed
@KrisBraun
KrisBraun deleted the feat/link-sources-array-and-granola branch May 18, 2026 18:28
@github-actionsgithub-actionsBot mentioned this pull request May 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KrisBraun