Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0fdcb3
feat(connector-google): scaffold combined Google connector package
KrisBraun Jun 23, 2026
b690682
feat(connector-google): product channel-id namespacing
KrisBraun Jun 23, 2026
a49a813
feat(connector-google): per-product optional scope groups + products …
KrisBraun Jun 23, 2026
c9d8bde
feat(connector-google): product module interface + registry
KrisBraun Jun 23, 2026
d972e7d
feat(connector-google): scope-gated composition, demux, and Google class
KrisBraun Jun 23, 2026
76b2cf7
refactor(connector-google-calendar): extract channel listing + scopes…
KrisBraun Jun 23, 2026
e3d8ba5
feat(connector-google): wire calendar product (channel listing, reuse…
KrisBraun Jun 23, 2026
e2cea3e
chore(connector-google-calendar): drop dead listCalendarsWithApi afte…
KrisBraun Jun 23, 2026
6e36fb1
refactor(connector-gmail): extract channel listing + scopes + link ty…
KrisBraun Jun 23, 2026
55075d8
refactor(connector-google-tasks): extract channel listing + scopes + …
KrisBraun Jun 23, 2026
4f40ef3
refactor(connector-google-contacts): export channel listing + scopes …
KrisBraun Jun 23, 2026
837e2f6
feat(connector-google): wire mail, tasks, contacts products (channel …
KrisBraun Jun 23, 2026
8a6737f
feat(connector-google): pure per-product status derivation (spec 2.4/…
KrisBraun Jun 23, 2026
f7bfc60
refactor(connector-google-calendar): extract initial-backfill sync in…
KrisBraun Jun 23, 2026
f450ca1
chore(connector-google-calendar): drop dead methods/imports orphaned …
KrisBraun Jun 23, 2026
0f876ac
feat(twister): add Connector.products metadata for combined connectors
KrisBraun Jun 23, 2026
40e42e5
fix(twister): hide internal underscore-prefixed markers from deploy p…
KrisBraun Jun 23, 2026
77548f7
fix(connector-google): combine product descriptions with scope reason…
KrisBraun Jun 23, 2026
5386140
fix(connector-google): use the Google Contacts brand logo
KrisBraun Jun 23, 2026
b78287c
fix(connector-google): point Contacts icon at hosted plot.day asset (…
KrisBraun Jun 24, 2026
eafc404
feat(google): re-home calendar live-updates (watch/webhook) + RSVP wr…
KrisBraun Jun 24, 2026
f2b0027
feat(google): re-home Mail (Gmail) sync into the combined connector
KrisBraun Jun 24, 2026
01bdae0
feat(google): re-home Tasks sync into the combined connector
KrisBraun Jun 24, 2026
e2e02b2
feat(google): re-home Contacts sync into the combined connector
KrisBraun Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-products.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: Connector.products metadata for combined (multi-product) connectors.
5 changes: 5 additions & 0 deletions .changeset/deploy-hide-internal-perms.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": patch
---

Fixed: the `plot deploy` permissions summary no longer prints internal underscore-prefixed runtime markers (`_products`, `_dynamic_link_types`, `_providers`, `_ai_required`, …) — only user-facing tool permissions are shown.
76 changes: 76 additions & 0 deletions connectors/gmail/src/channels.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
import type { AuthToken, Channel } from "@plotday/twister/tools/integrations";
import type { LinkTypeConfig } from "@plotday/twister/tools/integrations";

import { GmailApi } from "./gmail-api";

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

export const GMAIL_MODIFY_SCOPE = "https://www.googleapis.com/auth/gmail.modify";

// ---------------------------------------------------------------------------
// Link types
// ---------------------------------------------------------------------------

export const GMAIL_LINK_TYPES: LinkTypeConfig[] = [
{
type: "email",
label: "Thread",
noteLabel: "Reply",
sharingModel: "message" as const,
composePlaceholder: "Send a Gmail email",
composeVerb: "Send",
replyPlaceholder: "Reply",
replyVerb: "Send",
supportsFileAttachments: true,
logo: "https://api.iconify.design/logos/google-gmail.svg",
logoMono: "https://api.iconify.design/simple-icons/gmail.svg",
contactRoles: [
{ id: "to", label: "To", default: true },
{ id: "cc", label: "CC" },
{ id: "bcc", label: "BCC", hidden: true },
],
supportsContactChanges: true,
// Gmail composes target any address — a Plot contact (with or without
// a Gmail-connection row) or a free-form typed email delivered via
// `inviteEmails`. The runtime fills `recipients` from the
// connection-scoped row when available and falls back to
// `contact.email` otherwise.
compose: {
targets: "addresses" as const,
},
},
];

// ---------------------------------------------------------------------------
// Channel listing
// ---------------------------------------------------------------------------

/**
* Returns available Gmail labels as channel resources for a given token.
*
* Filters out system labels except for INBOX, SENT, DRAFT, IMPORTANT, and
* STARRED. Defaults to enabling INBOX and SENT only.
*/
export async function getGmailChannels(token: AuthToken): Promise<Channel[]> {
const api = new GmailApi(token.token);
const labels = await api.getLabels();
return labels
.filter(
(l: any) =>
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}
Loading
Loading