@plotday/twister 0.34.0 → 0.36.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 (150) hide show
  1. package/bin/templates/AGENTS.template.md +187 -182
  2. package/cli/templates/AGENTS.template.md +187 -182
  3. package/dist/common/calendar.d.ts +20 -90
  4. package/dist/common/calendar.d.ts.map +1 -1
  5. package/dist/common/documents.d.ts +26 -39
  6. package/dist/common/documents.d.ts.map +1 -1
  7. package/dist/common/messaging.d.ts +21 -40
  8. package/dist/common/messaging.d.ts.map +1 -1
  9. package/dist/common/projects.d.ts +27 -51
  10. package/dist/common/projects.d.ts.map +1 -1
  11. package/dist/docs/assets/hierarchy.js +1 -1
  12. package/dist/docs/assets/navigation.js +1 -1
  13. package/dist/docs/assets/search.js +1 -1
  14. package/dist/docs/classes/index.Options.html +10 -0
  15. package/dist/docs/classes/tool.ITool.html +1 -1
  16. package/dist/docs/classes/tool.Tool.html +24 -20
  17. package/dist/docs/classes/tools_ai.AI.html +1 -1
  18. package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
  19. package/dist/docs/classes/tools_integrations.Integrations.html +33 -26
  20. package/dist/docs/classes/tools_network.Network.html +1 -1
  21. package/dist/docs/classes/tools_plot.Plot.html +3 -3
  22. package/dist/docs/classes/tools_store.Store.html +10 -4
  23. package/dist/docs/classes/tools_tasks.Tasks.html +1 -1
  24. package/dist/docs/classes/tools_twists.Twists.html +1 -1
  25. package/dist/docs/enums/plot.ActorType.html +4 -4
  26. package/dist/docs/enums/tools_integrations.AuthProvider.html +11 -11
  27. package/dist/docs/hierarchy.html +1 -1
  28. package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
  29. package/dist/docs/modules/common_calendar.html +1 -1
  30. package/dist/docs/modules/index.html +1 -1
  31. package/dist/docs/modules/plot.html +1 -1
  32. package/dist/docs/modules/tool.html +1 -1
  33. package/dist/docs/modules/tools_integrations.html +1 -1
  34. package/dist/docs/types/common_calendar.Calendar.html +5 -5
  35. package/dist/docs/types/common_calendar.CalendarTool.html +17 -43
  36. package/dist/docs/types/common_calendar.SyncOptions.html +3 -3
  37. package/dist/docs/types/index.BooleanDef.html +7 -0
  38. package/dist/docs/types/index.NumberDef.html +9 -0
  39. package/dist/docs/types/index.OptionDef.html +2 -0
  40. package/dist/docs/types/index.OptionsSchema.html +3 -0
  41. package/dist/docs/types/index.ResolvedOptions.html +4 -0
  42. package/dist/docs/types/index.SelectDef.html +8 -0
  43. package/dist/docs/types/index.TextDef.html +8 -0
  44. package/dist/docs/types/plot.Activity.html +1 -1
  45. package/dist/docs/types/plot.ActivityFilter.html +3 -0
  46. package/dist/docs/types/plot.ActivityOccurrence.html +7 -7
  47. package/dist/docs/types/plot.ActivityOccurrenceUpdate.html +1 -1
  48. package/dist/docs/types/plot.ActivityUpdate.html +3 -31
  49. package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
  50. package/dist/docs/types/plot.Actor.html +5 -5
  51. package/dist/docs/types/plot.ContentType.html +1 -1
  52. package/dist/docs/types/plot.NewActivity.html +1 -1
  53. package/dist/docs/types/plot.NewActivityOccurrence.html +1 -1
  54. package/dist/docs/types/plot.NewActivityWithNotes.html +1 -1
  55. package/dist/docs/types/plot.NewActor.html +1 -1
  56. package/dist/docs/types/plot.NewContact.html +5 -5
  57. package/dist/docs/types/plot.NewNote.html +1 -1
  58. package/dist/docs/types/plot.Note.html +1 -1
  59. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  60. package/dist/docs/types/plot.PickPriorityConfig.html +2 -2
  61. package/dist/docs/types/tool.SyncToolOptions.html +9 -0
  62. package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
  63. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  64. package/dist/docs/types/tools_integrations.IntegrationOptions.html +4 -0
  65. package/dist/docs/types/tools_integrations.IntegrationProviderConfig.html +13 -0
  66. package/dist/docs/types/tools_integrations.Syncable.html +9 -0
  67. package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
  68. package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
  69. package/dist/docs/types/utils_types.InferOptions.html +1 -1
  70. package/dist/docs/types/utils_types.InferTools.html +1 -1
  71. package/dist/docs/types/utils_types.JSONValue.html +1 -1
  72. package/dist/docs/types/utils_types.PromiseValues.html +1 -1
  73. package/dist/docs/types/utils_types.ToolBuilder.html +2 -2
  74. package/dist/index.d.ts +1 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/llm-docs/common/calendar.d.ts +1 -1
  79. package/dist/llm-docs/common/calendar.d.ts.map +1 -1
  80. package/dist/llm-docs/common/calendar.js +1 -1
  81. package/dist/llm-docs/common/calendar.js.map +1 -1
  82. package/dist/llm-docs/common/documents.d.ts +1 -1
  83. package/dist/llm-docs/common/documents.d.ts.map +1 -1
  84. package/dist/llm-docs/common/documents.js +1 -1
  85. package/dist/llm-docs/common/documents.js.map +1 -1
  86. package/dist/llm-docs/common/messaging.d.ts +1 -1
  87. package/dist/llm-docs/common/messaging.d.ts.map +1 -1
  88. package/dist/llm-docs/common/messaging.js +1 -1
  89. package/dist/llm-docs/common/messaging.js.map +1 -1
  90. package/dist/llm-docs/common/projects.d.ts +1 -1
  91. package/dist/llm-docs/common/projects.d.ts.map +1 -1
  92. package/dist/llm-docs/common/projects.js +1 -1
  93. package/dist/llm-docs/common/projects.js.map +1 -1
  94. package/dist/llm-docs/index.d.ts.map +1 -1
  95. package/dist/llm-docs/index.js +2 -0
  96. package/dist/llm-docs/index.js.map +1 -1
  97. package/dist/llm-docs/options.d.ts +9 -0
  98. package/dist/llm-docs/options.d.ts.map +1 -0
  99. package/dist/llm-docs/options.js +8 -0
  100. package/dist/llm-docs/options.js.map +1 -0
  101. package/dist/llm-docs/plot.d.ts +1 -1
  102. package/dist/llm-docs/plot.d.ts.map +1 -1
  103. package/dist/llm-docs/plot.js +1 -1
  104. package/dist/llm-docs/plot.js.map +1 -1
  105. package/dist/llm-docs/tool.d.ts +1 -1
  106. package/dist/llm-docs/tool.d.ts.map +1 -1
  107. package/dist/llm-docs/tool.js +1 -1
  108. package/dist/llm-docs/tool.js.map +1 -1
  109. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  110. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  111. package/dist/llm-docs/tools/integrations.js +1 -1
  112. package/dist/llm-docs/tools/integrations.js.map +1 -1
  113. package/dist/llm-docs/tools/plot.d.ts +1 -1
  114. package/dist/llm-docs/tools/plot.d.ts.map +1 -1
  115. package/dist/llm-docs/tools/plot.js +1 -1
  116. package/dist/llm-docs/tools/plot.js.map +1 -1
  117. package/dist/llm-docs/tools/store.d.ts +1 -1
  118. package/dist/llm-docs/tools/store.d.ts.map +1 -1
  119. package/dist/llm-docs/tools/store.js +1 -1
  120. package/dist/llm-docs/tools/store.js.map +1 -1
  121. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  122. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  123. package/dist/llm-docs/twist-guide-template.js +1 -1
  124. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  125. package/dist/options.d.ts +104 -0
  126. package/dist/options.d.ts.map +1 -0
  127. package/dist/options.js +40 -0
  128. package/dist/options.js.map +1 -0
  129. package/dist/plot.d.ts +32 -13
  130. package/dist/plot.d.ts.map +1 -1
  131. package/dist/plot.js.map +1 -1
  132. package/dist/tool.d.ts +22 -1
  133. package/dist/tool.d.ts.map +1 -1
  134. package/dist/tool.js +9 -0
  135. package/dist/tool.js.map +1 -1
  136. package/dist/tools/integrations.d.ts +88 -46
  137. package/dist/tools/integrations.d.ts.map +1 -1
  138. package/dist/tools/integrations.js +36 -21
  139. package/dist/tools/integrations.js.map +1 -1
  140. package/dist/tools/plot.d.ts +3 -1
  141. package/dist/tools/plot.d.ts.map +1 -1
  142. package/dist/tools/store.d.ts +10 -0
  143. package/dist/tools/store.d.ts.map +1 -1
  144. package/dist/tools/store.js.map +1 -1
  145. package/dist/twist-guide.d.ts +1 -1
  146. package/dist/twist-guide.d.ts.map +1 -1
  147. package/dist/utils/types.d.ts +5 -1
  148. package/dist/utils/types.d.ts.map +1 -1
  149. package/package.json +6 -1
  150. package/dist/docs/types/common_calendar.CalendarAuth.html +0 -7
@@ -4,5 +4,5 @@
4
4
  * This file is auto-generated during build. Do not edit manually.
5
5
  * Generated from: prebuild.ts
6
6
  */
7
- export default "import {\n type Activity,\n type ActivityOccurrence,\n type ActivityUpdate,\n type Actor,\n type ActorId,\n ITool,\n type NewActivity,\n type NewActivityWithNotes,\n type NewContact,\n type NewNote,\n type NewPriority,\n type Note,\n type NoteUpdate,\n type Priority,\n type PriorityUpdate,\n type Tag,\n Uuid,\n} from \"..\";\n\nexport enum ActivityAccess {\n /**\n * Create new Note on an Activity where the twist was mentioned.\n * Add/remove tags on Activity or Note where the twist was mentioned.\n */\n Respond,\n /**\n * Create new Activity.\n * Create new Note in an Activity the twist created.\n * All Respond permissions.\n */\n Create,\n}\n\nexport enum PriorityAccess {\n /**\n * Create a new Priority within the twist's Priority.\n * Update Priority created by the twist.\n */\n Create,\n /**\n * Read all Priority within the twist's Priority.\n * Create a new Priority within the twist's Priority.\n * Update and archive any Priority within the twist's Priority.\n */\n Full,\n}\n\nexport enum ContactAccess {\n /** Read existing contact details. Without this, only the ID will be provided. */\n Read,\n /** Create and update contacts. */\n Write,\n}\n\n/**\n * Intent handler for activity mentions.\n * Defines how the twist should respond when mentioned in an activity.\n */\nexport type NoteIntentHandler = {\n /** Human-readable description of what this intent handles */\n description: string;\n /** Example phrases or activity content that would match this intent */\n examples: string[];\n /** The function to call when this intent is matched */\n handler: (note: Note) => Promise<void>;\n};\n\n/**\n * Built-in tool for interacting with the core Plot data layer.\n *\n * The Plot tool provides twists with the ability to create and manage activities,\n * priorities, and contacts within the Plot system. This is the primary interface\n * for twists to persist data and interact with the Plot database.\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist {\n * private plot: Plot;\n *\n * constructor(id: string, tools: ToolBuilder) {\n * super();\n * this.plot = tools.get(Plot);\n * }\n *\n * async activate(priority) {\n * // Create a welcome activity\n * await this.plot.createActivity({\n * type: ActivityType.Note,\n * title: \"Welcome to Plot!\",\n * links: [{\n * title: \"Get Started\",\n * type: ActivityLinkType.external,\n * url: \"https://plot.day/docs\"\n * }]\n * });\n * }\n * }\n * ```\n */\nexport abstract class Plot extends ITool {\n /**\n * Configuration options for the Plot tool.\n *\n * **Important**: All permissions must be explicitly requested. There are no default permissions.\n *\n * @example\n * ```typescript\n * // Minimal configuration with required permissions\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * activity: {\n * access: ActivityAccess.Create\n * }\n * })\n * };\n * }\n *\n * // Full configuration with callbacks\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * activity: {\n * access: ActivityAccess.Create,\n * updated: this.onActivityUpdated\n * },\n * note: {\n * intents: [{\n * description: \"Schedule meetings\",\n * examples: [\"Schedule a meeting tomorrow\"],\n * handler: this.onSchedulingIntent\n * }],\n * created: this.onNoteCreated\n * },\n * priority: {\n * access: PriorityAccess.Full\n * },\n * contact: {\n * access: ContactAccess.Write\n * }\n * })\n * };\n * }\n * ```\n */\n static readonly Options: {\n activity?: {\n /**\n * Capability to create Notes and modify tags.\n * Must be explicitly set to grant permissions.\n */\n access?: ActivityAccess;\n /**\n * Called when an activity created by this twist is updated.\n * This is often used to implement two-way sync with an external system.\n *\n * @param activity - The updated activity\n * @param changes - Changes to the activity and the previous version\n */\n updated?: (\n activity: Activity,\n changes: {\n tagsAdded: Record<Tag, ActorId[]>;\n tagsRemoved: Record<Tag, ActorId[]>;\n /**\n * If present, this update is for a specific occurrence of a recurring activity.\n */\n occurrence?: ActivityOccurrence;\n }\n ) => Promise<void>;\n };\n note?: {\n /**\n * Respond to mentions in notes.\n *\n * When a note mentions this twist, the system will match the note\n * content against these intents and call the matching handler.\n *\n * @example\n * ```typescript\n * intents: [{\n * description: \"Schedule or reschedule calendar events\",\n * examples: [\"Schedule a meeting tomorrow at 2pm\", \"Move my 3pm meeting to 4pm\"],\n * handler: this.onSchedulingRequest\n * }, {\n * description: \"Find available meeting times\",\n * examples: [\"When am I free this week?\", \"Find time for a 1 hour meeting\"],\n * handler: this.onAvailabilityRequest\n * }]\n * ```\n */\n intents?: NoteIntentHandler[];\n /**\n * Called when a note is created on an activity created by this twist.\n * This is often used to implement two-way sync with an external system,\n * such as syncing notes as comments back to the source system.\n *\n * Notes created by the twist itself are automatically filtered out to prevent loops.\n * The parent activity is available via note.activity.\n *\n * @param note - The newly created note\n */\n created?: (note: Note) => Promise<void>;\n };\n priority?: {\n access?: PriorityAccess;\n };\n contact?: {\n access?: ContactAccess;\n };\n };\n\n /**\n * Creates a new activity in the Plot system.\n *\n * The activity will be automatically assigned an ID and author information\n * based on the current execution context. All other fields from NewActivity\n * will be preserved in the created activity.\n *\n * @param activity - The activity data to create\n * @returns Promise resolving to the created activity's ID\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createActivity(\n activity: NewActivity | NewActivityWithNotes\n ): Promise<Uuid>;\n\n /**\n * Creates multiple activities in a single batch operation.\n *\n * This method efficiently creates multiple activities at once, which is\n * more performant than calling createActivity() multiple times individually.\n * All activities are created with the same author and access control rules.\n *\n * @param activities - Array of activity data to create\n * @returns Promise resolving to array of created activity IDs\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createActivities(\n activities: (NewActivity | NewActivityWithNotes)[]\n ): Promise<Uuid[]>;\n\n /**\n * Updates an existing activity in the Plot system.\n *\n * **Important**: This method only updates existing activities. It will throw an error\n * if the activity does not exist. Use `createActivity()` to create or update (upsert)\n * activities.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire activity object.\n *\n * For tags, provide a Record<number, boolean> where true adds a tag and false removes it.\n * Tags not included in the update remain unchanged.\n *\n * When updating the parent, the activity's path will be automatically recalculated to\n * maintain the correct hierarchical structure.\n *\n * When updating scheduling fields (start, end, recurrence*), the database will\n * automatically recalculate duration and range values to maintain consistency.\n *\n * @param activity - The activity update containing the ID or source and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the activity does not exist\n *\n * @example\n * ```typescript\n * // Mark a task as complete\n * await this.plot.updateActivity({\n * id: \"task-123\",\n * done: new Date()\n * });\n *\n * // Reschedule an event\n * await this.plot.updateActivity({\n * id: \"event-456\",\n * start: new Date(\"2024-03-15T10:00:00Z\"),\n * end: new Date(\"2024-03-15T11:00:00Z\")\n * });\n *\n * // Add and remove tags\n * await this.plot.updateActivity({\n * id: \"activity-789\",\n * tags: {\n * 1: true, // Add tag with ID 1\n * 2: false // Remove tag with ID 2\n * }\n * });\n *\n * // Update a recurring event exception\n * await this.plot.updateActivity({\n * id: \"exception-123\",\n * occurrence: new Date(\"2024-03-20T09:00:00Z\"),\n * title: \"Rescheduled meeting\"\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateActivity(activity: ActivityUpdate): Promise<void>;\n\n /**\n * Retrieves all notes within an activity.\n *\n * Notes are detailed entries within an activity, ordered by creation time.\n * Each note can contain markdown content, links, and other detailed information\n * related to the parent activity.\n *\n * @param activity - The activity whose notes to retrieve\n * @returns Promise resolving to array of notes in the activity\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNotes(activity: Activity): Promise<Note[]>;\n\n /**\n * Creates a new note in an activity.\n *\n * Notes provide detailed content within an activity, supporting markdown,\n * links, and other rich content. The note will be automatically assigned\n * an ID and author information based on the current execution context.\n *\n * @param note - The note data to create\n * @returns Promise resolving to the created note's ID\n *\n * @example\n * ```typescript\n * // Create a note with content\n * await this.plot.createNote({\n * activity: { id: \"activity-123\" },\n * note: \"Discussion notes from the meeting...\",\n * contentType: \"markdown\"\n * });\n *\n * // Create a note with links\n * await this.plot.createNote({\n * activity: { id: \"activity-456\" },\n * note: \"Meeting recording available\",\n * links: [{\n * type: ActivityLinkType.external,\n * title: \"View Recording\",\n * url: \"https://example.com/recording\"\n * }]\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNote(note: NewNote): Promise<Uuid>;\n\n /**\n * Creates multiple notes in a single batch operation.\n *\n * This method efficiently creates multiple notes at once, which is\n * more performant than calling createNote() multiple times individually.\n * All notes are created with the same author and access control rules.\n *\n * @param notes - Array of note data to create\n * @returns Promise resolving to array of created note IDs\n *\n * @example\n * ```typescript\n * // Create multiple notes in one batch\n * await this.plot.createNotes([\n * {\n * activity: { id: \"activity-123\" },\n * note: \"First message in thread\"\n * },\n * {\n * activity: { id: \"activity-123\" },\n * note: \"Second message in thread\"\n * }\n * ]);\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNotes(notes: NewNote[]): Promise<Uuid[]>;\n\n /**\n * Updates an existing note in the Plot system.\n *\n * **Important**: This method only updates existing notes. It will throw an error\n * if the note does not exist. Use `createNote()` to create or update (upsert) notes.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire note object.\n *\n * @param note - The note update containing the ID or key and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the note does not exist\n *\n * @example\n * ```typescript\n * // Update note content\n * await this.plot.updateNote({\n * id: \"note-123\",\n * note: \"Updated content with more details\"\n * });\n *\n * // Add tags to a note\n * await this.plot.updateNote({\n * id: \"note-456\",\n * twistTags: {\n * [Tag.Important]: true\n * }\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateNote(note: NoteUpdate): Promise<void>;\n\n /**\n * Retrieves an activity by ID or source.\n *\n * This method enables lookup of activities either by their unique ID or by their\n * source identifier (canonical URL from an external system). Archived activities\n * are included in the results.\n *\n * @param activity - Activity lookup by ID or source\n * @returns Promise resolving to the matching activity or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getActivity(\n activity: { id: Uuid } | { source: string }\n ): Promise<Activity | null>;\n\n /**\n * Retrieves a note by ID or key.\n *\n * This method enables lookup of notes either by their unique ID or by their\n * key (unique identifier within the activity). Archived notes are included\n * in the results.\n *\n * @param note - Note lookup by ID or key\n * @returns Promise resolving to the matching note or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNote(note: { id: Uuid } | { key: string }): Promise<Note | null>;\n\n /**\n * Creates a new priority in the Plot system.\n *\n * Priorities serve as organizational containers for activities and twists.\n * The created priority will be automatically assigned a unique ID.\n *\n * @param priority - The priority data to create\n * @returns Promise resolving to the complete created priority\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createPriority(priority: NewPriority): Promise<Priority>;\n\n /**\n * Retrieves a priority by ID or key.\n *\n * Archived priorities are included in the results.\n *\n * @param priority - Priority lookup by ID or key\n * @returns Promise resolving to the matching priority or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getPriority(\n priority: { id: Uuid } | { key: string }\n ): Promise<Priority | null>;\n\n /**\n * Updates an existing priority in the Plot system.\n *\n * The priority is identified by either its ID or key.\n * Only the fields specified in the update will be changed.\n *\n * @param update - Priority update containing ID/key and fields to change\n * @returns Promise that resolves when the update is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updatePriority(update: PriorityUpdate): Promise<void>;\n\n /**\n * Adds contacts to the Plot system.\n *\n * Contacts are used for associating people with activities, such as\n * event attendees or task assignees. Duplicate contacts (by email)\n * will be merged or updated as appropriate.\n * This method requires ContactAccess.Write permission.\n *\n * @param contacts - Array of contact information to add\n * @returns Promise resolving to array of created/updated actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract addContacts(contacts: Array<NewContact>): Promise<Actor[]>;\n\n /**\n * Retrieves actors by their IDs.\n *\n * Actors represent users, contacts, or twists in the Plot system.\n * This method requires ContactAccess.Read permission.\n *\n * @param ids - Array of actor IDs to retrieve\n * @returns Promise resolving to array of actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getActors(ids: ActorId[]): Promise<Actor[]>;\n}\n";
7
+ export default "import {\n type Activity,\n type ActivityOccurrence,\n type ActivityUpdate,\n type Actor,\n type ActorId,\n ITool,\n type NewActivity,\n type NewActivityWithNotes,\n type NewContact,\n type NewNote,\n type NewPriority,\n type Note,\n type NoteUpdate,\n type Priority,\n type PriorityUpdate,\n type Tag,\n Uuid,\n} from \"..\";\n\nexport enum ActivityAccess {\n /**\n * Create new Note on an Activity where the twist was mentioned.\n * Add/remove tags on Activity or Note where the twist was mentioned.\n */\n Respond,\n /**\n * Create new Activity.\n * Create new Note in an Activity the twist created.\n * All Respond permissions.\n */\n Create,\n}\n\nexport enum PriorityAccess {\n /**\n * Create a new Priority within the twist's Priority.\n * Update Priority created by the twist.\n */\n Create,\n /**\n * Read all Priority within the twist's Priority.\n * Create a new Priority within the twist's Priority.\n * Update and archive any Priority within the twist's Priority.\n */\n Full,\n}\n\nexport enum ContactAccess {\n /** Read existing contact details. Without this, only the ID will be provided. */\n Read,\n /** Create and update contacts. */\n Write,\n}\n\n/**\n * Intent handler for activity mentions.\n * Defines how the twist should respond when mentioned in an activity.\n */\nexport type NoteIntentHandler = {\n /** Human-readable description of what this intent handles */\n description: string;\n /** Example phrases or activity content that would match this intent */\n examples: string[];\n /** The function to call when this intent is matched */\n handler: (note: Note) => Promise<void>;\n};\n\n/**\n * Built-in tool for interacting with the core Plot data layer.\n *\n * The Plot tool provides twists with the ability to create and manage activities,\n * priorities, and contacts within the Plot system. This is the primary interface\n * for twists to persist data and interact with the Plot database.\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist {\n * private plot: Plot;\n *\n * constructor(id: string, tools: ToolBuilder) {\n * super();\n * this.plot = tools.get(Plot);\n * }\n *\n * async activate(priority) {\n * // Create a welcome activity\n * await this.plot.createActivity({\n * type: ActivityType.Note,\n * title: \"Welcome to Plot!\",\n * links: [{\n * title: \"Get Started\",\n * type: ActivityLinkType.external,\n * url: \"https://plot.day/docs\"\n * }]\n * });\n * }\n * }\n * ```\n */\nexport abstract class Plot extends ITool {\n /**\n * Configuration options for the Plot tool.\n *\n * **Important**: All permissions must be explicitly requested. There are no default permissions.\n *\n * @example\n * ```typescript\n * // Minimal configuration with required permissions\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * activity: {\n * access: ActivityAccess.Create\n * }\n * })\n * };\n * }\n *\n * // Full configuration with callbacks\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * activity: {\n * access: ActivityAccess.Create,\n * updated: this.onActivityUpdated\n * },\n * note: {\n * intents: [{\n * description: \"Schedule meetings\",\n * examples: [\"Schedule a meeting tomorrow\"],\n * handler: this.onSchedulingIntent\n * }],\n * created: this.onNoteCreated\n * },\n * priority: {\n * access: PriorityAccess.Full\n * },\n * contact: {\n * access: ContactAccess.Write\n * }\n * })\n * };\n * }\n * ```\n */\n static readonly Options: {\n activity?: {\n /**\n * Capability to create Notes and modify tags.\n * Must be explicitly set to grant permissions.\n */\n access?: ActivityAccess;\n /**\n * Called when an activity created by this twist is updated.\n * This is often used to implement two-way sync with an external system.\n *\n * @param activity - The updated activity\n * @param changes - Changes to the activity and the previous version\n */\n updated?: (\n activity: Activity,\n changes: {\n tagsAdded: Record<Tag, ActorId[]>;\n tagsRemoved: Record<Tag, ActorId[]>;\n /**\n * If present, this update is for a specific occurrence of a recurring activity.\n */\n occurrence?: ActivityOccurrence;\n }\n ) => Promise<void>;\n };\n note?: {\n /**\n * Respond to mentions in notes.\n *\n * When a note mentions this twist, the system will match the note\n * content against these intents and call the matching handler.\n *\n * @example\n * ```typescript\n * intents: [{\n * description: \"Schedule or reschedule calendar events\",\n * examples: [\"Schedule a meeting tomorrow at 2pm\", \"Move my 3pm meeting to 4pm\"],\n * handler: this.onSchedulingRequest\n * }, {\n * description: \"Find available meeting times\",\n * examples: [\"When am I free this week?\", \"Find time for a 1 hour meeting\"],\n * handler: this.onAvailabilityRequest\n * }]\n * ```\n */\n intents?: NoteIntentHandler[];\n /**\n * Called when a note is created on an activity created by this twist.\n * This is often used to implement two-way sync with an external system,\n * such as syncing notes as comments back to the source system.\n *\n * Notes created by the twist itself are automatically filtered out to prevent loops.\n * The parent activity is available via note.activity.\n *\n * @param note - The newly created note\n */\n created?: (note: Note) => Promise<void>;\n };\n priority?: {\n access?: PriorityAccess;\n };\n contact?: {\n access?: ContactAccess;\n };\n };\n\n /**\n * Creates a new activity in the Plot system.\n *\n * The activity will be automatically assigned an ID and author information\n * based on the current execution context. All other fields from NewActivity\n * will be preserved in the created activity.\n *\n * @param activity - The activity data to create\n * @returns Promise resolving to the created activity's ID\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createActivity(\n activity: NewActivity | NewActivityWithNotes\n ): Promise<Uuid>;\n\n /**\n * Creates multiple activities in a single batch operation.\n *\n * This method efficiently creates multiple activities at once, which is\n * more performant than calling createActivity() multiple times individually.\n * All activities are created with the same author and access control rules.\n *\n * @param activities - Array of activity data to create\n * @returns Promise resolving to array of created activity IDs\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createActivities(\n activities: (NewActivity | NewActivityWithNotes)[]\n ): Promise<Uuid[]>;\n\n /**\n * Updates an existing activity in the Plot system.\n *\n * **Important**: This method only updates existing activities. It will throw an error\n * if the activity does not exist. Use `createActivity()` to create or update (upsert)\n * activities.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire activity object.\n *\n * For tags, provide a Record<number, boolean> where true adds a tag and false removes it.\n * Tags not included in the update remain unchanged.\n *\n * When updating the parent, the activity's path will be automatically recalculated to\n * maintain the correct hierarchical structure.\n *\n * When updating scheduling fields (start, end, recurrence*), the database will\n * automatically recalculate duration and range values to maintain consistency.\n *\n * @param activity - The activity update containing the ID or source and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the activity does not exist\n *\n * @example\n * ```typescript\n * // Mark a task as complete\n * await this.plot.updateActivity({\n * id: \"task-123\",\n * done: new Date()\n * });\n *\n * // Reschedule an event\n * await this.plot.updateActivity({\n * id: \"event-456\",\n * start: new Date(\"2024-03-15T10:00:00Z\"),\n * end: new Date(\"2024-03-15T11:00:00Z\")\n * });\n *\n * // Add and remove tags\n * await this.plot.updateActivity({\n * id: \"activity-789\",\n * tags: {\n * 1: true, // Add tag with ID 1\n * 2: false // Remove tag with ID 2\n * }\n * });\n *\n * // Update a recurring event exception\n * await this.plot.updateActivity({\n * id: \"exception-123\",\n * occurrence: new Date(\"2024-03-20T09:00:00Z\"),\n * title: \"Rescheduled meeting\"\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateActivity(activity: ActivityUpdate): Promise<void>;\n\n /**\n * Retrieves all notes within an activity.\n *\n * Notes are detailed entries within an activity, ordered by creation time.\n * Each note can contain markdown content, links, and other detailed information\n * related to the parent activity.\n *\n * @param activity - The activity whose notes to retrieve\n * @returns Promise resolving to array of notes in the activity\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNotes(activity: Activity): Promise<Note[]>;\n\n /**\n * Creates a new note in an activity.\n *\n * Notes provide detailed content within an activity, supporting markdown,\n * links, and other rich content. The note will be automatically assigned\n * an ID and author information based on the current execution context.\n *\n * @param note - The note data to create\n * @returns Promise resolving to the created note's ID\n *\n * @example\n * ```typescript\n * // Create a note with content\n * await this.plot.createNote({\n * activity: { id: \"activity-123\" },\n * note: \"Discussion notes from the meeting...\",\n * contentType: \"markdown\"\n * });\n *\n * // Create a note with links\n * await this.plot.createNote({\n * activity: { id: \"activity-456\" },\n * note: \"Meeting recording available\",\n * links: [{\n * type: ActivityLinkType.external,\n * title: \"View Recording\",\n * url: \"https://example.com/recording\"\n * }]\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNote(note: NewNote): Promise<Uuid>;\n\n /**\n * Creates multiple notes in a single batch operation.\n *\n * This method efficiently creates multiple notes at once, which is\n * more performant than calling createNote() multiple times individually.\n * All notes are created with the same author and access control rules.\n *\n * @param notes - Array of note data to create\n * @returns Promise resolving to array of created note IDs\n *\n * @example\n * ```typescript\n * // Create multiple notes in one batch\n * await this.plot.createNotes([\n * {\n * activity: { id: \"activity-123\" },\n * note: \"First message in thread\"\n * },\n * {\n * activity: { id: \"activity-123\" },\n * note: \"Second message in thread\"\n * }\n * ]);\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNotes(notes: NewNote[]): Promise<Uuid[]>;\n\n /**\n * Updates an existing note in the Plot system.\n *\n * **Important**: This method only updates existing notes. It will throw an error\n * if the note does not exist. Use `createNote()` to create or update (upsert) notes.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire note object.\n *\n * @param note - The note update containing the ID or key and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the note does not exist\n *\n * @example\n * ```typescript\n * // Update note content\n * await this.plot.updateNote({\n * id: \"note-123\",\n * note: \"Updated content with more details\"\n * });\n *\n * // Add tags to a note\n * await this.plot.updateNote({\n * id: \"note-456\",\n * twistTags: {\n * [Tag.Important]: true\n * }\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateNote(note: NoteUpdate): Promise<void>;\n\n /**\n * Retrieves an activity by ID or source.\n *\n * This method enables lookup of activities either by their unique ID or by their\n * source identifier (canonical URL from an external system). Archived activities\n * are included in the results.\n *\n * @param activity - Activity lookup by ID or source\n * @returns Promise resolving to the matching activity or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getActivity(\n activity: { id: Uuid } | { source: string }\n ): Promise<Activity | null>;\n\n /**\n * Retrieves a note by ID or key.\n *\n * This method enables lookup of notes either by their unique ID or by their\n * key (unique identifier within the activity). Archived notes are included\n * in the results.\n *\n * @param note - Note lookup by ID or key\n * @returns Promise resolving to the matching note or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNote(note: { id: Uuid } | { key: string }): Promise<Note | null>;\n\n /**\n * Creates a new priority in the Plot system.\n *\n * Priorities serve as organizational containers for activities and twists.\n * The created priority will be automatically assigned a unique ID.\n *\n * @param priority - The priority data to create\n * @returns Promise resolving to the complete created priority\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createPriority(priority: NewPriority): Promise<Priority & { created: boolean }>;\n\n /**\n * Retrieves a priority by ID or key.\n *\n * Archived priorities are included in the results.\n *\n * @param priority - Priority lookup by ID or key\n * @returns Promise resolving to the matching priority or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getPriority(\n priority: { id: Uuid } | { key: string }\n ): Promise<Priority | null>;\n\n /**\n * Updates an existing priority in the Plot system.\n *\n * The priority is identified by either its ID or key.\n * Only the fields specified in the update will be changed.\n *\n * @param update - Priority update containing ID/key and fields to change\n * @returns Promise that resolves when the update is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updatePriority(update: PriorityUpdate): Promise<void>;\n\n /**\n * Adds contacts to the Plot system.\n *\n * Contacts are used for associating people with activities, such as\n * event attendees or task assignees. Duplicate contacts (by email)\n * will be merged or updated as appropriate.\n * This method requires ContactAccess.Write permission.\n *\n * @param contacts - Array of contact information to add\n * @returns Promise resolving to array of created/updated actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract addContacts(contacts: Array<NewContact>): Promise<Actor[]>;\n\n /**\n * Retrieves actors by their IDs.\n *\n * Actors represent users, contacts, or twists in the Plot system.\n * This method requires ContactAccess.Read permission.\n *\n * @param ids - Array of actor IDs to retrieve\n * @returns Promise resolving to array of actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getActors(ids: ActorId[]): Promise<Actor[]>;\n}\n";
8
8
  //# sourceMappingURL=plot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plot.js","sourceRoot":"","sources":["../../../src/llm-docs/tools/plot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,w0gBAAw0gB,CAAC"}
1
+ {"version":3,"file":"plot.js","sourceRoot":"","sources":["../../../src/llm-docs/tools/plot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,+1gBAA+1gB,CAAC"}
@@ -4,6 +4,6 @@
4
4
  * This file is auto-generated during build. Do not edit manually.
5
5
  * Generated from: prebuild.ts
6
6
  */
7
- declare const _default: "import { ITool, type Serializable } from \"..\";\n\n/**\n * Built-in tool for persistent key-value storage.\n *\n * The Store tool provides twists and tools with a simple, persistent storage\n * mechanism that survives worker restarts and invocations. Each twist/tool\n * instance gets its own isolated storage namespace.\n *\n * **Note:** Store methods are also available directly on Twist and Tool classes\n * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.\n * This is the recommended approach for most use cases.\n *\n * **Storage Characteristics:**\n * - Persistent across worker restarts\n * - Isolated per twist/tool instance\n * - Supports SuperJSON-serializable data (see below)\n * - Async operations for scalability\n *\n * **Supported Data Types (via SuperJSON):**\n * - Primitives: string, number, boolean, null, undefined\n * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt\n * - Collections: Arrays and objects (recursively)\n *\n * **NOT Supported (will throw validation errors):**\n * - Functions (use callback tokens instead - see Callbacks tool)\n * - Symbols\n * - Circular references\n * - Custom class instances\n *\n * **Use Cases:**\n * - Storing authentication tokens\n * - Caching configuration data\n * - Maintaining sync state\n * - Persisting user preferences\n * - Tracking processing checkpoints\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool {\n * async saveAuthToken(provider: string, token: string) {\n * // Using built-in set method (recommended)\n * await this.set(`auth_token_${provider}`, token);\n * }\n *\n * async getAuthToken(provider: string): Promise<string | null> {\n * // Using built-in get method (recommended)\n * return await this.get<string>(`auth_token_${provider}`);\n * }\n *\n * async clearAllTokens() {\n * // Using built-in clearAll method (recommended)\n * await this.clearAll();\n * }\n * }\n * ```\n */\nexport abstract class Store extends ITool {\n /**\n * Retrieves a value from storage by key.\n *\n * Returns the stored value deserialized to the specified type,\n * or null if the key doesn't exist or the value is null.\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 // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get<T extends Serializable>(key: string): Promise<T | null>;\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * Any existing value at the same key will be overwritten.\n *\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\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 * @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 }); // \u2705 Works\n *\n * // Arrays with undefined are supported\n * await this.set('items', [1, undefined, 3]); // \u2705 Works\n * await this.set('items', [1, null, 3]); // \u2705 Also works\n *\n * // Maps and Sets are supported\n * await this.set('mapping', new Map([['key', 'value']])); // \u2705 Works\n * await this.set('tags', new Set(['tag1', 'tag2'])); // \u2705 Works\n *\n * // Functions are NOT supported - use callback tokens instead\n * const token = await this.callback(this.myFunction);\n * await this.set('callback_ref', token); // \u2705 Use callback token\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract set<T extends Serializable>(key: string, value: T): Promise<void>;\n\n /**\n * Removes a specific key from storage.\n *\n * After this operation, get() calls for this key will return null.\n * No error is thrown if the key doesn't exist.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract clear(key: string): Promise<void>;\n\n /**\n * Removes all keys from this storage instance.\n *\n * This operation clears all data stored by this twist/tool instance\n * but does not affect storage for other twists or tools.\n *\n * @returns Promise that resolves when all keys are removed\n */\n abstract clearAll(): Promise<void>;\n}\n";
7
+ declare const _default: "import { ITool, type Serializable } from \"..\";\n\n/**\n * Built-in tool for persistent key-value storage.\n *\n * The Store tool provides twists and tools with a simple, persistent storage\n * mechanism that survives worker restarts and invocations. Each twist/tool\n * instance gets its own isolated storage namespace.\n *\n * **Note:** Store methods are also available directly on Twist and Tool classes\n * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.\n * This is the recommended approach for most use cases.\n *\n * **Storage Characteristics:**\n * - Persistent across worker restarts\n * - Isolated per twist/tool instance\n * - Supports SuperJSON-serializable data (see below)\n * - Async operations for scalability\n *\n * **Supported Data Types (via SuperJSON):**\n * - Primitives: string, number, boolean, null, undefined\n * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt\n * - Collections: Arrays and objects (recursively)\n *\n * **NOT Supported (will throw validation errors):**\n * - Functions (use callback tokens instead - see Callbacks tool)\n * - Symbols\n * - Circular references\n * - Custom class instances\n *\n * **Use Cases:**\n * - Storing authentication tokens\n * - Caching configuration data\n * - Maintaining sync state\n * - Persisting user preferences\n * - Tracking processing checkpoints\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool {\n * async saveAuthToken(provider: string, token: string) {\n * // Using built-in set method (recommended)\n * await this.set(`auth_token_${provider}`, token);\n * }\n *\n * async getAuthToken(provider: string): Promise<string | null> {\n * // Using built-in get method (recommended)\n * return await this.get<string>(`auth_token_${provider}`);\n * }\n *\n * async clearAllTokens() {\n * // Using built-in clearAll method (recommended)\n * await this.clearAll();\n * }\n * }\n * ```\n */\nexport abstract class Store extends ITool {\n /**\n * Retrieves a value from storage by key.\n *\n * Returns the stored value deserialized to the specified type,\n * or null if the key doesn't exist or the value is null.\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 // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get<T extends Serializable>(key: string): Promise<T | null>;\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * Any existing value at the same key will be overwritten.\n *\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\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 * @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 }); // \u2705 Works\n *\n * // Arrays with undefined are supported\n * await this.set('items', [1, undefined, 3]); // \u2705 Works\n * await this.set('items', [1, null, 3]); // \u2705 Also works\n *\n * // Maps and Sets are supported\n * await this.set('mapping', new Map([['key', 'value']])); // \u2705 Works\n * await this.set('tags', new Set(['tag1', 'tag2'])); // \u2705 Works\n *\n * // Functions are NOT supported - use callback tokens instead\n * const token = await this.callback(this.myFunction);\n * await this.set('callback_ref', token); // \u2705 Use callback token\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract set<T extends Serializable>(key: string, value: T): Promise<void>;\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * Returns an array of key strings that start with the given prefix.\n * Useful for finding all keys in a namespace (e.g., all sync locks).\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract list(prefix: string): Promise<string[]>;\n\n /**\n * Removes a specific key from storage.\n *\n * After this operation, get() calls for this key will return null.\n * No error is thrown if the key doesn't exist.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract clear(key: string): Promise<void>;\n\n /**\n * Removes all keys from this storage instance.\n *\n * This operation clears all data stored by this twist/tool instance\n * but does not affect storage for other twists or tools.\n *\n * @returns Promise that resolves when all keys are removed\n */\n abstract clearAll(): Promise<void>;\n}\n";
8
8
  export default _default;
9
9
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/llm-docs/tools/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;wBAEY,s9JAAw7J;AAAv8J,wBAAw8J"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/llm-docs/tools/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;wBAEY,q6KAAu4K;AAAt5K,wBAAu5K"}
@@ -4,5 +4,5 @@
4
4
  * This file is auto-generated during build. Do not edit manually.
5
5
  * Generated from: prebuild.ts
6
6
  */
7
- export default "import { ITool, type Serializable } from \"..\";\n\n/**\n * Built-in tool for persistent key-value storage.\n *\n * The Store tool provides twists and tools with a simple, persistent storage\n * mechanism that survives worker restarts and invocations. Each twist/tool\n * instance gets its own isolated storage namespace.\n *\n * **Note:** Store methods are also available directly on Twist and Tool classes\n * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.\n * This is the recommended approach for most use cases.\n *\n * **Storage Characteristics:**\n * - Persistent across worker restarts\n * - Isolated per twist/tool instance\n * - Supports SuperJSON-serializable data (see below)\n * - Async operations for scalability\n *\n * **Supported Data Types (via SuperJSON):**\n * - Primitives: string, number, boolean, null, undefined\n * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt\n * - Collections: Arrays and objects (recursively)\n *\n * **NOT Supported (will throw validation errors):**\n * - Functions (use callback tokens instead - see Callbacks tool)\n * - Symbols\n * - Circular references\n * - Custom class instances\n *\n * **Use Cases:**\n * - Storing authentication tokens\n * - Caching configuration data\n * - Maintaining sync state\n * - Persisting user preferences\n * - Tracking processing checkpoints\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool {\n * async saveAuthToken(provider: string, token: string) {\n * // Using built-in set method (recommended)\n * await this.set(`auth_token_${provider}`, token);\n * }\n *\n * async getAuthToken(provider: string): Promise<string | null> {\n * // Using built-in get method (recommended)\n * return await this.get<string>(`auth_token_${provider}`);\n * }\n *\n * async clearAllTokens() {\n * // Using built-in clearAll method (recommended)\n * await this.clearAll();\n * }\n * }\n * ```\n */\nexport abstract class Store extends ITool {\n /**\n * Retrieves a value from storage by key.\n *\n * Returns the stored value deserialized to the specified type,\n * or null if the key doesn't exist or the value is null.\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 // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get<T extends Serializable>(key: string): Promise<T | null>;\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * Any existing value at the same key will be overwritten.\n *\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\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 * @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 }); // ✅ Works\n *\n * // Arrays with undefined are supported\n * await this.set('items', [1, undefined, 3]); // ✅ Works\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']])); // ✅ Works\n * await this.set('tags', new Set(['tag1', 'tag2'])); // ✅ Works\n *\n * // Functions are NOT supported - use callback tokens instead\n * const token = await this.callback(this.myFunction);\n * await this.set('callback_ref', token); // ✅ Use callback token\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract set<T extends Serializable>(key: string, value: T): Promise<void>;\n\n /**\n * Removes a specific key from storage.\n *\n * After this operation, get() calls for this key will return null.\n * No error is thrown if the key doesn't exist.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract clear(key: string): Promise<void>;\n\n /**\n * Removes all keys from this storage instance.\n *\n * This operation clears all data stored by this twist/tool instance\n * but does not affect storage for other twists or tools.\n *\n * @returns Promise that resolves when all keys are removed\n */\n abstract clearAll(): Promise<void>;\n}\n";
7
+ export default "import { ITool, type Serializable } from \"..\";\n\n/**\n * Built-in tool for persistent key-value storage.\n *\n * The Store tool provides twists and tools with a simple, persistent storage\n * mechanism that survives worker restarts and invocations. Each twist/tool\n * instance gets its own isolated storage namespace.\n *\n * **Note:** Store methods are also available directly on Twist and Tool classes\n * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.\n * This is the recommended approach for most use cases.\n *\n * **Storage Characteristics:**\n * - Persistent across worker restarts\n * - Isolated per twist/tool instance\n * - Supports SuperJSON-serializable data (see below)\n * - Async operations for scalability\n *\n * **Supported Data Types (via SuperJSON):**\n * - Primitives: string, number, boolean, null, undefined\n * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt\n * - Collections: Arrays and objects (recursively)\n *\n * **NOT Supported (will throw validation errors):**\n * - Functions (use callback tokens instead - see Callbacks tool)\n * - Symbols\n * - Circular references\n * - Custom class instances\n *\n * **Use Cases:**\n * - Storing authentication tokens\n * - Caching configuration data\n * - Maintaining sync state\n * - Persisting user preferences\n * - Tracking processing checkpoints\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool {\n * async saveAuthToken(provider: string, token: string) {\n * // Using built-in set method (recommended)\n * await this.set(`auth_token_${provider}`, token);\n * }\n *\n * async getAuthToken(provider: string): Promise<string | null> {\n * // Using built-in get method (recommended)\n * return await this.get<string>(`auth_token_${provider}`);\n * }\n *\n * async clearAllTokens() {\n * // Using built-in clearAll method (recommended)\n * await this.clearAll();\n * }\n * }\n * ```\n */\nexport abstract class Store extends ITool {\n /**\n * Retrieves a value from storage by key.\n *\n * Returns the stored value deserialized to the specified type,\n * or null if the key doesn't exist or the value is null.\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 // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get<T extends Serializable>(key: string): Promise<T | null>;\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * Any existing value at the same key will be overwritten.\n *\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\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 * @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 }); // ✅ Works\n *\n * // Arrays with undefined are supported\n * await this.set('items', [1, undefined, 3]); // ✅ Works\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']])); // ✅ Works\n * await this.set('tags', new Set(['tag1', 'tag2'])); // ✅ Works\n *\n * // Functions are NOT supported - use callback tokens instead\n * const token = await this.callback(this.myFunction);\n * await this.set('callback_ref', token); // ✅ Use callback token\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract set<T extends Serializable>(key: string, value: T): Promise<void>;\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * Returns an array of key strings that start with the given prefix.\n * Useful for finding all keys in a namespace (e.g., all sync locks).\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract list(prefix: string): Promise<string[]>;\n\n /**\n * Removes a specific key from storage.\n *\n * After this operation, get() calls for this key will return null.\n * No error is thrown if the key doesn't exist.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract clear(key: string): Promise<void>;\n\n /**\n * Removes all keys from this storage instance.\n *\n * This operation clears all data stored by this twist/tool instance\n * but does not affect storage for other twists or tools.\n *\n * @returns Promise that resolves when all keys are removed\n */\n abstract clearAll(): Promise<void>;\n}\n";
8
8
  //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/llm-docs/tools/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,w7JAAw7J,CAAC"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/llm-docs/tools/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,u4KAAu4K,CAAC"}
@@ -4,6 +4,6 @@
4
4
  * This file is auto-generated during build. Do not edit manually.
5
5
  * Generated from: cli/templates/AGENTS.template.md
6
6
  */
7
- declare const _default: "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Activities and Notes\n\n**CRITICAL CONCEPT**: An **Activity** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that activity.\n\n**Think of an Activity as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Activities with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Activities for updates** - Don't create a new Activity for each related message\n3. **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot activities per external item (see SYNC_STRATEGIES.md)\n5. **Most Activities should be `ActivityType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n \u251C\u2500 Has stable URL or ID?\n \u2502 \u2514\u2500 Yes \u2192 Set Activity.source to the canonical URL/ID\n \u2502 Create Activity (Plot handles deduplication automatically)\n \u2502 Use Note.key for different note types:\n \u2502 - \"description\" for main content\n \u2502 - \"metadata\" for status/priority/assignee\n \u2502 - \"comment-{id}\" for individual comments\n \u2502\n \u2514\u2500 No stable identifier OR need multiple Plot activities per external item?\n \u2514\u2500 Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple activities from one external item):\n\n```\nNew event/task/conversation?\n \u251C\u2500 Yes \u2192 Generate UUID with Uuid.Generate()\n \u2502 Create new Activity with that UUID\n \u2502 Store mapping: external_id \u2192 activity_uuid\n \u2502\n \u2514\u2500 No (update/reply/comment) \u2192 Look up mapping by external_id\n \u251C\u2500 Found \u2192 Add Note to existing Activity using stored UUID\n \u2514\u2500 Not found \u2192 Create new Activity with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Activity,\n type Priority,\n type ToolBuilder,\n twist,\n} from \"@plotday/twister\";\nimport { Plot } from \"@plotday/twister/tools/plot\";\nimport { Uuid } from \"@plotday/twister/utils/uuid\";\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n plot: build(Plot),\n };\n }\n\n async activate(priority: Pick<Priority, \"id\">) {\n // Called when twist is enabled for a priority\n // Common actions: request auth, create setup activities\n }\n\n async activity(activity: Activity) {\n // Called when an activity is routed to this twist\n // Common actions: process external events, update activities\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n### External Tools (Add to package.json)\n\nAdd tool dependencies to `package.json`:\n\n```json\n{\n \"dependencies\": {\n \"@plotday/twister\": \"workspace:^\",\n \"@plotday/tool-google-calendar\": \"workspace:^\"\n }\n}\n```\n\n#### Common External Tools\n\n- `@plotday/tool-google-calendar`: Google Calendar integration\n- `@plotday/tool-outlook-calendar`: Outlook Calendar integration\n- `@plotday/tool-google-contacts`: Google Contacts integration\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Common patterns:\n\n**Request Authentication:**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const authLink = await this.tools.externalTool.requestAuth(\n this.onAuthComplete,\n \"google\"\n );\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Connect your account\",\n notes: [\n {\n content: \"Click the link below to connect your account and start syncing.\",\n links: [authLink],\n },\n ],\n });\n}\n```\n\n**Store Parent Activity for Later:**\n\n```typescript\nconst activity = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Setup\",\n notes: [\n {\n content: \"Your twist is being set up. Configuration steps will appear here.\",\n },\n ],\n});\n\nawait this.set(\"setup_activity_id\", activity.id);\n```\n\n### activity(activity: Activity)\n\nCalled when an activity is routed to the twist. Common patterns:\n\n**Create Activities from External Events:**\n\n```typescript\nasync activity(activity: Activity) {\n await this.tools.plot.createActivity(activity);\n}\n```\n\n**Update Based on User Action:**\n\n```typescript\nasync activity(activity: Activity) {\n if (activity.completed) {\n await this.handleCompletion(activity);\n }\n}\n```\n\n## Activity Links\n\nActivity links enable user interaction:\n\n```typescript\nimport { type ActivityLink, ActivityLinkType } from \"@plotday/twister\";\n\n// URL link\nconst urlLink: ActivityLink = {\n title: \"Open website\",\n type: ActivityLinkType.url,\n url: \"https://example.com\",\n};\n\n// Callback link (uses Callbacks tool)\nconst token = await this.callback(this.onLinkClicked, \"context\");\nconst callbackLink: ActivityLink = {\n title: \"Click me\",\n type: ActivityLinkType.callback,\n token: token,\n};\n\n// Add to activity note\nawait this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Task with links\",\n notes: [\n {\n content: \"Click the links below to take action.\",\n links: [urlLink, callbackLink],\n },\n ],\n});\n```\n\n## Authentication Pattern\n\nCommon pattern for OAuth authentication:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Request auth link from tool with callback\n const authLink = await this.tools.googleTool.requestAuth(\n this.onAuthComplete,\n \"google\"\n );\n\n // Create activity with auth link\n const activity = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Connect Google account\",\n notes: [\n {\n content: \"Click below to connect your Google account and start syncing.\",\n links: [authLink],\n },\n ],\n });\n\n // Store for later use\n await this.set(\"auth_activity_id\", activity.id);\n}\n\nasync onAuthComplete(authResult: { authToken: string }, provider: string) {\n // Store auth token\n await this.set(`${provider}_auth`, authResult.authToken);\n\n // Continue setup flow\n await this.setupSyncOptions(authResult.authToken);\n}\n```\n\n## Sync Pattern\n\n### Recommended: Upsert via Source/Key (Strategy 2)\n\nPattern for syncing external data using automatic upserts - **no manual ID tracking needed**:\n\n```typescript\nasync startSync(calendarId: string): Promise<void> {\n const authToken = await this.get<string>(\"auth_token\");\n\n await this.tools.calendarTool.startSync(\n authToken,\n calendarId,\n this.handleEvent,\n calendarId\n );\n}\n\nasync handleEvent(\n event: ExternalEvent,\n calendarId: string\n): Promise<void> {\n // Use the event's canonical URL as the source for automatic deduplication\n const activity: NewActivityWithNotes = {\n source: event.htmlLink, // or event.url, depending on your external system\n type: ActivityType.Event,\n title: event.summary || \"(No title)\",\n start: event.start?.dateTime || event.start?.date || null,\n end: event.end?.dateTime || event.end?.date || null,\n notes: [],\n };\n\n // Add description as an upsertable note\n if (event.description) {\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"description\", // This key enables upserts - same key updates the note\n content: event.description,\n });\n }\n\n // Add attendees as an upsertable note\n if (event.attendees?.length) {\n const attendeeList = event.attendees\n .map(a => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ''}`)\n .join('\\n');\n\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"attendees\", // Different key for different note types\n content: `## Attendees\\n${attendeeList}`,\n });\n }\n\n // Create or update - Plot automatically handles deduplication based on source\n await this.tools.plot.createActivity(activity);\n}\n\nasync stopSync(calendarId: string): Promise<void> {\n const authToken = await this.get<string>(\"auth_token\");\n await this.tools.calendarTool.stopSync(authToken, calendarId);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot activities from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingActivity: NewActivityWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingActivity.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingActivityId = await this.get<Uuid>(mappingKey);\n\n if (existingActivityId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingActivity.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n activity: { id: existingActivityId },\n content: incomingActivity.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const activityId = Uuid.Generate();\n await this.set(mappingKey, activityId);\n\n // Create new Activity with initial Note (new thread with first message)\n await this.tools.plot.createActivity({\n ...incomingActivity,\n id: activityId,\n });\n}\n```\n\n## Calendar Selection Pattern\n\nPattern for letting users select from multiple calendars/accounts:\n\n```typescript\nprivate async createCalendarSelectionActivity(\n provider: string,\n calendars: Calendar[],\n authToken: string\n): Promise<void> {\n const links: ActivityLink[] = [];\n\n for (const calendar of calendars) {\n const token = await this.callback(\n this.onCalendarSelected,\n provider,\n calendar.id,\n calendar.name,\n authToken\n );\n\n links.push({\n title: `\uD83D\uDCC5 ${calendar.name}${calendar.primary ? \" (Primary)\" : \"\"}`,\n type: ActivityLinkType.callback,\n token: token,\n });\n }\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Which calendars would you like to connect?\",\n notes: [\n {\n content: \"Select the calendars you want to sync:\",\n links,\n },\n ],\n });\n}\n\nasync onCalendarSelected(\n link: ActivityLink,\n provider: string,\n calendarId: string,\n calendarName: string,\n authToken: string\n): Promise<void> {\n // Start sync for selected calendar\n await this.tools.tool.startSync(\n authToken,\n calendarId,\n this.handleEvent,\n provider,\n calendarId\n );\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(args: any, resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size \u2264 100 items to stay under limit\n for (const item of result.items) {\n // Each createActivity may make ~5-10 requests depending on notes/links\n await this.tools.plot.createActivity({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ActivityType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n unread: !state.initialSync, // false for initial sync, true for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Activity Sync Best Practices\n\nWhen syncing activities from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | `true` | Avoid notification overload from historical items |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst activity: NewActivity = {\n type: ActivityType.Event,\n source: event.url,\n title: event.title,\n unread: !initialSync, // false for initial, true for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports\n- **Incremental sync**: New activities appear as unread, and archived state is preserved (respects user's archiving decisions)\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Activity/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Activity.id` / `Note.id` in the external item's metadata when creating it, and update `Activity.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes \u2014 that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created activities** - Other users may change an Activity created by the twist, resulting in an \\`activity\\` call. Be sure to check the \\`changes === null\\` and/or \\`activity.author.id !== this.id\\` to avoid re-processing.\n- **Always create Activities with Notes** - See \"Understanding Activities and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Activity types** - Most should be `ActivityType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
7
+ declare const _default: "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Activities and Notes\n\n**CRITICAL CONCEPT**: An **Activity** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that activity.\n\n**Think of an Activity as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Activities with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Activities for updates** - Don't create a new Activity for each related message\n3. **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot activities per external item (see SYNC_STRATEGIES.md)\n5. **Most Activities should be `ActivityType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n \u251C\u2500 Has stable URL or ID?\n \u2502 \u2514\u2500 Yes \u2192 Set Activity.source to the canonical URL/ID\n \u2502 Create Activity (Plot handles deduplication automatically)\n \u2502 Use Note.key for different note types:\n \u2502 - \"description\" for main content\n \u2502 - \"metadata\" for status/priority/assignee\n \u2502 - \"comment-{id}\" for individual comments\n \u2502\n \u2514\u2500 No stable identifier OR need multiple Plot activities per external item?\n \u2514\u2500 Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple activities from one external item):\n\n```\nNew event/task/conversation?\n \u251C\u2500 Yes \u2192 Generate UUID with Uuid.Generate()\n \u2502 Create new Activity with that UUID\n \u2502 Store mapping: external_id \u2192 activity_uuid\n \u2502\n \u2514\u2500 No (update/reply/comment) \u2192 Look up mapping by external_id\n \u251C\u2500 Found \u2192 Add Note to existing Activity using stored UUID\n \u2514\u2500 Not found \u2192 Create new Activity with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Activity,\n type NewActivityWithNotes,\n type ActivityFilter,\n type Priority,\n type ToolBuilder,\n Twist,\n ActivityType,\n} from \"@plotday/twister\";\nimport { ActivityAccess, Plot } from \"@plotday/twister/tools/plot\";\n// Import your tools:\n// import { GoogleCalendar } from \"@plotday/tool-google-calendar\";\n// import { Linear } from \"@plotday/tool-linear\";\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n // myTool: build(MyTool, {\n // onItem: this.handleItem,\n // onSyncableDisabled: this.onSyncableDisabled,\n // }),\n plot: build(Plot, {\n activity: { access: ActivityAccess.Create },\n }),\n };\n }\n\n async activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection handled in the twist edit modal.\n }\n\n async handleItem(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n }\n\n async onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n### External Tools (Add to package.json)\n\nAdd tool dependencies to `package.json`:\n\n```json\n{\n \"dependencies\": {\n \"@plotday/twister\": \"workspace:^\",\n \"@plotday/tool-google-calendar\": \"workspace:^\"\n }\n}\n```\n\n#### Available External Tools\n\n- `@plotday/tool-google-calendar`: Google Calendar sync (CalendarTool)\n- `@plotday/tool-outlook-calendar`: Outlook Calendar sync (CalendarTool)\n- `@plotday/tool-google-contacts`: Google Contacts sync (supporting tool)\n- `@plotday/tool-google-drive`: Google Drive sync (DocumentTool)\n- `@plotday/tool-gmail`: Gmail sync (MessagingTool)\n- `@plotday/tool-slack`: Slack sync (MessagingTool)\n- `@plotday/tool-linear`: Linear sync (ProjectTool)\n- `@plotday/tool-jira`: Jira sync (ProjectTool)\n- `@plotday/tool-asana`: Asana sync (ProjectTool)\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.\n\nMost twists have an empty or minimal `activate()`:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection are handled in the twist edit modal.\n // Only add custom initialization here if needed.\n}\n```\n\n**Store Parent Activity for Later (optional):**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const activityId = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Setup complete\",\n notes: [{\n content: \"Your twist is ready. Activities will appear as they sync.\",\n }],\n });\n await this.set(\"setup_activity_id\", activityId);\n}\n```\n\n### Event Callbacks (via build options)\n\nTwists respond to events through callbacks declared in `build()`:\n\n**Receive synced items from a tool (most common):**\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n myTool: build(MyTool, {\n onItem: this.handleItem,\n onSyncableDisabled: this.onSyncableDisabled,\n }),\n plot: build(Plot, { activity: { access: ActivityAccess.Create } }),\n };\n}\n\nasync handleItem(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n}\n\nasync onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n}\n```\n\n**React to activity changes (for two-way sync):**\n\n```typescript\nplot: build(Plot, {\n activity: {\n access: ActivityAccess.Create,\n updated: this.onActivityUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\nasync onActivityUpdated(activity: Activity, changes: { tagsAdded, tagsRemoved }): Promise<void> {\n const tool = this.getToolForActivity(activity);\n if (tool?.updateIssue) await tool.updateIssue(activity);\n}\n\nasync onNoteCreated(note: Note): Promise<void> {\n if (note.author.type === ActorType.Twist) return; // Prevent loops\n // Sync note to external service as a comment\n}\n```\n\n**Respond to mentions (AI twist pattern):**\n\n```typescript\nplot: build(Plot, {\n activity: { access: ActivityAccess.Respond },\n note: {\n intents: [{\n description: \"Respond to general questions\",\n examples: [\"What's the weather?\", \"Help me plan my week\"],\n handler: this.respond,\n }],\n },\n}),\n```\n\n## Activity Links\n\nActivity links enable user interaction:\n\n```typescript\nimport { type ActivityLink, ActivityLinkType } from \"@plotday/twister\";\n\n// External URL link\nconst urlLink: ActivityLink = {\n title: \"Open website\",\n type: ActivityLinkType.external,\n url: \"https://example.com\",\n};\n\n// Callback link (uses Callbacks tool \u2014 use linkCallback, not callback)\nconst token = await this.linkCallback(this.onLinkClicked, \"context\");\nconst callbackLink: ActivityLink = {\n title: \"Click me\",\n type: ActivityLinkType.callback,\n callback: token,\n};\n\n// Add to activity note\nawait this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Task with links\",\n notes: [\n {\n content: \"Click the links below to take action.\",\n links: [urlLink, callbackLink],\n },\n ],\n});\n\n// Callback handler receives the ActivityLink as first argument\nasync onLinkClicked(link: ActivityLink, context: string): Promise<void> {\n // Handle link click\n}\n```\n\n## Authentication Pattern\n\nAuth is handled automatically via the Integrations tool. Tools declare their OAuth provider in `build()`, and users connect in the twist edit modal. **You do not need to create auth activities manually.**\n\n```typescript\n// In your tool's build() method:\nbuild(build: ToolBuilder) {\n return {\n integrations: build(Integrations, {\n providers: [{\n provider: AuthProvider.Google,\n scopes: [\"https://www.googleapis.com/auth/calendar\"],\n getSyncables: this.getSyncables, // List available resources after auth\n onSyncEnabled: this.onSyncEnabled, // User enabled a resource\n onSyncDisabled: this.onSyncDisabled, // User disabled a resource\n }],\n }),\n // ...\n };\n}\n\n// Get a token for API calls:\nconst token = await this.tools.integrations.get(AuthProvider.Google, syncableId);\nif (!token) throw new Error(\"No auth token available\");\nconst client = new ApiClient({ accessToken: token.token });\n```\n\nFor per-user write-backs (e.g., RSVP, comments attributed to the acting user):\n\n```typescript\nawait this.tools.integrations.actAs(\n AuthProvider.Google,\n actorId, // The user who performed the action\n activityId, // Activity to prompt for auth if needed\n this.performWriteBack,\n ...extraArgs\n);\n```\n\n## Sync Pattern\n\n### Recommended: Using External Tools with SyncToolOptions\n\nMost twists use external tools (CalendarTool, ProjectTool, etc.) that handle sync internally. The twist just receives `NewActivityWithNotes` objects and saves them:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n calendarTool: build(GoogleCalendar, {\n onItem: this.handleEvent, // Receives synced items\n onSyncableDisabled: this.onSyncableDisabled, // Clean up when disabled\n }),\n plot: build(Plot, { activity: { access: ActivityAccess.Create } }),\n };\n}\n\n// Tools deliver NewActivityWithNotes \u2014 twist saves them\nasync handleEvent(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n}\n\nasync onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n}\n```\n\n### Custom Sync: Upsert via Source/Key (Strategy 2)\n\nFor direct API integration without an external tool, use source/key for automatic upserts:\n\n```typescript\nasync handleEvent(event: ExternalEvent): Promise<void> {\n const activity: NewActivityWithNotes = {\n source: event.htmlLink, // Canonical URL for automatic deduplication\n type: ActivityType.Event,\n title: event.summary || \"(No title)\",\n start: event.start?.dateTime || event.start?.date || null,\n end: event.end?.dateTime || event.end?.date || null,\n notes: [],\n };\n\n if (event.description) {\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"description\", // This key enables note-level upserts\n content: event.description,\n });\n }\n\n // Create or update \u2014 Plot handles deduplication automatically\n await this.tools.plot.createActivity(activity);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot activities from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingActivity: NewActivityWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingActivity.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingActivityId = await this.get<Uuid>(mappingKey);\n\n if (existingActivityId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingActivity.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n activity: { id: existingActivityId },\n content: incomingActivity.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const activityId = Uuid.Generate();\n await this.set(mappingKey, activityId);\n\n // Create new Activity with initial Note (new thread with first message)\n await this.tools.plot.createActivity({\n ...incomingActivity,\n id: activityId,\n });\n}\n```\n\n## Resource Selection\n\nResource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your tool's `getSyncables()` method and toggle them on/off. You do **not** need to build custom selection UI.\n\n```typescript\n// In your tool:\nasync getSyncables(_auth: Authorization, token: AuthToken): Promise<Syncable[]> {\n const client = new ApiClient({ accessToken: token.token });\n const calendars = await client.listCalendars();\n return calendars.map(c => ({\n id: c.id,\n title: c.name,\n children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),\n }));\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size \u2264 100 items to stay under limit\n for (const item of result.items) {\n // Each createActivity may make ~5-10 requests depending on notes/links\n await this.tools.plot.createActivity({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ActivityType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Activity Sync Best Practices\n\nWhen syncing activities from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst activity: NewActivity = {\n type: ActivityType.Event,\n source: event.url,\n title: event.title,\n ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports\n- **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Activity/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Activity.id` / `Note.id` in the external item's metadata when creating it, and update `Activity.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes \u2014 that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created activities** - Other users may change an Activity created by the twist, resulting in an \\`activity\\` call. Be sure to check the \\`changes === null\\` and/or \\`activity.author.id !== this.id\\` to avoid re-processing.\n- **Always create Activities with Notes** - See \"Understanding Activities and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Activity types** - Most should be `ActivityType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
8
8
  export default _default;
9
9
  //# sourceMappingURL=twist-guide-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"twist-guide-template.d.ts","sourceRoot":"","sources":["../../src/llm-docs/twist-guide-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;wBAEY,s8vBAAkxvB;AAAjyvB,wBAAkyvB"}
1
+ {"version":3,"file":"twist-guide-template.d.ts","sourceRoot":"","sources":["../../src/llm-docs/twist-guide-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;wBAEY,02zBAAirzB;AAAhszB,wBAAiszB"}
@@ -4,5 +4,5 @@
4
4
  * This file is auto-generated during build. Do not edit manually.
5
5
  * Generated from: cli/templates/AGENTS.template.md
6
6
  */
7
- export default "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Activities and Notes\n\n**CRITICAL CONCEPT**: An **Activity** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that activity.\n\n**Think of an Activity as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Activities with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Activities for updates** - Don't create a new Activity for each related message\n3. **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot activities per external item (see SYNC_STRATEGIES.md)\n5. **Most Activities should be `ActivityType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n ├─ Has stable URL or ID?\n │ └─ Yes → Set Activity.source to the canonical URL/ID\n │ Create Activity (Plot handles deduplication automatically)\n │ Use Note.key for different note types:\n │ - \"description\" for main content\n │ - \"metadata\" for status/priority/assignee\n │ - \"comment-{id}\" for individual comments\n │\n └─ No stable identifier OR need multiple Plot activities per external item?\n └─ Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple activities from one external item):\n\n```\nNew event/task/conversation?\n ├─ Yes → Generate UUID with Uuid.Generate()\n │ Create new Activity with that UUID\n │ Store mapping: external_id → activity_uuid\n │\n └─ No (update/reply/comment) → Look up mapping by external_id\n ├─ Found → Add Note to existing Activity using stored UUID\n └─ Not found → Create new Activity with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Activity,\n type Priority,\n type ToolBuilder,\n twist,\n} from \"@plotday/twister\";\nimport { Plot } from \"@plotday/twister/tools/plot\";\nimport { Uuid } from \"@plotday/twister/utils/uuid\";\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n plot: build(Plot),\n };\n }\n\n async activate(priority: Pick<Priority, \"id\">) {\n // Called when twist is enabled for a priority\n // Common actions: request auth, create setup activities\n }\n\n async activity(activity: Activity) {\n // Called when an activity is routed to this twist\n // Common actions: process external events, update activities\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n### External Tools (Add to package.json)\n\nAdd tool dependencies to `package.json`:\n\n```json\n{\n \"dependencies\": {\n \"@plotday/twister\": \"workspace:^\",\n \"@plotday/tool-google-calendar\": \"workspace:^\"\n }\n}\n```\n\n#### Common External Tools\n\n- `@plotday/tool-google-calendar`: Google Calendar integration\n- `@plotday/tool-outlook-calendar`: Outlook Calendar integration\n- `@plotday/tool-google-contacts`: Google Contacts integration\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Common patterns:\n\n**Request Authentication:**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const authLink = await this.tools.externalTool.requestAuth(\n this.onAuthComplete,\n \"google\"\n );\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Connect your account\",\n notes: [\n {\n content: \"Click the link below to connect your account and start syncing.\",\n links: [authLink],\n },\n ],\n });\n}\n```\n\n**Store Parent Activity for Later:**\n\n```typescript\nconst activity = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Setup\",\n notes: [\n {\n content: \"Your twist is being set up. Configuration steps will appear here.\",\n },\n ],\n});\n\nawait this.set(\"setup_activity_id\", activity.id);\n```\n\n### activity(activity: Activity)\n\nCalled when an activity is routed to the twist. Common patterns:\n\n**Create Activities from External Events:**\n\n```typescript\nasync activity(activity: Activity) {\n await this.tools.plot.createActivity(activity);\n}\n```\n\n**Update Based on User Action:**\n\n```typescript\nasync activity(activity: Activity) {\n if (activity.completed) {\n await this.handleCompletion(activity);\n }\n}\n```\n\n## Activity Links\n\nActivity links enable user interaction:\n\n```typescript\nimport { type ActivityLink, ActivityLinkType } from \"@plotday/twister\";\n\n// URL link\nconst urlLink: ActivityLink = {\n title: \"Open website\",\n type: ActivityLinkType.url,\n url: \"https://example.com\",\n};\n\n// Callback link (uses Callbacks tool)\nconst token = await this.callback(this.onLinkClicked, \"context\");\nconst callbackLink: ActivityLink = {\n title: \"Click me\",\n type: ActivityLinkType.callback,\n token: token,\n};\n\n// Add to activity note\nawait this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Task with links\",\n notes: [\n {\n content: \"Click the links below to take action.\",\n links: [urlLink, callbackLink],\n },\n ],\n});\n```\n\n## Authentication Pattern\n\nCommon pattern for OAuth authentication:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Request auth link from tool with callback\n const authLink = await this.tools.googleTool.requestAuth(\n this.onAuthComplete,\n \"google\"\n );\n\n // Create activity with auth link\n const activity = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Connect Google account\",\n notes: [\n {\n content: \"Click below to connect your Google account and start syncing.\",\n links: [authLink],\n },\n ],\n });\n\n // Store for later use\n await this.set(\"auth_activity_id\", activity.id);\n}\n\nasync onAuthComplete(authResult: { authToken: string }, provider: string) {\n // Store auth token\n await this.set(`${provider}_auth`, authResult.authToken);\n\n // Continue setup flow\n await this.setupSyncOptions(authResult.authToken);\n}\n```\n\n## Sync Pattern\n\n### Recommended: Upsert via Source/Key (Strategy 2)\n\nPattern for syncing external data using automatic upserts - **no manual ID tracking needed**:\n\n```typescript\nasync startSync(calendarId: string): Promise<void> {\n const authToken = await this.get<string>(\"auth_token\");\n\n await this.tools.calendarTool.startSync(\n authToken,\n calendarId,\n this.handleEvent,\n calendarId\n );\n}\n\nasync handleEvent(\n event: ExternalEvent,\n calendarId: string\n): Promise<void> {\n // Use the event's canonical URL as the source for automatic deduplication\n const activity: NewActivityWithNotes = {\n source: event.htmlLink, // or event.url, depending on your external system\n type: ActivityType.Event,\n title: event.summary || \"(No title)\",\n start: event.start?.dateTime || event.start?.date || null,\n end: event.end?.dateTime || event.end?.date || null,\n notes: [],\n };\n\n // Add description as an upsertable note\n if (event.description) {\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"description\", // This key enables upserts - same key updates the note\n content: event.description,\n });\n }\n\n // Add attendees as an upsertable note\n if (event.attendees?.length) {\n const attendeeList = event.attendees\n .map(a => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ''}`)\n .join('\\n');\n\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"attendees\", // Different key for different note types\n content: `## Attendees\\n${attendeeList}`,\n });\n }\n\n // Create or update - Plot automatically handles deduplication based on source\n await this.tools.plot.createActivity(activity);\n}\n\nasync stopSync(calendarId: string): Promise<void> {\n const authToken = await this.get<string>(\"auth_token\");\n await this.tools.calendarTool.stopSync(authToken, calendarId);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot activities from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingActivity: NewActivityWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingActivity.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingActivityId = await this.get<Uuid>(mappingKey);\n\n if (existingActivityId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingActivity.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n activity: { id: existingActivityId },\n content: incomingActivity.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const activityId = Uuid.Generate();\n await this.set(mappingKey, activityId);\n\n // Create new Activity with initial Note (new thread with first message)\n await this.tools.plot.createActivity({\n ...incomingActivity,\n id: activityId,\n });\n}\n```\n\n## Calendar Selection Pattern\n\nPattern for letting users select from multiple calendars/accounts:\n\n```typescript\nprivate async createCalendarSelectionActivity(\n provider: string,\n calendars: Calendar[],\n authToken: string\n): Promise<void> {\n const links: ActivityLink[] = [];\n\n for (const calendar of calendars) {\n const token = await this.callback(\n this.onCalendarSelected,\n provider,\n calendar.id,\n calendar.name,\n authToken\n );\n\n links.push({\n title: `📅 ${calendar.name}${calendar.primary ? \" (Primary)\" : \"\"}`,\n type: ActivityLinkType.callback,\n token: token,\n });\n }\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Which calendars would you like to connect?\",\n notes: [\n {\n content: \"Select the calendars you want to sync:\",\n links,\n },\n ],\n });\n}\n\nasync onCalendarSelected(\n link: ActivityLink,\n provider: string,\n calendarId: string,\n calendarName: string,\n authToken: string\n): Promise<void> {\n // Start sync for selected calendar\n await this.tools.tool.startSync(\n authToken,\n calendarId,\n this.handleEvent,\n provider,\n calendarId\n );\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(args: any, resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size ≤ 100 items to stay under limit\n for (const item of result.items) {\n // Each createActivity may make ~5-10 requests depending on notes/links\n await this.tools.plot.createActivity({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ActivityType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n unread: !state.initialSync, // false for initial sync, true for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Activity Sync Best Practices\n\nWhen syncing activities from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | `true` | Avoid notification overload from historical items |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst activity: NewActivity = {\n type: ActivityType.Event,\n source: event.url,\n title: event.title,\n unread: !initialSync, // false for initial, true for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports\n- **Incremental sync**: New activities appear as unread, and archived state is preserved (respects user's archiving decisions)\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Activity/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Activity.id` / `Note.id` in the external item's metadata when creating it, and update `Activity.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes — that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created activities** - Other users may change an Activity created by the twist, resulting in an \\`activity\\` call. Be sure to check the \\`changes === null\\` and/or \\`activity.author.id !== this.id\\` to avoid re-processing.\n- **Always create Activities with Notes** - See \"Understanding Activities and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Activity types** - Most should be `ActivityType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
7
+ export default "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Activities and Notes\n\n**CRITICAL CONCEPT**: An **Activity** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that activity.\n\n**Think of an Activity as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Activities with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Activities for updates** - Don't create a new Activity for each related message\n3. **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot activities per external item (see SYNC_STRATEGIES.md)\n5. **Most Activities should be `ActivityType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n ├─ Has stable URL or ID?\n │ └─ Yes → Set Activity.source to the canonical URL/ID\n │ Create Activity (Plot handles deduplication automatically)\n │ Use Note.key for different note types:\n │ - \"description\" for main content\n │ - \"metadata\" for status/priority/assignee\n │ - \"comment-{id}\" for individual comments\n │\n └─ No stable identifier OR need multiple Plot activities per external item?\n └─ Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple activities from one external item):\n\n```\nNew event/task/conversation?\n ├─ Yes → Generate UUID with Uuid.Generate()\n │ Create new Activity with that UUID\n │ Store mapping: external_id → activity_uuid\n │\n └─ No (update/reply/comment) → Look up mapping by external_id\n ├─ Found → Add Note to existing Activity using stored UUID\n └─ Not found → Create new Activity with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Activity,\n type NewActivityWithNotes,\n type ActivityFilter,\n type Priority,\n type ToolBuilder,\n Twist,\n ActivityType,\n} from \"@plotday/twister\";\nimport { ActivityAccess, Plot } from \"@plotday/twister/tools/plot\";\n// Import your tools:\n// import { GoogleCalendar } from \"@plotday/tool-google-calendar\";\n// import { Linear } from \"@plotday/tool-linear\";\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n // myTool: build(MyTool, {\n // onItem: this.handleItem,\n // onSyncableDisabled: this.onSyncableDisabled,\n // }),\n plot: build(Plot, {\n activity: { access: ActivityAccess.Create },\n }),\n };\n }\n\n async activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection handled in the twist edit modal.\n }\n\n async handleItem(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n }\n\n async onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n### External Tools (Add to package.json)\n\nAdd tool dependencies to `package.json`:\n\n```json\n{\n \"dependencies\": {\n \"@plotday/twister\": \"workspace:^\",\n \"@plotday/tool-google-calendar\": \"workspace:^\"\n }\n}\n```\n\n#### Available External Tools\n\n- `@plotday/tool-google-calendar`: Google Calendar sync (CalendarTool)\n- `@plotday/tool-outlook-calendar`: Outlook Calendar sync (CalendarTool)\n- `@plotday/tool-google-contacts`: Google Contacts sync (supporting tool)\n- `@plotday/tool-google-drive`: Google Drive sync (DocumentTool)\n- `@plotday/tool-gmail`: Gmail sync (MessagingTool)\n- `@plotday/tool-slack`: Slack sync (MessagingTool)\n- `@plotday/tool-linear`: Linear sync (ProjectTool)\n- `@plotday/tool-jira`: Jira sync (ProjectTool)\n- `@plotday/tool-asana`: Asana sync (ProjectTool)\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.\n\nMost twists have an empty or minimal `activate()`:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection are handled in the twist edit modal.\n // Only add custom initialization here if needed.\n}\n```\n\n**Store Parent Activity for Later (optional):**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const activityId = await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Setup complete\",\n notes: [{\n content: \"Your twist is ready. Activities will appear as they sync.\",\n }],\n });\n await this.set(\"setup_activity_id\", activityId);\n}\n```\n\n### Event Callbacks (via build options)\n\nTwists respond to events through callbacks declared in `build()`:\n\n**Receive synced items from a tool (most common):**\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n myTool: build(MyTool, {\n onItem: this.handleItem,\n onSyncableDisabled: this.onSyncableDisabled,\n }),\n plot: build(Plot, { activity: { access: ActivityAccess.Create } }),\n };\n}\n\nasync handleItem(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n}\n\nasync onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n}\n```\n\n**React to activity changes (for two-way sync):**\n\n```typescript\nplot: build(Plot, {\n activity: {\n access: ActivityAccess.Create,\n updated: this.onActivityUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\nasync onActivityUpdated(activity: Activity, changes: { tagsAdded, tagsRemoved }): Promise<void> {\n const tool = this.getToolForActivity(activity);\n if (tool?.updateIssue) await tool.updateIssue(activity);\n}\n\nasync onNoteCreated(note: Note): Promise<void> {\n if (note.author.type === ActorType.Twist) return; // Prevent loops\n // Sync note to external service as a comment\n}\n```\n\n**Respond to mentions (AI twist pattern):**\n\n```typescript\nplot: build(Plot, {\n activity: { access: ActivityAccess.Respond },\n note: {\n intents: [{\n description: \"Respond to general questions\",\n examples: [\"What's the weather?\", \"Help me plan my week\"],\n handler: this.respond,\n }],\n },\n}),\n```\n\n## Activity Links\n\nActivity links enable user interaction:\n\n```typescript\nimport { type ActivityLink, ActivityLinkType } from \"@plotday/twister\";\n\n// External URL link\nconst urlLink: ActivityLink = {\n title: \"Open website\",\n type: ActivityLinkType.external,\n url: \"https://example.com\",\n};\n\n// Callback link (uses Callbacks tool — use linkCallback, not callback)\nconst token = await this.linkCallback(this.onLinkClicked, \"context\");\nconst callbackLink: ActivityLink = {\n title: \"Click me\",\n type: ActivityLinkType.callback,\n callback: token,\n};\n\n// Add to activity note\nawait this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Task with links\",\n notes: [\n {\n content: \"Click the links below to take action.\",\n links: [urlLink, callbackLink],\n },\n ],\n});\n\n// Callback handler receives the ActivityLink as first argument\nasync onLinkClicked(link: ActivityLink, context: string): Promise<void> {\n // Handle link click\n}\n```\n\n## Authentication Pattern\n\nAuth is handled automatically via the Integrations tool. Tools declare their OAuth provider in `build()`, and users connect in the twist edit modal. **You do not need to create auth activities manually.**\n\n```typescript\n// In your tool's build() method:\nbuild(build: ToolBuilder) {\n return {\n integrations: build(Integrations, {\n providers: [{\n provider: AuthProvider.Google,\n scopes: [\"https://www.googleapis.com/auth/calendar\"],\n getSyncables: this.getSyncables, // List available resources after auth\n onSyncEnabled: this.onSyncEnabled, // User enabled a resource\n onSyncDisabled: this.onSyncDisabled, // User disabled a resource\n }],\n }),\n // ...\n };\n}\n\n// Get a token for API calls:\nconst token = await this.tools.integrations.get(AuthProvider.Google, syncableId);\nif (!token) throw new Error(\"No auth token available\");\nconst client = new ApiClient({ accessToken: token.token });\n```\n\nFor per-user write-backs (e.g., RSVP, comments attributed to the acting user):\n\n```typescript\nawait this.tools.integrations.actAs(\n AuthProvider.Google,\n actorId, // The user who performed the action\n activityId, // Activity to prompt for auth if needed\n this.performWriteBack,\n ...extraArgs\n);\n```\n\n## Sync Pattern\n\n### Recommended: Using External Tools with SyncToolOptions\n\nMost twists use external tools (CalendarTool, ProjectTool, etc.) that handle sync internally. The twist just receives `NewActivityWithNotes` objects and saves them:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n calendarTool: build(GoogleCalendar, {\n onItem: this.handleEvent, // Receives synced items\n onSyncableDisabled: this.onSyncableDisabled, // Clean up when disabled\n }),\n plot: build(Plot, { activity: { access: ActivityAccess.Create } }),\n };\n}\n\n// Tools deliver NewActivityWithNotes — twist saves them\nasync handleEvent(activity: NewActivityWithNotes): Promise<void> {\n await this.tools.plot.createActivity(activity);\n}\n\nasync onSyncableDisabled(filter: ActivityFilter): Promise<void> {\n await this.tools.plot.updateActivity({ match: filter, archived: true });\n}\n```\n\n### Custom Sync: Upsert via Source/Key (Strategy 2)\n\nFor direct API integration without an external tool, use source/key for automatic upserts:\n\n```typescript\nasync handleEvent(event: ExternalEvent): Promise<void> {\n const activity: NewActivityWithNotes = {\n source: event.htmlLink, // Canonical URL for automatic deduplication\n type: ActivityType.Event,\n title: event.summary || \"(No title)\",\n start: event.start?.dateTime || event.start?.date || null,\n end: event.end?.dateTime || event.end?.date || null,\n notes: [],\n };\n\n if (event.description) {\n activity.notes.push({\n activity: { source: event.htmlLink },\n key: \"description\", // This key enables note-level upserts\n content: event.description,\n });\n }\n\n // Create or update — Plot handles deduplication automatically\n await this.tools.plot.createActivity(activity);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot activities from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingActivity: NewActivityWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingActivity.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingActivityId = await this.get<Uuid>(mappingKey);\n\n if (existingActivityId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingActivity.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n activity: { id: existingActivityId },\n content: incomingActivity.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const activityId = Uuid.Generate();\n await this.set(mappingKey, activityId);\n\n // Create new Activity with initial Note (new thread with first message)\n await this.tools.plot.createActivity({\n ...incomingActivity,\n id: activityId,\n });\n}\n```\n\n## Resource Selection\n\nResource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your tool's `getSyncables()` method and toggle them on/off. You do **not** need to build custom selection UI.\n\n```typescript\n// In your tool:\nasync getSyncables(_auth: Authorization, token: AuthToken): Promise<Syncable[]> {\n const client = new ApiClient({ accessToken: token.token });\n const calendars = await client.listCalendars();\n return calendars.map(c => ({\n id: c.id,\n title: c.name,\n children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),\n }));\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size ≤ 100 items to stay under limit\n for (const item of result.items) {\n // Each createActivity may make ~5-10 requests depending on notes/links\n await this.tools.plot.createActivity({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ActivityType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Activity Sync Best Practices\n\nWhen syncing activities from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst activity: NewActivity = {\n type: ActivityType.Event,\n source: event.url,\n title: event.title,\n ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports\n- **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Activity/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Activity.id` / `Note.id` in the external item's metadata when creating it, and update `Activity.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes — that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createActivity({\n type: ActivityType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created activities** - Other users may change an Activity created by the twist, resulting in an \\`activity\\` call. Be sure to check the \\`changes === null\\` and/or \\`activity.author.id !== this.id\\` to avoid re-processing.\n- **Always create Activities with Notes** - See \"Understanding Activities and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Activity types** - Most should be `ActivityType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
8
8
  //# sourceMappingURL=twist-guide-template.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"twist-guide-template.js","sourceRoot":"","sources":["../../src/llm-docs/twist-guide-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,kxvBAAkxvB,CAAC"}
1
+ {"version":3,"file":"twist-guide-template.js","sourceRoot":"","sources":["../../src/llm-docs/twist-guide-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe,irzBAAirzB,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { ITool } from "./tool";
2
+ /**
3
+ * A select option definition for twist configuration.
4
+ * Renders as a dropdown in the Flutter UI.
5
+ */
6
+ export type SelectDef = {
7
+ type: "select";
8
+ label: string;
9
+ description?: string;
10
+ choices: ReadonlyArray<{
11
+ value: string;
12
+ label: string;
13
+ }>;
14
+ default: string;
15
+ };
16
+ /**
17
+ * A text input option definition for twist configuration.
18
+ * Renders as a text field in the Flutter UI.
19
+ */
20
+ export type TextDef = {
21
+ type: "text";
22
+ label: string;
23
+ description?: string;
24
+ default: string;
25
+ placeholder?: string;
26
+ };
27
+ /**
28
+ * A number input option definition for twist configuration.
29
+ * Renders as a number field in the Flutter UI.
30
+ */
31
+ export type NumberDef = {
32
+ type: "number";
33
+ label: string;
34
+ description?: string;
35
+ default: number;
36
+ min?: number;
37
+ max?: number;
38
+ };
39
+ /**
40
+ * A boolean toggle option definition for twist configuration.
41
+ * Renders as a switch in the Flutter UI.
42
+ */
43
+ export type BooleanDef = {
44
+ type: "boolean";
45
+ label: string;
46
+ description?: string;
47
+ default: boolean;
48
+ };
49
+ /**
50
+ * Union of all option definition types.
51
+ */
52
+ export type OptionDef = SelectDef | TextDef | NumberDef | BooleanDef;
53
+ /**
54
+ * Schema defining all configurable options for a twist.
55
+ * Each key maps to an option definition that describes its type, label, and default value.
56
+ */
57
+ export type OptionsSchema = Record<string, OptionDef>;
58
+ /**
59
+ * Infers the resolved value types from an options schema.
60
+ * Boolean options resolve to `boolean`, number options to `number`,
61
+ * and select/text options to `string`.
62
+ */
63
+ export type ResolvedOptions<T extends OptionsSchema> = {
64
+ [K in keyof T]: T[K] extends BooleanDef ? boolean : T[K] extends NumberDef ? number : string;
65
+ };
66
+ /**
67
+ * Built-in marker class for twist configuration options.
68
+ *
69
+ * Declare options in your twist's `build()` method to expose configurable
70
+ * settings to users. The schema is introspected at deploy time and stored
71
+ * alongside permissions. At runtime, user values are merged with defaults.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * import { Options, type OptionsSchema } from "@plotday/twister/options";
76
+ *
77
+ * export default class MyTwist extends Twist<MyTwist> {
78
+ * build(build: ToolBuilder) {
79
+ * return {
80
+ * options: build(Options, {
81
+ * model: {
82
+ * type: 'select',
83
+ * label: 'AI Model',
84
+ * choices: [
85
+ * { value: 'fast', label: 'Fast' },
86
+ * { value: 'smart', label: 'Smart' },
87
+ * ],
88
+ * default: 'fast',
89
+ * },
90
+ * }),
91
+ * // ... other tools
92
+ * };
93
+ * }
94
+ *
95
+ * async respond(note: Note) {
96
+ * const model = this.tools.options.model; // typed as string
97
+ * }
98
+ * }
99
+ * ```
100
+ */
101
+ export declare abstract class Options extends ITool {
102
+ static readonly toolId = "Options";
103
+ }
104
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,aAAa,IAAI;KACpD,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GACnC,OAAO,GACP,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,GACpB,MAAM,GACN,MAAM;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,8BAAsB,OAAQ,SAAQ,KAAK;IACzC,MAAM,CAAC,QAAQ,CAAC,MAAM,aAAa;CACpC"}
@@ -0,0 +1,40 @@
1
+ import { ITool } from "./tool";
2
+ /**
3
+ * Built-in marker class for twist configuration options.
4
+ *
5
+ * Declare options in your twist's `build()` method to expose configurable
6
+ * settings to users. The schema is introspected at deploy time and stored
7
+ * alongside permissions. At runtime, user values are merged with defaults.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Options, type OptionsSchema } from "@plotday/twister/options";
12
+ *
13
+ * export default class MyTwist extends Twist<MyTwist> {
14
+ * build(build: ToolBuilder) {
15
+ * return {
16
+ * options: build(Options, {
17
+ * model: {
18
+ * type: 'select',
19
+ * label: 'AI Model',
20
+ * choices: [
21
+ * { value: 'fast', label: 'Fast' },
22
+ * { value: 'smart', label: 'Smart' },
23
+ * ],
24
+ * default: 'fast',
25
+ * },
26
+ * }),
27
+ * // ... other tools
28
+ * };
29
+ * }
30
+ *
31
+ * async respond(note: Note) {
32
+ * const model = this.tools.options.model; // typed as string
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ export class Options extends ITool {
38
+ static toolId = "Options";
39
+ }
40
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AA0E/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,OAAgB,OAAQ,SAAQ,KAAK;IACzC,MAAM,CAAU,MAAM,GAAG,SAAS,CAAC"}