@plotday/twister 0.59.0 → 0.61.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 (105) hide show
  1. package/dist/connector.d.ts +52 -3
  2. package/dist/connector.d.ts.map +1 -1
  3. package/dist/connector.js +29 -0
  4. package/dist/connector.js.map +1 -1
  5. package/dist/docs/assets/hierarchy.js +1 -1
  6. package/dist/docs/assets/navigation.js +1 -1
  7. package/dist/docs/assets/search.js +1 -1
  8. package/dist/docs/classes/index.Connector.html +73 -35
  9. package/dist/docs/classes/index.FileNotFoundError.html +1 -1
  10. package/dist/docs/classes/index.Files.html +1 -1
  11. package/dist/docs/classes/index.Imap.html +1 -1
  12. package/dist/docs/classes/index.Options.html +1 -1
  13. package/dist/docs/classes/index.Smtp.html +1 -1
  14. package/dist/docs/classes/tool.ITool.html +1 -1
  15. package/dist/docs/classes/tool.Tool.html +23 -7
  16. package/dist/docs/classes/tools_ai.AI.html +1 -1
  17. package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
  18. package/dist/docs/classes/tools_integrations.Integrations.html +1 -1
  19. package/dist/docs/classes/tools_network.Network.html +1 -1
  20. package/dist/docs/classes/tools_plot.Plot.html +1 -1
  21. package/dist/docs/classes/tools_store.Store.html +1 -1
  22. package/dist/docs/classes/tools_tasks.Tasks.html +32 -2
  23. package/dist/docs/classes/tools_twists.Twists.html +1 -1
  24. package/dist/docs/classes/twist.Twist.html +27 -11
  25. package/dist/docs/documents/Built-in_Tools.html +15 -1
  26. package/dist/docs/documents/Runtime_Environment.html +10 -2
  27. package/dist/docs/enums/plot.ActorType.html +4 -4
  28. package/dist/docs/hierarchy.html +1 -1
  29. package/dist/docs/media/AGENTS.md +44 -2
  30. package/dist/docs/modules/index.html +1 -1
  31. package/dist/docs/modules/plot.html +1 -1
  32. package/dist/docs/types/index.CreateLinkDraft.html +9 -9
  33. package/dist/docs/types/index.NoteWriteBackResult.html +21 -2
  34. package/dist/docs/types/index.OptionalScopeGroup.html +6 -6
  35. package/dist/docs/types/index.ResolvedRecipient.html +5 -5
  36. package/dist/docs/types/index.ScopeConfig.html +2 -4
  37. package/dist/docs/types/plot.Actor.html +5 -5
  38. package/dist/docs/types/plot.AutoThreadConfig.html +9 -0
  39. package/dist/docs/types/plot.AutoThreadMode.html +14 -0
  40. package/dist/docs/types/plot.Contact.html +4 -4
  41. package/dist/docs/types/plot.ContentType.html +1 -1
  42. package/dist/docs/types/plot.DeliveryError.html +17 -0
  43. package/dist/docs/types/plot.Link.html +17 -17
  44. package/dist/docs/types/plot.LinkUpdate.html +1 -1
  45. package/dist/docs/types/plot.NewActor.html +1 -1
  46. package/dist/docs/types/plot.NewContact.html +1 -1
  47. package/dist/docs/types/plot.NewLink.html +12 -2
  48. package/dist/docs/types/plot.NewLinkWithNotes.html +11 -3
  49. package/dist/docs/types/plot.NewNote.html +1 -1
  50. package/dist/docs/types/plot.Note.html +5 -5
  51. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  52. package/dist/docs/types/plot.PlanOperation.html +1 -1
  53. package/dist/facets.d.ts +17 -1
  54. package/dist/facets.d.ts.map +1 -1
  55. package/dist/llm-docs/connector.d.ts +1 -1
  56. package/dist/llm-docs/connector.d.ts.map +1 -1
  57. package/dist/llm-docs/connector.js +1 -1
  58. package/dist/llm-docs/connector.js.map +1 -1
  59. package/dist/llm-docs/facets.d.ts +1 -1
  60. package/dist/llm-docs/facets.d.ts.map +1 -1
  61. package/dist/llm-docs/facets.js +1 -1
  62. package/dist/llm-docs/facets.js.map +1 -1
  63. package/dist/llm-docs/plot.d.ts +1 -1
  64. package/dist/llm-docs/plot.d.ts.map +1 -1
  65. package/dist/llm-docs/plot.js +1 -1
  66. package/dist/llm-docs/plot.js.map +1 -1
  67. package/dist/llm-docs/tool.d.ts +1 -1
  68. package/dist/llm-docs/tool.d.ts.map +1 -1
  69. package/dist/llm-docs/tool.js +1 -1
  70. package/dist/llm-docs/tool.js.map +1 -1
  71. package/dist/llm-docs/tools/tasks.d.ts +1 -1
  72. package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
  73. package/dist/llm-docs/tools/tasks.js +1 -1
  74. package/dist/llm-docs/tools/tasks.js.map +1 -1
  75. package/dist/llm-docs/twist.d.ts +1 -1
  76. package/dist/llm-docs/twist.d.ts.map +1 -1
  77. package/dist/llm-docs/twist.js +1 -1
  78. package/dist/llm-docs/twist.js.map +1 -1
  79. package/dist/plot.d.ts +84 -1
  80. package/dist/plot.d.ts.map +1 -1
  81. package/dist/plot.js.map +1 -1
  82. package/dist/tool.d.ts +25 -0
  83. package/dist/tool.d.ts.map +1 -1
  84. package/dist/tool.js +27 -0
  85. package/dist/tool.js.map +1 -1
  86. package/dist/tools/tasks.d.ts +46 -0
  87. package/dist/tools/tasks.d.ts.map +1 -1
  88. package/dist/tools/tasks.js.map +1 -1
  89. package/dist/twist.d.ts +25 -0
  90. package/dist/twist.d.ts.map +1 -1
  91. package/dist/twist.js +27 -0
  92. package/dist/twist.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/connector.ts +55 -3
  95. package/src/facets.ts +21 -1
  96. package/src/llm-docs/connector.ts +1 -1
  97. package/src/llm-docs/facets.ts +1 -1
  98. package/src/llm-docs/plot.ts +1 -1
  99. package/src/llm-docs/tool.ts +1 -1
  100. package/src/llm-docs/tools/tasks.ts +1 -1
  101. package/src/llm-docs/twist.ts +1 -1
  102. package/src/plot.ts +87 -1
  103. package/src/tool.ts +33 -0
  104. package/src/tools/tasks.ts +52 -0
  105. package/src/twist.ts +33 -0
package/src/connector.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Actor, type ActorId, type Contact, type Link, type NewLinkWithNotes, type Note, type Thread, type Uuid } from "./plot";
1
+ import { type Actor, type ActorId, type Contact, type DeliveryError, type Link, type NewLinkWithNotes, type Note, type Thread, type Uuid } from "./plot";
2
2
  import type { ScheduleContactStatus } from "./schedule";
3
3
  import {
4
4
  type AuthProvider,
@@ -82,6 +82,28 @@ export type NoteWriteBackResult = {
82
82
  * `NewNote.content` for this note on re-ingest.
83
83
  */
84
84
  externalContent?: string;
85
+ /**
86
+ * Reports that the outbound send / write-back for this note FAILED and
87
+ * could not be recovered (after the connector's own retries). The runtime
88
+ * records it on the note — surfacing a "Failed to send" affordance (Retry /
89
+ * Discard) to the user — and marks the thread unread.
90
+ *
91
+ * - object → record the failure.
92
+ * - `null` → clear a previously-recorded failure (e.g. a successful retry).
93
+ * - omitted (`undefined`) → leave any existing delivery state untouched.
94
+ *
95
+ * A successful write-back (any result without a `deliveryError`) also clears
96
+ * a previously-recorded failure, so connectors usually only need to SET this
97
+ * on failure.
98
+ *
99
+ * Prefer RETURNING this over throwing for expected, user-visible failures
100
+ * (rejected recipient, message too large, quota exhausted): a thrown error
101
+ * pages error tracking, whereas a returned `deliveryError` does not. Reserve
102
+ * throwing for genuinely unexpected errors. Connectors that simply throw on a
103
+ * failed write-back still get a generic "Failed to send" surfaced by the
104
+ * runtime, just without a specific reason.
105
+ */
106
+ deliveryError?: DeliveryError | null;
85
107
  };
86
108
 
87
109
  /**
@@ -195,8 +217,6 @@ export type OptionalScopeGroup = {
195
217
  */
196
218
  export type ScopeConfig = {
197
219
  required: string[];
198
- /** Friendly bullets describing what the always-on (required) access does. */
199
- description?: string[];
200
220
  optional?: OptionalScopeGroup[];
201
221
  };
202
222
 
@@ -261,6 +281,15 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
261
281
  * a {@link ScopeConfig} declaring required + optional scope groups. */
262
282
  readonly scopes?: string[] | ScopeConfig;
263
283
 
284
+ /**
285
+ * Plain-language bullets describing what access connecting this service
286
+ * grants the user — shown on the connect screen regardless of auth mechanism
287
+ * (OAuth, API key, or hosted). For OAuth connectors it also previews what the
288
+ * provider's consent screen will request. These are justifications for what
289
+ * Plot accesses, not a one-to-one mapping of scope strings.
290
+ */
291
+ readonly access?: string[];
292
+
264
293
  // ---- Auth model ----
265
294
 
266
295
  /**
@@ -328,6 +357,29 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
328
357
  */
329
358
  readonly autoEnableNewChannelsByDefault?: boolean;
330
359
 
360
+ /**
361
+ * Whether this connector supports the platform's sequential auto-threading —
362
+ * folding a conversation that arrives as a run of separate top-level messages
363
+ * into a single thread. Set `true` for conversational connectors (chat,
364
+ * messaging) that mark eligible links with {@link NewLink.autoThread}. The UI
365
+ * shows a per-connection "Group related messages into conversations" toggle
366
+ * only for connectors that declare this.
367
+ *
368
+ * Leave undefined/false for connectors whose items are not conversational
369
+ * (calendars, issue trackers, file storage) — marking a link does nothing
370
+ * unless the connection both declares support and the user opted in.
371
+ */
372
+ readonly autoThreading?: boolean;
373
+
374
+ /**
375
+ * Whether the per-connection auto-threading preference starts ON for newly
376
+ * added connections of this connector. Defaults to `false` (opt-in) — the
377
+ * least-surprise default, since a wrong fold is irreversible. Only meaningful
378
+ * when {@link autoThreading} is `true`. The user's explicit toggle always
379
+ * wins, and existing connections keep their stored preference.
380
+ */
381
+ readonly autoThreadingByDefault?: boolean;
382
+
331
383
  /**
332
384
  * Registry of link types this connector creates (e.g., issue, event, message).
333
385
  * Used for display in the UI (icons, labels, statuses).
package/src/facets.ts CHANGED
@@ -21,7 +21,9 @@ export type Format =
21
21
  | "notification"
22
22
  | "receipt"
23
23
  | "invoice"
24
- | "promotion";
24
+ | "promotion"
25
+ | "otp" // time-sensitive one-time passcode
26
+ | "confirm"; // confirm/verify-your-account call to action
25
27
 
26
28
  /** Whether a person or a system produced the message. */
27
29
  export type Automation = "human" | "automated";
@@ -38,3 +40,21 @@ export type ThreadFacets = {
38
40
  automation: Automation | null;
39
41
  reach: Reach | null;
40
42
  };
43
+
44
+ /**
45
+ * A time-sensitive call-to-action extracted from a message (e.g. an OTP code
46
+ * or a "confirm your account" link). Carried on a note so the client can show
47
+ * an ephemeral prompt. Heuristic + best-effort; null when nothing confident is
48
+ * found. Link (`url`) is only ever populated for DMARC-authenticated mail.
49
+ */
50
+ export type CtaKind = "otp" | "confirm";
51
+
52
+ export type Cta = {
53
+ kind: CtaKind;
54
+ /** Display name of the originating service, e.g. "Acme". */
55
+ service: string;
56
+ /** The one-time code, for kind === "otp". Null otherwise. */
57
+ code: string | null;
58
+ /** The confirm/verify URL, for kind === "confirm". Null otherwise. DMARC-verified. */
59
+ url: string | null;
60
+ };
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "import { type Actor, type ActorId, type Contact, type Link, type NewLinkWithNotes, type Note, type Thread, type Uuid } from \"./plot\";\nimport type { ScheduleContactStatus } from \"./schedule\";\nimport {\n type AuthProvider,\n type AuthToken,\n type Authorization,\n type Channel,\n type LinkTypeConfig,\n type SyncContext,\n} from \"./tools/integrations\";\nimport { Twist } from \"./twist\";\n\n/**\n * Declares how a connector's platform handles emoji reactions.\n *\n * Drives Plot UI behavior (e.g. the picker filters the available\n * reactions on notes whose primary connector declares `fixed`) and\n * outbound dispatch (Plot won't try to push an emoji the platform\n * can't accept).\n *\n * Variants:\n * - `open-unicode`: Platform accepts any Unicode emoji. `customEmoji`\n * indicates whether the platform additionally supports workspace\n * custom emoji (Slack, Google Chat).\n * - `unicode-subset`: Platform accepts Unicode but only a finite set.\n * `subset` lists the allowed emoji (omit for \"currently full Unicode\n * per docs, future-proofed for shrinkage\").\n * - `fixed`: Platform only accepts a fixed set (e.g. LinkedIn\n * Messaging's 7-reaction set). `allowed` lists every supported emoji.\n */\nexport type ReactionCapabilities =\n | { mode: \"open-unicode\"; customEmoji?: \"workspace\" | \"none\" }\n | { mode: \"unicode-subset\"; subset?: readonly string[] }\n | { mode: \"fixed\"; allowed: readonly string[] };\n\n/**\n * Result returned from {@link Connector.onNoteCreated} and\n * {@link Connector.onNoteUpdated} to report what the external system now\n * has stored for the note.\n *\n * The runtime hashes `externalContent` and stores it as the note's sync\n * baseline. On the next sync-in, if the incoming content hashes to the\n * same value, the runtime knows the external side hasn't changed and\n * preserves Plot's (possibly formatted) content. When the external side\n * is edited, the hash diverges and the runtime overwrites Plot's content\n * with the new external version.\n *\n * Omitting `externalContent` skips baseline tracking — the next sync-in\n * will overwrite Plot's content (previous behavior). Always provide it\n * when the write-back's return value reflects what the external system\n * actually stored (often lossy plain-text), so the round-trip does not\n * clobber the original Plot markdown.\n *\n * The hash covers only the content string — the runtime intentionally\n * does not include a content-type in the hash, so write-back and sync-in\n * do not have to agree on a content-type label for the same underlying\n * bytes. Return exactly the string your connector's sync-in path will\n * emit as `NewNote.content` for this note on the next re-ingest.\n *\n * For back-compat, `onNoteCreated` may also return a plain string, which\n * is treated as `{ key }` with no baseline.\n */\nexport type NoteWriteBackResult = {\n /**\n * External system identifier assigned to this note. Set as the note's\n * `key` for future upsert matching. Required when the runtime does not\n * already know the key (i.e., from `onNoteCreated`); ignored from\n * `onNoteUpdated` when the key was already established on create.\n */\n key?: string;\n /**\n * The content string as the external system now stores it, post-write.\n * For systems whose write-back returns a representation of what was\n * actually stored (e.g. Google Drive comment `content` after a create),\n * pass that verbatim. For systems that only accept plain text, this\n * will often be a lossy plain-text version of the Plot markdown — that\n * is exactly the point: storing the lossy form as baseline lets the\n * next sync-in recognize it and skip overwriting the richer Plot\n * version.\n *\n * Must exactly match the string your connector's sync-in path emits as\n * `NewNote.content` for this note on re-ingest.\n */\n externalContent?: string;\n};\n\n/**\n * A Plot contact pre-resolved to its platform account ID, ready for use\n * in a messaging dispatch.\n *\n * Populated by the runtime for link types with `compose.targets: \"contacts\"` before\n * `onCreateLink` is called. The connector should use `externalAccountId`\n * directly to address the recipient on the platform (e.g. Slack user ID,\n * LinkedIn URN, Gmail address) without performing its own contact lookup.\n */\nexport type ResolvedRecipient = {\n /** Plot contact UUID */\n id: Uuid;\n /** Display name, or null if not set */\n name: string | null;\n /** Platform-specific account identifier pre-resolved at dispatch time (e.g. Slack `U…`, LinkedIn URN, Gmail email address) */\n externalAccountId: string;\n /**\n * The contact's role on the originating thread, resolved from\n * `thread.contact_meta` against the link type's `contactRoles` (e.g.\n * `\"to\"` / `\"cc\"` / `\"bcc\"` for Gmail). `null` when the contact had no\n * explicit role entry — connectors should treat `null` as the link\n * type's default role (the `contactRoles` entry marked `default: true`).\n *\n * Connectors that distinguish roles MUST honor this when addressing the\n * recipient — e.g. Gmail must place `\"cc\"`/`\"bcc\"` recipients in the\n * Cc/Bcc headers, never the To header, so BCC recipients are not\n * exposed to the other recipients. Connectors that don't distinguish\n * roles (Slack, Linear) can ignore it.\n */\n role: string | null;\n};\n\n/**\n * Fields captured in Plot when a user initiates creation of a new external\n * item via a connector's `onCreateLink` hook.\n *\n * Thread-agnostic on purpose — connectors do not receive the Plot thread.\n * The platform attaches the returned `NewLinkWithNotes` to the originating\n * thread once `onCreateLink` resolves.\n */\nexport type CreateLinkDraft = {\n /** The channel (account + resource) the new item belongs to. */\n channelId: string;\n /** Link type identifier, matches a `LinkTypeConfig.type`. */\n type: string;\n /**\n * Status the user selected. Matches a `statuses[].status` for `type`,\n * or null for status-less link types (the parent linkType declares no\n * `statuses` and no `compose.status`).\n */\n status: string | null;\n /** Title of the originating Plot thread (post AI title generation). */\n title: string;\n /** Markdown content of the thread's first note, or null if none. */\n noteContent: string | null;\n /**\n * Contacts attached to the originating Plot thread, excluding the\n * creating user. Use these as recipients (email, chat DM members, etc.)\n * when the external item is a message or invite. An empty list means\n * the user did not add anyone to the thread.\n *\n * For link types with `compose.targets: \"contacts\"`, prefer `recipients` over\n * re-resolving contacts yourself: the runtime pre-resolves each contact\n * to its platform account ID (`externalAccountId`) and populates\n * `recipients` before `onCreateLink` is called.\n */\n contacts: Actor[];\n /**\n * Pre-resolved recipients for link types whose `compose.targets` is\n * `\"contacts\"` or `\"addresses\"`.\n *\n * Only populated for those link types; otherwise undefined. Each entry contains the Plot\n * contact UUID, the platform-specific account ID\n * (`externalAccountId`) the connector should use to address the\n * recipient without performing its own lookup, and the contact's\n * `role` on the thread (e.g. `\"to\"` / `\"cc\"` / `\"bcc\"`) resolved from\n * `thread.contact_meta`. For `\"addresses\"` link types, contacts without\n * a connection-scoped row fall back to `contact.email`.\n */\n recipients?: ResolvedRecipient[];\n /**\n * Free-form addresses the user typed into the picker (no Plot contact\n * row). Only populated for link types with `compose.targets: \"addresses\"`; otherwise\n * undefined. Connectors should append these alongside `recipients`\n * when constructing the recipient list (e.g. `To:` header for Gmail).\n */\n inviteEmails?: string[];\n};\n\n/** An optional OAuth scope group the user can toggle at connect time. */\nexport type OptionalScopeGroup = {\n /** Stable id used to track the user's selection. */\n id: string;\n /** Value-forward switch label, e.g. \"Add names to events using contacts\". */\n label: string;\n /** Optional secondary line shown under the label. */\n description?: string;\n /** The OAuth scope strings this group grants. */\n scopes: string[];\n /** Whether the group is requested by default (switch on). */\n default: boolean;\n};\n\n/**\n * Structured scope declaration. `required` scopes must be granted — auth fails\n * and re-prompts if any is declined. `optional` groups are requested by default\n * but auth still succeeds if the user declines them; the connector should detect\n * the absence via the granted `token.scopes` and degrade gracefully.\n */\nexport type ScopeConfig = {\n required: string[];\n /** Friendly bullets describing what the always-on (required) access does. */\n description?: string[];\n optional?: OptionalScopeGroup[];\n};\n\n/**\n * Base class for connectors — twists that sync data from external services.\n *\n * Connectors declare a single OAuth provider and scopes, and implement channel\n * lifecycle methods for discovering and syncing external resources. They save\n * data directly via `integrations.saveLink()` instead of using the Plot tool.\n *\n * @example\n * ```typescript\n * class LinearConnector extends Connector<LinearConnector> {\n * readonly provider = AuthProvider.Linear;\n * readonly scopes = [\"read\", \"write\"];\n * readonly linkTypes = [{\n * type: \"issue\",\n * label: \"Issue\",\n * statuses: [\n * { status: \"open\", label: \"Open\", icon: \"todo\" },\n * { status: \"done\", label: \"Done\", icon: \"done\", done: true },\n * ],\n * }];\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 teams = await this.listTeams(token);\n * return teams.map(t => ({ id: t.id, title: t.name }));\n * }\n *\n * async onChannelEnabled(channel: Channel) {\n * const issues = await this.fetchIssues(channel.id);\n * for (const issue of issues) {\n * await this.tools.integrations.saveLink(issue);\n * }\n * }\n *\n * async onChannelDisabled(channel: Channel) {\n * // Clean up webhooks, sync state, etc.\n * }\n * }\n * ```\n */\nexport abstract class Connector<TSelf> extends Twist<TSelf> {\n /**\n * Static marker to identify Connector subclasses without instanceof checks\n * across worker boundaries.\n */\n static readonly isConnector = true;\n\n // ---- Identity (abstract — every connector must declare) ----\n\n /** The OAuth provider this connector authenticates with. */\n readonly provider?: AuthProvider;\n\n /** OAuth scopes to request for this connector — a flat list (all required), or\n * a {@link ScopeConfig} declaring required + optional scope groups. */\n readonly scopes?: string[] | ScopeConfig;\n\n // ---- Auth model ----\n\n /**\n * When true, one credential is shared across all users in the workspace,\n * entered once by the installer. When false (default), each user provides\n * their own credential.\n *\n * Applies to both OAuth and key-based connectors:\n * - Shared OAuth: e.g. Slack bot token (workspace-level)\n * - Shared key: e.g. Attio workspace API key\n * - Individual OAuth: e.g. Google Calendar (per-user)\n * - Individual key: e.g. Fellow (per-user API key)\n */\n readonly shared?: boolean;\n\n /**\n * The Options field name that contains the authentication key (e.g. \"apiKey\").\n * Must reference a `secure: true` field in the Options schema.\n *\n * When set, this connector uses key-based auth instead of OAuth.\n * For individual connectors (`shared` is false), this field is stored\n * per-user rather than in shared config.\n */\n readonly keyOption?: string;\n\n // ---- Optional metadata ----\n\n /**\n * When true, this connector has a single implicit channel.\n * `getChannels()` must return exactly one Channel.\n * The UI will show channel config inline instead of a channel list.\n */\n readonly singleChannel?: boolean;\n\n /**\n * The user-facing noun for this connector's channels — what each\n * {@link Channel} returned by {@link getChannels} actually represents in the\n * external service. Many connectors map \"channels\" onto a domain concept\n * (folders, projects, calendars, labels, spaces, repositories, …), so the\n * generic word \"channel\" reads as jargon. Set this and the UI substitutes it\n * everywhere it would otherwise say \"channel(s)\" — e.g. the per-connection\n * toggle becomes \"Sync new folders\" / \"When a new folder is added, …\".\n *\n * Provide lowercase nouns (the UI capitalizes where needed):\n * `{ singular: \"folder\", plural: \"folders\" }`. Defaults to\n * `{ singular: \"channel\", plural: \"channels\" }` when omitted.\n */\n readonly channelNoun?: { singular: string; plural: string };\n\n /**\n * Whether the per-connection \"Sync new channels\" preference starts ON for\n * newly added connections of this connector. Defaults to `false` (opt-in).\n *\n * Set `true` for connectors that select **all** of their channels by default\n * (i.e. {@link getChannels} returns no channels marked\n * `enabledByDefault: false`). If syncing every channel is the intended\n * default, then channels discovered later should also sync automatically.\n * Leave `false`/omitted for selective connectors that exclude some channels\n * by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only\n * owner calendars) — for those, a newly discovered channel is just as\n * uncertain and should wait for the user to opt in.\n *\n * Only affects the **default** for new connections; the user's explicit\n * toggle always wins, and existing connections keep their stored preference.\n */\n readonly autoEnableNewChannelsByDefault?: boolean;\n\n /**\n * Registry of link types this connector creates (e.g., issue, event, message).\n * Used for display in the UI (icons, labels, statuses).\n */\n readonly linkTypes?: LinkTypeConfig[];\n\n /**\n * Declares how this connector's platform handles emoji reactions.\n * Used to filter the reaction picker for notes whose primary connector\n * is this one, and to guard outbound dispatch from sending emoji the\n * platform can't accept.\n *\n * Leave undefined for connectors whose platform has no concept of\n * reactions (calendar, file storage, issue trackers without reactions).\n */\n readonly reactionCapabilities?: ReactionCapabilities;\n\n /**\n * When true, this connector is mentioned by default on replies to threads it created.\n * When false (default), this connector cannot be mentioned at all.\n *\n * Set this to true for connectors with bidirectional sync (e.g., issue trackers,\n * messaging) where user replies should be written back to the external service.\n */\n static readonly handleReplies?: boolean;\n\n // ---- Account identity (abstract — every connector must implement) ----\n\n /**\n * Returns a human-readable name for the connected account.\n * Shown in the connections list and edit modal to identify this connection.\n *\n * For OAuth connectors, this is typically the workspace or organization name\n * (e.g., \"Acme Corp\" for a Linear workspace). For API key connectors, this\n * could be the workspace name from the external service.\n *\n * Override this in your connector to return a meaningful account name.\n *\n * @param auth - The authorization (null for no-provider connectors)\n * @param token - The access token (null for no-provider connectors)\n * @returns Promise resolving to the account display name\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n getAccountName(\n auth: Authorization | null,\n token: AuthToken | null\n ): Promise<string | null> {\n return Promise.resolve(null);\n }\n\n // ---- Channel lifecycle (abstract — every connector must implement) ----\n\n /**\n * Returns available channels for the authorized actor.\n * Called after OAuth is complete, during the setup/edit modal.\n *\n * @param auth - The completed authorization with provider and actor info\n * @param token - The access token for making API calls\n * @returns Promise resolving to available channels for the user to select\n */\n abstract getChannels(\n auth: Authorization | null,\n token: AuthToken | null\n ): Promise<Channel[]>;\n\n /**\n * Called when a channel resource is enabled for syncing.\n *\n * The framework dispatches this in three cases:\n * 1. **Initial enable** — user toggled the channel on for the first time.\n * 2. **Auto-enable** — `setChannels` discovered a new channel on a\n * connection with `auto_enable_new_channels` set.\n * 3. **Recovery after re-auth** — the user re-authorized a previously-\n * broken connection. The framework calls `onChannelEnabled` for every\n * channel that was already enabled at the time of re-auth, with\n * `context.recovering = true`. See {@link SyncContext.recovering}.\n *\n * Implementations should be **idempotent and overwrite stored state**:\n * the same channel may receive multiple `onChannelEnabled` calls across\n * its lifetime. Use unconditional `this.set()` writes rather than\n * coalesce/skip-if-present logic so a recovery dispatch wipes stale\n * cursors and state from the prior session.\n *\n * **Sync state tracking is automatic.** The framework stamps the\n * connection as \"syncing\" when it dispatches this method and clears\n * that state when:\n * - the connector calls `tools.integrations.channelSyncCompleted(id)`\n * once the initial backfill is done, OR\n * - this method throws an unhandled exception (auto-cleared so the UI\n * doesn't get stuck in \"syncing\" forever).\n *\n * **IMPORTANT: This method runs inline in the HTTP request handler.**\n * Any long-running work (webhook setup, API calls, sync) MUST be queued\n * as a separate task via `this.runTask()`, not executed inline. Blocking\n * here causes the client to spin waiting for the response.\n *\n * Only lightweight operations should appear directly in this method:\n * `this.set()`, `this.get()`, `this.callback()`, and `this.runTask()`.\n *\n * @example\n * ```typescript\n * async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {\n * // Recovery: drop stale cursors so the next sync re-walks history.\n * if (context?.recovering) {\n * await this.clear(`last_sync_token_${channel.id}`);\n * }\n *\n * await this.set(`sync_state_${channel.id}`, { channelId: channel.id });\n *\n * // Queue sync as a task — do NOT use this.run() or call sync methods inline\n * const syncCallback = await this.callback(this.syncBatch, 1, \"full\", channel.id, true);\n * await this.runTask(syncCallback);\n *\n * // Queue webhook setup as a task — do NOT call setupWebhook() inline\n * const webhookCallback = await this.callback(this.setupWebhook, channel.id);\n * await this.runTask(webhookCallback);\n * }\n * ```\n *\n * @param channel - The channel that was enabled\n * @param context - Optional sync context (plan-based hints, recovery flag)\n */\n abstract onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void>;\n\n /**\n * Called when a channel resource is disabled.\n * Should stop sync, clean up webhooks, and remove state.\n *\n * @param channel - The channel that was disabled\n */\n abstract onChannelDisabled(channel: Channel): Promise<void>;\n\n // ---- Write-back hooks (optional, default no-ops) ----\n\n /**\n * Called when a link created by this connector is updated by the user.\n * Override to write back changes to the external service\n * (e.g., changing issue status in Linear when marked done in Plot).\n *\n * @param link - The updated link\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkUpdated(link: Link): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user creates a thread in Plot that should create a new\n * item in this connector's external system.\n *\n * A connector opts in to Plot-initiated creation by declaring a\n * `compose` block on the relevant `LinkTypeConfig` (see\n * {@link ComposeConfig}). When a user picks \"Create new <type>\" from the\n * Add link modal and the thread is synced, the runtime calls this method\n * with the draft fields.\n *\n * Implementations should create the item in the external service and\n * return a `NewLinkWithNotes` describing the created item. The platform\n * attaches the returned link to the originating thread — do not call\n * `integrations.saveLink` yourself.\n *\n * Returning `null` aborts creation silently (the thread is still saved\n * without a link).\n *\n * @param draft - The fields captured in Plot for the new item.\n * @returns The link to attach, or null to abort creation.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onCreateLink(draft: CreateLinkDraft): Promise<NewLinkWithNotes | null> {\n return Promise.resolve(null);\n }\n\n /**\n * Called when a note is created on a thread owned by this connector.\n * Override to write back comments to the external service\n * (e.g., adding a comment to a Linear issue).\n *\n * Returning a string or {@link NoteWriteBackResult} links the Plot note\n * to its external counterpart. A plain string sets the note's `key`.\n * A `NoteWriteBackResult` additionally sets a sync baseline (via\n * `externalContent`) so the next sync-in can recognize the round-tripped\n * content and preserve Plot's formatted version. See\n * {@link NoteWriteBackResult} for details.\n *\n * @param note - The created note\n * @param thread - The thread the note belongs to (includes thread.meta with connector-specific data)\n * @returns Optional note key or NoteWriteBackResult for external dedup + baseline tracking\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteCreated(note: Note, thread: Thread): Promise<string | NoteWriteBackResult | void> {\n return Promise.resolve();\n }\n\n /**\n * Resolve a `fileRef` action's bytes for download. Called when a user opens\n * an attachment in Plot. Return either a redirect URL (preferred for sources\n * that issue signed URLs, like Linear S3 or Slack permalink_public) or a\n * streamed body (required when bytes are only reachable through an\n * authenticated API call, like Gmail attachments.get).\n *\n * @param ref Opaque value the connector previously emitted on a fileRef action.\n * @returns Either `{ redirectUrl }` or `{ body, mimeType, fileName? }`.\n * @throws If the source is unavailable, the connection is broken, or `ref` is invalid.\n *\n * If not overridden, fileRef actions on this connector's notes will return 410 Gone.\n */\n async downloadAttachment(\n ref: string,\n ): Promise<\n | { redirectUrl: string }\n | { body: ReadableStream | Uint8Array; mimeType: string; fileName?: string }\n > {\n throw new Error(\n `downloadAttachment not implemented for ${this.constructor.name} (ref=${ref})`,\n );\n }\n\n /**\n * Called when a note on a thread owned by this connector is updated.\n * Override to write back changes to the external service\n * (e.g., syncing reaction tags as emoji reactions, or editing a comment\n * whose content changed in Plot).\n *\n * Return a {@link NoteWriteBackResult} with `externalContent` to update\n * the sync baseline after a successful write-back, so the next sync-in\n * recognizes the external version as already-seen and preserves Plot's\n * content.\n *\n * @param note - The updated note (includes current tags)\n * @param thread - The thread the note belongs to (includes thread.meta with connector-specific data)\n * @returns Optional NoteWriteBackResult for baseline tracking\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteUpdated(note: Note, thread: Thread): Promise<NoteWriteBackResult | void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user reads or unreads a thread owned by this connector.\n * Override to write back read status to the external service\n * (e.g., marking an email as read in Gmail).\n *\n * @param thread - The thread that was read/unread (includes thread.meta with connector-specific data)\n * @param actor - The user who performed the action\n * @param unread - false when marked as read, true when marked as unread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onThreadRead(thread: Thread, actor: Actor, unread: boolean): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user changes the **thread-level sharing** of a thread owned\n * by this connector — adding or removing a contact, or (for connectors with\n * roles) changing a contact's role. Override on connectors whose external\n * source can reflect that membership change, e.g. a group DM / multi-party\n * chat (`LinkTypeConfig.sharingModel: \"thread\"`) or an email's To/Cc/Bcc\n * recipients (`sharingModel: \"message\"`). Connectors backed by an immutable\n * roster (most group DMs today) or by channel-level membership\n * (`sharingModel: \"channel\"`) leave this as the default no-op.\n *\n * `role`/`from`/`to` are **null** for connectors without roles (group DMs\n * have no roles); they carry a `contactRoles` id only for connectors that\n * declare roles (e.g. email `to`/`cc`/`bcc`).\n *\n * The dispatch fires after Plot has persisted the change. A connector may\n * reflect it actively (e.g. add/remove a participant on the external chat)\n * or passively on the next outbound note (e.g. building To/Cc/Bcc headers\n * from the current `thread.contacts` × `thread.contactMeta`) — this callback\n * is not the right place to send a standalone notification.\n *\n * @param thread - The thread whose contacts changed\n * @param changes - The added/removed contacts and any role transitions on existing contacts\n */\n /* eslint-disable @typescript-eslint/no-unused-vars */\n onContactsChanged(\n thread: Thread,\n changes: {\n added: Array<{ contact: Contact; role: string | null }>;\n removed: Array<{ contact: Contact; role: string | null }>;\n changed: Array<{ contact: Contact; from: string | null; to: string | null }>;\n },\n ): Promise<void> {\n return Promise.resolve();\n }\n /* eslint-enable @typescript-eslint/no-unused-vars */\n\n /**\n * Called when a user marks or unmarks a thread as todo.\n * Override to sync todo status to the external service\n * (e.g., starring an email in Gmail when marked as todo).\n *\n * @param thread - The thread (includes thread.meta with connector-specific data)\n * @param actor - The user who changed the todo status\n * @param todo - true when marked as todo, false when done or removed\n * @param options - Additional context\n * @param options.date - The todo date (when todo=true)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onThreadToDo(thread: Thread, actor: Actor, todo: boolean, options: { date?: Date }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a schedule contact's RSVP status changes on a thread owned by this connector.\n * Override to sync RSVP changes back to the external calendar.\n *\n * @param thread - The thread (includes thread.meta with connector-specific data)\n * @param scheduleId - The schedule ID\n * @param contactId - The contact whose status changed\n * @param status - The new RSVP status ('attend', 'skip', or null)\n * @param actor - The user who changed the status\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onScheduleContactUpdated(thread: Thread, scheduleId: string, contactId: ActorId, status: ScheduleContactStatus | null, actor: Actor): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user adds or removes a single emoji reaction on a note\n * (one event per `(note, actor, emoji)` state transition).\n *\n * Dispatch is routed to the reacting user's own connector instance via\n * `twist_instance_for_actor` on `note_reaction.actor_id`, so this method\n * already runs under the reactor's auth. Fetch the API client with the\n * connector's normal token-fetch path (`this.tools.integrations.get(...)`)\n * and the external write — e.g. Slack `reactions.add` — will be attributed\n * to the correct user. No `actAs` step required.\n *\n * If the reacting user has no connection of this type, no dispatch fires\n * for that reaction (it stays in Plot only).\n *\n * Override to sync per-actor reactions back to the external system.\n *\n * @param note - The note that was reacted on (partial; `id`, `key`, `content` populated)\n * @param thread - The thread the note belongs to (partial; `id`, `title`, `archived`, `meta` populated)\n * @param actor - The contact who added/removed the reaction\n * @param emoji - The emoji (Unicode grapheme or `provider:workspace/name` custom-emoji ref)\n * @param added - `true` if the reaction is now present, `false` if it was removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteReactionChanged(note: Note, thread: Thread, actor: Actor, emoji: string, added: boolean): Promise<void> {\n return Promise.resolve();\n }\n\n // ---- Activation ----\n\n /**\n * Called when the connector is activated after OAuth is complete.\n *\n * Connectors receive the authorization in addition to the activating actor.\n * When this runs, `this.userId` is already populated with the installing\n * user's ID.\n *\n * Default implementation does nothing. Override for custom setup.\n *\n * @param context - The activation context\n * @param context.auth - The completed OAuth authorization\n * @param context.actor - The actor who activated the connector\n */\n // @ts-ignore - Connector.activate() has a Connector-specific context type.\n activate(context: { auth?: Authorization; actor?: Actor }): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/** @deprecated Use `Connector` instead. */\nexport { Connector as Source };\n";
8
+ export default "import { type Actor, type ActorId, type Contact, type DeliveryError, type Link, type NewLinkWithNotes, type Note, type Thread, type Uuid } from \"./plot\";\nimport type { ScheduleContactStatus } from \"./schedule\";\nimport {\n type AuthProvider,\n type AuthToken,\n type Authorization,\n type Channel,\n type LinkTypeConfig,\n type SyncContext,\n} from \"./tools/integrations\";\nimport { Twist } from \"./twist\";\n\n/**\n * Declares how a connector's platform handles emoji reactions.\n *\n * Drives Plot UI behavior (e.g. the picker filters the available\n * reactions on notes whose primary connector declares `fixed`) and\n * outbound dispatch (Plot won't try to push an emoji the platform\n * can't accept).\n *\n * Variants:\n * - `open-unicode`: Platform accepts any Unicode emoji. `customEmoji`\n * indicates whether the platform additionally supports workspace\n * custom emoji (Slack, Google Chat).\n * - `unicode-subset`: Platform accepts Unicode but only a finite set.\n * `subset` lists the allowed emoji (omit for \"currently full Unicode\n * per docs, future-proofed for shrinkage\").\n * - `fixed`: Platform only accepts a fixed set (e.g. LinkedIn\n * Messaging's 7-reaction set). `allowed` lists every supported emoji.\n */\nexport type ReactionCapabilities =\n | { mode: \"open-unicode\"; customEmoji?: \"workspace\" | \"none\" }\n | { mode: \"unicode-subset\"; subset?: readonly string[] }\n | { mode: \"fixed\"; allowed: readonly string[] };\n\n/**\n * Result returned from {@link Connector.onNoteCreated} and\n * {@link Connector.onNoteUpdated} to report what the external system now\n * has stored for the note.\n *\n * The runtime hashes `externalContent` and stores it as the note's sync\n * baseline. On the next sync-in, if the incoming content hashes to the\n * same value, the runtime knows the external side hasn't changed and\n * preserves Plot's (possibly formatted) content. When the external side\n * is edited, the hash diverges and the runtime overwrites Plot's content\n * with the new external version.\n *\n * Omitting `externalContent` skips baseline tracking — the next sync-in\n * will overwrite Plot's content (previous behavior). Always provide it\n * when the write-back's return value reflects what the external system\n * actually stored (often lossy plain-text), so the round-trip does not\n * clobber the original Plot markdown.\n *\n * The hash covers only the content string — the runtime intentionally\n * does not include a content-type in the hash, so write-back and sync-in\n * do not have to agree on a content-type label for the same underlying\n * bytes. Return exactly the string your connector's sync-in path will\n * emit as `NewNote.content` for this note on the next re-ingest.\n *\n * For back-compat, `onNoteCreated` may also return a plain string, which\n * is treated as `{ key }` with no baseline.\n */\nexport type NoteWriteBackResult = {\n /**\n * External system identifier assigned to this note. Set as the note's\n * `key` for future upsert matching. Required when the runtime does not\n * already know the key (i.e., from `onNoteCreated`); ignored from\n * `onNoteUpdated` when the key was already established on create.\n */\n key?: string;\n /**\n * The content string as the external system now stores it, post-write.\n * For systems whose write-back returns a representation of what was\n * actually stored (e.g. Google Drive comment `content` after a create),\n * pass that verbatim. For systems that only accept plain text, this\n * will often be a lossy plain-text version of the Plot markdown — that\n * is exactly the point: storing the lossy form as baseline lets the\n * next sync-in recognize it and skip overwriting the richer Plot\n * version.\n *\n * Must exactly match the string your connector's sync-in path emits as\n * `NewNote.content` for this note on re-ingest.\n */\n externalContent?: string;\n /**\n * Reports that the outbound send / write-back for this note FAILED and\n * could not be recovered (after the connector's own retries). The runtime\n * records it on the note — surfacing a \"Failed to send\" affordance (Retry /\n * Discard) to the user — and marks the thread unread.\n *\n * - object → record the failure.\n * - `null` → clear a previously-recorded failure (e.g. a successful retry).\n * - omitted (`undefined`) → leave any existing delivery state untouched.\n *\n * A successful write-back (any result without a `deliveryError`) also clears\n * a previously-recorded failure, so connectors usually only need to SET this\n * on failure.\n *\n * Prefer RETURNING this over throwing for expected, user-visible failures\n * (rejected recipient, message too large, quota exhausted): a thrown error\n * pages error tracking, whereas a returned `deliveryError` does not. Reserve\n * throwing for genuinely unexpected errors. Connectors that simply throw on a\n * failed write-back still get a generic \"Failed to send\" surfaced by the\n * runtime, just without a specific reason.\n */\n deliveryError?: DeliveryError | null;\n};\n\n/**\n * A Plot contact pre-resolved to its platform account ID, ready for use\n * in a messaging dispatch.\n *\n * Populated by the runtime for link types with `compose.targets: \"contacts\"` before\n * `onCreateLink` is called. The connector should use `externalAccountId`\n * directly to address the recipient on the platform (e.g. Slack user ID,\n * LinkedIn URN, Gmail address) without performing its own contact lookup.\n */\nexport type ResolvedRecipient = {\n /** Plot contact UUID */\n id: Uuid;\n /** Display name, or null if not set */\n name: string | null;\n /** Platform-specific account identifier pre-resolved at dispatch time (e.g. Slack `U…`, LinkedIn URN, Gmail email address) */\n externalAccountId: string;\n /**\n * The contact's role on the originating thread, resolved from\n * `thread.contact_meta` against the link type's `contactRoles` (e.g.\n * `\"to\"` / `\"cc\"` / `\"bcc\"` for Gmail). `null` when the contact had no\n * explicit role entry — connectors should treat `null` as the link\n * type's default role (the `contactRoles` entry marked `default: true`).\n *\n * Connectors that distinguish roles MUST honor this when addressing the\n * recipient — e.g. Gmail must place `\"cc\"`/`\"bcc\"` recipients in the\n * Cc/Bcc headers, never the To header, so BCC recipients are not\n * exposed to the other recipients. Connectors that don't distinguish\n * roles (Slack, Linear) can ignore it.\n */\n role: string | null;\n};\n\n/**\n * Fields captured in Plot when a user initiates creation of a new external\n * item via a connector's `onCreateLink` hook.\n *\n * Thread-agnostic on purpose — connectors do not receive the Plot thread.\n * The platform attaches the returned `NewLinkWithNotes` to the originating\n * thread once `onCreateLink` resolves.\n */\nexport type CreateLinkDraft = {\n /** The channel (account + resource) the new item belongs to. */\n channelId: string;\n /** Link type identifier, matches a `LinkTypeConfig.type`. */\n type: string;\n /**\n * Status the user selected. Matches a `statuses[].status` for `type`,\n * or null for status-less link types (the parent linkType declares no\n * `statuses` and no `compose.status`).\n */\n status: string | null;\n /** Title of the originating Plot thread (post AI title generation). */\n title: string;\n /** Markdown content of the thread's first note, or null if none. */\n noteContent: string | null;\n /**\n * Contacts attached to the originating Plot thread, excluding the\n * creating user. Use these as recipients (email, chat DM members, etc.)\n * when the external item is a message or invite. An empty list means\n * the user did not add anyone to the thread.\n *\n * For link types with `compose.targets: \"contacts\"`, prefer `recipients` over\n * re-resolving contacts yourself: the runtime pre-resolves each contact\n * to its platform account ID (`externalAccountId`) and populates\n * `recipients` before `onCreateLink` is called.\n */\n contacts: Actor[];\n /**\n * Pre-resolved recipients for link types whose `compose.targets` is\n * `\"contacts\"` or `\"addresses\"`.\n *\n * Only populated for those link types; otherwise undefined. Each entry contains the Plot\n * contact UUID, the platform-specific account ID\n * (`externalAccountId`) the connector should use to address the\n * recipient without performing its own lookup, and the contact's\n * `role` on the thread (e.g. `\"to\"` / `\"cc\"` / `\"bcc\"`) resolved from\n * `thread.contact_meta`. For `\"addresses\"` link types, contacts without\n * a connection-scoped row fall back to `contact.email`.\n */\n recipients?: ResolvedRecipient[];\n /**\n * Free-form addresses the user typed into the picker (no Plot contact\n * row). Only populated for link types with `compose.targets: \"addresses\"`; otherwise\n * undefined. Connectors should append these alongside `recipients`\n * when constructing the recipient list (e.g. `To:` header for Gmail).\n */\n inviteEmails?: string[];\n};\n\n/** An optional OAuth scope group the user can toggle at connect time. */\nexport type OptionalScopeGroup = {\n /** Stable id used to track the user's selection. */\n id: string;\n /** Value-forward switch label, e.g. \"Add names to events using contacts\". */\n label: string;\n /** Optional secondary line shown under the label. */\n description?: string;\n /** The OAuth scope strings this group grants. */\n scopes: string[];\n /** Whether the group is requested by default (switch on). */\n default: boolean;\n};\n\n/**\n * Structured scope declaration. `required` scopes must be granted — auth fails\n * and re-prompts if any is declined. `optional` groups are requested by default\n * but auth still succeeds if the user declines them; the connector should detect\n * the absence via the granted `token.scopes` and degrade gracefully.\n */\nexport type ScopeConfig = {\n required: string[];\n optional?: OptionalScopeGroup[];\n};\n\n/**\n * Base class for connectors — twists that sync data from external services.\n *\n * Connectors declare a single OAuth provider and scopes, and implement channel\n * lifecycle methods for discovering and syncing external resources. They save\n * data directly via `integrations.saveLink()` instead of using the Plot tool.\n *\n * @example\n * ```typescript\n * class LinearConnector extends Connector<LinearConnector> {\n * readonly provider = AuthProvider.Linear;\n * readonly scopes = [\"read\", \"write\"];\n * readonly linkTypes = [{\n * type: \"issue\",\n * label: \"Issue\",\n * statuses: [\n * { status: \"open\", label: \"Open\", icon: \"todo\" },\n * { status: \"done\", label: \"Done\", icon: \"done\", done: true },\n * ],\n * }];\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 teams = await this.listTeams(token);\n * return teams.map(t => ({ id: t.id, title: t.name }));\n * }\n *\n * async onChannelEnabled(channel: Channel) {\n * const issues = await this.fetchIssues(channel.id);\n * for (const issue of issues) {\n * await this.tools.integrations.saveLink(issue);\n * }\n * }\n *\n * async onChannelDisabled(channel: Channel) {\n * // Clean up webhooks, sync state, etc.\n * }\n * }\n * ```\n */\nexport abstract class Connector<TSelf> extends Twist<TSelf> {\n /**\n * Static marker to identify Connector subclasses without instanceof checks\n * across worker boundaries.\n */\n static readonly isConnector = true;\n\n // ---- Identity (abstract — every connector must declare) ----\n\n /** The OAuth provider this connector authenticates with. */\n readonly provider?: AuthProvider;\n\n /** OAuth scopes to request for this connector — a flat list (all required), or\n * a {@link ScopeConfig} declaring required + optional scope groups. */\n readonly scopes?: string[] | ScopeConfig;\n\n /**\n * Plain-language bullets describing what access connecting this service\n * grants the user — shown on the connect screen regardless of auth mechanism\n * (OAuth, API key, or hosted). For OAuth connectors it also previews what the\n * provider's consent screen will request. These are justifications for what\n * Plot accesses, not a one-to-one mapping of scope strings.\n */\n readonly access?: string[];\n\n // ---- Auth model ----\n\n /**\n * When true, one credential is shared across all users in the workspace,\n * entered once by the installer. When false (default), each user provides\n * their own credential.\n *\n * Applies to both OAuth and key-based connectors:\n * - Shared OAuth: e.g. Slack bot token (workspace-level)\n * - Shared key: e.g. Attio workspace API key\n * - Individual OAuth: e.g. Google Calendar (per-user)\n * - Individual key: e.g. Fellow (per-user API key)\n */\n readonly shared?: boolean;\n\n /**\n * The Options field name that contains the authentication key (e.g. \"apiKey\").\n * Must reference a `secure: true` field in the Options schema.\n *\n * When set, this connector uses key-based auth instead of OAuth.\n * For individual connectors (`shared` is false), this field is stored\n * per-user rather than in shared config.\n */\n readonly keyOption?: string;\n\n // ---- Optional metadata ----\n\n /**\n * When true, this connector has a single implicit channel.\n * `getChannels()` must return exactly one Channel.\n * The UI will show channel config inline instead of a channel list.\n */\n readonly singleChannel?: boolean;\n\n /**\n * The user-facing noun for this connector's channels — what each\n * {@link Channel} returned by {@link getChannels} actually represents in the\n * external service. Many connectors map \"channels\" onto a domain concept\n * (folders, projects, calendars, labels, spaces, repositories, …), so the\n * generic word \"channel\" reads as jargon. Set this and the UI substitutes it\n * everywhere it would otherwise say \"channel(s)\" — e.g. the per-connection\n * toggle becomes \"Sync new folders\" / \"When a new folder is added, …\".\n *\n * Provide lowercase nouns (the UI capitalizes where needed):\n * `{ singular: \"folder\", plural: \"folders\" }`. Defaults to\n * `{ singular: \"channel\", plural: \"channels\" }` when omitted.\n */\n readonly channelNoun?: { singular: string; plural: string };\n\n /**\n * Whether the per-connection \"Sync new channels\" preference starts ON for\n * newly added connections of this connector. Defaults to `false` (opt-in).\n *\n * Set `true` for connectors that select **all** of their channels by default\n * (i.e. {@link getChannels} returns no channels marked\n * `enabledByDefault: false`). If syncing every channel is the intended\n * default, then channels discovered later should also sync automatically.\n * Leave `false`/omitted for selective connectors that exclude some channels\n * by default (e.g. Gmail syncs only Inbox/Sent, Google Calendar only\n * owner calendars) — for those, a newly discovered channel is just as\n * uncertain and should wait for the user to opt in.\n *\n * Only affects the **default** for new connections; the user's explicit\n * toggle always wins, and existing connections keep their stored preference.\n */\n readonly autoEnableNewChannelsByDefault?: boolean;\n\n /**\n * Whether this connector supports the platform's sequential auto-threading —\n * folding a conversation that arrives as a run of separate top-level messages\n * into a single thread. Set `true` for conversational connectors (chat,\n * messaging) that mark eligible links with {@link NewLink.autoThread}. The UI\n * shows a per-connection \"Group related messages into conversations\" toggle\n * only for connectors that declare this.\n *\n * Leave undefined/false for connectors whose items are not conversational\n * (calendars, issue trackers, file storage) — marking a link does nothing\n * unless the connection both declares support and the user opted in.\n */\n readonly autoThreading?: boolean;\n\n /**\n * Whether the per-connection auto-threading preference starts ON for newly\n * added connections of this connector. Defaults to `false` (opt-in) — the\n * least-surprise default, since a wrong fold is irreversible. Only meaningful\n * when {@link autoThreading} is `true`. The user's explicit toggle always\n * wins, and existing connections keep their stored preference.\n */\n readonly autoThreadingByDefault?: boolean;\n\n /**\n * Registry of link types this connector creates (e.g., issue, event, message).\n * Used for display in the UI (icons, labels, statuses).\n */\n readonly linkTypes?: LinkTypeConfig[];\n\n /**\n * Declares how this connector's platform handles emoji reactions.\n * Used to filter the reaction picker for notes whose primary connector\n * is this one, and to guard outbound dispatch from sending emoji the\n * platform can't accept.\n *\n * Leave undefined for connectors whose platform has no concept of\n * reactions (calendar, file storage, issue trackers without reactions).\n */\n readonly reactionCapabilities?: ReactionCapabilities;\n\n /**\n * When true, this connector is mentioned by default on replies to threads it created.\n * When false (default), this connector cannot be mentioned at all.\n *\n * Set this to true for connectors with bidirectional sync (e.g., issue trackers,\n * messaging) where user replies should be written back to the external service.\n */\n static readonly handleReplies?: boolean;\n\n // ---- Account identity (abstract — every connector must implement) ----\n\n /**\n * Returns a human-readable name for the connected account.\n * Shown in the connections list and edit modal to identify this connection.\n *\n * For OAuth connectors, this is typically the workspace or organization name\n * (e.g., \"Acme Corp\" for a Linear workspace). For API key connectors, this\n * could be the workspace name from the external service.\n *\n * Override this in your connector to return a meaningful account name.\n *\n * @param auth - The authorization (null for no-provider connectors)\n * @param token - The access token (null for no-provider connectors)\n * @returns Promise resolving to the account display name\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n getAccountName(\n auth: Authorization | null,\n token: AuthToken | null\n ): Promise<string | null> {\n return Promise.resolve(null);\n }\n\n // ---- Channel lifecycle (abstract — every connector must implement) ----\n\n /**\n * Returns available channels for the authorized actor.\n * Called after OAuth is complete, during the setup/edit modal.\n *\n * @param auth - The completed authorization with provider and actor info\n * @param token - The access token for making API calls\n * @returns Promise resolving to available channels for the user to select\n */\n abstract getChannels(\n auth: Authorization | null,\n token: AuthToken | null\n ): Promise<Channel[]>;\n\n /**\n * Called when a channel resource is enabled for syncing.\n *\n * The framework dispatches this in three cases:\n * 1. **Initial enable** — user toggled the channel on for the first time.\n * 2. **Auto-enable** — `setChannels` discovered a new channel on a\n * connection with `auto_enable_new_channels` set.\n * 3. **Recovery after re-auth** — the user re-authorized a previously-\n * broken connection. The framework calls `onChannelEnabled` for every\n * channel that was already enabled at the time of re-auth, with\n * `context.recovering = true`. See {@link SyncContext.recovering}.\n *\n * Implementations should be **idempotent and overwrite stored state**:\n * the same channel may receive multiple `onChannelEnabled` calls across\n * its lifetime. Use unconditional `this.set()` writes rather than\n * coalesce/skip-if-present logic so a recovery dispatch wipes stale\n * cursors and state from the prior session.\n *\n * **Sync state tracking is automatic.** The framework stamps the\n * connection as \"syncing\" when it dispatches this method and clears\n * that state when:\n * - the connector calls `tools.integrations.channelSyncCompleted(id)`\n * once the initial backfill is done, OR\n * - this method throws an unhandled exception (auto-cleared so the UI\n * doesn't get stuck in \"syncing\" forever).\n *\n * **IMPORTANT: This method runs inline in the HTTP request handler.**\n * Any long-running work (webhook setup, API calls, sync) MUST be queued\n * as a separate task via `this.runTask()`, not executed inline. Blocking\n * here causes the client to spin waiting for the response.\n *\n * Only lightweight operations should appear directly in this method:\n * `this.set()`, `this.get()`, `this.callback()`, and `this.runTask()`.\n *\n * @example\n * ```typescript\n * async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {\n * // Recovery: drop stale cursors so the next sync re-walks history.\n * if (context?.recovering) {\n * await this.clear(`last_sync_token_${channel.id}`);\n * }\n *\n * await this.set(`sync_state_${channel.id}`, { channelId: channel.id });\n *\n * // Queue sync as a task — do NOT use this.run() or call sync methods inline\n * const syncCallback = await this.callback(this.syncBatch, 1, \"full\", channel.id, true);\n * await this.runTask(syncCallback);\n *\n * // Queue webhook setup as a task — do NOT call setupWebhook() inline\n * const webhookCallback = await this.callback(this.setupWebhook, channel.id);\n * await this.runTask(webhookCallback);\n * }\n * ```\n *\n * @param channel - The channel that was enabled\n * @param context - Optional sync context (plan-based hints, recovery flag)\n */\n abstract onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void>;\n\n /**\n * Called when a channel resource is disabled.\n * Should stop sync, clean up webhooks, and remove state.\n *\n * @param channel - The channel that was disabled\n */\n abstract onChannelDisabled(channel: Channel): Promise<void>;\n\n // ---- Write-back hooks (optional, default no-ops) ----\n\n /**\n * Called when a link created by this connector is updated by the user.\n * Override to write back changes to the external service\n * (e.g., changing issue status in Linear when marked done in Plot).\n *\n * @param link - The updated link\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkUpdated(link: Link): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user creates a thread in Plot that should create a new\n * item in this connector's external system.\n *\n * A connector opts in to Plot-initiated creation by declaring a\n * `compose` block on the relevant `LinkTypeConfig` (see\n * {@link ComposeConfig}). When a user picks \"Create new <type>\" from the\n * Add link modal and the thread is synced, the runtime calls this method\n * with the draft fields.\n *\n * Implementations should create the item in the external service and\n * return a `NewLinkWithNotes` describing the created item. The platform\n * attaches the returned link to the originating thread — do not call\n * `integrations.saveLink` yourself.\n *\n * Returning `null` aborts creation silently (the thread is still saved\n * without a link).\n *\n * @param draft - The fields captured in Plot for the new item.\n * @returns The link to attach, or null to abort creation.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onCreateLink(draft: CreateLinkDraft): Promise<NewLinkWithNotes | null> {\n return Promise.resolve(null);\n }\n\n /**\n * Called when a note is created on a thread owned by this connector.\n * Override to write back comments to the external service\n * (e.g., adding a comment to a Linear issue).\n *\n * Returning a string or {@link NoteWriteBackResult} links the Plot note\n * to its external counterpart. A plain string sets the note's `key`.\n * A `NoteWriteBackResult` additionally sets a sync baseline (via\n * `externalContent`) so the next sync-in can recognize the round-tripped\n * content and preserve Plot's formatted version. See\n * {@link NoteWriteBackResult} for details.\n *\n * @param note - The created note\n * @param thread - The thread the note belongs to (includes thread.meta with connector-specific data)\n * @returns Optional note key or NoteWriteBackResult for external dedup + baseline tracking\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteCreated(note: Note, thread: Thread): Promise<string | NoteWriteBackResult | void> {\n return Promise.resolve();\n }\n\n /**\n * Resolve a `fileRef` action's bytes for download. Called when a user opens\n * an attachment in Plot. Return either a redirect URL (preferred for sources\n * that issue signed URLs, like Linear S3 or Slack permalink_public) or a\n * streamed body (required when bytes are only reachable through an\n * authenticated API call, like Gmail attachments.get).\n *\n * @param ref Opaque value the connector previously emitted on a fileRef action.\n * @returns Either `{ redirectUrl }` or `{ body, mimeType, fileName? }`.\n * @throws If the source is unavailable, the connection is broken, or `ref` is invalid.\n *\n * If not overridden, fileRef actions on this connector's notes will return 410 Gone.\n */\n async downloadAttachment(\n ref: string,\n ): Promise<\n | { redirectUrl: string }\n | { body: ReadableStream | Uint8Array; mimeType: string; fileName?: string }\n > {\n throw new Error(\n `downloadAttachment not implemented for ${this.constructor.name} (ref=${ref})`,\n );\n }\n\n /**\n * Called when a note on a thread owned by this connector is updated.\n * Override to write back changes to the external service\n * (e.g., syncing reaction tags as emoji reactions, or editing a comment\n * whose content changed in Plot).\n *\n * Return a {@link NoteWriteBackResult} with `externalContent` to update\n * the sync baseline after a successful write-back, so the next sync-in\n * recognizes the external version as already-seen and preserves Plot's\n * content.\n *\n * @param note - The updated note (includes current tags)\n * @param thread - The thread the note belongs to (includes thread.meta with connector-specific data)\n * @returns Optional NoteWriteBackResult for baseline tracking\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteUpdated(note: Note, thread: Thread): Promise<NoteWriteBackResult | void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user reads or unreads a thread owned by this connector.\n * Override to write back read status to the external service\n * (e.g., marking an email as read in Gmail).\n *\n * @param thread - The thread that was read/unread (includes thread.meta with connector-specific data)\n * @param actor - The user who performed the action\n * @param unread - false when marked as read, true when marked as unread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onThreadRead(thread: Thread, actor: Actor, unread: boolean): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user changes the **thread-level sharing** of a thread owned\n * by this connector — adding or removing a contact, or (for connectors with\n * roles) changing a contact's role. Override on connectors whose external\n * source can reflect that membership change, e.g. a group DM / multi-party\n * chat (`LinkTypeConfig.sharingModel: \"thread\"`) or an email's To/Cc/Bcc\n * recipients (`sharingModel: \"message\"`). Connectors backed by an immutable\n * roster (most group DMs today) or by channel-level membership\n * (`sharingModel: \"channel\"`) leave this as the default no-op.\n *\n * `role`/`from`/`to` are **null** for connectors without roles (group DMs\n * have no roles); they carry a `contactRoles` id only for connectors that\n * declare roles (e.g. email `to`/`cc`/`bcc`).\n *\n * The dispatch fires after Plot has persisted the change. A connector may\n * reflect it actively (e.g. add/remove a participant on the external chat)\n * or passively on the next outbound note (e.g. building To/Cc/Bcc headers\n * from the current `thread.contacts` × `thread.contactMeta`) — this callback\n * is not the right place to send a standalone notification.\n *\n * @param thread - The thread whose contacts changed\n * @param changes - The added/removed contacts and any role transitions on existing contacts\n */\n /* eslint-disable @typescript-eslint/no-unused-vars */\n onContactsChanged(\n thread: Thread,\n changes: {\n added: Array<{ contact: Contact; role: string | null }>;\n removed: Array<{ contact: Contact; role: string | null }>;\n changed: Array<{ contact: Contact; from: string | null; to: string | null }>;\n },\n ): Promise<void> {\n return Promise.resolve();\n }\n /* eslint-enable @typescript-eslint/no-unused-vars */\n\n /**\n * Called when a user marks or unmarks a thread as todo.\n * Override to sync todo status to the external service\n * (e.g., starring an email in Gmail when marked as todo).\n *\n * @param thread - The thread (includes thread.meta with connector-specific data)\n * @param actor - The user who changed the todo status\n * @param todo - true when marked as todo, false when done or removed\n * @param options - Additional context\n * @param options.date - The todo date (when todo=true)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onThreadToDo(thread: Thread, actor: Actor, todo: boolean, options: { date?: Date }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a schedule contact's RSVP status changes on a thread owned by this connector.\n * Override to sync RSVP changes back to the external calendar.\n *\n * @param thread - The thread (includes thread.meta with connector-specific data)\n * @param scheduleId - The schedule ID\n * @param contactId - The contact whose status changed\n * @param status - The new RSVP status ('attend', 'skip', or null)\n * @param actor - The user who changed the status\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onScheduleContactUpdated(thread: Thread, scheduleId: string, contactId: ActorId, status: ScheduleContactStatus | null, actor: Actor): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a user adds or removes a single emoji reaction on a note\n * (one event per `(note, actor, emoji)` state transition).\n *\n * Dispatch is routed to the reacting user's own connector instance via\n * `twist_instance_for_actor` on `note_reaction.actor_id`, so this method\n * already runs under the reactor's auth. Fetch the API client with the\n * connector's normal token-fetch path (`this.tools.integrations.get(...)`)\n * and the external write — e.g. Slack `reactions.add` — will be attributed\n * to the correct user. No `actAs` step required.\n *\n * If the reacting user has no connection of this type, no dispatch fires\n * for that reaction (it stays in Plot only).\n *\n * Override to sync per-actor reactions back to the external system.\n *\n * @param note - The note that was reacted on (partial; `id`, `key`, `content` populated)\n * @param thread - The thread the note belongs to (partial; `id`, `title`, `archived`, `meta` populated)\n * @param actor - The contact who added/removed the reaction\n * @param emoji - The emoji (Unicode grapheme or `provider:workspace/name` custom-emoji ref)\n * @param added - `true` if the reaction is now present, `false` if it was removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteReactionChanged(note: Note, thread: Thread, actor: Actor, emoji: string, added: boolean): Promise<void> {\n return Promise.resolve();\n }\n\n // ---- Activation ----\n\n /**\n * Called when the connector is activated after OAuth is complete.\n *\n * Connectors receive the authorization in addition to the activating actor.\n * When this runs, `this.userId` is already populated with the installing\n * user's ID.\n *\n * Default implementation does nothing. Override for custom setup.\n *\n * @param context - The activation context\n * @param context.auth - The completed OAuth authorization\n * @param context.actor - The actor who activated the connector\n */\n // @ts-ignore - Connector.activate() has a Connector-specific context type.\n activate(context: { auth?: Authorization; actor?: Actor }): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/** @deprecated Use `Connector` instead. */\nexport { Connector as Source };\n";
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "/**\n * Thread facets — heuristic, message-derived attributes used as internal\n * classifier signal (never user-facing). A connector emits the intrinsic\n * facets on the link it saves; the server stores them on the thread and the\n * focus classifier filters on them.\n *\n * Facets are best-effort: a connector sets a dimension only when a heuristic\n * is confident, leaving it `null` otherwise. The classifier never excludes a\n * thread on a `null` facet.\n *\n * `relationship` (a sender's relationship to the viewing user) is intentionally\n * NOT here: it is recipient-relative and evaluated live by the server, never\n * emitted by a connector.\n */\n\n/** The kind of content. Single-valued. */\nexport type Format =\n | \"chat\"\n | \"message\"\n | \"reading\"\n | \"notification\"\n | \"receipt\"\n | \"invoice\"\n | \"promotion\";\n\n/** Whether a person or a system produced the message. */\nexport type Automation = \"human\" | \"automated\";\n\n/** How the user was addressed. */\nexport type Reach = \"direct\" | \"list\";\n\n/**\n * Intrinsic facets a connector may set on a `NewLink`. Each is nullable —\n * omit (or set `null`) when no heuristic is confident.\n */\nexport type ThreadFacets = {\n format: Format | null;\n automation: Automation | null;\n reach: Reach | null;\n};\n";
8
+ export default "/**\n * Thread facets — heuristic, message-derived attributes used as internal\n * classifier signal (never user-facing). A connector emits the intrinsic\n * facets on the link it saves; the server stores them on the thread and the\n * focus classifier filters on them.\n *\n * Facets are best-effort: a connector sets a dimension only when a heuristic\n * is confident, leaving it `null` otherwise. The classifier never excludes a\n * thread on a `null` facet.\n *\n * `relationship` (a sender's relationship to the viewing user) is intentionally\n * NOT here: it is recipient-relative and evaluated live by the server, never\n * emitted by a connector.\n */\n\n/** The kind of content. Single-valued. */\nexport type Format =\n | \"chat\"\n | \"message\"\n | \"reading\"\n | \"notification\"\n | \"receipt\"\n | \"invoice\"\n | \"promotion\"\n | \"otp\" // time-sensitive one-time passcode\n | \"confirm\"; // confirm/verify-your-account call to action\n\n/** Whether a person or a system produced the message. */\nexport type Automation = \"human\" | \"automated\";\n\n/** How the user was addressed. */\nexport type Reach = \"direct\" | \"list\";\n\n/**\n * Intrinsic facets a connector may set on a `NewLink`. Each is nullable —\n * omit (or set `null`) when no heuristic is confident.\n */\nexport type ThreadFacets = {\n format: Format | null;\n automation: Automation | null;\n reach: Reach | null;\n};\n\n/**\n * A time-sensitive call-to-action extracted from a message (e.g. an OTP code\n * or a \"confirm your account\" link). Carried on a note so the client can show\n * an ephemeral prompt. Heuristic + best-effort; null when nothing confident is\n * found. Link (`url`) is only ever populated for DMARC-authenticated mail.\n */\nexport type CtaKind = \"otp\" | \"confirm\";\n\nexport type Cta = {\n kind: CtaKind;\n /** Display name of the originating service, e.g. \"Acme\". */\n service: string;\n /** The one-time code, for kind === \"otp\". Null otherwise. */\n code: string | null;\n /** The confirm/verify URL, for kind === \"confirm\". Null otherwise. DMARC-verified. */\n url: string | null;\n};\n";
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "import type { ThreadFacets } from \"./facets\";\nimport type { NewSchedule, NewScheduleOccurrence, Schedule } from \"./schedule\";\nimport { type Tag } from \"./tag\";\nimport { type Callback } from \"./tools/callbacks\";\nimport { type JSONValue } from \"./utils/types\";\nimport { Uuid } from \"./utils/uuid\";\n\nexport { Tag } from \"./tag\";\nexport { Uuid } from \"./utils/uuid\";\nexport { type JSONValue } from \"./utils/types\";\nexport { type AuthProvider } from \"./tools/integrations\";\n\n/**\n * @fileoverview\n * Core Plot entity types for working with threads, notes, focuses, and contacts.\n *\n * ## Type Pattern: Null vs Undefined Semantics\n *\n * Plot entity types use a consistent pattern to distinguish between missing, unset, and explicitly cleared values:\n *\n * ### Entity Types (Thread, Focus, Note, Actor)\n * - **Required fields**: No `?`, cannot be `undefined`\n * - Example: `id: Uuid`, `title: string`\n * - **Nullable fields**: Use `| null` to allow explicit clearing\n * - Example: `key: string | null`, `status: string | null`\n * - `null` = field is explicitly unset/cleared\n * - Non-null value = field has a value\n * - **Optional nullable fields**: Use `?` with `| null` for permission-based access\n * - Example: `email?: string | null`, `name?: string | null`\n * - `undefined` = field not included (e.g., no permission to access)\n * - `null` = field included but not set\n * - Value = field has a value\n *\n * ### New* Types (NewThread, NewNote, NewFocus)\n * Used for creating or updating entities. Support partial updates by distinguishing omitted vs cleared fields:\n * - **Required fields**: Must be provided (no `?`)\n * - Example: `title: string` in NewFocus\n * - **Optional fields**: Use `?` to make them optional\n * - Example: `title?: string`, `author?: NewActor`\n * - `undefined` (omitted) = don't set/update this field\n * - Provided value = set/update this field\n * - **Optional nullable fields**: Use `?` with `| null` to support clearing\n * - Example: `assignee?: NewActor | null`\n * - `undefined` (omitted) = don't change assignee\n * - `null` = clear the assignee\n * - NewActor = set/update the assignee\n *\n * This pattern allows API consumers to:\n * 1. Omit fields they don't want to change (undefined)\n * 2. Explicitly clear fields by setting to null\n * 3. Set or update fields by providing values\n *\n * @example\n * ```typescript\n * // Creating a new thread\n * const newThread: NewThread = {\n * title: \"Review pull request\",\n * };\n *\n * // Updating a thread - only change what's specified\n * const update: ThreadUpdate = {\n * id: threadId,\n * archived: true,\n * };\n * ```\n */\n\n/**\n * Represents a unique user, contact, or twist in Plot.\n *\n * ActorIds are used throughout Plot for:\n * - Thread and note authors, link assignees\n * - Tag creators (actor_id in thread_tag/note_tag)\n * - Mentions in notes\n * - Any entity that can perform actions in Plot\n */\nexport type ActorId = string & { readonly __brand: \"ActorId\" };\n\n/**\n * Theme colors for focuses.\n */\nexport enum ThemeColor {\n /** Catalyst - Green */\n Catalyst = 0,\n /** Call to Adventure - Blue */\n CallToAdventure = 1,\n /** Rising Action - Purple */\n RisingAction = 2,\n /** Momentum - Pink-Purple */\n Momentum = 3,\n /** Turning Point - Pink */\n TurningPoint = 4,\n /** Breakthrough - Orange */\n Breakthrough = 5,\n /** Climax - Olive */\n Climax = 6,\n /** Resolution - Blue-Gray */\n Resolution = 7,\n}\n\n/**\n * Represents a focus within Plot.\n *\n * A focus is similar to a project or area-of-life. Every thread is in a focus.\n * Focuses are flat — they have no parent and no children. Threads not matched\n * to any focus live in the Inbox.\n */\nexport type Focus = {\n /** Unique identifier for the focus */\n id: Uuid;\n /** Human-readable title for the focus */\n title: string;\n /** Whether this focus has been archived */\n archived: boolean;\n /**\n * Optional key for referencing this focus.\n * Keys are unique per user.\n */\n key: string | null;\n /** Optional theme color for the focus (0-7). Defaults to 7 (Resolution) when not set. */\n color: ThemeColor | null;\n /** Optional icon for the focus (a curated icon key). Defaults to the focus icon when not set. */\n icon: string | null;\n};\n\n/**\n * Type for creating new focuses.\n *\n * Supports multiple creation patterns:\n * - Provide a specific UUID for the focus\n * - Provide a key for upsert within the user's focuses\n * - Omit both to auto-generate a new UUID\n */\nexport type NewFocus = Pick<Focus, \"title\"> &\n Partial<Omit<Focus, \"id\" | \"title\">> &\n (\n | {\n /**\n * Unique identifier for the focus, generated by Uuid.Generate().\n * Specifying an ID allows tools to track and upsert focuses.\n */\n id: Uuid;\n }\n | {\n /**\n * Unique key for the focus within the user's focuses.\n * Can be used to upsert without knowing the UUID.\n * For example, \"@plot\" identifies the Plot focus.\n */\n key: string;\n }\n | {\n /* Neither id nor key is required. An id will be generated and returned. */\n }\n );\n\n/**\n * Type for updating existing focuses.\n * Must provide either id or key to identify the focus to update.\n */\nexport type FocusUpdate = ({ id: Uuid } | { key: string }) &\n Partial<Pick<Focus, \"title\" | \"archived\">>;\n\n/**\n * Enumeration of supported action types.\n *\n * Different action types have different behaviors when clicked by users\n * and may require different rendering approaches.\n */\nexport enum ActionType {\n /** External web links that open in browser */\n external = \"external\",\n /** Authentication flows for connecting services */\n auth = \"auth\",\n /** Callback actions that trigger twist methods when clicked */\n callback = \"callback\",\n /** Video conferencing links with provider-specific handling */\n conferencing = \"conferencing\",\n /** File attachment links stored in R2 */\n file = \"file\",\n /** Reference to an attachment hosted by a connector's source system */\n fileRef = \"fileRef\",\n /** Thread reference links for navigating to related threads */\n thread = \"thread\",\n /** Structured plan of operations for user approval */\n plan = \"plan\",\n}\n\n/**\n * Video conferencing providers for conferencing links.\n *\n * Used to identify the conferencing platform and provide\n * provider-specific UI elements (titles, icons, etc.).\n */\nexport enum ConferencingProvider {\n /** Google Meet */\n googleMeet = \"googleMeet\",\n /** Zoom */\n zoom = \"zoom\",\n /** Microsoft Teams */\n microsoftTeams = \"microsoftTeams\",\n /** Cisco Webex */\n webex = \"webex\",\n /** Other or unknown conferencing provider */\n other = \"other\",\n}\n\n/**\n * Represents a clickable action attached to a thread.\n *\n * Thread actions are rendered as buttons that enable user interaction with threads.\n * Different action types have specific behaviors and required fields for proper functionality.\n *\n * @example\n * ```typescript\n * // External action - opens URL in browser\n * const externalAction: Action = {\n * type: ActionType.external,\n * title: \"Open in Google Calendar\",\n * url: \"https://calendar.google.com/event/123\",\n * };\n *\n * // Conferencing action - opens video conference with provider info\n * const conferencingAction: Action = {\n * type: ActionType.conferencing,\n * url: \"https://meet.google.com/abc-defg-hij\",\n * provider: ConferencingProvider.googleMeet,\n * };\n *\n * // Integrations action - initiates OAuth flow\n * const authAction: Action = {\n * type: ActionType.auth,\n * title: \"Continue with Google\",\n * provider: AuthProvider.Google,\n * scopes: [\"https://www.googleapis.com/auth/calendar.readonly\"],\n * callback: \"callback-token-for-auth-completion\"\n * };\n *\n * // Callback action - triggers a twist method\n * const callbackAction: Action = {\n * type: ActionType.callback,\n * title: \"📅 Primary Calendar\",\n * token: \"callback-token-here\"\n * };\n * ```\n */\nexport type Action =\n | {\n /** External web link that opens in browser */\n type: ActionType.external;\n /** Display text for the action button */\n title: string;\n /** URL to open when clicked */\n url: string;\n }\n | {\n /** Video conferencing action with provider-specific handling */\n type: ActionType.conferencing;\n /** URL to join the conference */\n url: string;\n /** Conferencing provider for UI customization */\n provider: ConferencingProvider;\n }\n | {\n /** Authentication action that initiates an OAuth flow */\n type: ActionType.auth;\n /** Display text for the auth button */\n title: string;\n /** OAuth provider (e.g., \"google\", \"microsoft\") */\n provider: string;\n /** Array of OAuth scopes to request */\n scopes: string[];\n /** Callback token for auth completion notification */\n callback: Callback;\n }\n | {\n /** Callback action that triggers a twist method when clicked */\n type: ActionType.callback;\n /** Display text for the callback button */\n title: string;\n /** Token identifying the callback to execute */\n callback: Callback;\n }\n | {\n /** File attachment action stored in R2 */\n type: ActionType.file;\n /** Unique identifier for the stored file */\n fileId: string;\n /** Original filename */\n fileName: string;\n /** File size in bytes */\n fileSize: number;\n /** MIME type of the file */\n mimeType: string;\n /** Intrinsic width of the image in pixels (only for image files) */\n imageWidth?: number | null;\n /** Intrinsic height of the image in pixels (only for image files) */\n imageHeight?: number | null;\n }\n | {\n /** Reference to an attachment hosted by a connector's source system */\n type: ActionType.fileRef;\n /** Opaque identifier interpreted only by the owning connector */\n ref: string;\n /** Display filename */\n fileName: string;\n /** File size in bytes if known */\n fileSize: number | null;\n /** MIME type */\n mimeType: string;\n /** Intrinsic width of the image in pixels (only for image files) */\n imageWidth?: number | null;\n /** Intrinsic height of the image in pixels (only for image files) */\n imageHeight?: number | null;\n }\n | {\n /** Thread reference action for navigating to a related thread */\n type: ActionType.thread;\n /** UUID of the referenced thread */\n threadId: Uuid;\n }\n | {\n /** Structured plan of operations for user approval */\n type: ActionType.plan;\n /** Human-readable summary of the plan */\n title: string;\n /** Operations to execute on approval */\n operations: PlanOperation[];\n /** Callback invoked with (action, approved: boolean) */\n callback: Callback;\n };\n\n/**\n * Represents metadata about a thread, typically from an external system.\n *\n * Thread metadata enables storing additional information about threads,\n * which is useful for synchronization, linking back to external systems,\n * and storing tool-specific data.\n *\n * Must be valid JSON data (strings, numbers, booleans, null, objects, arrays).\n * Functions and other non-JSON values are not supported.\n *\n * @example\n * ```typescript\n * // Calendar event metadata\n * await plot.createThread({\n * title: \"Team Meeting\",\n * meta: {\n * calendarId: \"primary\",\n * htmlLink: \"https://calendar.google.com/event/abc123\",\n * conferenceData: { ... }\n * }\n * });\n *\n * // Project issue metadata\n * await plot.createThread({\n * title: \"Fix login bug\",\n * meta: {\n * projectId: \"TEAM\",\n * issueNumber: 123,\n * url: \"https://linear.app/team/issue/TEAM-123\"\n * }\n * });\n * ```\n */\nexport type ThreadMeta = {\n /** Source-specific properties and metadata */\n [key: string]: JSONValue;\n};\n\n/**\n * Thread sub-type that determines the thread's icon and category.\n * Available types depend on whether the focus is shared:\n * - Private focuses: \"action\" (default for tasks), \"notes\" (default), \"idea\", \"goal\", \"decision\"\n * - Shared focuses: all above plus \"discussion\" (default), \"announcement\", \"ask\"\n */\nexport type ThreadType =\n | \"action\"\n | \"notes\"\n | \"idea\"\n | \"goal\"\n | \"decision\"\n | \"discussion\"\n | \"announcement\"\n | \"ask\";\n\n/**\n * Tags on an item, along with the actors who added each tag.\n */\nexport type Tags = { [K in Tag]?: ActorId[] };\n\n/**\n * A set of tags to add to an item, along with the actors adding each tag.\n */\nexport type NewTags = { [K in Tag]?: NewActor[] };\n\n/**\n * A single emoji reaction key. Either:\n * - A Unicode emoji grapheme cluster (e.g. `\"👍\"`, `\"👨‍👩‍👧\"`), or\n * - A provider-scoped custom-emoji ref of the form\n * `\"<provider>:<workspaceId>/<name>\"` (e.g. `\"slack:T0123/party_parrot\"`).\n *\n * Anything matching a known provider prefix is treated as a custom-emoji\n * reference; everything else is rendered as the Unicode it contains.\n *\n * Reactions are the open-set replacement for {@link Tag}'s retired count\n * range (`1000+`). Use reactions for emoji that round-trip with chat platforms;\n * use tags for Plot-managed compute/toggle state (todo, pinned, urgent,\n * ...).\n */\nexport type Reaction = string;\n\n/**\n * Emoji reactions on an item, keyed by emoji string, with the list of\n * actors who added each reaction.\n */\nexport type Reactions = Record<Reaction, ActorId[]>;\n\n/**\n * A set of reactions to add to an item, along with the actors adding each\n * reaction. To remove a reaction for a given actor, omit them from the\n * `NewActor[]` list — passing an empty list removes the reaction entirely.\n */\nexport type NewReactions = Record<Reaction, NewActor[]>;\n\n/**\n * Thread access level determining visibility.\n * - \"public\": Visible to all users with focus access\n * - \"members\": Visible to focus members (default for shared focuses)\n * - \"private\": Visible only to creator and contacts listed in accessContacts\n */\nexport type ThreadAccessLevel = \"public\" | \"members\" | \"private\";\n\n/**\n * Common fields shared by both Thread and Note entities.\n */\nexport type ThreadCommon = {\n /** Unique identifier for the thread */\n id: Uuid;\n /**\n * When this item was created.\n *\n * **For sources:** Set this to the external system's timestamp (e.g., email\n * sent date, comment creation date), NOT the sync time. If omitted, defaults\n * to the current time, which is almost never correct for synced data.\n */\n created: Date;\n /** Whether this thread has been archived */\n archived: boolean;\n /** Tags attached to this thread. Maps tag ID to array of actor IDs who added that tag. */\n tags: Tags;\n /**\n * Emoji reactions on this item. Maps each emoji (Unicode grapheme or\n * `provider:workspace/name` custom-emoji ref) to the list of actor IDs\n * who reacted with it.\n */\n reactions: Reactions;\n};\n\n/**\n * Fields on a Thread entity.\n * Threads are simple containers for links and notes.\n */\ntype ThreadFields = ThreadCommon & {\n /** The display title/summary of the thread */\n title: string;\n /** The focus context this thread belongs to */\n focus: Focus;\n /** The thread's sub-type/category. Determines the displayed icon. */\n type: ThreadType | null;\n /** Thread access level: \"public\", \"members\", or \"private\" */\n access: ThreadAccessLevel;\n /** Contacts who can see a private thread (empty array for creator-only). Only meaningful when access is \"private\". */\n accessContacts: Contact[];\n /** The schedule associated with this thread, if any */\n schedule?: Schedule;\n /** Source-specific metadata from the thread's link, populated on callbacks */\n meta?: ThreadMeta;\n};\n\nexport type Thread = ThreadFields;\n\nexport type ThreadWithNotes = Thread & {\n notes: Note[];\n};\n\nexport type NewThreadWithNotes = NewThread & {\n notes: Omit<NewNote, \"thread\">[];\n};\n\n/**\n * Type for creating new threads.\n *\n * Threads are simple containers. All other fields are optional.\n *\n * @example\n * ```typescript\n * const thread: NewThread = {\n * title: \"Review pull request\"\n * };\n * ```\n */\nexport type NewThread = Partial<\n Omit<ThreadFields, \"focus\" | \"tags\" | \"reactions\" | \"id\" | \"accessContacts\">\n> &\n (\n | {\n /** Unique identifier for the thread, generated by Uuid.Generate(). */\n id: Uuid;\n }\n | {\n /* id is optional. An id will be generated and returned. */\n }\n ) &\n {\n /** Explicit focus - disables automatic focus matching. When omitted, the server classifies the thread using the user's focus rules. */\n focus?: Pick<Focus, \"id\">;\n } & {\n /**\n * All tags to set on the new thread.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to set on the new thread.\n * Each emoji maps to the list of actors who reacted with it.\n */\n reactions?: NewReactions;\n\n /**\n * The thread's sub-type/category. Sets the thread's icon.\n * If omitted, defaults to \"notes\" (private) or \"discussion\" (shared).\n */\n type?: ThreadType;\n\n /**\n * Contacts who can see a private thread.\n * Pass email-based NewContact objects; they are resolved to contact IDs by the API.\n * If omitted for a private thread, defaults to the connection owner.\n */\n accessContacts?: NewContact[];\n\n /**\n * Whether the thread should be marked as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - true: Thread is explicitly unread for ALL users (use sparingly)\n * - false: Thread is marked as read for all users in the focus at creation time\n */\n unread?: boolean;\n\n /**\n * Whether the thread is archived.\n * - true: Archive the thread\n * - false: Unarchive the thread\n * - undefined (default): Preserve current archive state\n */\n archived?: boolean;\n\n /**\n * Optional preview content for the thread. Can be Markdown formatted.\n * The preview will be automatically generated from this content (truncated to 100 chars).\n */\n preview?: string | null;\n\n /**\n * Optional schedules to create alongside the thread.\n */\n schedules?: Array<Omit<NewSchedule, \"threadId\">>;\n\n /**\n * Optional schedule occurrence overrides.\n */\n scheduleOccurrences?: NewScheduleOccurrence[];\n };\n\nexport type ThreadFilter = {\n meta?: {\n [key: string]: JSONValue;\n };\n};\n\n/**\n * Fields supported by bulk updates via `match`. Only simple scalar fields\n * that can be applied uniformly across many threads are included.\n */\ntype ThreadBulkUpdateFields = Partial<\n Pick<ThreadFields, \"title\" | \"access\" | \"archived\">\n> & {\n /** Contacts who can see a private thread. Pass NewContact objects (email-based); resolved by the API. */\n accessContacts?: NewContact[];\n};\n\n/**\n * Fields supported by single-thread updates via `id` or `source`.\n * Includes all bulk fields plus tags and preview.\n */\ntype ThreadSingleUpdateFields = ThreadBulkUpdateFields & {\n /**\n * Tags to change on the thread. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to change on the thread. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Add or remove the twist's tags.\n * Maps tag ID to boolean: true = add tag, false = remove tag.\n * This is allowed on all threads the twist has access to.\n */\n twistTags?: Partial<Record<Tag, boolean>>;\n\n /**\n * Update the thread's sub-type/category.\n */\n type?: ThreadType;\n\n /**\n * Optional preview content for the thread. Can be Markdown formatted.\n * The preview will be automatically generated from this content (truncated to 100 chars).\n *\n * - string: Use this content for preview generation\n * - null: Explicitly disable preview (no preview will be shown)\n * - undefined (omitted): Preserve current preview value\n *\n * This field is write-only and won't be returned when reading threads.\n */\n preview?: string | null;\n\n /**\n * Move the thread to a different focus. Requires ThreadAccess.Full.\n * The target focus must be owned by the twist's user.\n */\n focus?: Pick<Focus, \"id\">;\n};\n\nexport type ThreadUpdate =\n | (({ id: Uuid } | { source: string }) & ThreadSingleUpdateFields)\n | ({\n /**\n * Update all threads matching the specified criteria. Only threads\n * that match all provided fields and were created by the twist will be updated.\n */\n match: ThreadFilter;\n } & ThreadBulkUpdateFields);\n\n/**\n * Represents a note within a thread.\n *\n * Notes contain the detailed content (note text, actions) associated with a thread.\n * They are always ordered by creation time within their parent thread.\n */\nexport type Note = ThreadCommon & {\n /** The author of this note */\n author: Actor;\n /**\n * Globally unique, stable identifier for the note within its thread + link.\n * Can be used to upsert without knowing the id.\n *\n * Note keys are scoped to a `(thread, link)` pair — two links on the same\n * thread (e.g. after a merge) can each carry a `\"description\"` note without\n * colliding. The runtime infers the link from the surrounding `saveLink`\n * call. For bare `saveNote` calls outside a `saveLink`, the runtime\n * resolves the link by looking up the connector's links on the thread\n * and errors if more than one matches.\n *\n * Use one of these patterns:\n * - Hardcoded semantic keys for fixed note types: \"description\", \"cancellation\"\n * - External service IDs for dynamic collections: `comment:${immutableId}`\n *\n * Examples:\n * - `\"description\"` (for a Jira issue's description note)\n * - `\"comment:12345\"` (for a specific comment by ID)\n * - `\"gmail:msg:18d4e5f2a3b1c9d7\"` (for a Gmail message within a thread)\n *\n * Ensure IDs are immutable - avoid human-readable slugs or titles.\n */\n key: string | null;\n /** The parent thread this note belongs to */\n thread: Thread;\n /** Primary content for the note (markdown) */\n content: string | null;\n /** Array of interactive actions attached to the note */\n actions: Array<Action> | null;\n /** The note this is a reply to, or null if not a reply */\n reNote: { id: Uuid } | null;\n /**\n * Contacts who can see this note, or null if the note inherits thread visibility.\n * When set (even to []), the note is private to the listed contacts plus the creator.\n */\n accessContacts: ActorId[] | null;\n /** Focus twist IDs (twists/connectors) mentioned for dispatch routing. Does not include user contacts. */\n mentions: ActorId[];\n};\n\n/**\n * Type for creating new notes.\n *\n * Requires the thread reference, with all other fields optional.\n * Can provide id, key, or neither for note identification:\n * - id: Provide a specific UUID for the note\n * - key: Provide an external identifier for upsert within the thread\n * - neither: A new note with auto-generated UUID will be created\n */\nexport type NewNote = Partial<\n Omit<\n Note,\n \"author\" | \"thread\" | \"tags\" | \"reactions\" | \"mentions\" | \"accessContacts\" | \"id\" | \"key\" | \"reNote\"\n >\n> &\n ({ id: Uuid } | { key: string } | {}) & {\n /** Reference to the parent thread (required) */\n thread:\n | Pick<Thread, \"id\">\n | {\n source: string;\n };\n\n /**\n * The person that created the item, or leave undefined to use the twist as author.\n */\n author?: NewActor;\n\n /**\n * Format of the note content. Determines how the note is processed:\n * - 'text': Plain text that will be converted to markdown (auto-links URLs, preserves line breaks)\n * - 'markdown': Already in markdown format (default, no conversion)\n * - 'html': HTML content that will be converted to markdown\n */\n contentType?: ContentType;\n\n /**\n * Tags to change on the thread. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to set on the note. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Contacts who can see this note, or null/undefined to inherit thread visibility.\n * Accepts resolved ActorId UUIDs or email-based NewContact objects (resolved server-side).\n * Include all participants who should see the note (sender + recipients).\n * The note author is NOT implicitly included — add them explicitly.\n * When set (even to []), the note is private to the listed contacts plus the creator.\n */\n accessContacts?: (ActorId | NewContact)[] | null;\n\n /**\n * Twist/connector IDs to mention for dispatch routing.\n * Does not include user contacts — use accessContacts for visibility.\n */\n mentions?: NewActor[];\n\n /**\n * Whether the note should mark the parent thread as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - true: Thread is explicitly unread for ALL users (use sparingly)\n * - false: Thread is marked as read for all users in the focus at note creation time\n *\n * For the default behavior, omit this field entirely.\n * Use false for initial sync to avoid marking historical items as unread.\n */\n unread?: boolean;\n\n /**\n * When true, the server will use AI to detect tasks in this note's content\n * and create separate Plot-authored reply notes for each detected task.\n * Use for messaging connectors (email, chat) where tasks are implicit\n * in conversation rather than explicitly structured.\n */\n checkForTasks?: boolean;\n\n /**\n * Reference to a parent note this note is a reply to.\n * - `{ id }`: reply by UUID\n * - `{ key }`: reply by key, resolved after creation (for batch ops)\n * - `null`: explicitly not a reply\n * - `undefined` (omitted): not a reply\n */\n reNote?: { id: Uuid } | { key: string } | null;\n\n /**\n * A link carried by this note (note-attached, NOT a thread-level canonical\n * link). Use for augmenter content (e.g. Granola meeting notes) that should\n * attach to an existing canonical thread without becoming its primary link.\n * The runtime creates the link note-scoped, binds `note.link_id` to it, and\n * — when `thread: { source }` resolves to no existing thread — find-or-creates\n * the thread by that source so a later canonical sync can fill the primary.\n */\n link?: NewLink;\n };\n\n/**\n * Type for updating existing notes.\n * Must provide either id or key to identify the note to update.\n */\nexport type NoteUpdate = ({ id: Uuid; key?: string } | { key: string }) &\n Partial<\n Pick<Note, \"accessContacts\" | \"archived\" | \"content\" | \"actions\" | \"reNote\">\n > & {\n /**\n * Format of the note content. Determines how the note is processed:\n * - 'text': Plain text that will be converted to markdown (auto-links URLs, preserves line breaks)\n * - 'markdown': Already in markdown format (default, no conversion)\n * - 'html': HTML content that will be converted to markdown\n */\n contentType?: ContentType;\n\n /**\n * Tags to change on the note. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to change on the note. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Add or remove the twist's tags.\n * Maps tag ID to boolean: true = add tag, false = remove tag.\n * This is allowed on all notes the twist has access to.\n */\n twistTags?: Partial<Record<Tag, boolean>>;\n\n /**\n * Twist/connector IDs to mention for dispatch routing.\n * Does not include user contacts — use accessContacts for visibility.\n */\n mentions?: NewActor[];\n };\n\n/**\n * Represents an actor in Plot - a user, contact, or twist.\n *\n * Actors can be associated with threads as authors, assignees, or mentions.\n * The email field is only included when ContactAccess.Read permission is granted.\n *\n * @example\n * ```typescript\n * const actor: Actor = {\n * id: \"f0ffd5f8-1635-4b13-9532-35f97446db90\" as ActorId,\n * type: ActorType.Contact,\n * email: \"john.doe@example.com\", // Only if ContactAccess.Read\n * name: \"John Doe\"\n * };\n * ```\n */\nexport type Actor = {\n /** Unique identifier for the actor */\n id: ActorId;\n /** Type of actor (User, Contact, or Twist) */\n type: ActorType;\n /**\n * Email address (only included with ContactAccess.Read permission).\n * - `undefined`: No permission to read email\n * - `null`: Permission granted but email not set\n * - `string`: Email address\n */\n email?: string | null;\n /**\n * Display name.\n * - `undefined`: Not included due to permissions\n * - `null`: Not set\n * - `string`: Display name\n */\n name?: string | null;\n};\n\n/**\n * A resolved contact with identity info. Used for access control lists\n * where only human contacts (not twists) are relevant.\n */\nexport type Contact = {\n /** Unique identifier for the contact */\n id: ActorId;\n /** Email address, or null if not set */\n email: string | null;\n /** Display name, or null if not set */\n name: string | null;\n};\n\n/**\n * An existing or new contact.\n */\nexport type NewActor =\n | {\n /** Unique identifier for the actor */\n id: ActorId;\n }\n | NewContact;\n\n/**\n * Enumeration of author types that can create threads.\n *\n * The author type affects how threads are displayed and processed\n * within the Plot system.\n */\nexport enum ActorType {\n /** Threads created by human users */\n User,\n /** Threads created by external contacts */\n Contact,\n /** Threads created by automated twists */\n Twist,\n}\n\n/**\n * Represents contact information for creating a new contact.\n *\n * Contacts are used throughout Plot for representing people associated\n * with threads, such as event attendees or task assignees.\n *\n * @example\n * ```typescript\n * const newContact: NewContact = {\n * email: \"john.doe@example.com\",\n * name: \"John Doe\",\n * avatar: \"https://avatar.example.com/john.jpg\"\n * };\n * ```\n */\n/**\n * Common fields shared by all NewContact variants.\n */\ntype NewContactBase = {\n /** Optional avatar image URL for the contact */\n avatar?: string;\n /**\n * External provider account source. Used for identity resolution\n * when email is unavailable and for privacy compliance reporting.\n *\n * The runtime scopes the resulting `contact_external_account` row to\n * the dispatching twist instance (i.e. one row per connection per\n * contact), so the same Plot contact can have multiple rows when\n * reachable through multiple connections (e.g. two Slack workspaces,\n * Gmail + Google Chat sharing one Google account).\n */\n source?: { accountId: string };\n /**\n * Optional connector-defined role for this contact on the thread, matching\n * a `LinkTypeConfig.contactRoles[].id` (e.g. \"to\" / \"cc\" / \"bcc\" for\n * email, \"required\" / \"optional\" for calendar). Omitted ⇒ default role.\n * Connectors set this on inbound sync; the runtime persists it under\n * `thread.contact_meta[contact_id].role`.\n */\n role?: string;\n};\n\n/**\n * At least one of `email` or `name` must be provided so the contact can be\n * identified in the UI. Contacts with neither would display as \"Unknown\".\n */\nexport type NewContact = NewContactBase &\n ({ email: string; name?: string } | { email?: string; name: string });\n\nexport type ContentType = \"text\" | \"markdown\" | \"html\";\n\n/**\n * Represents an external entity linked to a thread.\n *\n * Links are created by sources to represent external entities (issues, emails, calendar events)\n * attached to a thread container. A thread can have multiple links (1:many).\n * Links store source-specific data like type, status, metadata, and embeddings.\n *\n * @example\n * ```typescript\n * // A link representing a Linear issue\n * const link: Link = {\n * threadId: \"...\" as Uuid,\n * source: \"linear:issue:549dd8bd-2bc9-43d1-95d5-4b4af0c5af1b\",\n * created: new Date(),\n * author: { id: \"...\" as ActorId, type: ActorType.Contact, name: \"Alice\" },\n * title: \"Fix login bug\",\n * type: \"issue\",\n * status: \"open\",\n * meta: { projectId: \"TEAM\", url: \"https://linear.app/team/TEAM-123\" },\n * assignee: null,\n * actions: null,\n * };\n * ```\n */\nexport type Link = {\n /** The thread this link belongs to */\n threadId: Uuid;\n /** External source identifier for dedup/upsert */\n source: string | null;\n /** When this link was originally created in its source system */\n created: Date;\n /** The actor credited with creating this link */\n author: Actor | null;\n /** Display title */\n title: string;\n /** Truncated preview */\n preview: string | null;\n /** The actor assigned to this link */\n assignee: Actor | null;\n /** Source-defined type string (e.g., issue, pull_request, email, event) */\n type: string | null;\n /** Source-defined status string (e.g., open, done, closed) */\n status: string | null;\n /** Interactive action buttons */\n actions: Array<Action> | null;\n /** Source metadata */\n meta: ThreadMeta | null;\n /** URL to open the original item in its source application (e.g., \"Open in Linear\") */\n sourceUrl: string | null;\n /** Channel ID that produced this link (matches source_channel.channel_id) */\n channelId: string | null;\n /**\n * Cross-connector thread bundling key.\n *\n * @deprecated Use `sources` instead. Reads return the first element of\n * `sources` for backward compatibility; new writes should populate `sources`.\n */\n relatedSource: string | null;\n /**\n * Canonical identifiers for this link. Two links whose `sources` arrays\n * overlap share the same thread (array overlap, `sources && new.sources`).\n *\n * Use this to bundle with another connector via a canonical alias. For\n * example, every calendar connector emits `icaluid:<iCalUID>` so any\n * meeting-notes connector can bundle by setting the same alias.\n */\n sources: string[];\n /**\n * Connector-supplied ranking used by clients to choose the single displayed\n * (primary) canonical link when a thread has more than one. Higher wins;\n * ties break on earliest creation. Default 0.\n */\n priority: number;\n};\n\n/**\n * Type for creating new links.\n *\n * Links are created by sources to represent external entities.\n * Requires a source identifier for dedup/upsert.\n */\nexport type NewLink = Partial<\n Omit<Link, \"author\" | \"assignee\" | \"threadId\">\n> & {\n /**\n * Canonical ID for the item in an external system.\n * When set, uniquely identifies the link for the user. This performs\n * an upsert.\n *\n * @deprecated Pass `sources: [...]` instead. Both fields can be set during\n * the transition; the runtime will normalize.\n */\n source?: string;\n /**\n * Canonical identifiers for this item. Any element shared with another\n * link's `sources` bundles the two links into the same thread. Used for\n * cross-connector bundling — e.g. a meeting-notes connector setting\n * `[\"granola:<id>\", \"icaluid:<uid>\"]` to attach onto a calendar event\n * thread that includes `icaluid:<uid>` in its own `sources`.\n */\n sources?: string[];\n /**\n * Heuristic facets describing this item (format / automation / reach),\n * used as internal classifier signal. Omit a dimension (or set null) when\n * no heuristic is confident. See `@plotday/twister/facets`.\n */\n facets?: ThreadFacets;\n /** The person that created the item. By default, it will be the twist itself. */\n author?: NewActor;\n /** The person assigned to the item. */\n assignee?: NewActor | null;\n /**\n * Thread access level: \"public\", \"members\" (default), or \"private\".\n * When \"private\", thread visibility is limited to the creator and contacts in accessContacts.\n */\n access?: ThreadAccessLevel;\n /**\n * Contacts who can see a private thread.\n * Pass email-based NewContact objects; they are resolved to contact IDs by the API.\n * If omitted for a private thread, defaults to the connection owner.\n */\n accessContacts?: NewContact[];\n /**\n * Whether the thread should be marked as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - false: Thread is marked as read for all users in the focus at creation time\n */\n unread?: boolean;\n /**\n * Whether the thread is archived.\n * - true: Archive the thread\n * - false: Unarchive the thread\n * - undefined (default): Preserve current archive state\n */\n archived?: boolean;\n /**\n * Mark the thread as the connection owner's to-do at create time.\n * - true: the thread is added to the owner's to-do (active) bucket and\n * their per-user archive is lifted, atomically with the save — no\n * separate `integrations.setThreadToDo()` round-trip needed.\n * - false: the owner's thread_state is marked read (cleared from to-do).\n * - undefined (omitted, default): leave to-do state untouched.\n *\n * Use for messaging-style \"saved for later\" flags (e.g. a starred Slack\n * thread). This is the first-class replacement for overloading a\n * `statuses[]` entry with `active: true`.\n */\n todo?: boolean;\n /**\n * The to-do date used when `todo` is true. Defaults to the \"Now\"\n * sentinel (today's bucket) when omitted. Ignored when `todo` is not true.\n */\n todoDate?: Date | string;\n /**\n * Explicit focus (disables automatic focus matching).\n * Only used when the link creates a new thread. When omitted, the\n * server classifies the thread using the user's focus rules.\n */\n focus?: Pick<Focus, \"id\">;\n /**\n * Primary-link ranking for this canonical link (default 0). Set higher on\n * the connection that \"owns\" the external item (e.g. the calendar that owns\n * an event vs a subscribed copy) so clients display it as the primary.\n */\n priority?: number;\n };\n\n/**\n * A new link with notes to save via integrations.saveLink().\n * Creates a thread+link pair, with notes attached to the thread.\n */\nexport type NewLinkWithNotes = NewLink & {\n /**\n * Title for the link and its thread container.\n * Must be the real entity title (e.g. issue title, message subject),\n * never a placeholder or ID. This value overwrites the existing title on upsert.\n * Omit to preserve the existing title (e.g. for cancelled events where the\n * title may not be available in the webhook payload).\n */\n title?: string;\n /** Notes to attach to the thread */\n notes?: Omit<NewNote, \"thread\">[];\n /** Schedules to create for the link */\n schedules?: Array<Omit<NewSchedule, \"threadId\">>;\n /** Schedule occurrence overrides */\n scheduleOccurrences?: NewScheduleOccurrence[];\n /**\n * For `onCreateLink` only: binds the thread's opening note (the message the\n * user composed in Plot, which this hook just posted to the external system)\n * to its external counterpart. Mirrors the `NoteWriteBackResult` a reply\n * returns from `onNoteCreated` — `key` is the external message id and\n * `externalContent` is the post-write content baseline. Without this the\n * opening note stays keyless, so reactions and edits on it can't be routed\n * back to the external system. Ignored outside `onCreateLink`.\n */\n originatingNote?: {\n /** External message id; set as the opening note's `key`. */\n key?: string;\n /**\n * Content as the external system stored it post-write, for the sync\n * baseline. Must equal what your sync-in path emits as this note's\n * `content` on re-ingest (same contract as `NoteWriteBackResult.externalContent`).\n */\n externalContent?: string;\n };\n};\n\n/**\n * Type for updating existing links.\n *\n * Set `threadId` to move the link to a different thread.\n * Requires LinkAccess.Full.\n */\nexport type LinkUpdate = { id: Uuid } & {\n /** Move the link to a different thread owned by the twist's user. */\n threadId?: Uuid;\n};\n\n/**\n * A single operation within a plan submitted for user approval.\n *\n * Operations include display metadata (titles) so the app can render\n * a human-readable summary without additional lookups.\n */\nexport type PlanOperation =\n | {\n type: \"updateThread\";\n threadId: Uuid;\n /** Current thread title for display */\n threadTitle: string;\n changes: Partial<Pick<ThreadFields, \"archived\" | \"title\" | \"type\">> & {\n /** Move to this focus */\n focus?: { id: Uuid; title: string };\n };\n }\n | {\n type: \"updateLink\";\n linkId: Uuid;\n /** Current link title for display */\n linkTitle: string;\n changes: {\n /** Move to this thread */\n threadId?: Uuid;\n threadTitle?: string;\n };\n }\n | {\n type: \"createThread\";\n title: string;\n focusId: Uuid;\n /** Focus title for display */\n focusTitle: string;\n }\n | {\n type: \"createNote\";\n threadId: Uuid;\n /** Thread title for display */\n threadTitle: string;\n content: string;\n }\n | {\n type: \"updateFocus\";\n focusId: Uuid;\n /** Current focus title for display */\n focusTitle: string;\n changes: Partial<Pick<Focus, \"title\" | \"archived\">>;\n };\n";
8
+ export default "import type { Cta, ThreadFacets } from \"./facets\";\nimport type { NewSchedule, NewScheduleOccurrence, Schedule } from \"./schedule\";\nimport { type Tag } from \"./tag\";\nimport { type Callback } from \"./tools/callbacks\";\nimport { type JSONValue } from \"./utils/types\";\nimport { Uuid } from \"./utils/uuid\";\n\nexport { Tag } from \"./tag\";\nexport { Uuid } from \"./utils/uuid\";\nexport { type JSONValue } from \"./utils/types\";\nexport { type AuthProvider } from \"./tools/integrations\";\n\n/**\n * @fileoverview\n * Core Plot entity types for working with threads, notes, focuses, and contacts.\n *\n * ## Type Pattern: Null vs Undefined Semantics\n *\n * Plot entity types use a consistent pattern to distinguish between missing, unset, and explicitly cleared values:\n *\n * ### Entity Types (Thread, Focus, Note, Actor)\n * - **Required fields**: No `?`, cannot be `undefined`\n * - Example: `id: Uuid`, `title: string`\n * - **Nullable fields**: Use `| null` to allow explicit clearing\n * - Example: `key: string | null`, `status: string | null`\n * - `null` = field is explicitly unset/cleared\n * - Non-null value = field has a value\n * - **Optional nullable fields**: Use `?` with `| null` for permission-based access\n * - Example: `email?: string | null`, `name?: string | null`\n * - `undefined` = field not included (e.g., no permission to access)\n * - `null` = field included but not set\n * - Value = field has a value\n *\n * ### New* Types (NewThread, NewNote, NewFocus)\n * Used for creating or updating entities. Support partial updates by distinguishing omitted vs cleared fields:\n * - **Required fields**: Must be provided (no `?`)\n * - Example: `title: string` in NewFocus\n * - **Optional fields**: Use `?` to make them optional\n * - Example: `title?: string`, `author?: NewActor`\n * - `undefined` (omitted) = don't set/update this field\n * - Provided value = set/update this field\n * - **Optional nullable fields**: Use `?` with `| null` to support clearing\n * - Example: `assignee?: NewActor | null`\n * - `undefined` (omitted) = don't change assignee\n * - `null` = clear the assignee\n * - NewActor = set/update the assignee\n *\n * This pattern allows API consumers to:\n * 1. Omit fields they don't want to change (undefined)\n * 2. Explicitly clear fields by setting to null\n * 3. Set or update fields by providing values\n *\n * @example\n * ```typescript\n * // Creating a new thread\n * const newThread: NewThread = {\n * title: \"Review pull request\",\n * };\n *\n * // Updating a thread - only change what's specified\n * const update: ThreadUpdate = {\n * id: threadId,\n * archived: true,\n * };\n * ```\n */\n\n/**\n * Represents a unique user, contact, or twist in Plot.\n *\n * ActorIds are used throughout Plot for:\n * - Thread and note authors, link assignees\n * - Tag creators (actor_id in thread_tag/note_tag)\n * - Mentions in notes\n * - Any entity that can perform actions in Plot\n */\nexport type ActorId = string & { readonly __brand: \"ActorId\" };\n\n/**\n * Theme colors for focuses.\n */\nexport enum ThemeColor {\n /** Catalyst - Green */\n Catalyst = 0,\n /** Call to Adventure - Blue */\n CallToAdventure = 1,\n /** Rising Action - Purple */\n RisingAction = 2,\n /** Momentum - Pink-Purple */\n Momentum = 3,\n /** Turning Point - Pink */\n TurningPoint = 4,\n /** Breakthrough - Orange */\n Breakthrough = 5,\n /** Climax - Olive */\n Climax = 6,\n /** Resolution - Blue-Gray */\n Resolution = 7,\n}\n\n/**\n * Represents a focus within Plot.\n *\n * A focus is similar to a project or area-of-life. Every thread is in a focus.\n * Focuses are flat — they have no parent and no children. Threads not matched\n * to any focus live in the Inbox.\n */\nexport type Focus = {\n /** Unique identifier for the focus */\n id: Uuid;\n /** Human-readable title for the focus */\n title: string;\n /** Whether this focus has been archived */\n archived: boolean;\n /**\n * Optional key for referencing this focus.\n * Keys are unique per user.\n */\n key: string | null;\n /** Optional theme color for the focus (0-7). Defaults to 7 (Resolution) when not set. */\n color: ThemeColor | null;\n /** Optional icon for the focus (a curated icon key). Defaults to the focus icon when not set. */\n icon: string | null;\n};\n\n/**\n * Type for creating new focuses.\n *\n * Supports multiple creation patterns:\n * - Provide a specific UUID for the focus\n * - Provide a key for upsert within the user's focuses\n * - Omit both to auto-generate a new UUID\n */\nexport type NewFocus = Pick<Focus, \"title\"> &\n Partial<Omit<Focus, \"id\" | \"title\">> &\n (\n | {\n /**\n * Unique identifier for the focus, generated by Uuid.Generate().\n * Specifying an ID allows tools to track and upsert focuses.\n */\n id: Uuid;\n }\n | {\n /**\n * Unique key for the focus within the user's focuses.\n * Can be used to upsert without knowing the UUID.\n * For example, \"@plot\" identifies the Plot focus.\n */\n key: string;\n }\n | {\n /* Neither id nor key is required. An id will be generated and returned. */\n }\n );\n\n/**\n * Type for updating existing focuses.\n * Must provide either id or key to identify the focus to update.\n */\nexport type FocusUpdate = ({ id: Uuid } | { key: string }) &\n Partial<Pick<Focus, \"title\" | \"archived\">>;\n\n/**\n * Enumeration of supported action types.\n *\n * Different action types have different behaviors when clicked by users\n * and may require different rendering approaches.\n */\nexport enum ActionType {\n /** External web links that open in browser */\n external = \"external\",\n /** Authentication flows for connecting services */\n auth = \"auth\",\n /** Callback actions that trigger twist methods when clicked */\n callback = \"callback\",\n /** Video conferencing links with provider-specific handling */\n conferencing = \"conferencing\",\n /** File attachment links stored in R2 */\n file = \"file\",\n /** Reference to an attachment hosted by a connector's source system */\n fileRef = \"fileRef\",\n /** Thread reference links for navigating to related threads */\n thread = \"thread\",\n /** Structured plan of operations for user approval */\n plan = \"plan\",\n}\n\n/**\n * Video conferencing providers for conferencing links.\n *\n * Used to identify the conferencing platform and provide\n * provider-specific UI elements (titles, icons, etc.).\n */\nexport enum ConferencingProvider {\n /** Google Meet */\n googleMeet = \"googleMeet\",\n /** Zoom */\n zoom = \"zoom\",\n /** Microsoft Teams */\n microsoftTeams = \"microsoftTeams\",\n /** Cisco Webex */\n webex = \"webex\",\n /** Other or unknown conferencing provider */\n other = \"other\",\n}\n\n/**\n * Represents a clickable action attached to a thread.\n *\n * Thread actions are rendered as buttons that enable user interaction with threads.\n * Different action types have specific behaviors and required fields for proper functionality.\n *\n * @example\n * ```typescript\n * // External action - opens URL in browser\n * const externalAction: Action = {\n * type: ActionType.external,\n * title: \"Open in Google Calendar\",\n * url: \"https://calendar.google.com/event/123\",\n * };\n *\n * // Conferencing action - opens video conference with provider info\n * const conferencingAction: Action = {\n * type: ActionType.conferencing,\n * url: \"https://meet.google.com/abc-defg-hij\",\n * provider: ConferencingProvider.googleMeet,\n * };\n *\n * // Integrations action - initiates OAuth flow\n * const authAction: Action = {\n * type: ActionType.auth,\n * title: \"Continue with Google\",\n * provider: AuthProvider.Google,\n * scopes: [\"https://www.googleapis.com/auth/calendar.readonly\"],\n * callback: \"callback-token-for-auth-completion\"\n * };\n *\n * // Callback action - triggers a twist method\n * const callbackAction: Action = {\n * type: ActionType.callback,\n * title: \"📅 Primary Calendar\",\n * token: \"callback-token-here\"\n * };\n * ```\n */\nexport type Action =\n | {\n /** External web link that opens in browser */\n type: ActionType.external;\n /** Display text for the action button */\n title: string;\n /** URL to open when clicked */\n url: string;\n }\n | {\n /** Video conferencing action with provider-specific handling */\n type: ActionType.conferencing;\n /** URL to join the conference */\n url: string;\n /** Conferencing provider for UI customization */\n provider: ConferencingProvider;\n }\n | {\n /** Authentication action that initiates an OAuth flow */\n type: ActionType.auth;\n /** Display text for the auth button */\n title: string;\n /** OAuth provider (e.g., \"google\", \"microsoft\") */\n provider: string;\n /** Array of OAuth scopes to request */\n scopes: string[];\n /** Callback token for auth completion notification */\n callback: Callback;\n }\n | {\n /** Callback action that triggers a twist method when clicked */\n type: ActionType.callback;\n /** Display text for the callback button */\n title: string;\n /** Token identifying the callback to execute */\n callback: Callback;\n }\n | {\n /** File attachment action stored in R2 */\n type: ActionType.file;\n /** Unique identifier for the stored file */\n fileId: string;\n /** Original filename */\n fileName: string;\n /** File size in bytes */\n fileSize: number;\n /** MIME type of the file */\n mimeType: string;\n /** Intrinsic width of the image in pixels (only for image files) */\n imageWidth?: number | null;\n /** Intrinsic height of the image in pixels (only for image files) */\n imageHeight?: number | null;\n }\n | {\n /** Reference to an attachment hosted by a connector's source system */\n type: ActionType.fileRef;\n /** Opaque identifier interpreted only by the owning connector */\n ref: string;\n /** Display filename */\n fileName: string;\n /** File size in bytes if known */\n fileSize: number | null;\n /** MIME type */\n mimeType: string;\n /** Intrinsic width of the image in pixels (only for image files) */\n imageWidth?: number | null;\n /** Intrinsic height of the image in pixels (only for image files) */\n imageHeight?: number | null;\n }\n | {\n /** Thread reference action for navigating to a related thread */\n type: ActionType.thread;\n /** UUID of the referenced thread */\n threadId: Uuid;\n }\n | {\n /** Structured plan of operations for user approval */\n type: ActionType.plan;\n /** Human-readable summary of the plan */\n title: string;\n /** Operations to execute on approval */\n operations: PlanOperation[];\n /** Callback invoked with (action, approved: boolean) */\n callback: Callback;\n };\n\n/**\n * Represents metadata about a thread, typically from an external system.\n *\n * Thread metadata enables storing additional information about threads,\n * which is useful for synchronization, linking back to external systems,\n * and storing tool-specific data.\n *\n * Must be valid JSON data (strings, numbers, booleans, null, objects, arrays).\n * Functions and other non-JSON values are not supported.\n *\n * @example\n * ```typescript\n * // Calendar event metadata\n * await plot.createThread({\n * title: \"Team Meeting\",\n * meta: {\n * calendarId: \"primary\",\n * htmlLink: \"https://calendar.google.com/event/abc123\",\n * conferenceData: { ... }\n * }\n * });\n *\n * // Project issue metadata\n * await plot.createThread({\n * title: \"Fix login bug\",\n * meta: {\n * projectId: \"TEAM\",\n * issueNumber: 123,\n * url: \"https://linear.app/team/issue/TEAM-123\"\n * }\n * });\n * ```\n */\nexport type ThreadMeta = {\n /** Source-specific properties and metadata */\n [key: string]: JSONValue;\n};\n\n/**\n * Thread sub-type that determines the thread's icon and category.\n * Available types depend on whether the focus is shared:\n * - Private focuses: \"action\" (default for tasks), \"notes\" (default), \"idea\", \"goal\", \"decision\"\n * - Shared focuses: all above plus \"discussion\" (default), \"announcement\", \"ask\"\n */\nexport type ThreadType =\n | \"action\"\n | \"notes\"\n | \"idea\"\n | \"goal\"\n | \"decision\"\n | \"discussion\"\n | \"announcement\"\n | \"ask\";\n\n/**\n * Tags on an item, along with the actors who added each tag.\n */\nexport type Tags = { [K in Tag]?: ActorId[] };\n\n/**\n * A set of tags to add to an item, along with the actors adding each tag.\n */\nexport type NewTags = { [K in Tag]?: NewActor[] };\n\n/**\n * A single emoji reaction key. Either:\n * - A Unicode emoji grapheme cluster (e.g. `\"👍\"`, `\"👨‍👩‍👧\"`), or\n * - A provider-scoped custom-emoji ref of the form\n * `\"<provider>:<workspaceId>/<name>\"` (e.g. `\"slack:T0123/party_parrot\"`).\n *\n * Anything matching a known provider prefix is treated as a custom-emoji\n * reference; everything else is rendered as the Unicode it contains.\n *\n * Reactions are the open-set replacement for {@link Tag}'s retired count\n * range (`1000+`). Use reactions for emoji that round-trip with chat platforms;\n * use tags for Plot-managed compute/toggle state (todo, pinned, urgent,\n * ...).\n */\nexport type Reaction = string;\n\n/**\n * Emoji reactions on an item, keyed by emoji string, with the list of\n * actors who added each reaction.\n */\nexport type Reactions = Record<Reaction, ActorId[]>;\n\n/**\n * A set of reactions to add to an item, along with the actors adding each\n * reaction. To remove a reaction for a given actor, omit them from the\n * `NewActor[]` list — passing an empty list removes the reaction entirely.\n */\nexport type NewReactions = Record<Reaction, NewActor[]>;\n\n/**\n * Thread access level determining visibility.\n * - \"public\": Visible to all users with focus access\n * - \"members\": Visible to focus members (default for shared focuses)\n * - \"private\": Visible only to creator and contacts listed in accessContacts\n */\nexport type ThreadAccessLevel = \"public\" | \"members\" | \"private\";\n\n/**\n * Common fields shared by both Thread and Note entities.\n */\nexport type ThreadCommon = {\n /** Unique identifier for the thread */\n id: Uuid;\n /**\n * When this item was created.\n *\n * **For sources:** Set this to the external system's timestamp (e.g., email\n * sent date, comment creation date), NOT the sync time. If omitted, defaults\n * to the current time, which is almost never correct for synced data.\n */\n created: Date;\n /** Whether this thread has been archived */\n archived: boolean;\n /** Tags attached to this thread. Maps tag ID to array of actor IDs who added that tag. */\n tags: Tags;\n /**\n * Emoji reactions on this item. Maps each emoji (Unicode grapheme or\n * `provider:workspace/name` custom-emoji ref) to the list of actor IDs\n * who reacted with it.\n */\n reactions: Reactions;\n};\n\n/**\n * Fields on a Thread entity.\n * Threads are simple containers for links and notes.\n */\ntype ThreadFields = ThreadCommon & {\n /** The display title/summary of the thread */\n title: string;\n /** The focus context this thread belongs to */\n focus: Focus;\n /** The thread's sub-type/category. Determines the displayed icon. */\n type: ThreadType | null;\n /** Thread access level: \"public\", \"members\", or \"private\" */\n access: ThreadAccessLevel;\n /** Contacts who can see a private thread (empty array for creator-only). Only meaningful when access is \"private\". */\n accessContacts: Contact[];\n /** The schedule associated with this thread, if any */\n schedule?: Schedule;\n /** Source-specific metadata from the thread's link, populated on callbacks */\n meta?: ThreadMeta;\n};\n\nexport type Thread = ThreadFields;\n\nexport type ThreadWithNotes = Thread & {\n notes: Note[];\n};\n\nexport type NewThreadWithNotes = NewThread & {\n notes: Omit<NewNote, \"thread\">[];\n};\n\n/**\n * Type for creating new threads.\n *\n * Threads are simple containers. All other fields are optional.\n *\n * @example\n * ```typescript\n * const thread: NewThread = {\n * title: \"Review pull request\"\n * };\n * ```\n */\nexport type NewThread = Partial<\n Omit<ThreadFields, \"focus\" | \"tags\" | \"reactions\" | \"id\" | \"accessContacts\">\n> &\n (\n | {\n /** Unique identifier for the thread, generated by Uuid.Generate(). */\n id: Uuid;\n }\n | {\n /* id is optional. An id will be generated and returned. */\n }\n ) &\n {\n /** Explicit focus - disables automatic focus matching. When omitted, the server classifies the thread using the user's focus rules. */\n focus?: Pick<Focus, \"id\">;\n } & {\n /**\n * All tags to set on the new thread.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to set on the new thread.\n * Each emoji maps to the list of actors who reacted with it.\n */\n reactions?: NewReactions;\n\n /**\n * The thread's sub-type/category. Sets the thread's icon.\n * If omitted, defaults to \"notes\" (private) or \"discussion\" (shared).\n */\n type?: ThreadType;\n\n /**\n * Contacts who can see a private thread.\n * Pass email-based NewContact objects; they are resolved to contact IDs by the API.\n * If omitted for a private thread, defaults to the connection owner.\n */\n accessContacts?: NewContact[];\n\n /**\n * Whether the thread should be marked as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - true: Thread is explicitly unread for ALL users (use sparingly)\n * - false: Thread is marked as read for all users in the focus at creation time\n */\n unread?: boolean;\n\n /**\n * Whether the thread is archived.\n * - true: Archive the thread\n * - false: Unarchive the thread\n * - undefined (default): Preserve current archive state\n */\n archived?: boolean;\n\n /**\n * Optional preview content for the thread. Can be Markdown formatted.\n * The preview will be automatically generated from this content (truncated to 100 chars).\n */\n preview?: string | null;\n\n /**\n * Optional schedules to create alongside the thread.\n */\n schedules?: Array<Omit<NewSchedule, \"threadId\">>;\n\n /**\n * Optional schedule occurrence overrides.\n */\n scheduleOccurrences?: NewScheduleOccurrence[];\n };\n\nexport type ThreadFilter = {\n meta?: {\n [key: string]: JSONValue;\n };\n};\n\n/**\n * Fields supported by bulk updates via `match`. Only simple scalar fields\n * that can be applied uniformly across many threads are included.\n */\ntype ThreadBulkUpdateFields = Partial<\n Pick<ThreadFields, \"title\" | \"access\" | \"archived\">\n> & {\n /** Contacts who can see a private thread. Pass NewContact objects (email-based); resolved by the API. */\n accessContacts?: NewContact[];\n};\n\n/**\n * Fields supported by single-thread updates via `id` or `source`.\n * Includes all bulk fields plus tags and preview.\n */\ntype ThreadSingleUpdateFields = ThreadBulkUpdateFields & {\n /**\n * Tags to change on the thread. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to change on the thread. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Add or remove the twist's tags.\n * Maps tag ID to boolean: true = add tag, false = remove tag.\n * This is allowed on all threads the twist has access to.\n */\n twistTags?: Partial<Record<Tag, boolean>>;\n\n /**\n * Update the thread's sub-type/category.\n */\n type?: ThreadType;\n\n /**\n * Optional preview content for the thread. Can be Markdown formatted.\n * The preview will be automatically generated from this content (truncated to 100 chars).\n *\n * - string: Use this content for preview generation\n * - null: Explicitly disable preview (no preview will be shown)\n * - undefined (omitted): Preserve current preview value\n *\n * This field is write-only and won't be returned when reading threads.\n */\n preview?: string | null;\n\n /**\n * Move the thread to a different focus. Requires ThreadAccess.Full.\n * The target focus must be owned by the twist's user.\n */\n focus?: Pick<Focus, \"id\">;\n};\n\nexport type ThreadUpdate =\n | (({ id: Uuid } | { source: string }) & ThreadSingleUpdateFields)\n | ({\n /**\n * Update all threads matching the specified criteria. Only threads\n * that match all provided fields and were created by the twist will be updated.\n */\n match: ThreadFilter;\n } & ThreadBulkUpdateFields);\n\n/**\n * Represents a note within a thread.\n *\n * Notes contain the detailed content (note text, actions) associated with a thread.\n * They are always ordered by creation time within their parent thread.\n */\n/**\n * Reports that an outbound send / write-back for a note could not be\n * delivered to the external system, so Plot can surface it to the user.\n *\n * Returned (not thrown) from a write-back hook — see\n * `NoteWriteBackResult.deliveryError` and `NewLinkWithNotes.originatingNote` —\n * after the connector has exhausted its own retries (or immediately for a\n * permanent failure such as a rejected recipient). The runtime records it on\n * the note (the app then shows a \"Failed to send\" affordance with Retry /\n * Discard) and marks the thread unread so the user notices.\n */\nexport type DeliveryError = {\n /**\n * Short, stable machine code for the failure category, e.g. `\"rejected\"`,\n * `\"too_large\"`, `\"rate_limited\"`, `\"invalid_recipient\"`, or the generic\n * `\"send_failed\"`. Used for logging/diagnostics, not shown verbatim to users.\n */\n code: string;\n /**\n * Human-readable, user-safe explanation to show beside \"Failed to send\"\n * (e.g. \"Recipient address rejected\"). Null/omitted when the connector has\n * no safe message to surface — the app then shows just \"Failed to send\".\n */\n message?: string | null;\n};\n\nexport type Note = ThreadCommon & {\n /** The author of this note */\n author: Actor;\n /**\n * Globally unique, stable identifier for the note within its thread + link.\n * Can be used to upsert without knowing the id.\n *\n * Note keys are scoped to a `(thread, link)` pair — two links on the same\n * thread (e.g. after a merge) can each carry a `\"description\"` note without\n * colliding. The runtime infers the link from the surrounding `saveLink`\n * call. For bare `saveNote` calls outside a `saveLink`, the runtime\n * resolves the link by looking up the connector's links on the thread\n * and errors if more than one matches.\n *\n * Use one of these patterns:\n * - Hardcoded semantic keys for fixed note types: \"description\", \"cancellation\"\n * - External service IDs for dynamic collections: `comment:${immutableId}`\n *\n * Examples:\n * - `\"description\"` (for a Jira issue's description note)\n * - `\"comment:12345\"` (for a specific comment by ID)\n * - `\"gmail:msg:18d4e5f2a3b1c9d7\"` (for a Gmail message within a thread)\n *\n * Ensure IDs are immutable - avoid human-readable slugs or titles.\n */\n key: string | null;\n /** The parent thread this note belongs to */\n thread: Thread;\n /** Primary content for the note (markdown) */\n content: string | null;\n /** Array of interactive actions attached to the note */\n actions: Array<Action> | null;\n /** The note this is a reply to, or null if not a reply */\n reNote: { id: Uuid } | null;\n /**\n * Contacts who can see this note, or null if the note inherits thread visibility.\n * When set (even to []), the note is private to the listed contacts plus the creator.\n */\n accessContacts: ActorId[] | null;\n /** Focus twist IDs (twists/connectors) mentioned for dispatch routing. Does not include user contacts. */\n mentions: ActorId[];\n /**\n * A time-sensitive call-to-action extracted from this note's message\n * (OTP code or confirm link). Null when none detected. Set by the runtime\n * from the connector's extraction; clients read it to show an ephemeral prompt.\n */\n cta: Cta | null;\n};\n\n/**\n * Type for creating new notes.\n *\n * Requires the thread reference, with all other fields optional.\n * Can provide id, key, or neither for note identification:\n * - id: Provide a specific UUID for the note\n * - key: Provide an external identifier for upsert within the thread\n * - neither: A new note with auto-generated UUID will be created\n */\nexport type NewNote = Partial<\n Omit<\n Note,\n \"author\" | \"thread\" | \"tags\" | \"reactions\" | \"mentions\" | \"accessContacts\" | \"id\" | \"key\" | \"reNote\"\n >\n> &\n ({ id: Uuid } | { key: string } | {}) & {\n /** Reference to the parent thread (required) */\n thread:\n | Pick<Thread, \"id\">\n | {\n source: string;\n };\n\n /**\n * The person that created the item, or leave undefined to use the twist as author.\n */\n author?: NewActor;\n\n /**\n * Format of the note content. Determines how the note is processed:\n * - 'text': Plain text that will be converted to markdown (auto-links URLs, preserves line breaks)\n * - 'markdown': Already in markdown format (default, no conversion)\n * - 'html': HTML content that will be converted to markdown\n */\n contentType?: ContentType;\n\n /**\n * Tags to change on the thread. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to set on the note. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Contacts who can see this note, or null/undefined to inherit thread visibility.\n * Accepts resolved ActorId UUIDs or email-based NewContact objects (resolved server-side).\n * Include all participants who should see the note (sender + recipients).\n * The note author is NOT implicitly included — add them explicitly.\n * When set (even to []), the note is private to the listed contacts plus the creator.\n */\n accessContacts?: (ActorId | NewContact)[] | null;\n\n /**\n * Twist/connector IDs to mention for dispatch routing.\n * Does not include user contacts — use accessContacts for visibility.\n */\n mentions?: NewActor[];\n\n /**\n * Whether the note should mark the parent thread as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - true: Thread is explicitly unread for ALL users (use sparingly)\n * - false: Thread is marked as read for all users in the focus at note creation time\n *\n * For the default behavior, omit this field entirely.\n * Use false for initial sync to avoid marking historical items as unread.\n */\n unread?: boolean;\n\n /**\n * When true, the server will use AI to detect tasks in this note's content\n * and create separate Plot-authored reply notes for each detected task.\n * Use for messaging connectors (email, chat) where tasks are implicit\n * in conversation rather than explicitly structured.\n */\n checkForTasks?: boolean;\n\n /**\n * Reference to a parent note this note is a reply to.\n * - `{ id }`: reply by UUID\n * - `{ key }`: reply by key, resolved after creation (for batch ops)\n * - `null`: explicitly not a reply\n * - `undefined` (omitted): not a reply\n */\n reNote?: { id: Uuid } | { key: string } | null;\n\n /**\n * A link carried by this note (note-attached, NOT a thread-level canonical\n * link). Use for augmenter content (e.g. Granola meeting notes) that should\n * attach to an existing canonical thread without becoming its primary link.\n * The runtime creates the link note-scoped, binds `note.link_id` to it, and\n * — when `thread: { source }` resolves to no existing thread — find-or-creates\n * the thread by that source so a later canonical sync can fill the primary.\n */\n link?: NewLink;\n };\n\n/**\n * Type for updating existing notes.\n * Must provide either id or key to identify the note to update.\n */\nexport type NoteUpdate = ({ id: Uuid; key?: string } | { key: string }) &\n Partial<\n Pick<Note, \"accessContacts\" | \"archived\" | \"content\" | \"actions\" | \"reNote\">\n > & {\n /**\n * Format of the note content. Determines how the note is processed:\n * - 'text': Plain text that will be converted to markdown (auto-links URLs, preserves line breaks)\n * - 'markdown': Already in markdown format (default, no conversion)\n * - 'html': HTML content that will be converted to markdown\n */\n contentType?: ContentType;\n\n /**\n * Tags to change on the note. Use an empty array of NewActor to remove a tag.\n * Use twistTags to add/remove the twist from tags to avoid clearing other actors' tags.\n */\n tags?: NewTags;\n\n /**\n * Emoji reactions to change on the note. Pass an empty `NewActor[]` to\n * remove a reaction entirely; omit an emoji to leave it untouched.\n */\n reactions?: NewReactions;\n\n /**\n * Add or remove the twist's tags.\n * Maps tag ID to boolean: true = add tag, false = remove tag.\n * This is allowed on all notes the twist has access to.\n */\n twistTags?: Partial<Record<Tag, boolean>>;\n\n /**\n * Twist/connector IDs to mention for dispatch routing.\n * Does not include user contacts — use accessContacts for visibility.\n */\n mentions?: NewActor[];\n };\n\n/**\n * Represents an actor in Plot - a user, contact, or twist.\n *\n * Actors can be associated with threads as authors, assignees, or mentions.\n * The email field is only included when ContactAccess.Read permission is granted.\n *\n * @example\n * ```typescript\n * const actor: Actor = {\n * id: \"f0ffd5f8-1635-4b13-9532-35f97446db90\" as ActorId,\n * type: ActorType.Contact,\n * email: \"john.doe@example.com\", // Only if ContactAccess.Read\n * name: \"John Doe\"\n * };\n * ```\n */\nexport type Actor = {\n /** Unique identifier for the actor */\n id: ActorId;\n /** Type of actor (User, Contact, or Twist) */\n type: ActorType;\n /**\n * Email address (only included with ContactAccess.Read permission).\n * - `undefined`: No permission to read email\n * - `null`: Permission granted but email not set\n * - `string`: Email address\n */\n email?: string | null;\n /**\n * Display name.\n * - `undefined`: Not included due to permissions\n * - `null`: Not set\n * - `string`: Display name\n */\n name?: string | null;\n};\n\n/**\n * A resolved contact with identity info. Used for access control lists\n * where only human contacts (not twists) are relevant.\n */\nexport type Contact = {\n /** Unique identifier for the contact */\n id: ActorId;\n /** Email address, or null if not set */\n email: string | null;\n /** Display name, or null if not set */\n name: string | null;\n};\n\n/**\n * An existing or new contact.\n */\nexport type NewActor =\n | {\n /** Unique identifier for the actor */\n id: ActorId;\n }\n | NewContact;\n\n/**\n * Enumeration of author types that can create threads.\n *\n * The author type affects how threads are displayed and processed\n * within the Plot system.\n */\nexport enum ActorType {\n /** Threads created by human users */\n User,\n /** Threads created by external contacts */\n Contact,\n /** Threads created by automated twists */\n Twist,\n}\n\n/**\n * Represents contact information for creating a new contact.\n *\n * Contacts are used throughout Plot for representing people associated\n * with threads, such as event attendees or task assignees.\n *\n * @example\n * ```typescript\n * const newContact: NewContact = {\n * email: \"john.doe@example.com\",\n * name: \"John Doe\",\n * avatar: \"https://avatar.example.com/john.jpg\"\n * };\n * ```\n */\n/**\n * Common fields shared by all NewContact variants.\n */\ntype NewContactBase = {\n /** Optional avatar image URL for the contact */\n avatar?: string;\n /**\n * External provider account source. Used for identity resolution\n * when email is unavailable and for privacy compliance reporting.\n *\n * The runtime scopes the resulting `contact_external_account` row to\n * the dispatching twist instance (i.e. one row per connection per\n * contact), so the same Plot contact can have multiple rows when\n * reachable through multiple connections (e.g. two Slack workspaces,\n * Gmail + Google Chat sharing one Google account).\n */\n source?: { accountId: string };\n /**\n * Optional connector-defined role for this contact on the thread, matching\n * a `LinkTypeConfig.contactRoles[].id` (e.g. \"to\" / \"cc\" / \"bcc\" for\n * email, \"required\" / \"optional\" for calendar). Omitted ⇒ default role.\n * Connectors set this on inbound sync; the runtime persists it under\n * `thread.contact_meta[contact_id].role`.\n */\n role?: string;\n};\n\n/**\n * At least one of `email` or `name` must be provided so the contact can be\n * identified in the UI. Contacts with neither would display as \"Unknown\".\n */\nexport type NewContact = NewContactBase &\n ({ email: string; name?: string } | { email?: string; name: string });\n\nexport type ContentType = \"text\" | \"markdown\" | \"html\";\n\n/**\n * Represents an external entity linked to a thread.\n *\n * Links are created by sources to represent external entities (issues, emails, calendar events)\n * attached to a thread container. A thread can have multiple links (1:many).\n * Links store source-specific data like type, status, metadata, and embeddings.\n *\n * @example\n * ```typescript\n * // A link representing a Linear issue\n * const link: Link = {\n * threadId: \"...\" as Uuid,\n * source: \"linear:issue:549dd8bd-2bc9-43d1-95d5-4b4af0c5af1b\",\n * created: new Date(),\n * author: { id: \"...\" as ActorId, type: ActorType.Contact, name: \"Alice\" },\n * title: \"Fix login bug\",\n * type: \"issue\",\n * status: \"open\",\n * meta: { projectId: \"TEAM\", url: \"https://linear.app/team/TEAM-123\" },\n * assignee: null,\n * actions: null,\n * };\n * ```\n */\nexport type Link = {\n /** The thread this link belongs to */\n threadId: Uuid;\n /** External source identifier for dedup/upsert */\n source: string | null;\n /** When this link was originally created in its source system */\n created: Date;\n /** The actor credited with creating this link */\n author: Actor | null;\n /** Display title */\n title: string;\n /** Truncated preview */\n preview: string | null;\n /** The actor assigned to this link */\n assignee: Actor | null;\n /** Source-defined type string (e.g., issue, pull_request, email, event) */\n type: string | null;\n /** Source-defined status string (e.g., open, done, closed) */\n status: string | null;\n /** Interactive action buttons */\n actions: Array<Action> | null;\n /** Source metadata */\n meta: ThreadMeta | null;\n /** URL to open the original item in its source application (e.g., \"Open in Linear\") */\n sourceUrl: string | null;\n /** Channel ID that produced this link (matches source_channel.channel_id) */\n channelId: string | null;\n /**\n * Cross-connector thread bundling key.\n *\n * @deprecated Use `sources` instead. Reads return the first element of\n * `sources` for backward compatibility; new writes should populate `sources`.\n */\n relatedSource: string | null;\n /**\n * Canonical identifiers for this link. Two links whose `sources` arrays\n * overlap share the same thread (array overlap, `sources && new.sources`).\n *\n * Use this to bundle with another connector via a canonical alias. For\n * example, every calendar connector emits `icaluid:<iCalUID>` so any\n * meeting-notes connector can bundle by setting the same alias.\n */\n sources: string[];\n /**\n * Connector-supplied ranking used by clients to choose the single displayed\n * (primary) canonical link when a thread has more than one. Higher wins;\n * ties break on earliest creation. Default 0.\n */\n priority: number;\n};\n\n/**\n * Type for creating new links.\n *\n * Links are created by sources to represent external entities.\n * Requires a source identifier for dedup/upsert.\n */\nexport type NewLink = Partial<\n Omit<Link, \"author\" | \"assignee\" | \"threadId\">\n> & {\n /**\n * Canonical ID for the item in an external system.\n * When set, uniquely identifies the link for the user. This performs\n * an upsert.\n *\n * @deprecated Pass `sources: [...]` instead. Both fields can be set during\n * the transition; the runtime will normalize.\n */\n source?: string;\n /**\n * Canonical identifiers for this item. Any element shared with another\n * link's `sources` bundles the two links into the same thread. Used for\n * cross-connector bundling — e.g. a meeting-notes connector setting\n * `[\"granola:<id>\", \"icaluid:<uid>\"]` to attach onto a calendar event\n * thread that includes `icaluid:<uid>` in its own `sources`.\n */\n sources?: string[];\n /**\n * Heuristic facets describing this item (format / automation / reach),\n * used as internal classifier signal. Omit a dimension (or set null) when\n * no heuristic is confident. See `@plotday/twister/facets`.\n */\n facets?: ThreadFacets;\n /** The person that created the item. By default, it will be the twist itself. */\n author?: NewActor;\n /** The person assigned to the item. */\n assignee?: NewActor | null;\n /**\n * Thread access level: \"public\", \"members\" (default), or \"private\".\n * When \"private\", thread visibility is limited to the creator and contacts in accessContacts.\n */\n access?: ThreadAccessLevel;\n /**\n * Contacts who can see a private thread.\n * Pass email-based NewContact objects; they are resolved to contact IDs by the API.\n * If omitted for a private thread, defaults to the connection owner.\n */\n accessContacts?: NewContact[];\n /**\n * Whether the thread should be marked as unread for users.\n * - undefined/omitted (default): Thread is unread for users, except auto-marked\n * as read for the author if they are the twist owner (user)\n * - false: Thread is marked as read for all users in the focus at creation time\n */\n unread?: boolean;\n /**\n * Whether the thread is archived.\n * - true: Archive the thread\n * - false: Unarchive the thread\n * - undefined (default): Preserve current archive state\n */\n archived?: boolean;\n /**\n * Mark the thread as the connection owner's to-do at create time.\n * - true: the thread is added to the owner's to-do (active) bucket and\n * their per-user archive is lifted, atomically with the save — no\n * separate `integrations.setThreadToDo()` round-trip needed.\n * - false: the owner's thread_state is marked read (cleared from to-do).\n * - undefined (omitted, default): leave to-do state untouched.\n *\n * Use for messaging-style \"saved for later\" flags (e.g. a starred Slack\n * thread). This is the first-class replacement for overloading a\n * `statuses[]` entry with `active: true`.\n */\n todo?: boolean;\n /**\n * The to-do date used when `todo` is true. Defaults to the \"Now\"\n * sentinel (today's bucket) when omitted. Ignored when `todo` is not true.\n */\n todoDate?: Date | string;\n /**\n * Explicit focus (disables automatic focus matching).\n * Only used when the link creates a new thread. When omitted, the\n * server classifies the thread using the user's focus rules.\n */\n focus?: Pick<Focus, \"id\">;\n /**\n * Primary-link ranking for this canonical link (default 0). Set higher on\n * the connection that \"owns\" the external item (e.g. the calendar that owns\n * an event vs a subscribed copy) so clients display it as the primary.\n */\n priority?: number;\n /**\n * Opt this message into the platform's sequential auto-threading. When a\n * connection has auto-threading enabled, the runtime decides — once,\n * globally, at ingest — whether this message starts a new thread or folds\n * (as a note) into the thread of the conversation it continues. Set it on\n * every message of a conversational surface (a chat channel, a DM); the\n * runtime keys the sequential chain on {@link AutoThreadConfig.key}.\n *\n * Marking a link is a no-op unless the connection opted in, so connectors\n * can set it unconditionally on eligible links. Leave undefined/null for\n * non-conversational items (issues, events, files). See\n * {@link Connector.autoThreading}.\n */\n autoThread?: AutoThreadConfig | null;\n };\n\n/**\n * How the runtime groups a connector's messages into threads when\n * auto-threading is enabled for the connection.\n *\n * - `\"sequential\"`: a chat **channel**. Each message either continues the\n * previous message's conversation (folds in as a note) or starts a new\n * thread, decided by a high-confidence continuation check. Use for\n * multi-participant channels where consecutive top-level messages often\n * form one conversation.\n * - `\"fold\"`: a **DM** / one-to-one surface. Every message folds into a\n * single running thread for that {@link AutoThreadConfig.key} (no per-message\n * judgment). Use for direct messages, which read naturally as one continuous\n * conversation.\n */\nexport type AutoThreadMode = \"sequential\" | \"fold\";\n\n/** Per-link auto-threading directive. See {@link NewLink.autoThread}. */\nexport type AutoThreadConfig = {\n /**\n * The conversation grouping the sequential chain runs within — typically the\n * connector's channel id ({@link Link.channelId}). DMs and each channel form\n * independent chains. Messages are only ever folded into another message\n * that shares this key.\n */\n key: string;\n /** Channel (`\"sequential\"`) vs DM (`\"fold\"`) grouping. */\n mode: AutoThreadMode;\n};\n\n/**\n * A new link with notes to save via integrations.saveLink().\n * Creates a thread+link pair, with notes attached to the thread.\n */\nexport type NewLinkWithNotes = NewLink & {\n /**\n * Title for the link and its thread container.\n * Must be the real entity title (e.g. issue title, message subject),\n * never a placeholder or ID. This value overwrites the existing title on upsert.\n * Omit to preserve the existing title (e.g. for cancelled events where the\n * title may not be available in the webhook payload).\n */\n title?: string;\n /** Notes to attach to the thread */\n notes?: Omit<NewNote, \"thread\">[];\n /** Schedules to create for the link */\n schedules?: Array<Omit<NewSchedule, \"threadId\">>;\n /** Schedule occurrence overrides */\n scheduleOccurrences?: NewScheduleOccurrence[];\n /**\n * For `onCreateLink` only: binds the thread's opening note (the message the\n * user composed in Plot, which this hook just posted to the external system)\n * to its external counterpart. Mirrors the `NoteWriteBackResult` a reply\n * returns from `onNoteCreated` — `key` is the external message id and\n * `externalContent` is the post-write content baseline. Without this the\n * opening note stays keyless, so reactions and edits on it can't be routed\n * back to the external system. Ignored outside `onCreateLink`.\n */\n originatingNote?: {\n /** External message id; set as the opening note's `key`. */\n key?: string;\n /**\n * Content as the external system stored it post-write, for the sync\n * baseline. Must equal what your sync-in path emits as this note's\n * `content` on re-ingest (same contract as `NoteWriteBackResult.externalContent`).\n */\n externalContent?: string;\n /**\n * Reports that sending the composed message FAILED (the `onCreateLink`\n * send could not be delivered). The runtime records it on the opening\n * note — surfacing a \"Failed to send\" affordance (Retry / Discard) — and\n * marks the thread unread. Same contract as\n * `NoteWriteBackResult.deliveryError`: object records, `null` clears,\n * omitted leaves untouched. Return the link anyway (so the user's composed\n * content is preserved in Plot) with this set, rather than returning\n * `null`, when a compose send fails.\n */\n deliveryError?: DeliveryError | null;\n };\n};\n\n/**\n * Type for updating existing links.\n *\n * Set `threadId` to move the link to a different thread.\n * Requires LinkAccess.Full.\n */\nexport type LinkUpdate = { id: Uuid } & {\n /** Move the link to a different thread owned by the twist's user. */\n threadId?: Uuid;\n};\n\n/**\n * A single operation within a plan submitted for user approval.\n *\n * Operations include display metadata (titles) so the app can render\n * a human-readable summary without additional lookups.\n */\nexport type PlanOperation =\n | {\n type: \"updateThread\";\n threadId: Uuid;\n /** Current thread title for display */\n threadTitle: string;\n changes: Partial<Pick<ThreadFields, \"archived\" | \"title\" | \"type\">> & {\n /** Move to this focus */\n focus?: { id: Uuid; title: string };\n };\n }\n | {\n type: \"updateLink\";\n linkId: Uuid;\n /** Current link title for display */\n linkTitle: string;\n changes: {\n /** Move to this thread */\n threadId?: Uuid;\n threadTitle?: string;\n };\n }\n | {\n type: \"createThread\";\n title: string;\n focusId: Uuid;\n /** Focus title for display */\n focusTitle: string;\n }\n | {\n type: \"createNote\";\n threadId: Uuid;\n /** Thread title for display */\n threadTitle: string;\n content: string;\n }\n | {\n type: \"updateFocus\";\n focusId: Uuid;\n /** Current focus title for display */\n focusTitle: string;\n changes: Partial<Pick<Focus, \"title\" | \"archived\">>;\n };\n";
@@ -5,4 +5,4 @@
5
5
  * Generated from: prebuild.ts
6
6
  */
7
7
 
8
- export default "import {\n type Actor,\n} from \"./plot\";\nimport type { Callback } from \"./tools/callbacks\";\nimport type {\n InferOptions,\n InferTools,\n Serializable,\n ToolBuilder,\n ToolShed,\n} from \"./utils/types\";\n\nexport type { ToolBuilder };\n\n/**\n * Abstract parent for both built-in tools and regular Tools.\n * Regular tools extend Tool.\n */\nexport abstract class ITool {}\n\n/**\n * Base class for regular tools.\n *\n * Regular tools run in isolation and can only access other tools declared\n * in their build method. They are ideal for external API integrations\n * and reusable functionality that doesn't require Plot's internal infrastructure.\n *\n * @example\n * ```typescript\n * class GoogleCalendarTool extends Tool<GoogleCalendarTool> {\n * constructor(id: string, options: { clientId: string }) {\n * super(id, options);\n * }\n *\n * build(tools: ToolBuilder) {\n * return {\n * auth: tools.build(Integrations),\n * network: tools.build(Network),\n * };\n * }\n *\n * async getCalendars() {\n * const token = await this.tools.auth.get(...);\n * // Implementation\n * }\n * }\n * ```\n */\nexport abstract class Tool<TSelf> implements ITool {\n constructor(\n protected id: string,\n protected options: InferOptions<TSelf>,\n private toolShed: ToolShed\n ) {}\n\n /**\n * Gets the initialized tools for this tool.\n * @throws Error if called before initialization is complete\n */\n protected get tools() {\n return this.toolShed.getTools<InferTools<TSelf>>();\n }\n\n /**\n * Declares tool dependencies for this tool.\n * Return an object mapping tool names to build() promises.\n * Default implementation returns empty object (no custom tools).\n *\n * @param build - The build function to use for declaring dependencies\n * @returns Object mapping tool names to tool promises\n *\n * @example\n * ```typescript\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, { urls: [\"https://api.example.com/*\"] }),\n * };\n * }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n build(build: ToolBuilder): Record<string, Promise<ITool>> {\n return {};\n }\n\n /**\n * Creates a persistent callback to a method on this tool.\n *\n * ExtraArgs are strongly typed to match the method's signature.\n *\n * @param fn - The method to callback\n * @param extraArgs - Additional arguments to pass (type-checked, must be serializable)\n * @returns Promise resolving to a persistent callback token\n *\n * @example\n * ```typescript\n * const callback = await this.callback(this.onWebhook, \"calendar\", 123);\n * ```\n */\n protected async callback<\n TArgs extends Serializable[],\n Fn extends (...args: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback> {\n return this.tools.callbacks.create(fn, ...extraArgs);\n }\n\n /**\n * Deletes a specific callback by its token.\n *\n * @param token - The callback token to delete\n * @returns Promise that resolves when the callback is deleted\n */\n protected async deleteCallback(token: Callback): Promise<void> {\n return this.tools.callbacks.delete(token);\n }\n\n /**\n * Deletes all callbacks for this tool.\n *\n * @returns Promise that resolves when all callbacks are deleted\n */\n protected async deleteAllCallbacks(): Promise<void> {\n return this.tools.callbacks.deleteAll();\n }\n\n /**\n * Executes a callback by its token inline in the current execution.\n *\n * **Use `this.runTask()` instead for batch continuations and long-running work.**\n * `this.run()` executes inline, sharing the current request count (~1000 limit)\n * and blocking the HTTP response. This causes timeouts when used in lifecycle\n * methods like `onChannelEnabled` or `syncBatch` continuations.\n *\n * `this.run()` is appropriate when you need the callback's **return value** —\n * e.g., running a parent callback token that returns data. For fire-and-forget\n * work, always prefer `this.runTask()`.\n *\n * @param token - The callback token to execute\n * @param args - Optional arguments to pass to the callback\n * @returns Promise resolving to the callback result\n */\n protected async run(token: Callback, ...args: any[]): Promise<any> {\n return this.tools.callbacks.run(token, ...args);\n }\n\n /**\n * Retrieves a value from persistent storage by key.\n *\n * Values are automatically deserialized using SuperJSON, which\n * properly restores Date objects, Maps, Sets, and other complex types.\n *\n * @template T - The expected type of the stored value (must be Serializable)\n * @param key - The storage key to retrieve\n * @returns Promise resolving to the stored value or null\n */\n protected async get<T extends Serializable>(key: string): Promise<T | null> {\n return this.tools.store.get(key);\n }\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\n *\n * **Important**: Functions and Symbols cannot be stored.\n * **For function references**: Use callbacks instead of storing functions directly.\n *\n * @example\n * ```typescript\n * // ✅ Date objects are preserved\n * await this.set(\"sync_state\", {\n * lastSync: new Date(),\n * minDate: new Date(2024, 0, 1)\n * });\n *\n * // ✅ undefined is now supported\n * await this.set(\"data\", { name: \"test\", optional: undefined });\n *\n * // ✅ Arrays with undefined are supported\n * await this.set(\"items\", [1, undefined, 3]);\n * await this.set(\"items\", [1, null, 3]); // Also works\n *\n * // ✅ Maps and Sets are supported\n * await this.set(\"mapping\", new Map([[\"key\", \"value\"]]));\n * await this.set(\"tags\", new Set([\"tag1\", \"tag2\"]));\n *\n * // ❌ WRONG: Cannot store functions directly\n * await this.set(\"handler\", this.myHandler);\n *\n * // ✅ CORRECT: Create a callback token first\n * const token = await this.callback(this.myHandler, \"arg1\", \"arg2\");\n * await this.set(\"handler_token\", token);\n *\n * // Later, execute the callback\n * const token = await this.get<Callback>(\"handler_token\");\n * await this.run(token);\n * ```\n *\n * @template T - The type of value being stored (must be Serializable)\n * @param key - The storage key to use\n * @param value - The value to store (must be SuperJSON-serializable)\n * @returns Promise that resolves when the value is stored\n */\n protected async set<T extends Serializable>(\n key: string,\n value: T\n ): Promise<void> {\n return this.tools.store.set(key, value);\n }\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n protected async list(prefix: string): Promise<string[]> {\n return this.tools.store.list(prefix);\n }\n\n /**\n * Removes a specific key from persistent storage.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n protected async clear(key: string): Promise<void> {\n return this.tools.store.clear(key);\n }\n\n /**\n * Removes all keys from this tool's storage.\n *\n * @returns Promise that resolves when all keys are removed\n */\n protected async clearAll(): Promise<void> {\n return this.tools.store.clearAll();\n }\n\n /**\n * Queues a callback to execute in a separate worker context with a fresh request limit.\n *\n * **Creates a NEW execution** with its own request limit of ~1000 requests (HTTP requests,\n * tool calls, database operations). This is the primary way to stay under request limits\n * when processing large datasets or making many API calls.\n *\n * Use this to break long loops into chunks that each stay under the ~1000 request limit.\n * Each task runs in an isolated execution environment with ~1000 requests and ~60 seconds CPU time.\n *\n * @param callback - The callback token created with `this.callback()`\n * @param options - Optional configuration for the execution\n * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately\n * @returns Promise resolving to a cancellation token (only for scheduled executions)\n *\n * @example\n * ```typescript\n * // Break large loop into batches\n * const callback = await this.callback(this.processBatch, 1);\n * await this.runTask(callback); // New execution with fresh request limit\n * ```\n */\n protected async runTask(\n callback: Callback,\n options?: { runAt?: Date }\n ): Promise<string | void> {\n return this.tools.tasks.runTask(callback, options);\n }\n\n /**\n * Cancels a previously scheduled execution.\n *\n * @param token - The cancellation token returned by runTask() with runAt option\n * @returns Promise that resolves when the cancellation is processed\n */\n protected async cancelTask(token: string): Promise<void> {\n return this.tools.tasks.cancelTask(token);\n }\n\n /**\n * Cancels all scheduled executions for this tool.\n *\n * @returns Promise that resolves when all cancellations are processed\n */\n protected async cancelAllTasks(): Promise<void> {\n return this.tools.tasks.cancelAllTasks();\n }\n\n /**\n * Called before the twist's activate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * activate method is called.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when pre-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n preActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's activate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when post-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n postActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's upgrade method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * upgrade method is called.\n *\n * @returns Promise that resolves when pre-upgrade is complete\n */\n preUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's upgrade method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-upgrade is complete\n */\n postUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's deactivate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * deactivate method is called.\n *\n * @returns Promise that resolves when pre-deactivation is complete\n */\n preDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's deactivate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-deactivation is complete\n */\n postDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Waits for tool initialization to complete.\n * Called automatically by the entrypoint before lifecycle methods.\n * @internal\n */\n async waitForReady(): Promise<void> {\n await this.toolShed.waitForReady();\n }\n}\n";
8
+ export default "import {\n type Actor,\n} from \"./plot\";\nimport type { Callback } from \"./tools/callbacks\";\nimport type {\n InferOptions,\n InferTools,\n Serializable,\n ToolBuilder,\n ToolShed,\n} from \"./utils/types\";\n\nexport type { ToolBuilder };\n\n/**\n * Abstract parent for both built-in tools and regular Tools.\n * Regular tools extend Tool.\n */\nexport abstract class ITool {}\n\n/**\n * Base class for regular tools.\n *\n * Regular tools run in isolation and can only access other tools declared\n * in their build method. They are ideal for external API integrations\n * and reusable functionality that doesn't require Plot's internal infrastructure.\n *\n * @example\n * ```typescript\n * class GoogleCalendarTool extends Tool<GoogleCalendarTool> {\n * constructor(id: string, options: { clientId: string }) {\n * super(id, options);\n * }\n *\n * build(tools: ToolBuilder) {\n * return {\n * auth: tools.build(Integrations),\n * network: tools.build(Network),\n * };\n * }\n *\n * async getCalendars() {\n * const token = await this.tools.auth.get(...);\n * // Implementation\n * }\n * }\n * ```\n */\nexport abstract class Tool<TSelf> implements ITool {\n constructor(\n protected id: string,\n protected options: InferOptions<TSelf>,\n private toolShed: ToolShed\n ) {}\n\n /**\n * Gets the initialized tools for this tool.\n * @throws Error if called before initialization is complete\n */\n protected get tools() {\n return this.toolShed.getTools<InferTools<TSelf>>();\n }\n\n /**\n * Declares tool dependencies for this tool.\n * Return an object mapping tool names to build() promises.\n * Default implementation returns empty object (no custom tools).\n *\n * @param build - The build function to use for declaring dependencies\n * @returns Object mapping tool names to tool promises\n *\n * @example\n * ```typescript\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, { urls: [\"https://api.example.com/*\"] }),\n * };\n * }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n build(build: ToolBuilder): Record<string, Promise<ITool>> {\n return {};\n }\n\n /**\n * Creates a persistent callback to a method on this tool.\n *\n * ExtraArgs are strongly typed to match the method's signature.\n *\n * @param fn - The method to callback\n * @param extraArgs - Additional arguments to pass (type-checked, must be serializable)\n * @returns Promise resolving to a persistent callback token\n *\n * @example\n * ```typescript\n * const callback = await this.callback(this.onWebhook, \"calendar\", 123);\n * ```\n */\n protected async callback<\n TArgs extends Serializable[],\n Fn extends (...args: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback> {\n return this.tools.callbacks.create(fn, ...extraArgs);\n }\n\n /**\n * Deletes a specific callback by its token.\n *\n * @param token - The callback token to delete\n * @returns Promise that resolves when the callback is deleted\n */\n protected async deleteCallback(token: Callback): Promise<void> {\n return this.tools.callbacks.delete(token);\n }\n\n /**\n * Deletes all callbacks for this tool.\n *\n * @returns Promise that resolves when all callbacks are deleted\n */\n protected async deleteAllCallbacks(): Promise<void> {\n return this.tools.callbacks.deleteAll();\n }\n\n /**\n * Executes a callback by its token inline in the current execution.\n *\n * **Use `this.runTask()` instead for batch continuations and long-running work.**\n * `this.run()` executes inline, sharing the current request count (~1000 limit)\n * and blocking the HTTP response. This causes timeouts when used in lifecycle\n * methods like `onChannelEnabled` or `syncBatch` continuations.\n *\n * `this.run()` is appropriate when you need the callback's **return value** —\n * e.g., running a parent callback token that returns data. For fire-and-forget\n * work, always prefer `this.runTask()`.\n *\n * @param token - The callback token to execute\n * @param args - Optional arguments to pass to the callback\n * @returns Promise resolving to the callback result\n */\n protected async run(token: Callback, ...args: any[]): Promise<any> {\n return this.tools.callbacks.run(token, ...args);\n }\n\n /**\n * Retrieves a value from persistent storage by key.\n *\n * Values are automatically deserialized using SuperJSON, which\n * properly restores Date objects, Maps, Sets, and other complex types.\n *\n * @template T - The expected type of the stored value (must be Serializable)\n * @param key - The storage key to retrieve\n * @returns Promise resolving to the stored value or null\n */\n protected async get<T extends Serializable>(key: string): Promise<T | null> {\n return this.tools.store.get(key);\n }\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\n *\n * **Important**: Functions and Symbols cannot be stored.\n * **For function references**: Use callbacks instead of storing functions directly.\n *\n * @example\n * ```typescript\n * // ✅ Date objects are preserved\n * await this.set(\"sync_state\", {\n * lastSync: new Date(),\n * minDate: new Date(2024, 0, 1)\n * });\n *\n * // ✅ undefined is now supported\n * await this.set(\"data\", { name: \"test\", optional: undefined });\n *\n * // ✅ Arrays with undefined are supported\n * await this.set(\"items\", [1, undefined, 3]);\n * await this.set(\"items\", [1, null, 3]); // Also works\n *\n * // ✅ Maps and Sets are supported\n * await this.set(\"mapping\", new Map([[\"key\", \"value\"]]));\n * await this.set(\"tags\", new Set([\"tag1\", \"tag2\"]));\n *\n * // ❌ WRONG: Cannot store functions directly\n * await this.set(\"handler\", this.myHandler);\n *\n * // ✅ CORRECT: Create a callback token first\n * const token = await this.callback(this.myHandler, \"arg1\", \"arg2\");\n * await this.set(\"handler_token\", token);\n *\n * // Later, execute the callback\n * const token = await this.get<Callback>(\"handler_token\");\n * await this.run(token);\n * ```\n *\n * @template T - The type of value being stored (must be Serializable)\n * @param key - The storage key to use\n * @param value - The value to store (must be SuperJSON-serializable)\n * @returns Promise that resolves when the value is stored\n */\n protected async set<T extends Serializable>(\n key: string,\n value: T\n ): Promise<void> {\n return this.tools.store.set(key, value);\n }\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n protected async list(prefix: string): Promise<string[]> {\n return this.tools.store.list(prefix);\n }\n\n /**\n * Removes a specific key from persistent storage.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n protected async clear(key: string): Promise<void> {\n return this.tools.store.clear(key);\n }\n\n /**\n * Removes all keys from this tool's storage.\n *\n * @returns Promise that resolves when all keys are removed\n */\n protected async clearAll(): Promise<void> {\n return this.tools.store.clearAll();\n }\n\n /**\n * Queues a callback to execute in a separate worker context with a fresh request limit.\n *\n * **Creates a NEW execution** with its own request limit of ~1000 requests (HTTP requests,\n * tool calls, database operations). This is the primary way to stay under request limits\n * when processing large datasets or making many API calls.\n *\n * Use this to break long loops into chunks that each stay under the ~1000 request limit.\n * Each task runs in an isolated execution environment with ~1000 requests and ~60 seconds CPU time.\n *\n * @param callback - The callback token created with `this.callback()`\n * @param options - Optional configuration for the execution\n * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately\n * @returns Promise resolving to a cancellation token (only for scheduled executions)\n *\n * @example\n * ```typescript\n * // Break large loop into batches\n * const callback = await this.callback(this.processBatch, 1);\n * await this.runTask(callback); // New execution with fresh request limit\n * ```\n */\n protected async runTask(\n callback: Callback,\n options?: { runAt?: Date }\n ): Promise<string | void> {\n return this.tools.tasks.runTask(callback, options);\n }\n\n /**\n * Cancels a previously scheduled execution.\n *\n * @param token - The cancellation token returned by runTask() with runAt option\n * @returns Promise that resolves when the cancellation is processed\n */\n protected async cancelTask(token: string): Promise<void> {\n return this.tools.tasks.cancelTask(token);\n }\n\n /**\n * Cancels all scheduled executions for this tool.\n *\n * @returns Promise that resolves when all cancellations are processed\n */\n protected async cancelAllTasks(): Promise<void> {\n return this.tools.tasks.cancelAllTasks();\n }\n\n /**\n * Schedules a **singleton** task keyed by `key`: re-scheduling under the same\n * key atomically replaces any pending task, so at most one is ever live.\n *\n * Prefer this over `runTask({ runAt })` for recurring/self-renewing jobs\n * (watch renewals, polling, deferred cleanup) — it removes the error-prone\n * \"store token, cancel before re-scheduling\" bookkeeping that otherwise leaks\n * parallel task chains. See {@link Tasks.scheduleTask}.\n *\n * @param key - Stable identifier scoped to what the task renews\n * @param callback - The callback token created with `this.callback()`\n * @param options.runAt - When to run (required)\n * @returns Promise resolving to the scheduled task's cancellation token\n */\n protected async scheduleTask(\n key: string,\n callback: Callback,\n options: { runAt: Date }\n ): Promise<string | void> {\n return this.tools.tasks.scheduleTask(key, callback, options);\n }\n\n /**\n * Cancels the singleton task previously scheduled under `key` (if any).\n * No-op if none exists or it already ran. See {@link Tasks.cancelScheduledTask}.\n *\n * @param key - The same key passed to {@link scheduleTask}\n * @returns Promise that resolves when the cancellation is processed\n */\n protected async cancelScheduledTask(key: string): Promise<void> {\n return this.tools.tasks.cancelScheduledTask(key);\n }\n\n /**\n * Called before the twist's activate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * activate method is called.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when pre-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n preActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's activate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when post-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n postActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's upgrade method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * upgrade method is called.\n *\n * @returns Promise that resolves when pre-upgrade is complete\n */\n preUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's upgrade method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-upgrade is complete\n */\n postUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's deactivate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * deactivate method is called.\n *\n * @returns Promise that resolves when pre-deactivation is complete\n */\n preDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's deactivate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-deactivation is complete\n */\n postDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Waits for tool initialization to complete.\n * Called automatically by the entrypoint before lifecycle methods.\n * @internal\n */\n async waitForReady(): Promise<void> {\n await this.toolShed.waitForReady();\n }\n}\n";