Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
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
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/channel-noun-and-auto-enable-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `Connector.channelNoun` ({ singular, plural }) so connectors can name what their channels represent (folders, projects, calendars, …) instead of the generic "channel", and `Connector.autoEnableNewChannelsByDefault` so connectors that select all channels by default can also default the per-connection "Sync new channels" preference ON.
2 changes: 2 additions & 0 deletions connectors/airtable/src/airtable.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,8 @@ export class Airtable extends Connector<Airtable> {
];

readonly provider = AuthProvider.Airtable;
readonly channelNoun = { singular: "base", plural: "bases" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Airtable.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/apple-calendar/src/apple-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ export class AppleCalendar extends Connector<AppleCalendar> {
logoMono: "https://api.iconify.design/simple-icons/apple.svg",
},
];
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;

// Lock TTL covering the worst-case full backfill. The framework releases
// the lock automatically after this window even if a worker crashes, so
Expand Down
2 changes: 2 additions & 0 deletions connectors/asana/src/asana.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,8 @@ export class Asana extends Connector<Asana> {
static readonly SCOPES = ["default"];

readonly provider = AuthProvider.Asana;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Asana.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,6 +118,7 @@ export class GitHub extends Connector<GitHub> {
static readonly handleReplies = true;

readonly provider = AuthProvider.GitHub;
readonly channelNoun = { singular: "repository", plural: "repositories" };
readonly scopes = GitHub.SCOPES;
readonly linkTypes = [
{
Expand Down
1 change: 1 addition & 0 deletions connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,7 @@ export class Gmail extends Connector<Gmail> {
static readonly SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "label", plural: "labels" };
// Merge in People API scopes so we can enrich email-only contacts (Gmail
// headers carry name + address but no avatar) with photos from the user's
// Google Contacts and "other contacts" — without requiring the separate
Expand Down
1 change: 1 addition & 0 deletions connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
"https://www.googleapis.com/auth/calendar.calendarlist.readonly";

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly scopes = {
required: [GoogleCalendar.EVENTS_SCOPE],
description: [
Expand Down
1 change: 1 addition & 0 deletions connectors/google-chat/src/google-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,6 +88,7 @@ export class GoogleChat extends Connector<GoogleChat> {
];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "space", plural: "spaces" };
readonly scopes = Integrations.MergeScopes(
GoogleChat.SCOPES,
GoogleContacts.SCOPES
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,8 @@ export class GoogleDrive extends Connector<GoogleDrive> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "folder", plural: "folders" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Integrations.MergeScopes(GoogleDrive.SCOPES, GoogleContacts.SCOPES);
readonly linkTypes = [
{ type: "doc", label: "Document", noteLabel: "Comment", sharingModel: "thread" as const, logo: "https://api.iconify.design/simple-icons/googledocs.svg?color=%234285F4", logoMono: "https://api.iconify.design/simple-icons/googledocs.svg" },
Expand Down
2 changes: 2 additions & 0 deletions connectors/google-tasks/src/google-tasks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,8 @@ export class GoogleTasks extends Connector<GoogleTasks> {
static readonly SCOPES = ["https://www.googleapis.com/auth/tasks"];

readonly provider = AuthProvider.Google;
readonly channelNoun = { singular: "list", plural: "lists" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = GoogleTasks.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/jira/src/jira.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ export class Jira extends Connector<Jira> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Atlassian;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Jira.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/linear/src/linear.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,8 @@ export class Linear extends Connector<Linear> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Linear;
readonly channelNoun = { singular: "team", plural: "teams" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Linear.SCOPES;
readonly linkTypes = [
{
Expand Down
2 changes: 2 additions & 0 deletions connectors/outlook-calendar/src/outlook-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,6 +154,8 @@ export class OutlookCalendar extends Connector<OutlookCalendar> {
static readonly SCOPES = ["https://graph.microsoft.com/calendars.readwrite"];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "calendar", plural: "calendars" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = OutlookCalendar.SCOPES;
readonly linkTypes = [{ type: "event", label: "Event", sharingModel: "thread" as const, includesSchedules: true, logo: "https://api.iconify.design/logos/microsoft-icon.svg", logoDark: "https://api.iconify.design/simple-icons/microsoftoutlook.svg?color=%230078D4", logoMono: "https://api.iconify.design/simple-icons/microsoftoutlook.svg" }];

Expand Down
1 change: 1 addition & 0 deletions connectors/outlook-mail/src/outlook-mail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,7 @@ export class OutlookMail extends Connector<OutlookMail> {
];

readonly provider = AuthProvider.Microsoft;
readonly channelNoun = { singular: "folder", plural: "folders" };
// Merge in People/Contacts scopes so email-only contacts (Graph messages
// carry name + address but nothing else) can be enriched with display
// names from the user's People/Contacts data.
Expand Down
2 changes: 2 additions & 0 deletions connectors/posthog/src/posthog.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ type SyncState = {
*/
export class PostHog extends Connector<PostHog> {
// No provider or scopes — uses API key auth via options
readonly channelNoun = { singular: "event type", plural: "event types" };
readonly autoEnableNewChannelsByDefault = true;

build(build: ToolBuilder) {
return {
Expand Down
1 change: 1 addition & 0 deletions connectors/slack/src/slack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,6 +111,7 @@ export class Slack extends Connector<Slack> {
};

readonly provider = AuthProvider.Slack;
readonly autoEnableNewChannelsByDefault = true;
readonly reactionCapabilities = {
mode: "open-unicode" as const,
// Workspace custom emoji sync both ways: inbound reactions become
Expand Down
2 changes: 2 additions & 0 deletions connectors/todoist/src/todoist.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,8 @@ export class Todoist extends Connector<Todoist> {
static readonly handleReplies = true;

readonly provider = AuthProvider.Todoist;
readonly channelNoun = { singular: "project", plural: "projects" };
readonly autoEnableNewChannelsByDefault = true;
readonly scopes = Todoist.SCOPES;
readonly linkTypes = [
{
Expand Down
33 changes: 33 additions & 0 deletions twister/src/connector.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,39 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
*/
readonly singleChannel?: boolean;

/**
* The user-facing noun for this connector's channels — what each
* {@link Channel} returned by {@link getChannels} actually represents in the
* external service. Many connectors map "channels" onto a domain concept
* (folders, projects, calendars, labels, spaces, repositories, …), so the
* generic word "channel" reads as jargon. Set this and the UI substitutes it
* everywhere it would otherwise say "channel(s)" — e.g. the per-connection
* toggle becomes "Sync new folders" / "When a new folder is added, …".
*
* Provide lowercase nouns (the UI capitalizes where needed):
* `{ singular: "folder", plural: "folders" }`. Defaults to
* `{ singular: "channel", plural: "channels" }` when omitted.
*/
readonly channelNoun?: { singular: string; plural: string };

/**
* Whether the per-connection "Sync new channels" preference starts ON for
* newly added connections of this connector. Defaults to `false` (opt-in).
*
* Set `true` for connectors that select **all** of their channels by default
* (i.e. {@link getChannels} returns no channels marked
* `enabledByDefault: false`). If syncing every channel is the intended
* default, then channels discovered later should also sync automatically.
* Leave `false`/omitted for selective connectors that exclude some channels
* by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only
* owner calendars) — for those, a newly discovered channel is just as
* uncertain and should wait for the user to opt in.
*
* Only affects the **default** for new connections; the user's explicit
* toggle always wins, and existing connections keep their stored preference.
*/
readonly autoEnableNewChannelsByDefault?: boolean;

/**
* Registry of link types this connector creates (e.g., issue, event, message).
* Used for display in the UI (icons, labels, statuses).
Expand Down
Loading