@plotday/twister 0.27.0 → 0.29.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 (201) hide show
  1. package/README.md +10 -4
  2. package/bin/templates/AGENTS.template.md +91 -29
  3. package/cli/templates/AGENTS.template.md +91 -29
  4. package/dist/common/calendar.d.ts +35 -28
  5. package/dist/common/calendar.d.ts.map +1 -1
  6. package/dist/common/messaging.d.ts +24 -10
  7. package/dist/common/messaging.d.ts.map +1 -1
  8. package/dist/common/projects.d.ts +30 -15
  9. package/dist/common/projects.d.ts.map +1 -1
  10. package/dist/common/serializable.d.ts +40 -0
  11. package/dist/common/serializable.d.ts.map +1 -0
  12. package/dist/common/serializable.js +2 -0
  13. package/dist/common/serializable.js.map +1 -0
  14. package/dist/docs/assets/hierarchy.js +1 -1
  15. package/dist/docs/assets/navigation.js +1 -1
  16. package/dist/docs/assets/search.js +1 -1
  17. package/dist/docs/classes/tool.ITool.html +1 -1
  18. package/dist/docs/classes/tool.Tool.html +32 -28
  19. package/dist/docs/classes/tools_ai.AI.html +2 -2
  20. package/dist/docs/classes/tools_callbacks.Callbacks.html +6 -6
  21. package/dist/docs/classes/tools_integrations.Integrations.html +3 -3
  22. package/dist/docs/classes/tools_network.Network.html +12 -12
  23. package/dist/docs/classes/tools_plot.Plot.html +41 -31
  24. package/dist/docs/classes/tools_store.Store.html +31 -11
  25. package/dist/docs/classes/tools_tasks.Tasks.html +8 -8
  26. package/dist/docs/classes/tools_twists.Twists.html +5 -5
  27. package/dist/docs/classes/twist.Twist.html +28 -24
  28. package/dist/docs/documents/Building_Custom_Tools.html +8 -2
  29. package/dist/docs/documents/Built-in_Tools.html +19 -8
  30. package/dist/docs/documents/Core_Concepts.html +14 -6
  31. package/dist/docs/documents/Getting_Started.html +11 -4
  32. package/dist/docs/enums/plot.ActivityLinkType.html +5 -5
  33. package/dist/docs/enums/plot.ActivityType.html +4 -4
  34. package/dist/docs/enums/plot.ActorType.html +4 -4
  35. package/dist/docs/enums/plot.ConferencingProvider.html +6 -6
  36. package/dist/docs/enums/tag.Tag.html +2 -2
  37. package/dist/docs/enums/tools_ai.AIModel.html +2 -2
  38. package/dist/docs/enums/tools_integrations.AuthLevel.html +3 -3
  39. package/dist/docs/enums/tools_integrations.AuthProvider.html +11 -11
  40. package/dist/docs/enums/tools_plot.ActivityAccess.html +3 -3
  41. package/dist/docs/enums/tools_plot.ContactAccess.html +4 -4
  42. package/dist/docs/enums/tools_plot.PriorityAccess.html +3 -3
  43. package/dist/docs/functions/index.Uuid.Generate.html +1 -1
  44. package/dist/docs/functions/utils_hash.quickHash.html +1 -1
  45. package/dist/docs/hierarchy.html +1 -1
  46. package/dist/docs/index.html +10 -0
  47. package/dist/docs/interfaces/common_calendar.Calendar.html +5 -5
  48. package/dist/docs/interfaces/common_calendar.CalendarTool.html +29 -15
  49. package/dist/docs/interfaces/common_calendar.SyncOptions.html +3 -3
  50. package/dist/docs/interfaces/index.SerializableArray.html +2 -0
  51. package/dist/docs/interfaces/index.SerializableMap.html +2 -0
  52. package/dist/docs/interfaces/index.SerializableObject.html +2 -0
  53. package/dist/docs/interfaces/index.SerializableSet.html +2 -0
  54. package/dist/docs/interfaces/tools_ai.AIRequest.html +11 -11
  55. package/dist/docs/interfaces/tools_ai.AIResponse.html +9 -9
  56. package/dist/docs/interfaces/tools_ai.FilePart.html +5 -5
  57. package/dist/docs/interfaces/tools_ai.ImagePart.html +4 -4
  58. package/dist/docs/interfaces/tools_ai.ReasoningPart.html +4 -4
  59. package/dist/docs/interfaces/tools_ai.RedactedReasoningPart.html +3 -3
  60. package/dist/docs/interfaces/tools_ai.TextPart.html +3 -3
  61. package/dist/docs/interfaces/tools_ai.ToolCallPart.html +5 -5
  62. package/dist/docs/interfaces/tools_ai.ToolExecutionOptions.html +4 -4
  63. package/dist/docs/interfaces/tools_ai.ToolResultPart.html +5 -5
  64. package/dist/docs/interfaces/tools_twists.TwistSource.html +3 -3
  65. package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
  66. package/dist/docs/media/SYNC_STRATEGIES.md +651 -0
  67. package/dist/docs/modules/index.html +1 -1
  68. package/dist/docs/modules/plot.html +1 -1
  69. package/dist/docs/modules/utils_types.html +1 -1
  70. package/dist/docs/types/common_calendar.CalendarAuth.html +2 -2
  71. package/dist/docs/types/index.Serializable.html +17 -0
  72. package/dist/docs/types/index.Uuid.html +1 -1
  73. package/dist/docs/types/plot.Activity.html +17 -8
  74. package/dist/docs/types/plot.ActivityCommon.html +10 -10
  75. package/dist/docs/types/plot.ActivityLink.html +1 -1
  76. package/dist/docs/types/plot.ActivityMeta.html +5 -3
  77. package/dist/docs/types/plot.ActivityUpdate.html +4 -2
  78. package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
  79. package/dist/docs/types/plot.Actor.html +15 -5
  80. package/dist/docs/types/plot.ActorId.html +1 -1
  81. package/dist/docs/types/plot.ContentType.html +1 -1
  82. package/dist/docs/types/plot.NewActivity.html +26 -12
  83. package/dist/docs/types/plot.NewActivityWithNotes.html +1 -1
  84. package/dist/docs/types/plot.NewActor.html +1 -1
  85. package/dist/docs/types/plot.NewContact.html +4 -4
  86. package/dist/docs/types/plot.NewNote.html +9 -4
  87. package/dist/docs/types/plot.NewPriority.html +15 -5
  88. package/dist/docs/types/plot.NewTags.html +1 -1
  89. package/dist/docs/types/plot.Note.html +6 -3
  90. package/dist/docs/types/plot.NoteUpdate.html +4 -3
  91. package/dist/docs/types/plot.PickPriorityConfig.html +3 -3
  92. package/dist/docs/types/plot.Priority.html +9 -4
  93. package/dist/docs/types/plot.PriorityUpdate.html +3 -0
  94. package/dist/docs/types/plot.SyncUpdate.html +1 -1
  95. package/dist/docs/types/plot.Tags.html +1 -1
  96. package/dist/docs/types/tools_ai.AIAssistantMessage.html +2 -2
  97. package/dist/docs/types/tools_ai.AIMessage.html +1 -1
  98. package/dist/docs/types/tools_ai.AISource.html +1 -1
  99. package/dist/docs/types/tools_ai.AISystemMessage.html +2 -2
  100. package/dist/docs/types/tools_ai.AITool.html +1 -1
  101. package/dist/docs/types/tools_ai.AIToolMessage.html +2 -2
  102. package/dist/docs/types/tools_ai.AIToolSet.html +1 -1
  103. package/dist/docs/types/tools_ai.AIUsage.html +5 -5
  104. package/dist/docs/types/tools_ai.AIUserMessage.html +2 -2
  105. package/dist/docs/types/tools_ai.DataContent.html +1 -1
  106. package/dist/docs/types/tools_ai.ModelPreferences.html +4 -4
  107. package/dist/docs/types/tools_callbacks.Callback.html +1 -1
  108. package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
  109. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  110. package/dist/docs/types/tools_network.WebhookRequest.html +6 -6
  111. package/dist/docs/types/tools_plot.NoteIntentHandler.html +4 -4
  112. package/dist/docs/types/tools_twists.Log.html +2 -2
  113. package/dist/docs/types/tools_twists.TwistPermissions.html +1 -1
  114. package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
  115. package/dist/docs/types/utils_types.CallbackMethods.html +1 -1
  116. package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
  117. package/dist/docs/types/utils_types.InferOptions.html +1 -1
  118. package/dist/docs/types/utils_types.InferTools.html +1 -1
  119. package/dist/docs/types/utils_types.JSONValue.html +7 -0
  120. package/dist/docs/types/utils_types.NoFunctions.html +1 -1
  121. package/dist/docs/types/utils_types.NonFunction.html +1 -1
  122. package/dist/docs/types/utils_types.PromiseValues.html +1 -1
  123. package/dist/docs/types/utils_types.ToolBuilder.html +1 -1
  124. package/dist/index.d.ts +1 -0
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +1 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/llm-docs/common/calendar.d.ts +1 -1
  129. package/dist/llm-docs/common/calendar.d.ts.map +1 -1
  130. package/dist/llm-docs/common/calendar.js +1 -1
  131. package/dist/llm-docs/common/calendar.js.map +1 -1
  132. package/dist/llm-docs/common/messaging.d.ts +1 -1
  133. package/dist/llm-docs/common/messaging.d.ts.map +1 -1
  134. package/dist/llm-docs/common/messaging.js +1 -1
  135. package/dist/llm-docs/common/messaging.js.map +1 -1
  136. package/dist/llm-docs/common/projects.d.ts +1 -1
  137. package/dist/llm-docs/common/projects.d.ts.map +1 -1
  138. package/dist/llm-docs/common/projects.js +1 -1
  139. package/dist/llm-docs/common/projects.js.map +1 -1
  140. package/dist/llm-docs/plot.d.ts +1 -1
  141. package/dist/llm-docs/plot.d.ts.map +1 -1
  142. package/dist/llm-docs/plot.js +1 -1
  143. package/dist/llm-docs/plot.js.map +1 -1
  144. package/dist/llm-docs/tool.d.ts +1 -1
  145. package/dist/llm-docs/tool.d.ts.map +1 -1
  146. package/dist/llm-docs/tool.js +1 -1
  147. package/dist/llm-docs/tool.js.map +1 -1
  148. package/dist/llm-docs/tools/network.d.ts +1 -1
  149. package/dist/llm-docs/tools/network.d.ts.map +1 -1
  150. package/dist/llm-docs/tools/network.js +1 -1
  151. package/dist/llm-docs/tools/network.js.map +1 -1
  152. package/dist/llm-docs/tools/plot.d.ts +1 -1
  153. package/dist/llm-docs/tools/plot.d.ts.map +1 -1
  154. package/dist/llm-docs/tools/plot.js +1 -1
  155. package/dist/llm-docs/tools/plot.js.map +1 -1
  156. package/dist/llm-docs/tools/store.d.ts +1 -1
  157. package/dist/llm-docs/tools/store.d.ts.map +1 -1
  158. package/dist/llm-docs/tools/store.js +1 -1
  159. package/dist/llm-docs/tools/store.js.map +1 -1
  160. package/dist/llm-docs/tools/tasks.d.ts +1 -1
  161. package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
  162. package/dist/llm-docs/tools/tasks.js +1 -1
  163. package/dist/llm-docs/tools/tasks.js.map +1 -1
  164. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  165. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  166. package/dist/llm-docs/twist-guide-template.js +1 -1
  167. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  168. package/dist/llm-docs/twist.d.ts +1 -1
  169. package/dist/llm-docs/twist.d.ts.map +1 -1
  170. package/dist/llm-docs/twist.js +1 -1
  171. package/dist/llm-docs/twist.js.map +1 -1
  172. package/dist/plot.d.ts +232 -56
  173. package/dist/plot.d.ts.map +1 -1
  174. package/dist/plot.js.map +1 -1
  175. package/dist/tool.d.ts +30 -8
  176. package/dist/tool.d.ts.map +1 -1
  177. package/dist/tool.js +29 -7
  178. package/dist/tool.js.map +1 -1
  179. package/dist/tools/network.d.ts +19 -16
  180. package/dist/tools/network.d.ts.map +1 -1
  181. package/dist/tools/network.js +6 -4
  182. package/dist/tools/network.js.map +1 -1
  183. package/dist/tools/plot.d.ts +93 -24
  184. package/dist/tools/plot.d.ts.map +1 -1
  185. package/dist/tools/plot.js +46 -1
  186. package/dist/tools/plot.js.map +1 -1
  187. package/dist/tools/store.d.ts +49 -8
  188. package/dist/tools/store.d.ts.map +1 -1
  189. package/dist/tools/store.js +12 -1
  190. package/dist/tools/store.js.map +1 -1
  191. package/dist/tools/tasks.d.ts +4 -4
  192. package/dist/tools/tasks.js +4 -4
  193. package/dist/twist-guide.d.ts +1 -1
  194. package/dist/twist-guide.d.ts.map +1 -1
  195. package/dist/twist.d.ts +24 -10
  196. package/dist/twist.d.ts.map +1 -1
  197. package/dist/twist.js +19 -5
  198. package/dist/twist.js.map +1 -1
  199. package/dist/utils/types.d.ts +19 -0
  200. package/dist/utils/types.d.ts.map +1 -1
  201. package/package.json +1 -1
@@ -0,0 +1,651 @@
1
+ # Sync Strategies
2
+
3
+ This guide explains good ways to build tools that sync other services with Plot. Choosing the right strategy depends on whether you need to update items, deduplicate them, or simply create them once.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Strategy 1: Create Once (Fire and Forget)](#strategy-1-create-once-fire-and-forget)
9
+ - [Strategy 2: Upsert via Source and Key (Recommended)](#strategy-2-upsert-via-source-and-key-recommended)
10
+ - [Strategy 3: Generate and Store IDs (Advanced)](#strategy-3-generate-and-store-ids-advanced)
11
+ - [Tag Management](#tag-management)
12
+ - [Choosing the Right Strategy](#choosing-the-right-strategy)
13
+
14
+ ## Overview
15
+
16
+ Plot provides three main strategies for managing activities and notes:
17
+
18
+ | Strategy | Use Case | Complexity | Deduplication | Updates |
19
+ | -------------------------- | -------------------------------------------------- | ---------- | ------------- | ------- |
20
+ | **Create Once** | One-time notifications, transient events | Low | None | No |
21
+ | **Upsert via Source/Key** | Most integrations (calendars, tasks, issues) | Low | Automatic | Yes |
22
+ | **Generate and Store IDs** | Complex transformations, multiple items per source | High | Manual | Yes |
23
+
24
+ **Recommended for most use cases**: Strategy 2 (Upsert via Source/Key)
25
+
26
+ ## Strategy 1: Create Once (Fire and Forget)
27
+
28
+ ### When to Use
29
+
30
+ Use this strategy when:
31
+
32
+ - Items are created once and never need updates
33
+ - Duplicates are acceptable or expected
34
+ - You're creating notifications, alerts, or transient events
35
+ - The external system doesn't provide stable identifiers
36
+
37
+ ### How It Works
38
+
39
+ Simply create activities and notes without specifying `id` or `source` fields. Plot will generate unique IDs automatically.
40
+
41
+ ### Example: Simple Notification
42
+
43
+ ```typescript
44
+ export default class NotificationTwist extends Twist {
45
+ async sendAlert(title: string, message: string): Promise<void> {
46
+ // Create a simple note-only activity
47
+ await this.tools.plot.createActivity({
48
+ type: ActivityType.Note,
49
+ title: title,
50
+ notes: [
51
+ {
52
+ content: message,
53
+ },
54
+ ],
55
+ });
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Pros and Cons
61
+
62
+ **Pros:**
63
+
64
+ - Simplest approach
65
+ - No storage overhead
66
+ - No external API lookups needed
67
+ - Fast execution
68
+
69
+ **Cons:**
70
+
71
+ - No deduplication
72
+ - Cannot update existing items
73
+ - Can create duplicates if called multiple times
74
+
75
+ ## Strategy 2: Upsert via Source and Key (Recommended)
76
+
77
+ ### When to Use
78
+
79
+ Use this strategy when:
80
+
81
+ - You're integrating with external systems that provide stable URLs or IDs
82
+ - Items need to be updated when the external source changes
83
+ - You want automatic deduplication without manual tracking
84
+ - You're syncing calendars, tasks, issues, messages, or similar entities
85
+
86
+ ### How It Works
87
+
88
+ **For Activities:**
89
+ Set the `source` field to a canonical URL or stable identifier. When you create an activity with a source that already exists in the priority tree, Plot will **update** the existing activity instead of creating a duplicate.
90
+
91
+ **For Notes:**
92
+ Use the `key` field combined with the activity's source to enable upserts. When you create a note with a key that already exists on the activity, Plot will **update** that note instead of creating a duplicate.
93
+
94
+ ### Activity Upserts
95
+
96
+ The `source` field should be:
97
+
98
+ - A canonical URL from the external system (preferred)
99
+ - A stable identifier in a namespaced format (e.g., `gmail:thread-id-123`)
100
+ - Unique within the priority tree
101
+
102
+ ```typescript
103
+ // Activity.source field definition
104
+ interface Activity {
105
+ /**
106
+ * Canonical URL for the item in an external system.
107
+ * For example, https://acme.atlassian.net/browse/PROJ-42 could represent a Jira issue.
108
+ * When set, it uniquely identifies the activity within a priority tree.
109
+ */
110
+ source: string | null;
111
+ }
112
+ ```
113
+
114
+ ### Example: Calendar Event Sync
115
+
116
+ ```typescript
117
+ export default class GoogleCalendarTool extends Tool<GoogleCalendarTool> {
118
+ async syncEvent(event: calendar_v3.Schema$Event): Promise<void> {
119
+ const activity: NewActivityWithNotes = {
120
+ // Use the event's canonical URL as the source
121
+ source: event.htmlLink,
122
+ type: ActivityType.Event,
123
+ title: event.summary || "(No title)",
124
+ start: event.start?.dateTime || event.start?.date || null,
125
+ end: event.end?.dateTime || event.end?.date || null,
126
+ notes: [],
127
+ };
128
+
129
+ // Add description as an upsertable note
130
+ if (event.description) {
131
+ activity.notes.push({
132
+ // Reference the activity by its source
133
+ activity: { source: event.htmlLink },
134
+ // Use a key for this specific note type
135
+ key: "description",
136
+ content: event.description,
137
+ });
138
+ }
139
+
140
+ // Add attendees as an upsertable note
141
+ if (event.attendees?.length) {
142
+ const attendeeList = event.attendees
143
+ .map((a) => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ""}`)
144
+ .join("\n");
145
+
146
+ activity.notes.push({
147
+ activity: { source: event.htmlLink },
148
+ key: "attendees",
149
+ content: `## Attendees\n${attendeeList}`,
150
+ });
151
+ }
152
+
153
+ // Create or update the activity
154
+ await this.tools.plot.createActivity(activity);
155
+ }
156
+ }
157
+ ```
158
+
159
+ **How it works:**
160
+
161
+ 1. First sync: Activity with `source: event.htmlLink` is created with two notes (description and attendees)
162
+ 2. Event updated externally: Same `source` is used, so Plot updates the existing activity
163
+ 3. Description changes: Note with `key: "description"` is updated
164
+ 4. Attendees change: Note with `key: "attendees"` is updated
165
+ 5. No duplicates created, no manual ID tracking needed
166
+
167
+ ### Example: Task/Issue Sync
168
+
169
+ ```typescript
170
+ export default class LinearTool extends Tool<LinearTool> {
171
+ async syncIssue(issue: LinearIssue): Promise<void> {
172
+ const activity: NewActivityWithNotes = {
173
+ source: issue.url, // Linear provides stable URLs
174
+ type: ActivityType.Action,
175
+ title: `${issue.identifier}: ${issue.title}`,
176
+ done: issue.state.type === "completed" ? new Date() : null,
177
+ notes: [],
178
+ };
179
+
180
+ // Description note with upsert
181
+ if (issue.description) {
182
+ activity.notes.push({
183
+ activity: { source: issue.url },
184
+ key: "description",
185
+ content: issue.description,
186
+ });
187
+ }
188
+
189
+ // Metadata note with upsert
190
+ activity.notes.push({
191
+ activity: { source: issue.url },
192
+ key: "metadata",
193
+ content: [
194
+ `**Status**: ${issue.state.name}`,
195
+ `**Priority**: ${issue.priority}`,
196
+ `**Assignee**: ${issue.assignee?.name || "Unassigned"}`,
197
+ ].join("\n"),
198
+ });
199
+
200
+ await this.tools.plot.createActivity(activity);
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Referencing Activities When Creating Notes
206
+
207
+ When creating a note separately (not as part of `NewActivityWithNotes`), reference the activity by its source:
208
+
209
+ ```typescript
210
+ // Add a comment to an existing activity
211
+ await this.tools.plot.createNote({
212
+ activity: { source: "https://github.com/user/repo/issues/42" },
213
+ key: `comment-${comment.id}`, // Unique key per comment
214
+ content: comment.body,
215
+ });
216
+ ```
217
+
218
+ ### Note Key Patterns
219
+
220
+ The `key` field enables upsert behavior for notes. Choose keys based on your use case:
221
+
222
+ **Single instance notes** (will be updated on each sync):
223
+
224
+ - `key: "description"` - Main description/body
225
+ - `key: "metadata"` - Status, assignee, etc.
226
+ - `key: "attendees"` - Event attendees list
227
+
228
+ **Multiple instance notes** (use unique keys):
229
+
230
+ - `key: "comment-${commentId}"` - Each comment has unique ID
231
+ - `key: "attachment-${filename}"` - Each attachment has unique name
232
+ - `key: "change-${timestamp}"` - Each change log entry
233
+
234
+ **No key** (creates new note every time):
235
+
236
+ - Omit `key` field when you want new notes created on each sync
237
+ - Useful for chat messages, activity logs, or append-only data
238
+
239
+ ### Pros and Cons
240
+
241
+ **Pros:**
242
+
243
+ - Automatic deduplication
244
+ - No storage overhead for ID mappings
245
+ - No need to look up existing items before creating
246
+ - Clean, maintainable code
247
+ - Works with external URLs (user-friendly)
248
+
249
+ **Cons:**
250
+
251
+ - Requires stable identifiers from external system
252
+ - One Plot activity per external source item
253
+ - Cannot create multiple Plot items from single source item
254
+
255
+ ## Strategy 3: Generate and Store IDs (Advanced)
256
+
257
+ ### When to Use
258
+
259
+ Use this strategy when:
260
+
261
+ - You need to create multiple Plot activities from a single external item
262
+ - External system doesn't provide stable identifiers
263
+ - You need complex transformations or splitting
264
+ - Source-based upserts aren't flexible enough for your use case
265
+
266
+ ### How It Works
267
+
268
+ 1. Generate a unique ID using `Uuid.Generate()`
269
+ 2. Store the mapping between external ID and Plot ID
270
+ 3. Look up existing IDs before creating items
271
+ 4. Use stored IDs when updating
272
+
273
+ ### Example: Multiple Activities from Single Source
274
+
275
+ ```typescript
276
+ export default class EmailTool extends Tool<EmailTool> {
277
+ /**
278
+ * Creates separate activities for email threads and individual messages.
279
+ * One email thread can have multiple Plot activities.
280
+ */
281
+ async syncThread(thread: GmailThread): Promise<void> {
282
+ // Check if we've seen this thread before
283
+ const threadKey = `thread:${thread.id}`;
284
+ let threadActivityId = await this.get<string>(threadKey);
285
+
286
+ // Generate ID if this is a new thread
287
+ if (!threadActivityId) {
288
+ threadActivityId = Uuid.Generate();
289
+ await this.set(threadKey, threadActivityId);
290
+ }
291
+
292
+ // Create/update the thread activity
293
+ await this.tools.plot.createActivity({
294
+ id: threadActivityId,
295
+ type: ActivityType.Note,
296
+ title: thread.snippet,
297
+ // Note: we use `id` instead of `source` for manual control
298
+ });
299
+
300
+ // Create separate activities for each important message in thread
301
+ for (const message of thread.messages) {
302
+ if (this.isImportantMessage(message)) {
303
+ const messageKey = `message:${message.id}`;
304
+ let messageActivityId = await this.get<string>(messageKey);
305
+
306
+ if (!messageActivityId) {
307
+ messageActivityId = Uuid.Generate();
308
+ await this.set(messageKey, messageActivityId);
309
+ }
310
+
311
+ await this.tools.plot.createActivity({
312
+ id: messageActivityId,
313
+ type: ActivityType.Action,
314
+ title: `Reply to: ${message.subject}`,
315
+ notes: [
316
+ {
317
+ content: message.body,
318
+ },
319
+ ],
320
+ });
321
+ }
322
+ }
323
+ }
324
+
325
+ private isImportantMessage(message: GmailMessage): boolean {
326
+ // Custom logic to determine if message needs separate activity
327
+ return message.labelIds?.includes("IMPORTANT") || false;
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### Storage Patterns
333
+
334
+ **Simple mapping:**
335
+
336
+ ```typescript
337
+ // Store external ID → Plot ID
338
+ await this.set(`external:${externalId}`, plotId);
339
+
340
+ // Retrieve
341
+ const plotId = await this.get<string>(`external:${externalId}`);
342
+ ```
343
+
344
+ **Structured mapping:**
345
+
346
+ ```typescript
347
+ interface Mapping {
348
+ plotId: string;
349
+ externalId: string;
350
+ lastSynced: string;
351
+ syncCount: number;
352
+ }
353
+
354
+ await this.set(`mapping:${externalId}`, mapping);
355
+ ```
356
+
357
+ ### Lookup and Update Pattern
358
+
359
+ ```typescript
360
+ async syncItem(externalItem: ExternalItem): Promise<void> {
361
+ const key = `item:${externalItem.id}`;
362
+
363
+ // Look up existing Plot ID
364
+ let plotId = await this.get<string>(key);
365
+
366
+ // Generate new ID if not found
367
+ if (!plotId) {
368
+ plotId = Uuid.Generate();
369
+ await this.set(key, plotId);
370
+ }
371
+
372
+ // Create or update using the ID
373
+ await this.tools.plot.createActivity({
374
+ id: plotId,
375
+ type: ActivityType.Action,
376
+ title: externalItem.title,
377
+ // ... other fields
378
+ });
379
+ }
380
+ ```
381
+
382
+ ### Pros and Cons
383
+
384
+ **Pros:**
385
+
386
+ - Maximum flexibility
387
+ - Can create multiple Plot items per external item
388
+ - Works without stable external identifiers
389
+ - Full control over ID lifecycle
390
+
391
+ **Cons:**
392
+
393
+ - Requires storage for mappings
394
+ - Needs lookup before each create/update
395
+ - More code to maintain
396
+ - Slower due to additional storage operations
397
+ - Must manage cleanup of old mappings
398
+
399
+ ## Tag Management
400
+
401
+ Tags can be applied to both activities and notes. Plot provides helpers for managing tags during sync operations.
402
+
403
+ ### Tag Helpers
404
+
405
+ The Plot tool provides tag management methods:
406
+
407
+ ```typescript
408
+ // Activity tags
409
+ await this.tools.plot.setActivityTags(activity, ["work", "urgent"]);
410
+ await this.tools.plot.addActivityTag(activity, "client-meeting");
411
+ await this.tools.plot.removeActivityTag(activity, "draft");
412
+
413
+ // Note tags
414
+ await this.tools.plot.setNoteTags(note, ["comment", "external"]);
415
+ await this.tools.plot.addNoteTag(note, "resolved");
416
+ await this.tools.plot.removeNoteTag(note, "pending");
417
+ ```
418
+
419
+ ### Tag Sync Patterns
420
+
421
+ **Replace all tags (set):**
422
+
423
+ ```typescript
424
+ // Sync tags from external system, replacing existing tags
425
+ await this.tools.plot.setActivityTags(
426
+ { source: issue.url },
427
+ issue.labels.map((l) => l.name)
428
+ );
429
+ ```
430
+
431
+ **Additive tagging (add):**
432
+
433
+ ```typescript
434
+ // Add tags without removing existing ones
435
+ if (event.isRecurring) {
436
+ await this.tools.plot.addActivityTag({ source: event.htmlLink }, "recurring");
437
+ }
438
+ ```
439
+
440
+ **Conditional tagging:**
441
+
442
+ ```typescript
443
+ // Tag based on external state
444
+ if (task.priority === "high") {
445
+ await this.tools.plot.addActivityTag({ source: task.url }, "urgent");
446
+ } else {
447
+ await this.tools.plot.removeActivityTag({ source: task.url }, "urgent");
448
+ }
449
+ ```
450
+
451
+ ### Tag Namespacing
452
+
453
+ To avoid tag collisions between different twists and tools, consider namespacing:
454
+
455
+ ```typescript
456
+ // Namespace tags with tool name
457
+ const tags = externalLabels.map((label) => `jira:${label}`);
458
+ await this.tools.plot.setActivityTags({ source: issue.url }, tags);
459
+
460
+ // Or use a prefix constant
461
+ const TAG_PREFIX = "linear-";
462
+ await this.tools.plot.addActivityTag(
463
+ { source: issue.url },
464
+ `${TAG_PREFIX}${issue.state.name}`
465
+ );
466
+ ```
467
+
468
+ ### Referencing Items for Tag Operations
469
+
470
+ Like note creation, tag operations can reference activities by source:
471
+
472
+ ```typescript
473
+ // Using source
474
+ await this.tools.plot.addActivityTag(
475
+ { source: "https://app.asana.com/0/123/456" },
476
+ "in-progress"
477
+ );
478
+
479
+ // Using ID (if you're using Strategy 3)
480
+ await this.tools.plot.addActivityTag({ id: storedActivityId }, "synced");
481
+ ```
482
+
483
+ ### Tag Cleanup
484
+
485
+ When an external item is deleted or tags are removed, clean up tags in Plot:
486
+
487
+ ```typescript
488
+ // Remove all tags from external system
489
+ const externalTags = issue.labels.map((l) => `jira:${l}`);
490
+ await this.tools.plot.setActivityTags({ source: issue.url }, externalTags);
491
+
492
+ // Or remove specific tags
493
+ for (const removedLabel of removedLabels) {
494
+ await this.tools.plot.removeActivityTag(
495
+ { source: issue.url },
496
+ `jira:${removedLabel}`
497
+ );
498
+ }
499
+ ```
500
+
501
+ ## Choosing the Right Strategy
502
+
503
+ Use this decision tree to select the appropriate strategy:
504
+
505
+ ```
506
+ Do items need to be updated after creation?
507
+ ├─ No
508
+ │ └─ Use Strategy 1 (Create Once)
509
+ │ Example: Alerts, one-time notifications
510
+
511
+ └─ Yes
512
+
513
+ Does the external system provide stable URLs or IDs?
514
+ ├─ Yes
515
+ │ │
516
+ │ Do you need multiple Plot items per external item?
517
+ │ ├─ No
518
+ │ │ └─ Use Strategy 2 (Upsert via Source/Key) ⭐ RECOMMENDED
519
+ │ │ Example: Calendar events, tasks, issues
520
+ │ │
521
+ │ └─ Yes
522
+ │ └─ Use Strategy 3 (Generate and Store IDs)
523
+ │ Example: Email thread → multiple activities
524
+
525
+ └─ No
526
+ └─ Use Strategy 3 (Generate and Store IDs)
527
+ Example: Systems without stable identifiers
528
+ ```
529
+
530
+ ### Common Use Cases
531
+
532
+ | Integration | Recommended Strategy | Rationale |
533
+ | ---------------- | -------------------- | ------------------------------------------- |
534
+ | Google Calendar | Strategy 2 | Events have stable `htmlLink` URLs |
535
+ | Outlook Calendar | Strategy 2 | Events have `webLink` URLs |
536
+ | Jira | Strategy 2 | Issues have stable URLs |
537
+ | Linear | Strategy 2 | Issues have stable URLs |
538
+ | Asana | Strategy 2 | Tasks have stable URLs |
539
+ | GitHub Issues | Strategy 2 | Issues have stable URLs |
540
+ | Gmail (threads) | Strategy 2 or 3 | Use 2 for thread-level, 3 for message-level |
541
+ | Slack (threads) | Strategy 2 | Threads have stable channel:thread IDs |
542
+ | RSS Feeds | Strategy 2 | Items usually have GUIDs or links |
543
+ | Webhooks | Strategy 1 or 2 | Depends on whether updates are needed |
544
+ | Notifications | Strategy 1 | Usually one-time, no updates needed |
545
+
546
+ ### Migration Between Strategies
547
+
548
+ If you need to change strategies for an existing tool:
549
+
550
+ **From Strategy 1 to Strategy 2:**
551
+
552
+ - Existing items will remain as duplicates
553
+ - New syncs will use source-based deduplication
554
+ - Consider adding migration logic to clean up duplicates
555
+
556
+ **From Strategy 3 to Strategy 2:**
557
+
558
+ ```typescript
559
+ // Migration: lookup existing ID, add source, then clean up mapping
560
+ const existingId = await this.get<string>(`external:${item.id}`);
561
+ if (existingId) {
562
+ await this.tools.plot.createActivity({
563
+ id: existingId,
564
+ source: item.url, // Add source to existing activity
565
+ // ... other fields
566
+ });
567
+ await this.delete(`external:${item.id}`); // Clean up mapping
568
+ }
569
+ ```
570
+
571
+ **From Strategy 2 to Strategy 3:**
572
+
573
+ - Existing activities will remain with their sources
574
+ - New items can use generated IDs
575
+ - Both can coexist if needed
576
+
577
+ ## Best Practices
578
+
579
+ ### 1. Be Consistent Within a Tool
580
+
581
+ Choose one strategy per tool and stick with it. Mixing strategies in the same tool can lead to confusion and bugs.
582
+
583
+ ### 2. Use Descriptive Keys
584
+
585
+ ```typescript
586
+ // Good: descriptive, unique keys
587
+ key: "description";
588
+ key: "metadata";
589
+ key: "comment-${commentId}";
590
+ key: "attachment-${filename}";
591
+
592
+ // Bad: generic, collision-prone keys
593
+ key: "note";
594
+ key: "data";
595
+ key: "1";
596
+ ```
597
+
598
+ ### 3. Handle Missing Sources Gracefully
599
+
600
+ ```typescript
601
+ const source = event.htmlLink || event.id || `temp:${Uuid.Generate()}`;
602
+ ```
603
+
604
+ ### 4. Document Your Strategy
605
+
606
+ Add comments explaining which strategy you're using and why:
607
+
608
+ ```typescript
609
+ /**
610
+ * Syncs calendar events using Strategy 2 (Upsert via Source).
611
+ * Each Google Calendar event has a stable htmlLink that serves as the source.
612
+ * Event details and attendees are stored as upsertable notes using keys.
613
+ */
614
+ async syncEvents(): Promise<void> {
615
+ // ...
616
+ }
617
+ ```
618
+
619
+ ### 5. Clean Up When Needed
620
+
621
+ For Strategy 3, implement cleanup for old mappings:
622
+
623
+ ```typescript
624
+ async cleanupOldMappings(): Promise<void> {
625
+ // Remove mappings for items deleted externally
626
+ const allKeys = await this.keys();
627
+ for (const key of allKeys) {
628
+ if (key.startsWith('external:')) {
629
+ const externalId = key.replace('external:', '');
630
+ const exists = await this.checkExternalItemExists(externalId);
631
+ if (!exists) {
632
+ await this.delete(key);
633
+ }
634
+ }
635
+ }
636
+ }
637
+ ```
638
+
639
+ ## Summary
640
+
641
+ - **Strategy 1** (Create Once): Simplest, no deduplication, use for one-time items
642
+ - **Strategy 2** (Upsert via Source/Key): Recommended for most integrations, automatic deduplication
643
+ - **Strategy 3** (Generate and Store IDs): Advanced use cases, maximum flexibility, more complexity
644
+
645
+ Start with Strategy 2 for most integrations. Only use Strategy 3 when you have specific requirements that Strategy 2 cannot fulfill.
646
+
647
+ For more information:
648
+
649
+ - [Core Concepts](CORE_CONCEPTS.md) - Understanding activities, notes, and priorities
650
+ - [Tools Guide](TOOLS_GUIDE.md) - Complete reference for the Plot tool
651
+ - [Building Tools](BUILDING_TOOLS.md) - Creating custom tools