@plotday/twister 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/connector.d.ts +6 -2
  2. package/dist/connector.d.ts.map +1 -1
  3. package/dist/connector.js.map +1 -1
  4. package/dist/docs/assets/hierarchy.js +1 -1
  5. package/dist/docs/assets/navigation.js +1 -1
  6. package/dist/docs/assets/search.js +1 -1
  7. package/dist/docs/classes/index.Connector.html +25 -25
  8. package/dist/docs/classes/index.FileNotFoundError.html +1 -1
  9. package/dist/docs/classes/index.Files.html +1 -1
  10. package/dist/docs/classes/index.Imap.html +1 -1
  11. package/dist/docs/classes/index.Options.html +1 -1
  12. package/dist/docs/classes/index.Smtp.html +1 -1
  13. package/dist/docs/classes/tools_ai.AI.html +1 -1
  14. package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
  15. package/dist/docs/classes/tools_integrations.Integrations.html +16 -11
  16. package/dist/docs/classes/tools_network.Network.html +25 -16
  17. package/dist/docs/classes/tools_plot.Plot.html +1 -1
  18. package/dist/docs/classes/tools_store.Store.html +1 -1
  19. package/dist/docs/classes/tools_tasks.Tasks.html +1 -1
  20. package/dist/docs/classes/tools_twists.Twists.html +1 -1
  21. package/dist/docs/enums/plot.ActorType.html +4 -4
  22. package/dist/docs/enums/tools_integrations.AuthProvider.html +13 -13
  23. package/dist/docs/hierarchy.html +1 -1
  24. package/dist/docs/modules/index.html +1 -1
  25. package/dist/docs/modules/tools_integrations.html +1 -1
  26. package/dist/docs/types/index.CreateLinkDraft.html +10 -8
  27. package/dist/docs/types/index.OptionalScopeGroup.html +6 -6
  28. package/dist/docs/types/index.ScopeConfig.html +3 -3
  29. package/dist/docs/types/plot.Actor.html +5 -5
  30. package/dist/docs/types/plot.Contact.html +4 -4
  31. package/dist/docs/types/plot.ContentType.html +1 -1
  32. package/dist/docs/types/plot.Link.html +20 -16
  33. package/dist/docs/types/plot.LinkUpdate.html +1 -1
  34. package/dist/docs/types/plot.NewActor.html +1 -1
  35. package/dist/docs/types/plot.NewContact.html +1 -1
  36. package/dist/docs/types/plot.NewLink.html +18 -2
  37. package/dist/docs/types/plot.NewLinkWithNotes.html +13 -2
  38. package/dist/docs/types/plot.NewNote.html +7 -1
  39. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  40. package/dist/docs/types/plot.PlanOperation.html +1 -1
  41. package/dist/docs/types/tools_integrations.ArchiveLinkFilter.html +5 -5
  42. package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
  43. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  44. package/dist/docs/types/tools_integrations.ComposeConfig.html +7 -5
  45. package/dist/docs/types/tools_integrations.ContactRoleConfig.html +5 -5
  46. package/dist/docs/types/tools_integrations.LinkTypeConfig.html +25 -21
  47. package/dist/docs/types/tools_integrations.NewCustomEmoji.html +18 -0
  48. package/dist/docs/types/tools_integrations.StatusIcon.html +4 -0
  49. package/dist/docs/types/tools_integrations.SyncContext.html +15 -3
  50. package/dist/llm-docs/connector.d.ts +1 -1
  51. package/dist/llm-docs/connector.d.ts.map +1 -1
  52. package/dist/llm-docs/connector.js +1 -1
  53. package/dist/llm-docs/connector.js.map +1 -1
  54. package/dist/llm-docs/plot.d.ts +1 -1
  55. package/dist/llm-docs/plot.d.ts.map +1 -1
  56. package/dist/llm-docs/plot.js +1 -1
  57. package/dist/llm-docs/plot.js.map +1 -1
  58. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  59. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  60. package/dist/llm-docs/tools/integrations.js +1 -1
  61. package/dist/llm-docs/tools/integrations.js.map +1 -1
  62. package/dist/llm-docs/tools/network.d.ts +1 -1
  63. package/dist/llm-docs/tools/network.d.ts.map +1 -1
  64. package/dist/llm-docs/tools/network.js +1 -1
  65. package/dist/llm-docs/tools/network.js.map +1 -1
  66. package/dist/plot.d.ts +58 -0
  67. package/dist/plot.d.ts.map +1 -1
  68. package/dist/plot.js.map +1 -1
  69. package/dist/tools/integrations.d.ts +61 -1
  70. package/dist/tools/integrations.d.ts.map +1 -1
  71. package/dist/tools/integrations.js.map +1 -1
  72. package/dist/tools/network.d.ts +27 -15
  73. package/dist/tools/network.d.ts.map +1 -1
  74. package/dist/tools/network.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/connector.ts +6 -2
  77. package/src/llm-docs/connector.ts +1 -1
  78. package/src/llm-docs/plot.ts +1 -1
  79. package/src/llm-docs/tools/integrations.ts +1 -1
  80. package/src/llm-docs/tools/network.ts +1 -1
  81. package/src/plot.ts +59 -0
  82. package/src/tools/integrations.ts +74 -1
  83. package/src/tools/network.ts +27 -15
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "import {\n type Actor,\n type ActorId,\n type NewContact,\n type NewLinkWithNotes,\n ITool,\n} from \"..\";\nimport type { JSONValue } from \"../utils/types\";\nimport type { Uuid } from \"../utils/uuid\";\n\n/**\n * A resource that can be synced (e.g., a calendar, project, channel).\n * Returned by getChannels() and managed by users in the twist setup/edit modal.\n */\nexport type Channel = {\n /** External ID shared across users (e.g., Google calendar ID) */\n id: string;\n /** Display name shown in the UI */\n title: string;\n /**\n * Whether this channel should be selected by default when the user first\n * adds the connection. Tri-state:\n * - `true` — pre-select it (e.g. the user's own/primary calendar).\n * - `false` — exclude it from the default selection (low-value or\n * irrelevant resources that would crowd the user's view, or containers\n * whose contents are too broad to sync wholesale — e.g. a holiday or\n * someone-else's shared calendar, a GitHub org that cascades to every\n * repo, a Microsoft Teams team container). The user can still enable it\n * manually.\n * - `undefined` — no opinion; the client decides. The client defaults to\n * enabling the channel unless its title looks low-value (holidays,\n * birthdays, spam/sent/draft, …).\n *\n * The guiding principle is \"sync everything the user would reasonably want\n * by default\" — for most connectors that's all channels, so only set this\n * where the connector can distinguish the user's own/relevant channels from\n * low-value ones (e.g. Google Calendar via `accessRole === \"owner\"`).\n */\n enabledByDefault?: boolean;\n /** Optional nested channel resources (e.g., subfolders) */\n children?: Channel[];\n /** Per-channel link type configs. Overrides twist-level linkTypes when present. */\n linkTypes?: LinkTypeConfig[];\n};\n\n/**\n * Describes a link type that a connector creates.\n * Used for display in the UI (icons, labels).\n */\nexport type LinkTypeConfig = {\n /** Machine-readable type identifier (e.g., \"issue\", \"pull_request\") */\n type: string;\n /** Human-readable label (e.g., \"Issue\", \"Pull Request\") */\n label: string;\n /**\n * Connector's word for a note on a linked item of this type — used by the\n * Flutter app to adapt note/composer copy (\"Add a comment\" on Linear,\n * \"Add a message\" on Slack, \"Add a reply\" on Gmail). Defaults to \"note\"\n * when omitted. Use the singular noun in title case (e.g. \"Comment\").\n */\n noteLabel?: string;\n /**\n * Placeholder shown in the editor when this link type is the target of a\n * new thread (NewThreadPage). Example: \"Send a Gmail email\".\n * If unset, Plot derives \"Create a new {connector} {label.toLowerCase()}\".\n */\n composePlaceholder?: string;\n /**\n * Label for the Send button on NewThreadPage when this link type is the\n * target. Example: \"Send\". If unset, defaults to \"Create\".\n */\n composeVerb?: string;\n /**\n * Placeholder shown in the in-thread editor for the default reply mode.\n * Example: \"Reply\" (Gmail), \"Add a comment\" (Linear). If unset, Plot derives\n * \"Add a {noteLabel.toLowerCase()}\" or \"Add a note\".\n */\n replyPlaceholder?: string;\n /**\n * Label for the Send button in the in-thread editor. Example: \"Send\"\n * (Gmail), \"Comment\" (Linear). If unset, defaults to \"Send\".\n */\n replyVerb?: string;\n /** URL to an icon for this link type (light mode). Prefer Iconify `logos/*` URLs. */\n logo?: string;\n /** URL to an icon for dark mode. Use when the default logo is invisible on dark backgrounds (e.g., Iconify `simple-icons/*` with `?color=`). */\n logoDark?: string;\n /** URL to a monochrome icon (uses `currentColor`). Prefer Iconify `simple-icons/*` URLs without a `?color=` param. */\n logoMono?: string;\n /** Possible status values for this type */\n statuses?: Array<{\n /** Machine-readable status (e.g., \"open\", \"done\") */\n status: string;\n /** Human-readable label (e.g., \"Open\", \"Done\") */\n label: string;\n /** Whether this status represents completion (done, closed, merged, cancelled, etc.) */\n done?: boolean;\n /**\n * Mark the thread `active=true` in Plot when a link enters this status.\n * Use for messaging-style flags where the user has indicated they want\n * to act on the thread now — Gmail's \"starred\", Slack's \"later\", etc.\n * The Plot user can later un-flag the thread without breaking the\n * connector relationship.\n */\n active?: boolean;\n /**\n * Marks this status as the connector's \"to-do\" / active state. When a\n * user brings a done thread back into Plot's agenda, done-status links\n * are flipped to the status marked `todo: true` (e.g. Gmail's \"starred\",\n * Linear's \"unstarted\"); connectors that don't mark one fall back to the\n * first non-done status.\n */\n todo?: boolean;\n }>;\n /** Whether this link type supports displaying and changing the assignee */\n supportsAssignee?: boolean;\n /**\n * Whether this link type produces time-anchored schedule/agenda items\n * (i.e. calendar events). The Plot app shows the agenda (the bottom-nav\n * tab on mobile and the left-sidebar agenda on desktop) only when the\n * user has at least one active connection whose link types include one\n * with `includesSchedules: true`. Calendar connectors (Google / Apple /\n * Outlook Calendar) set this on their `event` link type. Defaults to\n * false — non-calendar link types (messages, issues, tasks, docs) omit it.\n */\n includesSchedules?: boolean;\n /** Default thread creation mode for this link type: 'all' | 'actionable' | 'manual' */\n defaultCreateThreads?: string;\n /**\n * Opt-in: declares this link type is composable from Plot via\n * `Connector.onCreateLink`. Omit to make the link type sync-only (no\n * \"Create new …\" picker entry).\n *\n * Connectors that need multiple compose modes for what users perceive as\n * the same kind of thing (e.g. Slack channel post vs DM) should declare\n * **separate linkTypes**, one per user-facing thread type. That keeps\n * each linkType isomorphic to one filter chip.\n */\n compose?: ComposeConfig;\n /**\n * Per-connector contact roles. Examples:\n * email → [{id:\"to\",label:\"To\",default:true},{id:\"cc\",label:\"CC\"},{id:\"bcc\",label:\"BCC\",hidden:true}]\n * calendar → [{id:\"required\",label:\"Required\",default:true},{id:\"optional\",label:\"Optional\"}]\n *\n * Plot uses this list to render a role picker on each contact chip in the\n * composer and to label non-default roles on existing threads. Exactly one\n * role should be marked `default: true`. Connectors that don't distinguish\n * roles (Slack, Linear) omit this field entirely.\n */\n contactRoles?: ContactRoleConfig[];\n /**\n * Whether contacts on an existing thread can be added, removed, or have\n * their role changed (email-style mid-thread recipient changes). When\n * false, the thread's contact list is fixed after creation. Defaults to\n * false when omitted.\n */\n supportsContactChanges?: boolean;\n /**\n * Whether a note/reply on this link type can carry a link (a pasted URL or\n * connector-created item) that Plot forwards to the source. When false (the\n * default), the \"Add link\" button is hidden for threads of this link type.\n * Only set true if the connector's reply path actually forwards the link\n * action to the source. Private Plot notes (no link type) always allow links.\n */\n supportsLinks?: boolean;\n /**\n * Whether a note/reply on this link type can carry an uploaded file that Plot\n * forwards to the source as an attachment. When false (the default), the\n * \"Attach file\" button is hidden for threads of this link type. Only set true\n * if the connector's reply path actually uploads file actions to the source.\n * Private Plot notes (no link type) always allow attachments.\n */\n supportsFileAttachments?: boolean;\n /**\n * Declares how sharing on threads of this link type is scoped:\n *\n * - `\"thread\"` (default): one roster shared across all notes in the\n * thread. Native Plot threads, Slack DMs, calendar events.\n * - `\"channel\"`: visibility is the external channel's membership;\n * the per-thread `contacts` array is ignored for sharing UI.\n * Slack channels, Linear projects.\n * - `\"message\"`: each note carries its own recipient set via\n * `note.access_contacts`; the thread roster is the union across\n * all messages. Email.\n * - `\"none\"`: the link type has no recipient roster at all. No\n * contacts/sharing UI is shown for these threads. Use for purely\n * personal destinations with no sharing concept (e.g. Google\n * Tasks). The per-thread `contacts` array is ignored for sharing UI.\n *\n * Omit to default to `\"thread\"`. When set to `\"message\"`, every\n * note this connector ingests must populate `access_contacts`\n * explicitly (never NULL).\n */\n sharingModel?: \"thread\" | \"channel\" | \"message\" | \"none\";\n};\n\n/**\n * Declares how a link type is composable from Plot via\n * `Connector.onCreateLink`. Attached to {@link LinkTypeConfig.compose}.\n */\nexport type ComposeConfig = {\n /**\n * Selects the destination model for the \"Create new …\" picker.\n *\n * - `\"channels\"` (default): one chip per enabled channel (e.g. a Linear\n * team, a Slack channel). Existing behaviour for task-tracker / calendar\n * connectors.\n * - `\"contacts\"`: one chip per connection (account); the user picks\n * recipients from their contacts. The runtime pre-resolves the chosen\n * Plot contacts to platform account IDs via the per-connection\n * `contact_external_account` rows and delivers them as\n * `CreateLinkDraft.recipients`. Contacts without a row for this specific\n * connection are filtered out of the picker — used by closed-roster\n * messaging platforms (Slack DM, Teams DM, Google Chat DM, LinkedIn DM).\n * - `\"addresses\"`: one chip per connection; the picker accepts any\n * contact with an addressable identifier (e.g. an email) or a free-form\n * typed address. The runtime fills `recipients` for contacts with a\n * connection-scoped row and falls back to the contact's primary address\n * (e.g. `contact.email`) when no row exists. Free-form addresses arrive\n * via the thread's `inviteEmails`. Used by open address spaces like\n * Gmail.\n */\n targets?: \"channels\" | \"contacts\" | \"addresses\";\n /**\n * Status to assign newly-created links. Should match an entry in the\n * parent linkType's `statuses[]`, OR a symbolic id that the connector's\n * `onCreateLink` resolves itself (e.g. Linear's `\"unstarted\"` category is\n * resolved per-team to a state UUID inside the connector — see\n * `connectors/linear/src/linear.ts`).\n */\n status: string;\n /**\n * Optional override for the picker chip / \"Create new …\" copy. Defaults\n * to the parent linkType's `label`. Use to disambiguate compose entries\n * when the parent label alone isn't specific enough (e.g. \"Direct\n * messages\" for a DM-mode compose on a chat connector).\n */\n label?: string;\n};\n\n/**\n * Declares one contact role for a connector's link type. See\n * `LinkTypeConfig.contactRoles`.\n */\nexport type ContactRoleConfig = {\n /** Stable machine id, e.g. \"to\" / \"cc\" / \"bcc\" / \"required\" / \"optional\". */\n id: string;\n /** Display label shown next to a contact chip, e.g. \"To\", \"CC\", \"Required\". */\n label: string;\n /** Exactly one role per linkType should be marked default. */\n default?: boolean;\n /**\n * Hidden roles are visible only to (a) the contact themselves and\n * (b) the user who added them. The API filters them out of every other\n * viewer's `thread.contacts` and `thread.contactMeta`. Use for BCC-style\n * semantics where other recipients must not see the hidden contact.\n */\n hidden?: boolean;\n};\n\n/**\n * Context passed to onChannelEnabled with plan-based sync hints.\n * Connectors can use these hints to limit initial sync scope.\n */\nexport type SyncContext = {\n /**\n * Earliest date to include in initial sync, based on the user's plan.\n *\n * Non-calendar connectors should use this as their date filter (timeMin,\n * created.gte, etc.) during initial sync. Calendar connectors should\n * ignore this for API queries (to avoid missing recurring events) — the\n * API layer filters non-recurring items automatically.\n *\n * Undefined when no limit applies.\n */\n syncHistoryMin?: Date;\n\n /**\n * True when this is a recovery dispatch after the connection's auth was\n * restored (the user re-authorized a previously-broken connection).\n *\n * The framework calls `onChannelEnabled` again for every channel that was\n * already enabled at the time of re-auth so the connector can recover from\n * the auth gap. Connectors should:\n *\n * 1. Drop any persisted incremental sync cursors / sync tokens so the\n * next sync re-walks history (the cursor may be stale or invalid —\n * Google Calendar invalidates syncTokens after ~7 days).\n * 2. Re-register webhooks (any prior subscription may have been\n * invalidated during the auth outage).\n * 3. Treat this as a backfill that walks history but does NOT spam\n * notifications — set `unread: false` and `archived: false` on\n * items as you would during initial sync.\n *\n * Most connectors can take the same code path as a fresh\n * `onChannelEnabled` for `recovering: true` as long as that path\n * overwrites stored state rather than appending to it.\n */\n recovering?: boolean;\n};\n\n/**\n * Built-in tool for managing OAuth authentication and channel resources.\n *\n * The Integrations tool:\n * 1. Manages channel resources (calendars, projects, etc.) per actor\n * 2. Returns tokens for the user who enabled sync on a channel\n * 3. Supports per-actor auth via actAs() for write-back operations\n * 4. Provides saveLink/saveContacts for Connectors to save data directly\n *\n * Connectors declare their provider, scopes, and channel lifecycle methods as\n * class properties and methods. The Integrations tool reads these automatically.\n * Auth and channel management is handled in the twist edit modal in Flutter.\n *\n * @example\n * ```typescript\n * class CalendarConnector extends Connector<CalendarConnector> {\n * readonly provider = AuthProvider.Google;\n * readonly scopes = [\"https://www.googleapis.com/auth/calendar\"];\n *\n * build(build: ToolBuilder) {\n * return {\n * integrations: build(Integrations),\n * };\n * }\n *\n * async getChannels(auth: Authorization, token: AuthToken): Promise<Channel[]> {\n * const calendars = await this.listCalendars(token);\n * return calendars.map(c => ({ id: c.id, title: c.name }));\n * }\n *\n * async onChannelEnabled(channel: Channel) {\n * // Start syncing\n * }\n *\n * async onChannelDisabled(channel: Channel) {\n * // Stop syncing\n * }\n * }\n * ```\n */\nexport abstract class Integrations extends ITool {\n /**\n * Merge scopes from multiple tools, deduplicating.\n *\n * @param scopeArrays - Arrays of scopes to merge\n * @returns Deduplicated array of scopes\n */\n static MergeScopes(...scopeArrays: string[][]): string[] {\n return Array.from(new Set(scopeArrays.flat()));\n }\n\n /**\n * Retrieves an access token for a channel resource.\n *\n * Returns the token of the user who enabled sync on the given channel.\n * If the channel is not enabled or the token is expired/invalid, returns null.\n *\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n */\n abstract get(channelId: string): Promise<AuthToken | null>;\n /**\n * Retrieves an access token for a channel resource.\n *\n * @param provider - The OAuth provider (deprecated, ignored for single-provider connectors)\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n * @deprecated Use get(channelId) instead. The provider is implicit from the connector.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get(provider: AuthProvider, channelId: string): Promise<AuthToken | null>;\n\n /**\n * Saves a link with notes to the connector's focus.\n *\n * Creates a thread+link pair. The thread is a lightweight container;\n * the link holds the external entity data (source, meta, type, status, etc.).\n *\n * This method is available only to Connectors (not regular Twists).\n *\n * @param link - The link with notes to save\n * @returns Promise resolving to the saved thread's UUID, or null if the\n * link was filtered out (e.g. older than the plan's sync history limit)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveLink(link: NewLinkWithNotes): Promise<Uuid | null>;\n\n /**\n * Batch version of {@link saveLink} — saves many links in one call.\n *\n * Connectors syncing many items per page (e.g. calendar events, issues,\n * messages) should prefer this over looping `saveLink`. Each `saveLink`\n * crosses the runtime boundary and counts against the per-execution\n * request budget; `saveLinks` collapses N saves into a single crossing.\n *\n * Failures on individual links DO NOT abort the batch. A bad item lands\n * as `null` in its slot and the rest still save. This prevents one\n * malformed record from losing an entire page of sync progress.\n *\n * This method is available only to Connectors (not regular Twists).\n *\n * @param links - Array of links with notes to save\n * @returns Array of the same length and order as `links`. Each entry is\n * the saved thread's UUID, or `null` if the link was filtered out\n * (e.g. older than the plan's sync history limit) OR failed to save.\n * The two null causes are not distinguished; the save failure is\n * logged server-side.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveLinks(links: NewLinkWithNotes[]): Promise<(Uuid | null)[]>;\n\n /**\n * Upserts contacts into the connector's focus without requiring a Link.\n *\n * Use this for messaging connectors to bulk-sync workspace members so the\n * recipient picker can filter contacts by reachable platform account. Populate\n * `NewContact.source` to persist `contact_external_account` rows (the platform\n * identity used to address the contact). Returns one `Actor` per input, in order.\n *\n * @param contacts - Contacts to upsert, keyed by `source`/`key`\n * @returns Promise resolving to the saved actors, 1:1 with input order\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveContacts(contacts: NewContact[]): Promise<Actor[]>;\n\n /**\n * Archives links matching the given filter that were created by this connector.\n *\n * For each archived link's thread, if no other non-archived links remain,\n * the thread is also archived.\n *\n * @param filter - Filter criteria for which links to archive\n * @returns Promise that resolves when archiving is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract archiveLinks(filter: ArchiveLinkFilter): Promise<void>;\n\n /**\n * Sets or clears todo status on a thread owned by this connector.\n *\n * @param source - The link source URL identifying the thread\n * @param actorId - The user to set the todo for\n * @param todo - true to mark as todo, false to clear/complete\n * @param options - Additional options\n * @param options.date - The todo date (when todo=true). Defaults to today.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract setThreadToDo(\n source: string,\n actorId: ActorId,\n todo: boolean,\n options?: { date?: Date | string }\n ): Promise<void>;\n\n /**\n * Signal that initial bulk-sync (or recovery sync) for a channel is fully\n * complete. The Flutter app uses this to clear the \"syncing…\" indicator\n * on the connection.\n *\n * The framework automatically marks a channel as syncing when it dispatches\n * `onChannelEnabled` (whether initial-enable, auto-enable from\n * `setChannels`, or recovery after re-auth). Connectors do NOT need to\n * call anything to start tracking — only to signal completion.\n *\n * Call this exactly once when the initial backfill has finished (no more\n * pages, all phases exhausted). Do NOT call it on every incremental sync.\n *\n * If `onChannelEnabled` throws an unhandled exception, the framework\n * automatically clears the syncing state — connectors don't need a\n * `try/catch` to clear state on failure.\n *\n * No-op when no auth/user mapping exists for the channel (e.g. key-based\n * connectors that don't have a per-user OAuth association).\n *\n * @param channelId - The channel resource ID whose initial sync just finished\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract channelSyncCompleted(channelId: string): Promise<void>;\n\n /**\n * Flag a connection as needing re-authentication so the Flutter app\n * surfaces a re-auth prompt on the next sync.\n *\n * Call this when a connector's API call returns a permanent auth-style\n * error that the runtime can't observe through token refresh — e.g.\n * Slack `invalid_auth` / `token_revoked` / `not_authed`, or a 401 on a\n * provider that doesn't refresh. The runtime already flags reauth\n * automatically when an OAuth refresh permanently fails or when the\n * stored token is missing on a get(); only call this for cases the\n * runtime can't see.\n *\n * Idempotent: safe to call repeatedly; existing reauth flags are not\n * overwritten. No-op when the channel has no `enabledBy` actor (e.g.\n * key-based connectors).\n *\n * @param channelId - The channel resource ID whose token is bad\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract markNeedsReauth(channelId: string): Promise<void>;\n\n}\n\n/**\n * Filter criteria for archiving links.\n * All fields are optional; only provided fields are used for matching.\n */\nexport type ArchiveLinkFilter = {\n /** Filter by channel ID */\n channelId?: string;\n /** Filter by link type (e.g., \"issue\", \"pull_request\") */\n type?: string;\n /** Filter by link status (e.g., \"open\", \"closed\") */\n status?: string;\n /** Filter by metadata fields (uses containment matching) */\n meta?: Record<string, JSONValue>;\n};\n\n/**\n * Enumeration of supported OAuth providers.\n *\n * Each provider has different OAuth endpoints, scopes, and token formats.\n * The Integrations tool handles the provider-specific implementation details.\n */\nexport enum AuthProvider {\n /** Google OAuth provider for Google Workspace services */\n Google = \"google\",\n /** Microsoft OAuth provider for Microsoft 365 services */\n Microsoft = \"microsoft\",\n /** Notion OAuth provider for Notion workspaces */\n Notion = \"notion\",\n /** Slack OAuth provider for Slack workspaces */\n Slack = \"slack\",\n /** Atlassian OAuth provider for Jira and Confluence */\n Atlassian = \"atlassian\",\n /** Linear OAuth provider for Linear workspaces */\n Linear = \"linear\",\n /** Monday.com OAuth provider */\n Monday = \"monday\",\n /** GitHub OAuth provider for GitHub repositories and organizations */\n GitHub = \"github\",\n /** Asana OAuth provider for Asana workspaces */\n Asana = \"asana\",\n /** HubSpot OAuth provider for HubSpot CRM */\n HubSpot = \"hubspot\",\n /** Todoist OAuth provider for Todoist task management */\n Todoist = \"todoist\",\n /** Airtable OAuth provider for Airtable bases */\n Airtable = \"airtable\",\n}\n\n/**\n * Represents a completed authorization from an OAuth flow.\n *\n * Contains the provider, granted scopes, and the actor (contact) that was authorized.\n * Tokens are looked up by (provider, actorId) rather than a random ID.\n */\nexport type Authorization = {\n /** The OAuth provider this authorization is for */\n provider: AuthProvider;\n /** Array of OAuth scopes this authorization covers */\n scopes: string[];\n /** The external account that was authorized (e.g., the Google account) */\n actor: Actor;\n};\n\n/**\n * Represents a stored OAuth authentication token.\n *\n * Contains the actual access token and the scopes it was granted,\n * which may be a subset of the originally requested scopes.\n */\nexport type AuthToken = {\n /** The OAuth access token */\n token: string;\n /** Array of granted OAuth scopes */\n scopes: string[];\n /**\n * Provider-specific metadata as key-value pairs.\n *\n * For Slack (AuthProvider.Slack):\n * - authed_user_id: The authenticated user's Slack ID\n * - bot_user_id: The bot user's Slack ID\n * - team_name: The Slack workspace/team name\n */\n provider?: Record<string, string>;\n};\n";
8
+ export default "import {\n type Actor,\n type ActorId,\n type NewContact,\n type NewLinkWithNotes,\n ITool,\n} from \"..\";\nimport type { JSONValue } from \"../utils/types\";\nimport type { Uuid } from \"../utils/uuid\";\n\n/**\n * A resource that can be synced (e.g., a calendar, project, channel).\n * Returned by getChannels() and managed by users in the twist setup/edit modal.\n */\nexport type Channel = {\n /** External ID shared across users (e.g., Google calendar ID) */\n id: string;\n /** Display name shown in the UI */\n title: string;\n /**\n * Whether this channel should be selected by default when the user first\n * adds the connection. Tri-state:\n * - `true` — pre-select it (e.g. the user's own/primary calendar).\n * - `false` — exclude it from the default selection (low-value or\n * irrelevant resources that would crowd the user's view, or containers\n * whose contents are too broad to sync wholesale — e.g. a holiday or\n * someone-else's shared calendar, a GitHub org that cascades to every\n * repo, a Microsoft Teams team container). The user can still enable it\n * manually.\n * - `undefined` — no opinion; the client decides. The client defaults to\n * enabling the channel unless its title looks low-value (holidays,\n * birthdays, spam/sent/draft, …).\n *\n * The guiding principle is \"sync everything the user would reasonably want\n * by default\" — for most connectors that's all channels, so only set this\n * where the connector can distinguish the user's own/relevant channels from\n * low-value ones (e.g. Google Calendar via `accessRole === \"owner\"`).\n */\n enabledByDefault?: boolean;\n /** Optional nested channel resources (e.g., subfolders) */\n children?: Channel[];\n /** Per-channel link type configs. Overrides twist-level linkTypes when present. */\n linkTypes?: LinkTypeConfig[];\n};\n\n/**\n * Curated status-icon vocabulary. Connectors map each declared status to the\n * closest icon; clients render a single glyph per value. Required on every\n * status so the UI always has something to show.\n */\nexport type StatusIcon =\n | \"backlog\"\n | \"todo\"\n | \"inProgress\"\n | \"blocked\"\n | \"done\"\n | \"cancelled\"\n | \"confirmed\"\n | \"tentative\";\n\n/**\n * Describes a link type that a connector creates.\n * Used for display in the UI (icons, labels).\n */\nexport type LinkTypeConfig = {\n /** Machine-readable type identifier (e.g., \"issue\", \"pull_request\") */\n type: string;\n /** Human-readable label (e.g., \"Issue\", \"Pull Request\") */\n label: string;\n /**\n * Connector's word for a note on a linked item of this type — used by the\n * Flutter app to adapt note/composer copy (\"Add a comment\" on Linear,\n * \"Add a message\" on Slack, \"Add a reply\" on Gmail). Defaults to \"note\"\n * when omitted. Use the singular noun in title case (e.g. \"Comment\").\n */\n noteLabel?: string;\n /**\n * Placeholder shown in the editor when this link type is the target of a\n * new thread (NewThreadPage). Example: \"Send a Gmail email\".\n * If unset, Plot derives \"Create a new {connector} {label.toLowerCase()}\".\n */\n composePlaceholder?: string;\n /**\n * Label for the Send button on NewThreadPage when this link type is the\n * target. Example: \"Send\". If unset, defaults to \"Create\".\n */\n composeVerb?: string;\n /**\n * Placeholder shown in the in-thread editor for the default reply mode.\n * Example: \"Reply\" (Gmail), \"Add a comment\" (Linear). If unset, Plot derives\n * \"Add a {noteLabel.toLowerCase()}\" or \"Add a note\".\n */\n replyPlaceholder?: string;\n /**\n * Label for the Send button in the in-thread editor. Example: \"Send\"\n * (Gmail), \"Comment\" (Linear). If unset, defaults to \"Send\".\n */\n replyVerb?: string;\n /** URL to an icon for this link type (light mode). Prefer Iconify `logos/*` URLs. */\n logo?: string;\n /** URL to an icon for dark mode. Use when the default logo is invisible on dark backgrounds (e.g., Iconify `simple-icons/*` with `?color=`). */\n logoDark?: string;\n /** URL to a monochrome icon (uses `currentColor`). Prefer Iconify `simple-icons/*` URLs without a `?color=` param. */\n logoMono?: string;\n /** Possible status values for this type */\n statuses?: Array<{\n /** Machine-readable status (e.g., \"open\", \"done\") */\n status: string;\n /** Human-readable label (e.g., \"Open\", \"Done\") */\n label: string;\n /** Curated icon for this status (required so the UI always has a glyph). */\n icon: StatusIcon;\n /**\n * Suppress this status's icon on the feed row (ThreadWidget) while still\n * showing it in the ThreadPage header. Use for a \"resting\" default state\n * that would otherwise clutter the feed (e.g. calendar \"Confirmed\").\n */\n hiddenDefault?: boolean;\n /** Whether this status represents completion (done, closed, merged, cancelled, etc.) */\n done?: boolean;\n /**\n * Mark the thread `active=true` in Plot when a link enters this status.\n * Use for messaging-style flags where the user has indicated they want\n * to act on the thread now — Gmail's \"starred\", Slack's \"later\", etc.\n * The Plot user can later un-flag the thread without breaking the\n * connector relationship.\n */\n active?: boolean;\n /**\n * Marks this status as the connector's \"to-do\" / active state. When a\n * user brings a done thread back into Plot's agenda, done-status links\n * are flipped to the status marked `todo: true` (e.g. Gmail's \"starred\",\n * Linear's \"unstarted\"); connectors that don't mark one fall back to the\n * first non-done status.\n */\n todo?: boolean;\n }>;\n /** Whether this link type supports displaying and changing the assignee */\n supportsAssignee?: boolean;\n /**\n * Whether this link type produces time-anchored schedule/agenda items\n * (i.e. calendar events). The Plot app shows the agenda (the bottom-nav\n * tab on mobile and the left-sidebar agenda on desktop) only when the\n * user has at least one active connection whose link types include one\n * with `includesSchedules: true`. Calendar connectors (Google / Apple /\n * Outlook Calendar) set this on their `event` link type. Defaults to\n * false — non-calendar link types (messages, issues, tasks, docs) omit it.\n */\n includesSchedules?: boolean;\n /** Default thread creation mode for this link type: 'all' | 'actionable' | 'manual' */\n defaultCreateThreads?: string;\n /**\n * Opt-in: declares this link type is composable from Plot via\n * `Connector.onCreateLink`. Omit to make the link type sync-only (no\n * \"Create new …\" picker entry).\n *\n * Connectors that need multiple compose modes for what users perceive as\n * the same kind of thing (e.g. Slack channel post vs DM) should declare\n * **separate linkTypes**, one per user-facing thread type. That keeps\n * each linkType isomorphic to one filter chip.\n */\n compose?: ComposeConfig;\n /**\n * Per-connector contact roles. Examples:\n * email → [{id:\"to\",label:\"To\",default:true},{id:\"cc\",label:\"CC\"},{id:\"bcc\",label:\"BCC\",hidden:true}]\n * calendar → [{id:\"required\",label:\"Required\",default:true},{id:\"optional\",label:\"Optional\"}]\n *\n * Plot uses this list to render a role picker on each contact chip in the\n * composer and to label non-default roles on existing threads. Exactly one\n * role should be marked `default: true`. Connectors that don't distinguish\n * roles (Slack, Linear) omit this field entirely.\n */\n contactRoles?: ContactRoleConfig[];\n /**\n * Whether contacts on an existing thread can be added, removed, or have\n * their role changed (email-style mid-thread recipient changes). When\n * false, the thread's contact list is fixed after creation. Defaults to\n * false when omitted.\n */\n supportsContactChanges?: boolean;\n /**\n * Whether a note/reply on this link type can carry a link (a pasted URL or\n * connector-created item) that Plot forwards to the source. When false (the\n * default), the \"Add link\" button is hidden for threads of this link type.\n * Only set true if the connector's reply path actually forwards the link\n * action to the source. Private Plot notes (no link type) always allow links.\n */\n supportsLinks?: boolean;\n /**\n * Whether a note/reply on this link type can carry an uploaded file that Plot\n * forwards to the source as an attachment. When false (the default), the\n * \"Attach file\" button is hidden for threads of this link type. Only set true\n * if the connector's reply path actually uploads file actions to the source.\n * Private Plot notes (no link type) always allow attachments.\n */\n supportsFileAttachments?: boolean;\n /**\n * Declares how sharing on threads of this link type is scoped:\n *\n * - `\"thread\"` (default): one roster shared across all notes in the\n * thread. Native Plot threads, Slack DMs, calendar events.\n * - `\"channel\"`: visibility is the external channel's membership;\n * the per-thread `contacts` array is ignored for sharing UI.\n * Slack channels, Linear projects.\n * - `\"message\"`: each note carries its own recipient set via\n * `note.access_contacts`; the thread roster is the union across\n * all messages. Email.\n * - `\"none\"`: the link type has no recipient roster at all. No\n * contacts/sharing UI is shown for these threads. Use for purely\n * personal destinations with no sharing concept (e.g. Google\n * Tasks). The per-thread `contacts` array is ignored for sharing UI.\n *\n * Omit to default to `\"thread\"`. When set to `\"message\"`, every\n * note this connector ingests must populate `access_contacts`\n * explicitly (never NULL).\n */\n sharingModel?: \"thread\" | \"channel\" | \"message\" | \"none\";\n};\n\n/**\n * Declares how a link type is composable from Plot via\n * `Connector.onCreateLink`. Attached to {@link LinkTypeConfig.compose}.\n */\nexport type ComposeConfig = {\n /**\n * Selects the destination model for the \"Create new …\" picker.\n *\n * - `\"channels\"` (default): one chip per enabled channel (e.g. a Linear\n * team, a Slack channel). Existing behaviour for task-tracker / calendar\n * connectors.\n * - `\"contacts\"`: one chip per connection (account); the user picks\n * recipients from their contacts. The runtime pre-resolves the chosen\n * Plot contacts to platform account IDs via the per-connection\n * `contact_external_account` rows and delivers them as\n * `CreateLinkDraft.recipients`. Contacts without a row for this specific\n * connection are filtered out of the picker — used by closed-roster\n * messaging platforms (Slack DM, Teams DM, Google Chat DM, LinkedIn DM).\n * - `\"addresses\"`: one chip per connection; the picker accepts any\n * contact with an addressable identifier (e.g. an email) or a free-form\n * typed address. The runtime fills `recipients` for contacts with a\n * connection-scoped row and falls back to the contact's primary address\n * (e.g. `contact.email`) when no row exists. Free-form addresses arrive\n * via the thread's `inviteEmails`. Used by open address spaces like\n * Gmail.\n */\n targets?: \"channels\" | \"contacts\" | \"addresses\";\n /**\n * Status to assign newly-created links. Should match an entry in the\n * parent linkType's `statuses[]`, OR a symbolic id that the connector's\n * `onCreateLink` resolves itself (e.g. Linear's `\"unstarted\"` category is\n * resolved per-team to a state UUID inside the connector — see\n * `connectors/linear/src/linear.ts`).\n *\n * Omit for status-less link types (e.g. messaging connectors that don't\n * model a status): composed links are created with no status.\n */\n status?: string;\n /**\n * Optional override for the picker chip / \"Create new …\" copy. Defaults\n * to the parent linkType's `label`. Use to disambiguate compose entries\n * when the parent label alone isn't specific enough (e.g. \"Direct\n * messages\" for a DM-mode compose on a chat connector).\n */\n label?: string;\n};\n\n/**\n * Declares one contact role for a connector's link type. See\n * `LinkTypeConfig.contactRoles`.\n */\nexport type ContactRoleConfig = {\n /** Stable machine id, e.g. \"to\" / \"cc\" / \"bcc\" / \"required\" / \"optional\". */\n id: string;\n /** Display label shown next to a contact chip, e.g. \"To\", \"CC\", \"Required\". */\n label: string;\n /** Exactly one role per linkType should be marked default. */\n default?: boolean;\n /**\n * Hidden roles are visible only to (a) the contact themselves and\n * (b) the user who added them. The API filters them out of every other\n * viewer's `thread.contacts` and `thread.contactMeta`. Use for BCC-style\n * semantics where other recipients must not see the hidden contact.\n */\n hidden?: boolean;\n};\n\n/**\n * Context passed to onChannelEnabled with plan-based sync hints.\n * Connectors can use these hints to limit initial sync scope.\n */\nexport type SyncContext = {\n /**\n * Earliest date to include in initial sync, based on the user's plan.\n *\n * Non-calendar connectors should use this as their date filter (timeMin,\n * created.gte, etc.) during initial sync. Calendar connectors should\n * ignore this for API queries (to avoid missing recurring events) — the\n * API layer filters non-recurring items automatically.\n *\n * Undefined when no limit applies.\n */\n syncHistoryMin?: Date;\n\n /**\n * True when this is a recovery dispatch after the connection's auth was\n * restored (the user re-authorized a previously-broken connection).\n *\n * The framework calls `onChannelEnabled` again for every channel that was\n * already enabled at the time of re-auth so the connector can recover from\n * the auth gap. Connectors should:\n *\n * 1. Drop any persisted incremental sync cursors / sync tokens so the\n * next sync re-walks history (the cursor may be stale or invalid —\n * Google Calendar invalidates syncTokens after ~7 days).\n * 2. Re-register webhooks (any prior subscription may have been\n * invalidated during the auth outage).\n * 3. Treat this as a backfill that walks history but does NOT spam\n * notifications — set `unread: false` and `archived: false` on\n * items as you would during initial sync.\n *\n * Most connectors can take the same code path as a fresh\n * `onChannelEnabled` for `recovering: true` as long as that path\n * overwrites stored state rather than appending to it.\n */\n recovering?: boolean;\n\n /**\n * True when the channel is being observed because the user composed a Plot\n * thread INTO it (via `onCreateLink`), not because they explicitly enabled\n * it. The connector should register webhooks / mark the channel observed so\n * inbound events (replies, reactions) on the composed thread sync back —\n * but must NOT backfill history. Only go-forward events matter; pulling the\n * channel's existing content would be surprising for a channel the user\n * only posted one thread into.\n *\n * Connectors whose `onChannelEnabled` already skips historical backfill can\n * ignore this flag. Connectors that initial-sync on enable MUST short-\n * circuit that backfill when `observeOnly` is true (still set up webhooks\n * and any go-forward state).\n */\n observeOnly?: boolean;\n};\n\n/**\n * Built-in tool for managing OAuth authentication and channel resources.\n *\n * The Integrations tool:\n * 1. Manages channel resources (calendars, projects, etc.) per actor\n * 2. Returns tokens for the user who enabled sync on a channel\n * 3. Supports per-actor auth via actAs() for write-back operations\n * 4. Provides saveLink/saveContacts for Connectors to save data directly\n *\n * Connectors declare their provider, scopes, and channel lifecycle methods as\n * class properties and methods. The Integrations tool reads these automatically.\n * Auth and channel management is handled in the twist edit modal in Flutter.\n *\n * @example\n * ```typescript\n * class CalendarConnector extends Connector<CalendarConnector> {\n * readonly provider = AuthProvider.Google;\n * readonly scopes = [\"https://www.googleapis.com/auth/calendar\"];\n *\n * build(build: ToolBuilder) {\n * return {\n * integrations: build(Integrations),\n * };\n * }\n *\n * async getChannels(auth: Authorization, token: AuthToken): Promise<Channel[]> {\n * const calendars = await this.listCalendars(token);\n * return calendars.map(c => ({ id: c.id, title: c.name }));\n * }\n *\n * async onChannelEnabled(channel: Channel) {\n * // Start syncing\n * }\n *\n * async onChannelDisabled(channel: Channel) {\n * // Stop syncing\n * }\n * }\n * ```\n */\nexport abstract class Integrations extends ITool {\n /**\n * Merge scopes from multiple tools, deduplicating.\n *\n * @param scopeArrays - Arrays of scopes to merge\n * @returns Deduplicated array of scopes\n */\n static MergeScopes(...scopeArrays: string[][]): string[] {\n return Array.from(new Set(scopeArrays.flat()));\n }\n\n /**\n * Retrieves an access token for a channel resource.\n *\n * Returns the token of the user who enabled sync on the given channel.\n * If the channel is not enabled or the token is expired/invalid, returns null.\n *\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n */\n abstract get(channelId: string): Promise<AuthToken | null>;\n /**\n * Retrieves an access token for a channel resource.\n *\n * @param provider - The OAuth provider (deprecated, ignored for single-provider connectors)\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n * @deprecated Use get(channelId) instead. The provider is implicit from the connector.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get(provider: AuthProvider, channelId: string): Promise<AuthToken | null>;\n\n /**\n * Saves a link with notes to the connector's focus.\n *\n * Creates a thread+link pair. The thread is a lightweight container;\n * the link holds the external entity data (source, meta, type, status, etc.).\n *\n * This method is available only to Connectors (not regular Twists).\n *\n * @param link - The link with notes to save\n * @returns Promise resolving to the saved thread's UUID, or null if the\n * link was filtered out (e.g. older than the plan's sync history limit)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveLink(link: NewLinkWithNotes): Promise<Uuid | null>;\n\n /**\n * Batch version of {@link saveLink} — saves many links in one call.\n *\n * Connectors syncing many items per page (e.g. calendar events, issues,\n * messages) should prefer this over looping `saveLink`. Each `saveLink`\n * crosses the runtime boundary and counts against the per-execution\n * request budget; `saveLinks` collapses N saves into a single crossing.\n *\n * Failures on individual links DO NOT abort the batch. A bad item lands\n * as `null` in its slot and the rest still save. This prevents one\n * malformed record from losing an entire page of sync progress.\n *\n * This method is available only to Connectors (not regular Twists).\n *\n * @param links - Array of links with notes to save\n * @returns Array of the same length and order as `links`. Each entry is\n * the saved thread's UUID, or `null` if the link was filtered out\n * (e.g. older than the plan's sync history limit) OR failed to save.\n * The two null causes are not distinguished; the save failure is\n * logged server-side.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveLinks(links: NewLinkWithNotes[]): Promise<(Uuid | null)[]>;\n\n /**\n * Upserts contacts into the connector's focus without requiring a Link.\n *\n * Use this for messaging connectors to bulk-sync workspace members so the\n * recipient picker can filter contacts by reachable platform account. Populate\n * `NewContact.source` to persist `contact_external_account` rows (the platform\n * identity used to address the contact). Returns one `Actor` per input, in order.\n *\n * @param contacts - Contacts to upsert, keyed by `source`/`key`\n * @returns Promise resolving to the saved actors, 1:1 with input order\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveContacts(contacts: NewContact[]): Promise<Actor[]>;\n\n /**\n * Archives links matching the given filter that were created by this connector.\n *\n * For each archived link's thread, if no other non-archived links remain,\n * the thread is also archived.\n *\n * @param filter - Filter criteria for which links to archive\n * @returns Promise that resolves when archiving is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract archiveLinks(filter: ArchiveLinkFilter): Promise<void>;\n\n /**\n * Sets or clears todo status on a thread owned by this connector.\n *\n * @param source - The link source URL identifying the thread\n * @param actorId - The user to set the todo for\n * @param todo - true to mark as todo, false to clear/complete\n * @param options - Additional options\n * @param options.date - The todo date (when todo=true). Defaults to today.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract setThreadToDo(\n source: string,\n actorId: ActorId,\n todo: boolean,\n options?: { date?: Date | string }\n ): Promise<void>;\n\n /**\n * Signal that initial bulk-sync (or recovery sync) for a channel is fully\n * complete. The Flutter app uses this to clear the \"syncing…\" indicator\n * on the connection.\n *\n * The framework automatically marks a channel as syncing when it dispatches\n * `onChannelEnabled` (whether initial-enable, auto-enable from\n * `setChannels`, or recovery after re-auth). Connectors do NOT need to\n * call anything to start tracking — only to signal completion.\n *\n * Call this exactly once when the initial backfill has finished (no more\n * pages, all phases exhausted). Do NOT call it on every incremental sync.\n *\n * If `onChannelEnabled` throws an unhandled exception, the framework\n * automatically clears the syncing state — connectors don't need a\n * `try/catch` to clear state on failure.\n *\n * No-op when no auth/user mapping exists for the channel (e.g. key-based\n * connectors that don't have a per-user OAuth association).\n *\n * @param channelId - The channel resource ID whose initial sync just finished\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract channelSyncCompleted(channelId: string): Promise<void>;\n\n /**\n * Flag a connection as needing re-authentication so the Flutter app\n * surfaces a re-auth prompt on the next sync.\n *\n * Call this when a connector's API call returns a permanent auth-style\n * error that the runtime can't observe through token refresh — e.g.\n * Slack `invalid_auth` / `token_revoked` / `not_authed`, or a 401 on a\n * provider that doesn't refresh. The runtime already flags reauth\n * automatically when an OAuth refresh permanently fails or when the\n * stored token is missing on a get(); only call this for cases the\n * runtime can't see.\n *\n * Idempotent: safe to call repeatedly; existing reauth flags are not\n * overwritten. No-op when the channel has no `enabledBy` actor (e.g.\n * key-based connectors).\n *\n * @param channelId - The channel resource ID whose token is bad\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract markNeedsReauth(channelId: string): Promise<void>;\n\n /**\n * Upsert workspace custom emoji into Plot's shared cache so reactions using\n * `provider:workspace/name` refs render as images and round-trip. Idempotent;\n * keyed on `id`. Pass `archived: true` to mark an emoji removed. Workspace-\n * scoped (shared across all users of that workspace).\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveCustomEmoji(emoji: NewCustomEmoji[]): Promise<void>;\n\n}\n\n/**\n * Filter criteria for archiving links.\n * All fields are optional; only provided fields are used for matching.\n */\nexport type ArchiveLinkFilter = {\n /** Filter by channel ID */\n channelId?: string;\n /** Filter by link type (e.g., \"issue\", \"pull_request\") */\n type?: string;\n /** Filter by link status (e.g., \"open\", \"closed\") */\n status?: string;\n /** Filter by metadata fields (uses containment matching) */\n meta?: Record<string, JSONValue>;\n};\n\n/**\n * A workspace custom emoji to cache so Plot can render and round-trip it as a\n * reaction. `id` is the provider-scoped ref stored on reactions, of the form\n * `<provider>:<workspace>/<name>` (e.g. `slack:T0123ABC/party_parrot`).\n */\nexport type NewCustomEmoji = {\n /** Provider-scoped ref: `<provider>:<workspace>/<name>`. The reaction value. */\n id: string;\n /** e.g. \"slack\". */\n provider: string;\n /** Provider workspace/team id (e.g. Slack team id `T0123ABC`). */\n workspace: string;\n /** Bare emoji name without colons (e.g. \"party_parrot\"). */\n name: string;\n /** Image URL to render, or null for an alias (see `aliasOf`). */\n imageUrl: string | null;\n /** When this emoji aliases another, the target ref (`id` of the canonical emoji); else null. */\n aliasOf: string | null;\n /** True to mark the emoji removed (archive it) rather than upsert it. */\n archived: boolean;\n};\n\n/**\n * Enumeration of supported OAuth providers.\n *\n * Each provider has different OAuth endpoints, scopes, and token formats.\n * The Integrations tool handles the provider-specific implementation details.\n */\nexport enum AuthProvider {\n /** Google OAuth provider for Google Workspace services */\n Google = \"google\",\n /** Microsoft OAuth provider for Microsoft 365 services */\n Microsoft = \"microsoft\",\n /** Notion OAuth provider for Notion workspaces */\n Notion = \"notion\",\n /** Slack OAuth provider for Slack workspaces */\n Slack = \"slack\",\n /** Atlassian OAuth provider for Jira and Confluence */\n Atlassian = \"atlassian\",\n /** Linear OAuth provider for Linear workspaces */\n Linear = \"linear\",\n /** Monday.com OAuth provider */\n Monday = \"monday\",\n /** GitHub OAuth provider for GitHub repositories and organizations */\n GitHub = \"github\",\n /** Asana OAuth provider for Asana workspaces */\n Asana = \"asana\",\n /** HubSpot OAuth provider for HubSpot CRM */\n HubSpot = \"hubspot\",\n /** Todoist OAuth provider for Todoist task management */\n Todoist = \"todoist\",\n /** Airtable OAuth provider for Airtable bases */\n Airtable = \"airtable\",\n}\n\n/**\n * Represents a completed authorization from an OAuth flow.\n *\n * Contains the provider, granted scopes, and the actor (contact) that was authorized.\n * Tokens are looked up by (provider, actorId) rather than a random ID.\n */\nexport type Authorization = {\n /** The OAuth provider this authorization is for */\n provider: AuthProvider;\n /** Array of OAuth scopes this authorization covers */\n scopes: string[];\n /** The external account that was authorized (e.g., the Google account) */\n actor: Actor;\n};\n\n/**\n * Represents a stored OAuth authentication token.\n *\n * Contains the actual access token and the scopes it was granted,\n * which may be a subset of the originally requested scopes.\n */\nexport type AuthToken = {\n /** The OAuth access token */\n token: string;\n /** Array of granted OAuth scopes */\n scopes: string[];\n /**\n * Provider-specific metadata as key-value pairs.\n *\n * For Slack (AuthProvider.Slack):\n * - authed_user_id: The authenticated user's Slack ID\n * - bot_user_id: The bot user's Slack ID\n * - team_name: The Slack workspace/team name\n */\n provider?: Record<string, string>;\n};\n";
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "import { ITool } from \"..\";\nimport { type JSONValue, Serializable } from \"../utils/types\";\nimport { type AuthProvider, type Authorization } from \"./integrations\";\n\n/**\n * Represents an incoming webhook request.\n *\n * This object is passed to webhook callback functions and contains all\n * the information about the HTTP request that triggered the webhook.\n *\n * @example\n * ```typescript\n * async onWebhookReceived(request: WebhookRequest, context: any) {\n * console.log(`${request.method} request received`);\n * console.log(\"Headers:\", request.headers);\n * console.log(\"Query params:\", request.params);\n * console.log(\"Body:\", request.body);\n * console.log(\"Context:\", context);\n * }\n * ```\n */\nexport type WebhookRequest = {\n /** HTTP method of the request (GET, POST, etc.) */\n method: string;\n /** HTTP headers from the request */\n headers: Record<string, string>;\n /** Query string parameters from the request URL */\n params: Record<string, string>;\n /** Request body (parsed as JSON if applicable) */\n body: JSONValue;\n /** Raw request body (for signature verification) */\n rawBody?: string;\n};\n\n/**\n * Built-in tool for requesting HTTP access permissions and managing webhooks.\n *\n * The Network tool serves two purposes:\n * 1. Declares which URLs a twist or tool is allowed to access via HTTP/HTTPS\n * 2. Provides webhook creation and management for receiving HTTP callbacks\n *\n * **IMPORTANT**: Must be requested in the Twist or Tool Init method to declare\n * HTTP access permissions. Without requesting this tool with the appropriate URLs,\n * all outbound HTTP requests (fetch, etc.) will be blocked.\n *\n * **Permission Patterns:**\n * - `*` - Allow access to all URLs\n * - `https://*.example.com` - Allow access to all subdomains\n * - `https://api.example.com/*` - Allow access to all paths on the domain\n * - `https://api.example.com/v1/*` - Allow access to specific path prefix\n *\n * **Webhook Characteristics:**\n * - Persistent across worker restarts\n * - Automatic callback routing to parent tool/twist\n * - Support for all HTTP methods\n * - Context preservation for callback execution\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist<MyTwist> {\n * build(build: ToolBuilder) {\n * return {\n * // Request HTTP access to specific APIs\n * network: build(Network, {\n * urls: [\n * 'https://api.github.com/*',\n * 'https://api.openai.com/*'\n * ]\n * })\n * };\n * }\n * }\n * ```\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool<CalendarTool> {\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, {\n * urls: ['https://www.googleapis.com/calendar/*']\n * })\n * };\n * }\n *\n * async setupCalendarWebhook(calendarId: string) {\n * // Create webhook URL that will call onCalendarEvent\n * const webhookUrl = await this.tools.network.createWebhook(\n * {},\n * this.onCalendarEvent,\n * calendarId,\n * \"google\"\n * );\n *\n * // Register webhook with Google Calendar API\n * await this.registerWithGoogleCalendar(calendarId, webhookUrl);\n *\n * return webhookUrl;\n * }\n *\n * async onCalendarEvent(request: WebhookRequest, calendarId: string, provider: string) {\n * console.log(\"Calendar event received:\", {\n * method: request.method,\n * calendarId,\n * provider,\n * body: request.body\n * });\n *\n * // Process the calendar event change\n * await this.processCalendarChange(request.body);\n * }\n *\n * async cleanup(webhookUrl: string) {\n * await this.tools.network.deleteWebhook(webhookUrl);\n * }\n * }\n * ```\n */\nexport abstract class Network extends ITool {\n static readonly Options: {\n /**\n * All network access is blocked except the specified URLs.\n * Wildcards (*) are supported for domains and paths.\n */\n urls: string[];\n };\n\n /**\n * Creates a new webhook endpoint.\n *\n * Generates a unique HTTP endpoint that will invoke the callback function\n * when requests are received. The callback receives the WebhookRequest plus any extraArgs.\n *\n * **Provider-Specific Behavior:**\n * - **Slack**: Uses provider-specific routing via team_id. Requires `authorization` parameter.\n * - **Gmail** (Google with Gmail scopes): Returns a Google Pub/Sub topic name instead of a webhook URL.\n * The topic name (e.g., \"projects/plot-prod/topics/gmail-webhook-abc123\") should be passed\n * to the Gmail API's `users.watch` endpoint. Requires `authorization` parameter with Gmail scopes.\n * - **Pub/Sub** (`pubsub: true`): Returns a Google Pub/Sub topic name instead of a webhook URL.\n * Use this for services that deliver events via Pub/Sub (e.g., Google Workspace Events API).\n * A Pub/Sub topic and push subscription are created automatically; the returned topic name\n * can be passed to any Google API that accepts a Pub/Sub notification endpoint.\n * - **Default**: Returns a standard webhook URL for all other cases.\n *\n * @param options - Webhook creation options\n * @param options.provider - Optional provider for provider-specific webhook routing\n * @param options.authorization - Optional authorization for provider-specific webhooks (required for Slack and Gmail)\n * @param callback - Function receiving (request, ...extraArgs)\n * @param extraArgs - Additional arguments to pass to the callback (type-checked, no functions allowed)\n * @returns Promise resolving to the webhook URL, or for Gmail/Pub/Sub, a Pub/Sub topic name\n *\n * @example\n * ```typescript\n * // Pub/Sub webhook for Workspace Events API\n * const topicName = await this.tools.network.createWebhook(\n * { pubsub: true },\n * this.onEventReceived,\n * channelId\n * );\n * // topicName: \"projects/plot-prod/topics/ps-abc123\"\n *\n * // Pass topic name to Workspace Events API\n * await api.createSubscription(targetResource, topicName, eventTypes);\n * ```\n *\n * @example\n * ```typescript\n * // Gmail webhook - auto-detected from scopes, returns Pub/Sub topic name\n * const topicName = await this.tools.network.createWebhook(\n * {},\n * this.onGmailNotification,\n * \"inbox\"\n * );\n * ```\n */\n abstract createWebhook<\n TArgs extends Serializable[],\n TCallback extends (request: WebhookRequest, ...args: TArgs) => any\n >(\n options: {\n provider?: AuthProvider;\n authorization?: Authorization;\n /** When true, creates a Google Pub/Sub topic instead of a webhook URL. */\n pubsub?: boolean;\n /**\n * Controls whether the returned webhook URL runs callbacks synchronously\n * or asynchronously.\n *\n * **Async (default, `async: true`)** — Plot enqueues each incoming\n * request and immediately returns `200 { queued: true }`. A background\n * queue consumer runs the callback with bounded concurrency. The\n * sender never sees the callback's return value or any error thrown\n * by it, and delivery is at-least-once (the callback must be\n * idempotent). This is the right default for the vast majority of\n * webhooks — service event notifications, bulk-import fan-out, etc. —\n * because it removes ingress-path database pressure and prevents\n * sender-side retry storms when callbacks are slow.\n *\n * **Sync (`async: false`)** — Plot runs the callback inline and\n * responds with the callback's return value. Required when:\n * - The sender reads the response body (e.g. Microsoft Graph\n * subscription validation, which POSTs with a `validationToken` and\n * expects the token echoed as `text/plain`).\n * - The sender uses the HTTP status code to decide whether to retry\n * (e.g. to surface 4xx for permanent failures).\n * - The handler must observe throws before the sender times out.\n *\n * When `async: false`, a callback returning a `string` is sent back\n * with `Content-Type: text/plain`; any other value is serialized as\n * JSON. `undefined` / `void` yields a plain `200 OK` body.\n *\n * Defaults to `true`.\n */\n async?: boolean;\n },\n callback: TCallback,\n ...extraArgs: TArgs\n ): Promise<string>;\n\n /**\n * Deletes an existing webhook endpoint.\n *\n * Removes the webhook endpoint and stops processing requests.\n * Works with all webhook types (standard, Slack, and Gmail).\n *\n * **For Gmail webhooks:** Also deletes the associated Google Pub/Sub topic and subscription.\n *\n * **For Slack webhooks:** Removes the callback registration for the specific team.\n *\n * **For standard webhooks:** Removes the webhook endpoint. Any subsequent requests\n * to the deleted webhook will return 404.\n *\n * @param url - The webhook identifier returned from `createWebhook()`.\n * This can be a URL (standard webhooks), a Pub/Sub topic name (Gmail),\n * or an opaque identifier (Slack). Always pass the exact value returned\n * from `createWebhook()`.\n * @returns Promise that resolves when the webhook is deleted\n */\n abstract deleteWebhook(url: string): Promise<void>;\n}\n";
8
+ export default "import { ITool } from \"..\";\nimport { type JSONValue, Serializable } from \"../utils/types\";\nimport { type AuthProvider, type Authorization } from \"./integrations\";\n\n/**\n * Represents an incoming webhook request.\n *\n * This object is passed to webhook callback functions and contains all\n * the information about the HTTP request that triggered the webhook.\n *\n * @example\n * ```typescript\n * async onWebhookReceived(request: WebhookRequest, context: any) {\n * console.log(`${request.method} request received`);\n * console.log(\"Headers:\", request.headers);\n * console.log(\"Query params:\", request.params);\n * console.log(\"Body:\", request.body);\n * console.log(\"Context:\", context);\n * }\n * ```\n */\nexport type WebhookRequest = {\n /** HTTP method of the request (GET, POST, etc.) */\n method: string;\n /** HTTP headers from the request */\n headers: Record<string, string>;\n /** Query string parameters from the request URL */\n params: Record<string, string>;\n /** Request body (parsed as JSON if applicable) */\n body: JSONValue;\n /** Raw request body (for signature verification) */\n rawBody?: string;\n};\n\n/**\n * Built-in tool for requesting HTTP access permissions and managing webhooks.\n *\n * The Network tool serves two purposes:\n * 1. Declares which URLs a twist or tool is allowed to access via HTTP/HTTPS\n * 2. Provides webhook creation and management for receiving HTTP callbacks\n *\n * **IMPORTANT**: Must be requested in the Twist or Tool Init method to declare\n * HTTP access permissions. Without requesting this tool with the appropriate URLs,\n * all outbound HTTP requests (fetch, etc.) will be blocked.\n *\n * **Permission Patterns:**\n * - `*` - Allow access to all URLs\n * - `https://*.example.com` - Allow access to all subdomains\n * - `https://api.example.com/*` - Allow access to all paths on the domain\n * - `https://api.example.com/v1/*` - Allow access to specific path prefix\n *\n * **Webhook Characteristics:**\n * - Persistent across worker restarts\n * - Automatic callback routing to parent tool/twist\n * - Support for all HTTP methods\n * - Context preservation for callback execution\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist<MyTwist> {\n * build(build: ToolBuilder) {\n * return {\n * // Request HTTP access to specific APIs\n * network: build(Network, {\n * urls: [\n * 'https://api.github.com/*',\n * 'https://api.openai.com/*'\n * ]\n * })\n * };\n * }\n * }\n * ```\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool<CalendarTool> {\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, {\n * urls: ['https://www.googleapis.com/calendar/*']\n * })\n * };\n * }\n *\n * async setupCalendarWebhook(calendarId: string) {\n * // Create webhook URL that will call onCalendarEvent\n * const webhookUrl = await this.tools.network.createWebhook(\n * {},\n * this.onCalendarEvent,\n * calendarId,\n * \"google\"\n * );\n *\n * // Register webhook with Google Calendar API\n * await this.registerWithGoogleCalendar(calendarId, webhookUrl);\n *\n * return webhookUrl;\n * }\n *\n * async onCalendarEvent(request: WebhookRequest, calendarId: string, provider: string) {\n * console.log(\"Calendar event received:\", {\n * method: request.method,\n * calendarId,\n * provider,\n * body: request.body\n * });\n *\n * // Process the calendar event change\n * await this.processCalendarChange(request.body);\n * }\n *\n * async cleanup(webhookUrl: string) {\n * await this.tools.network.deleteWebhook(webhookUrl);\n * }\n * }\n * ```\n */\nexport abstract class Network extends ITool {\n static readonly Options: {\n /**\n * All network access is blocked except the specified URLs.\n * Wildcards (*) are supported for domains and paths.\n */\n urls: string[];\n };\n\n /**\n * Creates a new webhook endpoint.\n *\n * Generates a unique HTTP endpoint that will invoke the callback function\n * when requests are received. The callback receives the WebhookRequest plus any extraArgs.\n *\n * **Provider-Specific Behavior:**\n * - **Slack**: Uses provider-specific routing via team_id. Requires `authorization` parameter.\n * - **Pub/Sub** (`pubsub: \"gmail\" | \"workspace\"`): Returns a Google Pub/Sub topic name instead\n * of a webhook URL. `\"gmail\"` targets Gmail `users.watch` (set this only on the Gmail\n * connector); `\"workspace\"` targets Google Workspace Events (Chat, etc.). A Pub/Sub topic and\n * push subscription are created automatically; the returned topic name (e.g.\n * \"projects/plot-prod/topics/gmail-abc123\") is passed to the relevant Google API. Other Google\n * connectors (Calendar, Drive) omit `pubsub` and use the default HTTPS webhook.\n * - **Default**: Returns a standard webhook URL for all other cases.\n *\n * @param options - Webhook creation options\n * @param options.provider - Optional provider for provider-specific webhook routing\n * @param options.authorization - Optional authorization for provider-specific webhooks (required for Slack)\n * @param options.pubsub - Optional Google Pub/Sub push product (\"gmail\" | \"workspace\")\n * @param callback - Function receiving (request, ...extraArgs)\n * @param extraArgs - Additional arguments to pass to the callback (type-checked, no functions allowed)\n * @returns Promise resolving to the webhook URL, or for Pub/Sub, a Pub/Sub topic name\n *\n * @example\n * ```typescript\n * // Pub/Sub webhook for Workspace Events API (Chat, etc.)\n * const topicName = await this.tools.network.createWebhook(\n * { pubsub: \"workspace\" },\n * this.onEventReceived,\n * channelId\n * );\n * // topicName: \"projects/plot-prod/topics/ps-abc123\"\n *\n * // Pass topic name to Workspace Events API\n * await api.createSubscription(targetResource, topicName, eventTypes);\n * ```\n *\n * @example\n * ```typescript\n * // Gmail webhook - returns a Gmail Pub/Sub topic name for users.watch\n * const topicName = await this.tools.network.createWebhook(\n * { pubsub: \"gmail\" },\n * this.onGmailNotification,\n * \"inbox\"\n * );\n * ```\n */\n abstract createWebhook<\n TArgs extends Serializable[],\n TCallback extends (request: WebhookRequest, ...args: TArgs) => any\n >(\n options: {\n provider?: AuthProvider;\n authorization?: Authorization;\n /**\n * Create a Google Pub/Sub topic instead of a webhook URL, and return\n * the topic name. Selects the push product:\n *\n * - `\"gmail\"` — Gmail `users.watch` (topic published to by\n * `gmail-api-push`). Set this only on the Gmail connector.\n * - `\"workspace\"` — Google Workspace Events (Chat, etc.).\n *\n * This opt-in must be explicit. Other Google connectors (Calendar,\n * Drive) omit it and receive a standard HTTPS webhook URL — they must\n * never be routed to a Pub/Sub topic, which `events.watch` /\n * `files.watch` reject as non-HTTPS.\n */\n pubsub?: \"gmail\" | \"workspace\";\n /**\n * Controls whether the returned webhook URL runs callbacks synchronously\n * or asynchronously.\n *\n * **Async (default, `async: true`)** — Plot enqueues each incoming\n * request and immediately returns `200 { queued: true }`. A background\n * queue consumer runs the callback with bounded concurrency. The\n * sender never sees the callback's return value or any error thrown\n * by it, and delivery is at-least-once (the callback must be\n * idempotent). This is the right default for the vast majority of\n * webhooks — service event notifications, bulk-import fan-out, etc. —\n * because it removes ingress-path database pressure and prevents\n * sender-side retry storms when callbacks are slow.\n *\n * **Sync (`async: false`)** — Plot runs the callback inline and\n * responds with the callback's return value. Required when:\n * - The sender reads the response body (e.g. Microsoft Graph\n * subscription validation, which POSTs with a `validationToken` and\n * expects the token echoed as `text/plain`).\n * - The sender uses the HTTP status code to decide whether to retry\n * (e.g. to surface 4xx for permanent failures).\n * - The handler must observe throws before the sender times out.\n *\n * When `async: false`, a callback returning a `string` is sent back\n * with `Content-Type: text/plain`; any other value is serialized as\n * JSON. `undefined` / `void` yields a plain `200 OK` body.\n *\n * Defaults to `true`.\n */\n async?: boolean;\n },\n callback: TCallback,\n ...extraArgs: TArgs\n ): Promise<string>;\n\n /**\n * Deletes an existing webhook endpoint.\n *\n * Removes the webhook endpoint and stops processing requests.\n * Works with all webhook types (standard, Slack, and Gmail).\n *\n * **For Gmail webhooks:** Also deletes the associated Google Pub/Sub topic and subscription.\n *\n * **For Slack webhooks:** Removes the callback registration for the specific team.\n *\n * **For standard webhooks:** Removes the webhook endpoint. Any subsequent requests\n * to the deleted webhook will return 404.\n *\n * @param url - The webhook identifier returned from `createWebhook()`.\n * This can be a URL (standard webhooks), a Pub/Sub topic name (Gmail),\n * or an opaque identifier (Slack). Always pass the exact value returned\n * from `createWebhook()`.\n * @returns Promise that resolves when the webhook is deleted\n */\n abstract deleteWebhook(url: string): Promise<void>;\n}\n";
package/src/plot.ts CHANGED
@@ -788,6 +788,16 @@ export type NewNote = Partial<
788
788
  * - `undefined` (omitted): not a reply
789
789
  */
790
790
  reNote?: { id: Uuid } | { key: string } | null;
791
+
792
+ /**
793
+ * A link carried by this note (note-attached, NOT a thread-level canonical
794
+ * link). Use for augmenter content (e.g. Granola meeting notes) that should
795
+ * attach to an existing canonical thread without becoming its primary link.
796
+ * The runtime creates the link note-scoped, binds `note.link_id` to it, and
797
+ * — when `thread: { source }` resolves to no existing thread — find-or-creates
798
+ * the thread by that source so a later canonical sync can fill the primary.
799
+ */
800
+ link?: NewLink;
791
801
  };
792
802
 
793
803
  /**
@@ -1025,6 +1035,12 @@ export type Link = {
1025
1035
  * meeting-notes connector can bundle by setting the same alias.
1026
1036
  */
1027
1037
  sources: string[];
1038
+ /**
1039
+ * Connector-supplied ranking used by clients to choose the single displayed
1040
+ * (primary) canonical link when a thread has more than one. Higher wins;
1041
+ * ties break on earliest creation. Default 0.
1042
+ */
1043
+ priority: number;
1028
1044
  };
1029
1045
 
1030
1046
  /**
@@ -1082,12 +1098,36 @@ export type NewLink = Partial<
1082
1098
  * - undefined (default): Preserve current archive state
1083
1099
  */
1084
1100
  archived?: boolean;
1101
+ /**
1102
+ * Mark the thread as the connection owner's to-do at create time.
1103
+ * - true: the thread is added to the owner's to-do (active) bucket and
1104
+ * their per-user archive is lifted, atomically with the save — no
1105
+ * separate `integrations.setThreadToDo()` round-trip needed.
1106
+ * - false: the owner's thread_state is marked read (cleared from to-do).
1107
+ * - undefined (omitted, default): leave to-do state untouched.
1108
+ *
1109
+ * Use for messaging-style "saved for later" flags (e.g. a starred Slack
1110
+ * thread). This is the first-class replacement for overloading a
1111
+ * `statuses[]` entry with `active: true`.
1112
+ */
1113
+ todo?: boolean;
1114
+ /**
1115
+ * The to-do date used when `todo` is true. Defaults to the "Now"
1116
+ * sentinel (today's bucket) when omitted. Ignored when `todo` is not true.
1117
+ */
1118
+ todoDate?: Date | string;
1085
1119
  /**
1086
1120
  * Explicit focus (disables automatic focus matching).
1087
1121
  * Only used when the link creates a new thread. When omitted, the
1088
1122
  * server classifies the thread using the user's focus rules.
1089
1123
  */
1090
1124
  focus?: Pick<Focus, "id">;
1125
+ /**
1126
+ * Primary-link ranking for this canonical link (default 0). Set higher on
1127
+ * the connection that "owns" the external item (e.g. the calendar that owns
1128
+ * an event vs a subscribed copy) so clients display it as the primary.
1129
+ */
1130
+ priority?: number;
1091
1131
  };
1092
1132
 
1093
1133
  /**
@@ -1109,6 +1149,25 @@ export type NewLinkWithNotes = NewLink & {
1109
1149
  schedules?: Array<Omit<NewSchedule, "threadId">>;
1110
1150
  /** Schedule occurrence overrides */
1111
1151
  scheduleOccurrences?: NewScheduleOccurrence[];
1152
+ /**
1153
+ * For `onCreateLink` only: binds the thread's opening note (the message the
1154
+ * user composed in Plot, which this hook just posted to the external system)
1155
+ * to its external counterpart. Mirrors the `NoteWriteBackResult` a reply
1156
+ * returns from `onNoteCreated` — `key` is the external message id and
1157
+ * `externalContent` is the post-write content baseline. Without this the
1158
+ * opening note stays keyless, so reactions and edits on it can't be routed
1159
+ * back to the external system. Ignored outside `onCreateLink`.
1160
+ */
1161
+ originatingNote?: {
1162
+ /** External message id; set as the opening note's `key`. */
1163
+ key?: string;
1164
+ /**
1165
+ * Content as the external system stored it post-write, for the sync
1166
+ * baseline. Must equal what your sync-in path emits as this note's
1167
+ * `content` on re-ingest (same contract as `NoteWriteBackResult.externalContent`).
1168
+ */
1169
+ externalContent?: string;
1170
+ };
1112
1171
  };
1113
1172
 
1114
1173
  /**
@@ -43,6 +43,21 @@ export type Channel = {
43
43
  linkTypes?: LinkTypeConfig[];
44
44
  };
45
45
 
46
+ /**
47
+ * Curated status-icon vocabulary. Connectors map each declared status to the
48
+ * closest icon; clients render a single glyph per value. Required on every
49
+ * status so the UI always has something to show.
50
+ */
51
+ export type StatusIcon =
52
+ | "backlog"
53
+ | "todo"
54
+ | "inProgress"
55
+ | "blocked"
56
+ | "done"
57
+ | "cancelled"
58
+ | "confirmed"
59
+ | "tentative";
60
+
46
61
  /**
47
62
  * Describes a link type that a connector creates.
48
63
  * Used for display in the UI (icons, labels).
@@ -93,6 +108,14 @@ export type LinkTypeConfig = {
93
108
  status: string;
94
109
  /** Human-readable label (e.g., "Open", "Done") */
95
110
  label: string;
111
+ /** Curated icon for this status (required so the UI always has a glyph). */
112
+ icon: StatusIcon;
113
+ /**
114
+ * Suppress this status's icon on the feed row (ThreadWidget) while still
115
+ * showing it in the ThreadPage header. Use for a "resting" default state
116
+ * that would otherwise clutter the feed (e.g. calendar "Confirmed").
117
+ */
118
+ hiddenDefault?: boolean;
96
119
  /** Whether this status represents completion (done, closed, merged, cancelled, etc.) */
97
120
  done?: boolean;
98
121
  /**
@@ -227,8 +250,11 @@ export type ComposeConfig = {
227
250
  * `onCreateLink` resolves itself (e.g. Linear's `"unstarted"` category is
228
251
  * resolved per-team to a state UUID inside the connector — see
229
252
  * `connectors/linear/src/linear.ts`).
253
+ *
254
+ * Omit for status-less link types (e.g. messaging connectors that don't
255
+ * model a status): composed links are created with no status.
230
256
  */
231
- status: string;
257
+ status?: string;
232
258
  /**
233
259
  * Optional override for the picker chip / "Create new …" copy. Defaults
234
260
  * to the parent linkType's `label`. Use to disambiguate compose entries
@@ -297,6 +323,22 @@ export type SyncContext = {
297
323
  * overwrites stored state rather than appending to it.
298
324
  */
299
325
  recovering?: boolean;
326
+
327
+ /**
328
+ * True when the channel is being observed because the user composed a Plot
329
+ * thread INTO it (via `onCreateLink`), not because they explicitly enabled
330
+ * it. The connector should register webhooks / mark the channel observed so
331
+ * inbound events (replies, reactions) on the composed thread sync back —
332
+ * but must NOT backfill history. Only go-forward events matter; pulling the
333
+ * channel's existing content would be surprising for a channel the user
334
+ * only posted one thread into.
335
+ *
336
+ * Connectors whose `onChannelEnabled` already skips historical backfill can
337
+ * ignore this flag. Connectors that initial-sync on enable MUST short-
338
+ * circuit that backfill when `observeOnly` is true (still set up webhooks
339
+ * and any go-forward state).
340
+ */
341
+ observeOnly?: boolean;
300
342
  };
301
343
 
302
344
  /**
@@ -499,6 +541,15 @@ export abstract class Integrations extends ITool {
499
541
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
500
542
  abstract markNeedsReauth(channelId: string): Promise<void>;
501
543
 
544
+ /**
545
+ * Upsert workspace custom emoji into Plot's shared cache so reactions using
546
+ * `provider:workspace/name` refs render as images and round-trip. Idempotent;
547
+ * keyed on `id`. Pass `archived: true` to mark an emoji removed. Workspace-
548
+ * scoped (shared across all users of that workspace).
549
+ */
550
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
551
+ abstract saveCustomEmoji(emoji: NewCustomEmoji[]): Promise<void>;
552
+
502
553
  }
503
554
 
504
555
  /**
@@ -516,6 +567,28 @@ export type ArchiveLinkFilter = {
516
567
  meta?: Record<string, JSONValue>;
517
568
  };
518
569
 
570
+ /**
571
+ * A workspace custom emoji to cache so Plot can render and round-trip it as a
572
+ * reaction. `id` is the provider-scoped ref stored on reactions, of the form
573
+ * `<provider>:<workspace>/<name>` (e.g. `slack:T0123ABC/party_parrot`).
574
+ */
575
+ export type NewCustomEmoji = {
576
+ /** Provider-scoped ref: `<provider>:<workspace>/<name>`. The reaction value. */
577
+ id: string;
578
+ /** e.g. "slack". */
579
+ provider: string;
580
+ /** Provider workspace/team id (e.g. Slack team id `T0123ABC`). */
581
+ workspace: string;
582
+ /** Bare emoji name without colons (e.g. "party_parrot"). */
583
+ name: string;
584
+ /** Image URL to render, or null for an alias (see `aliasOf`). */
585
+ imageUrl: string | null;
586
+ /** When this emoji aliases another, the target ref (`id` of the canonical emoji); else null. */
587
+ aliasOf: string | null;
588
+ /** True to mark the emoji removed (archive it) rather than upsert it. */
589
+ archived: boolean;
590
+ };
591
+
519
592
  /**
520
593
  * Enumeration of supported OAuth providers.
521
594
  *
@@ -133,27 +133,27 @@ export abstract class Network extends ITool {
133
133
  *
134
134
  * **Provider-Specific Behavior:**
135
135
  * - **Slack**: Uses provider-specific routing via team_id. Requires `authorization` parameter.
136
- * - **Gmail** (Google with Gmail scopes): Returns a Google Pub/Sub topic name instead of a webhook URL.
137
- * The topic name (e.g., "projects/plot-prod/topics/gmail-webhook-abc123") should be passed
138
- * to the Gmail API's `users.watch` endpoint. Requires `authorization` parameter with Gmail scopes.
139
- * - **Pub/Sub** (`pubsub: true`): Returns a Google Pub/Sub topic name instead of a webhook URL.
140
- * Use this for services that deliver events via Pub/Sub (e.g., Google Workspace Events API).
141
- * A Pub/Sub topic and push subscription are created automatically; the returned topic name
142
- * can be passed to any Google API that accepts a Pub/Sub notification endpoint.
136
+ * - **Pub/Sub** (`pubsub: "gmail" | "workspace"`): Returns a Google Pub/Sub topic name instead
137
+ * of a webhook URL. `"gmail"` targets Gmail `users.watch` (set this only on the Gmail
138
+ * connector); `"workspace"` targets Google Workspace Events (Chat, etc.). A Pub/Sub topic and
139
+ * push subscription are created automatically; the returned topic name (e.g.
140
+ * "projects/plot-prod/topics/gmail-abc123") is passed to the relevant Google API. Other Google
141
+ * connectors (Calendar, Drive) omit `pubsub` and use the default HTTPS webhook.
143
142
  * - **Default**: Returns a standard webhook URL for all other cases.
144
143
  *
145
144
  * @param options - Webhook creation options
146
145
  * @param options.provider - Optional provider for provider-specific webhook routing
147
- * @param options.authorization - Optional authorization for provider-specific webhooks (required for Slack and Gmail)
146
+ * @param options.authorization - Optional authorization for provider-specific webhooks (required for Slack)
147
+ * @param options.pubsub - Optional Google Pub/Sub push product ("gmail" | "workspace")
148
148
  * @param callback - Function receiving (request, ...extraArgs)
149
149
  * @param extraArgs - Additional arguments to pass to the callback (type-checked, no functions allowed)
150
- * @returns Promise resolving to the webhook URL, or for Gmail/Pub/Sub, a Pub/Sub topic name
150
+ * @returns Promise resolving to the webhook URL, or for Pub/Sub, a Pub/Sub topic name
151
151
  *
152
152
  * @example
153
153
  * ```typescript
154
- * // Pub/Sub webhook for Workspace Events API
154
+ * // Pub/Sub webhook for Workspace Events API (Chat, etc.)
155
155
  * const topicName = await this.tools.network.createWebhook(
156
- * { pubsub: true },
156
+ * { pubsub: "workspace" },
157
157
  * this.onEventReceived,
158
158
  * channelId
159
159
  * );
@@ -165,9 +165,9 @@ export abstract class Network extends ITool {
165
165
  *
166
166
  * @example
167
167
  * ```typescript
168
- * // Gmail webhook - auto-detected from scopes, returns Pub/Sub topic name
168
+ * // Gmail webhook - returns a Gmail Pub/Sub topic name for users.watch
169
169
  * const topicName = await this.tools.network.createWebhook(
170
- * {},
170
+ * { pubsub: "gmail" },
171
171
  * this.onGmailNotification,
172
172
  * "inbox"
173
173
  * );
@@ -180,8 +180,20 @@ export abstract class Network extends ITool {
180
180
  options: {
181
181
  provider?: AuthProvider;
182
182
  authorization?: Authorization;
183
- /** When true, creates a Google Pub/Sub topic instead of a webhook URL. */
184
- pubsub?: boolean;
183
+ /**
184
+ * Create a Google Pub/Sub topic instead of a webhook URL, and return
185
+ * the topic name. Selects the push product:
186
+ *
187
+ * - `"gmail"` — Gmail `users.watch` (topic published to by
188
+ * `gmail-api-push`). Set this only on the Gmail connector.
189
+ * - `"workspace"` — Google Workspace Events (Chat, etc.).
190
+ *
191
+ * This opt-in must be explicit. Other Google connectors (Calendar,
192
+ * Drive) omit it and receive a standard HTTPS webhook URL — they must
193
+ * never be routed to a Pub/Sub topic, which `events.watch` /
194
+ * `files.watch` reject as non-HTTPS.
195
+ */
196
+ pubsub?: "gmail" | "workspace";
185
197
  /**
186
198
  * Controls whether the returned webhook URL runs callbacks synchronously
187
199
  * or asynchronously.