@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.
- package/README.md +10 -4
- package/bin/templates/AGENTS.template.md +91 -29
- package/cli/templates/AGENTS.template.md +91 -29
- package/dist/common/calendar.d.ts +35 -28
- package/dist/common/calendar.d.ts.map +1 -1
- package/dist/common/messaging.d.ts +24 -10
- package/dist/common/messaging.d.ts.map +1 -1
- package/dist/common/projects.d.ts +30 -15
- package/dist/common/projects.d.ts.map +1 -1
- package/dist/common/serializable.d.ts +40 -0
- package/dist/common/serializable.d.ts.map +1 -0
- package/dist/common/serializable.js +2 -0
- package/dist/common/serializable.js.map +1 -0
- package/dist/docs/assets/hierarchy.js +1 -1
- package/dist/docs/assets/navigation.js +1 -1
- package/dist/docs/assets/search.js +1 -1
- package/dist/docs/classes/tool.ITool.html +1 -1
- package/dist/docs/classes/tool.Tool.html +32 -28
- package/dist/docs/classes/tools_ai.AI.html +2 -2
- package/dist/docs/classes/tools_callbacks.Callbacks.html +6 -6
- package/dist/docs/classes/tools_integrations.Integrations.html +3 -3
- package/dist/docs/classes/tools_network.Network.html +12 -12
- package/dist/docs/classes/tools_plot.Plot.html +41 -31
- package/dist/docs/classes/tools_store.Store.html +31 -11
- package/dist/docs/classes/tools_tasks.Tasks.html +8 -8
- package/dist/docs/classes/tools_twists.Twists.html +5 -5
- package/dist/docs/classes/twist.Twist.html +28 -24
- package/dist/docs/documents/Building_Custom_Tools.html +8 -2
- package/dist/docs/documents/Built-in_Tools.html +19 -8
- package/dist/docs/documents/Core_Concepts.html +14 -6
- package/dist/docs/documents/Getting_Started.html +11 -4
- package/dist/docs/enums/plot.ActivityLinkType.html +5 -5
- package/dist/docs/enums/plot.ActivityType.html +4 -4
- package/dist/docs/enums/plot.ActorType.html +4 -4
- package/dist/docs/enums/plot.ConferencingProvider.html +6 -6
- package/dist/docs/enums/tag.Tag.html +2 -2
- package/dist/docs/enums/tools_ai.AIModel.html +2 -2
- package/dist/docs/enums/tools_integrations.AuthLevel.html +3 -3
- package/dist/docs/enums/tools_integrations.AuthProvider.html +11 -11
- package/dist/docs/enums/tools_plot.ActivityAccess.html +3 -3
- package/dist/docs/enums/tools_plot.ContactAccess.html +4 -4
- package/dist/docs/enums/tools_plot.PriorityAccess.html +3 -3
- package/dist/docs/functions/index.Uuid.Generate.html +1 -1
- package/dist/docs/functions/utils_hash.quickHash.html +1 -1
- package/dist/docs/hierarchy.html +1 -1
- package/dist/docs/index.html +10 -0
- package/dist/docs/interfaces/common_calendar.Calendar.html +5 -5
- package/dist/docs/interfaces/common_calendar.CalendarTool.html +29 -15
- package/dist/docs/interfaces/common_calendar.SyncOptions.html +3 -3
- package/dist/docs/interfaces/index.SerializableArray.html +2 -0
- package/dist/docs/interfaces/index.SerializableMap.html +2 -0
- package/dist/docs/interfaces/index.SerializableObject.html +2 -0
- package/dist/docs/interfaces/index.SerializableSet.html +2 -0
- package/dist/docs/interfaces/tools_ai.AIRequest.html +11 -11
- package/dist/docs/interfaces/tools_ai.AIResponse.html +9 -9
- package/dist/docs/interfaces/tools_ai.FilePart.html +5 -5
- package/dist/docs/interfaces/tools_ai.ImagePart.html +4 -4
- package/dist/docs/interfaces/tools_ai.ReasoningPart.html +4 -4
- package/dist/docs/interfaces/tools_ai.RedactedReasoningPart.html +3 -3
- package/dist/docs/interfaces/tools_ai.TextPart.html +3 -3
- package/dist/docs/interfaces/tools_ai.ToolCallPart.html +5 -5
- package/dist/docs/interfaces/tools_ai.ToolExecutionOptions.html +4 -4
- package/dist/docs/interfaces/tools_ai.ToolResultPart.html +5 -5
- package/dist/docs/interfaces/tools_twists.TwistSource.html +3 -3
- package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
- package/dist/docs/media/SYNC_STRATEGIES.md +651 -0
- package/dist/docs/modules/index.html +1 -1
- package/dist/docs/modules/plot.html +1 -1
- package/dist/docs/modules/utils_types.html +1 -1
- package/dist/docs/types/common_calendar.CalendarAuth.html +2 -2
- package/dist/docs/types/index.Serializable.html +17 -0
- package/dist/docs/types/index.Uuid.html +1 -1
- package/dist/docs/types/plot.Activity.html +17 -8
- package/dist/docs/types/plot.ActivityCommon.html +10 -10
- package/dist/docs/types/plot.ActivityLink.html +1 -1
- package/dist/docs/types/plot.ActivityMeta.html +5 -3
- package/dist/docs/types/plot.ActivityUpdate.html +4 -2
- package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
- package/dist/docs/types/plot.Actor.html +15 -5
- package/dist/docs/types/plot.ActorId.html +1 -1
- package/dist/docs/types/plot.ContentType.html +1 -1
- package/dist/docs/types/plot.NewActivity.html +26 -12
- package/dist/docs/types/plot.NewActivityWithNotes.html +1 -1
- package/dist/docs/types/plot.NewActor.html +1 -1
- package/dist/docs/types/plot.NewContact.html +4 -4
- package/dist/docs/types/plot.NewNote.html +9 -4
- package/dist/docs/types/plot.NewPriority.html +15 -5
- package/dist/docs/types/plot.NewTags.html +1 -1
- package/dist/docs/types/plot.Note.html +6 -3
- package/dist/docs/types/plot.NoteUpdate.html +4 -3
- package/dist/docs/types/plot.PickPriorityConfig.html +3 -3
- package/dist/docs/types/plot.Priority.html +9 -4
- package/dist/docs/types/plot.PriorityUpdate.html +3 -0
- package/dist/docs/types/plot.SyncUpdate.html +1 -1
- package/dist/docs/types/plot.Tags.html +1 -1
- package/dist/docs/types/tools_ai.AIAssistantMessage.html +2 -2
- package/dist/docs/types/tools_ai.AIMessage.html +1 -1
- package/dist/docs/types/tools_ai.AISource.html +1 -1
- package/dist/docs/types/tools_ai.AISystemMessage.html +2 -2
- package/dist/docs/types/tools_ai.AITool.html +1 -1
- package/dist/docs/types/tools_ai.AIToolMessage.html +2 -2
- package/dist/docs/types/tools_ai.AIToolSet.html +1 -1
- package/dist/docs/types/tools_ai.AIUsage.html +5 -5
- package/dist/docs/types/tools_ai.AIUserMessage.html +2 -2
- package/dist/docs/types/tools_ai.DataContent.html +1 -1
- package/dist/docs/types/tools_ai.ModelPreferences.html +4 -4
- package/dist/docs/types/tools_callbacks.Callback.html +1 -1
- package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
- package/dist/docs/types/tools_integrations.Authorization.html +4 -4
- package/dist/docs/types/tools_network.WebhookRequest.html +6 -6
- package/dist/docs/types/tools_plot.NoteIntentHandler.html +4 -4
- package/dist/docs/types/tools_twists.Log.html +2 -2
- package/dist/docs/types/tools_twists.TwistPermissions.html +1 -1
- package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
- package/dist/docs/types/utils_types.CallbackMethods.html +1 -1
- package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
- package/dist/docs/types/utils_types.InferOptions.html +1 -1
- package/dist/docs/types/utils_types.InferTools.html +1 -1
- package/dist/docs/types/utils_types.JSONValue.html +7 -0
- package/dist/docs/types/utils_types.NoFunctions.html +1 -1
- package/dist/docs/types/utils_types.NonFunction.html +1 -1
- package/dist/docs/types/utils_types.PromiseValues.html +1 -1
- package/dist/docs/types/utils_types.ToolBuilder.html +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-docs/common/calendar.d.ts +1 -1
- package/dist/llm-docs/common/calendar.d.ts.map +1 -1
- package/dist/llm-docs/common/calendar.js +1 -1
- package/dist/llm-docs/common/calendar.js.map +1 -1
- package/dist/llm-docs/common/messaging.d.ts +1 -1
- package/dist/llm-docs/common/messaging.d.ts.map +1 -1
- package/dist/llm-docs/common/messaging.js +1 -1
- package/dist/llm-docs/common/messaging.js.map +1 -1
- package/dist/llm-docs/common/projects.d.ts +1 -1
- package/dist/llm-docs/common/projects.d.ts.map +1 -1
- package/dist/llm-docs/common/projects.js +1 -1
- package/dist/llm-docs/common/projects.js.map +1 -1
- package/dist/llm-docs/plot.d.ts +1 -1
- package/dist/llm-docs/plot.d.ts.map +1 -1
- package/dist/llm-docs/plot.js +1 -1
- package/dist/llm-docs/plot.js.map +1 -1
- package/dist/llm-docs/tool.d.ts +1 -1
- package/dist/llm-docs/tool.d.ts.map +1 -1
- package/dist/llm-docs/tool.js +1 -1
- package/dist/llm-docs/tool.js.map +1 -1
- package/dist/llm-docs/tools/network.d.ts +1 -1
- package/dist/llm-docs/tools/network.d.ts.map +1 -1
- package/dist/llm-docs/tools/network.js +1 -1
- package/dist/llm-docs/tools/network.js.map +1 -1
- package/dist/llm-docs/tools/plot.d.ts +1 -1
- package/dist/llm-docs/tools/plot.d.ts.map +1 -1
- package/dist/llm-docs/tools/plot.js +1 -1
- package/dist/llm-docs/tools/plot.js.map +1 -1
- package/dist/llm-docs/tools/store.d.ts +1 -1
- package/dist/llm-docs/tools/store.d.ts.map +1 -1
- package/dist/llm-docs/tools/store.js +1 -1
- package/dist/llm-docs/tools/store.js.map +1 -1
- package/dist/llm-docs/tools/tasks.d.ts +1 -1
- package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
- package/dist/llm-docs/tools/tasks.js +1 -1
- package/dist/llm-docs/tools/tasks.js.map +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
- package/dist/llm-docs/twist-guide-template.js +1 -1
- package/dist/llm-docs/twist-guide-template.js.map +1 -1
- package/dist/llm-docs/twist.d.ts +1 -1
- package/dist/llm-docs/twist.d.ts.map +1 -1
- package/dist/llm-docs/twist.js +1 -1
- package/dist/llm-docs/twist.js.map +1 -1
- package/dist/plot.d.ts +232 -56
- package/dist/plot.d.ts.map +1 -1
- package/dist/plot.js.map +1 -1
- package/dist/tool.d.ts +30 -8
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +29 -7
- package/dist/tool.js.map +1 -1
- package/dist/tools/network.d.ts +19 -16
- package/dist/tools/network.d.ts.map +1 -1
- package/dist/tools/network.js +6 -4
- package/dist/tools/network.js.map +1 -1
- package/dist/tools/plot.d.ts +93 -24
- package/dist/tools/plot.d.ts.map +1 -1
- package/dist/tools/plot.js +46 -1
- package/dist/tools/plot.js.map +1 -1
- package/dist/tools/store.d.ts +49 -8
- package/dist/tools/store.d.ts.map +1 -1
- package/dist/tools/store.js +12 -1
- package/dist/tools/store.js.map +1 -1
- package/dist/tools/tasks.d.ts +4 -4
- package/dist/tools/tasks.js +4 -4
- package/dist/twist-guide.d.ts +1 -1
- package/dist/twist-guide.d.ts.map +1 -1
- package/dist/twist.d.ts +24 -10
- package/dist/twist.d.ts.map +1 -1
- package/dist/twist.js +19 -5
- package/dist/twist.js.map +1 -1
- package/dist/utils/types.d.ts +19 -0
- package/dist/utils/types.d.ts.map +1 -1
- 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
|