@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.
- package/bin/templates/AGENTS.template.md +187 -182
- package/cli/templates/AGENTS.template.md +187 -182
- package/dist/common/calendar.d.ts +20 -90
- package/dist/common/calendar.d.ts.map +1 -1
- package/dist/common/documents.d.ts +26 -39
- package/dist/common/documents.d.ts.map +1 -1
- package/dist/common/messaging.d.ts +21 -40
- package/dist/common/messaging.d.ts.map +1 -1
- package/dist/common/projects.d.ts +27 -51
- package/dist/common/projects.d.ts.map +1 -1
- 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/index.Options.html +10 -0
- package/dist/docs/classes/tool.ITool.html +1 -1
- package/dist/docs/classes/tool.Tool.html +24 -20
- package/dist/docs/classes/tools_ai.AI.html +1 -1
- package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
- package/dist/docs/classes/tools_integrations.Integrations.html +33 -26
- package/dist/docs/classes/tools_network.Network.html +1 -1
- package/dist/docs/classes/tools_plot.Plot.html +3 -3
- package/dist/docs/classes/tools_store.Store.html +10 -4
- package/dist/docs/classes/tools_tasks.Tasks.html +1 -1
- package/dist/docs/classes/tools_twists.Twists.html +1 -1
- package/dist/docs/enums/plot.ActorType.html +4 -4
- package/dist/docs/enums/tools_integrations.AuthProvider.html +11 -11
- package/dist/docs/hierarchy.html +1 -1
- package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
- package/dist/docs/modules/common_calendar.html +1 -1
- package/dist/docs/modules/index.html +1 -1
- package/dist/docs/modules/plot.html +1 -1
- package/dist/docs/modules/tool.html +1 -1
- package/dist/docs/modules/tools_integrations.html +1 -1
- package/dist/docs/types/common_calendar.Calendar.html +5 -5
- package/dist/docs/types/common_calendar.CalendarTool.html +17 -43
- package/dist/docs/types/common_calendar.SyncOptions.html +3 -3
- package/dist/docs/types/index.BooleanDef.html +7 -0
- package/dist/docs/types/index.NumberDef.html +9 -0
- package/dist/docs/types/index.OptionDef.html +2 -0
- package/dist/docs/types/index.OptionsSchema.html +3 -0
- package/dist/docs/types/index.ResolvedOptions.html +4 -0
- package/dist/docs/types/index.SelectDef.html +8 -0
- package/dist/docs/types/index.TextDef.html +8 -0
- package/dist/docs/types/plot.Activity.html +1 -1
- package/dist/docs/types/plot.ActivityFilter.html +3 -0
- package/dist/docs/types/plot.ActivityOccurrence.html +7 -7
- package/dist/docs/types/plot.ActivityOccurrenceUpdate.html +1 -1
- package/dist/docs/types/plot.ActivityUpdate.html +3 -31
- package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
- package/dist/docs/types/plot.Actor.html +5 -5
- package/dist/docs/types/plot.ContentType.html +1 -1
- package/dist/docs/types/plot.NewActivity.html +1 -1
- package/dist/docs/types/plot.NewActivityOccurrence.html +1 -1
- 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 +5 -5
- package/dist/docs/types/plot.NewNote.html +1 -1
- package/dist/docs/types/plot.Note.html +1 -1
- package/dist/docs/types/plot.NoteUpdate.html +1 -1
- package/dist/docs/types/plot.PickPriorityConfig.html +2 -2
- package/dist/docs/types/tool.SyncToolOptions.html +9 -0
- 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_integrations.IntegrationOptions.html +4 -0
- package/dist/docs/types/tools_integrations.IntegrationProviderConfig.html +13 -0
- package/dist/docs/types/tools_integrations.Syncable.html +9 -0
- package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
- 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 +1 -1
- package/dist/docs/types/utils_types.PromiseValues.html +1 -1
- package/dist/docs/types/utils_types.ToolBuilder.html +2 -2
- 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/documents.d.ts +1 -1
- package/dist/llm-docs/common/documents.d.ts.map +1 -1
- package/dist/llm-docs/common/documents.js +1 -1
- package/dist/llm-docs/common/documents.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/index.d.ts.map +1 -1
- package/dist/llm-docs/index.js +2 -0
- package/dist/llm-docs/index.js.map +1 -1
- package/dist/llm-docs/options.d.ts +9 -0
- package/dist/llm-docs/options.d.ts.map +1 -0
- package/dist/llm-docs/options.js +8 -0
- package/dist/llm-docs/options.js.map +1 -0
- 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/integrations.d.ts +1 -1
- package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
- package/dist/llm-docs/tools/integrations.js +1 -1
- package/dist/llm-docs/tools/integrations.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/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/options.d.ts +104 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +40 -0
- package/dist/options.js.map +1 -0
- package/dist/plot.d.ts +32 -13
- package/dist/plot.d.ts.map +1 -1
- package/dist/plot.js.map +1 -1
- package/dist/tool.d.ts +22 -1
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +9 -0
- package/dist/tool.js.map +1 -1
- package/dist/tools/integrations.d.ts +88 -46
- package/dist/tools/integrations.d.ts.map +1 -1
- package/dist/tools/integrations.js +36 -21
- package/dist/tools/integrations.js.map +1 -1
- package/dist/tools/plot.d.ts +3 -1
- package/dist/tools/plot.d.ts.map +1 -1
- package/dist/tools/store.d.ts +10 -0
- package/dist/tools/store.d.ts.map +1 -1
- package/dist/tools/store.js.map +1 -1
- package/dist/twist-guide.d.ts +1 -1
- package/dist/twist-guide.d.ts.map +1 -1
- package/dist/utils/types.d.ts +5 -1
- package/dist/utils/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/dist/docs/types/common_calendar.CalendarAuth.html +0 -7
|
@@ -70,28 +70,41 @@ New event/task/conversation?
|
|
|
70
70
|
```typescript
|
|
71
71
|
import {
|
|
72
72
|
type Activity,
|
|
73
|
+
type NewActivityWithNotes,
|
|
74
|
+
type ActivityFilter,
|
|
73
75
|
type Priority,
|
|
74
76
|
type ToolBuilder,
|
|
75
|
-
|
|
77
|
+
Twist,
|
|
78
|
+
ActivityType,
|
|
76
79
|
} from "@plotday/twister";
|
|
77
|
-
import { Plot } from "@plotday/twister/tools/plot";
|
|
78
|
-
|
|
80
|
+
import { ActivityAccess, Plot } from "@plotday/twister/tools/plot";
|
|
81
|
+
// Import your tools:
|
|
82
|
+
// import { GoogleCalendar } from "@plotday/tool-google-calendar";
|
|
83
|
+
// import { Linear } from "@plotday/tool-linear";
|
|
79
84
|
|
|
80
85
|
export default class MyTwist extends Twist<MyTwist> {
|
|
81
86
|
build(build: ToolBuilder) {
|
|
82
87
|
return {
|
|
83
|
-
|
|
88
|
+
// myTool: build(MyTool, {
|
|
89
|
+
// onItem: this.handleItem,
|
|
90
|
+
// onSyncableDisabled: this.onSyncableDisabled,
|
|
91
|
+
// }),
|
|
92
|
+
plot: build(Plot, {
|
|
93
|
+
activity: { access: ActivityAccess.Create },
|
|
94
|
+
}),
|
|
84
95
|
};
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
async activate(
|
|
88
|
-
//
|
|
89
|
-
// Common actions: request auth, create setup activities
|
|
98
|
+
async activate(_priority: Pick<Priority, "id">) {
|
|
99
|
+
// Auth and resource selection handled in the twist edit modal.
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
async
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
async handleItem(activity: NewActivityWithNotes): Promise<void> {
|
|
103
|
+
await this.tools.plot.createActivity(activity);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
|
|
107
|
+
await this.tools.plot.updateActivity({ match: filter, archived: true });
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
```
|
|
@@ -145,76 +158,111 @@ Add tool dependencies to `package.json`:
|
|
|
145
158
|
}
|
|
146
159
|
```
|
|
147
160
|
|
|
148
|
-
####
|
|
161
|
+
#### Available External Tools
|
|
149
162
|
|
|
150
|
-
- `@plotday/tool-google-calendar`: Google Calendar
|
|
151
|
-
- `@plotday/tool-outlook-calendar`: Outlook Calendar
|
|
152
|
-
- `@plotday/tool-google-contacts`: Google Contacts
|
|
163
|
+
- `@plotday/tool-google-calendar`: Google Calendar sync (CalendarTool)
|
|
164
|
+
- `@plotday/tool-outlook-calendar`: Outlook Calendar sync (CalendarTool)
|
|
165
|
+
- `@plotday/tool-google-contacts`: Google Contacts sync (supporting tool)
|
|
166
|
+
- `@plotday/tool-google-drive`: Google Drive sync (DocumentTool)
|
|
167
|
+
- `@plotday/tool-gmail`: Gmail sync (MessagingTool)
|
|
168
|
+
- `@plotday/tool-slack`: Slack sync (MessagingTool)
|
|
169
|
+
- `@plotday/tool-linear`: Linear sync (ProjectTool)
|
|
170
|
+
- `@plotday/tool-jira`: Jira sync (ProjectTool)
|
|
171
|
+
- `@plotday/tool-asana`: Asana sync (ProjectTool)
|
|
153
172
|
|
|
154
173
|
## Lifecycle Methods
|
|
155
174
|
|
|
156
175
|
### activate(priority: Pick<Priority, "id">)
|
|
157
176
|
|
|
158
|
-
Called when the twist is enabled for a priority.
|
|
177
|
+
Called 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.
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
Most twists have an empty or minimal `activate()`:
|
|
161
180
|
|
|
162
181
|
```typescript
|
|
163
182
|
async activate(_priority: Pick<Priority, "id">) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"google"
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
await this.tools.plot.createActivity({
|
|
170
|
-
type: ActivityType.Note,
|
|
171
|
-
title: "Connect your account",
|
|
172
|
-
notes: [
|
|
173
|
-
{
|
|
174
|
-
content: "Click the link below to connect your account and start syncing.",
|
|
175
|
-
links: [authLink],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
});
|
|
183
|
+
// Auth and resource selection are handled in the twist edit modal.
|
|
184
|
+
// Only add custom initialization here if needed.
|
|
179
185
|
}
|
|
180
186
|
```
|
|
181
187
|
|
|
182
|
-
**Store Parent Activity for Later:**
|
|
188
|
+
**Store Parent Activity for Later (optional):**
|
|
183
189
|
|
|
184
190
|
```typescript
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{
|
|
190
|
-
content: "Your twist is
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await this.set("setup_activity_id", activity.id);
|
|
191
|
+
async activate(_priority: Pick<Priority, "id">) {
|
|
192
|
+
const activityId = await this.tools.plot.createActivity({
|
|
193
|
+
type: ActivityType.Note,
|
|
194
|
+
title: "Setup complete",
|
|
195
|
+
notes: [{
|
|
196
|
+
content: "Your twist is ready. Activities will appear as they sync.",
|
|
197
|
+
}],
|
|
198
|
+
});
|
|
199
|
+
await this.set("setup_activity_id", activityId);
|
|
200
|
+
}
|
|
196
201
|
```
|
|
197
202
|
|
|
198
|
-
###
|
|
203
|
+
### Event Callbacks (via build options)
|
|
199
204
|
|
|
200
|
-
|
|
205
|
+
Twists respond to events through callbacks declared in `build()`:
|
|
201
206
|
|
|
202
|
-
**
|
|
207
|
+
**Receive synced items from a tool (most common):**
|
|
203
208
|
|
|
204
209
|
```typescript
|
|
205
|
-
|
|
210
|
+
build(build: ToolBuilder) {
|
|
211
|
+
return {
|
|
212
|
+
myTool: build(MyTool, {
|
|
213
|
+
onItem: this.handleItem,
|
|
214
|
+
onSyncableDisabled: this.onSyncableDisabled,
|
|
215
|
+
}),
|
|
216
|
+
plot: build(Plot, { activity: { access: ActivityAccess.Create } }),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async handleItem(activity: NewActivityWithNotes): Promise<void> {
|
|
206
221
|
await this.tools.plot.createActivity(activity);
|
|
207
222
|
}
|
|
223
|
+
|
|
224
|
+
async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
|
|
225
|
+
await this.tools.plot.updateActivity({ match: filter, archived: true });
|
|
226
|
+
}
|
|
208
227
|
```
|
|
209
228
|
|
|
210
|
-
**
|
|
229
|
+
**React to activity changes (for two-way sync):**
|
|
211
230
|
|
|
212
231
|
```typescript
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
plot: build(Plot, {
|
|
233
|
+
activity: {
|
|
234
|
+
access: ActivityAccess.Create,
|
|
235
|
+
updated: this.onActivityUpdated,
|
|
236
|
+
},
|
|
237
|
+
note: {
|
|
238
|
+
created: this.onNoteCreated,
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
|
|
242
|
+
async onActivityUpdated(activity: Activity, changes: { tagsAdded, tagsRemoved }): Promise<void> {
|
|
243
|
+
const tool = this.getToolForActivity(activity);
|
|
244
|
+
if (tool?.updateIssue) await tool.updateIssue(activity);
|
|
217
245
|
}
|
|
246
|
+
|
|
247
|
+
async onNoteCreated(note: Note): Promise<void> {
|
|
248
|
+
if (note.author.type === ActorType.Twist) return; // Prevent loops
|
|
249
|
+
// Sync note to external service as a comment
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Respond to mentions (AI twist pattern):**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
plot: build(Plot, {
|
|
257
|
+
activity: { access: ActivityAccess.Respond },
|
|
258
|
+
note: {
|
|
259
|
+
intents: [{
|
|
260
|
+
description: "Respond to general questions",
|
|
261
|
+
examples: ["What's the weather?", "Help me plan my week"],
|
|
262
|
+
handler: this.respond,
|
|
263
|
+
}],
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
218
266
|
```
|
|
219
267
|
|
|
220
268
|
## Activity Links
|
|
@@ -224,19 +272,19 @@ Activity links enable user interaction:
|
|
|
224
272
|
```typescript
|
|
225
273
|
import { type ActivityLink, ActivityLinkType } from "@plotday/twister";
|
|
226
274
|
|
|
227
|
-
// URL link
|
|
275
|
+
// External URL link
|
|
228
276
|
const urlLink: ActivityLink = {
|
|
229
277
|
title: "Open website",
|
|
230
|
-
type: ActivityLinkType.
|
|
278
|
+
type: ActivityLinkType.external,
|
|
231
279
|
url: "https://example.com",
|
|
232
280
|
};
|
|
233
281
|
|
|
234
|
-
// Callback link (uses Callbacks tool)
|
|
235
|
-
const token = await this.
|
|
282
|
+
// Callback link (uses Callbacks tool — use linkCallback, not callback)
|
|
283
|
+
const token = await this.linkCallback(this.onLinkClicked, "context");
|
|
236
284
|
const callbackLink: ActivityLink = {
|
|
237
285
|
title: "Click me",
|
|
238
286
|
type: ActivityLinkType.callback,
|
|
239
|
-
|
|
287
|
+
callback: token,
|
|
240
288
|
};
|
|
241
289
|
|
|
242
290
|
// Add to activity note
|
|
@@ -250,70 +298,87 @@ await this.tools.plot.createActivity({
|
|
|
250
298
|
},
|
|
251
299
|
],
|
|
252
300
|
});
|
|
301
|
+
|
|
302
|
+
// Callback handler receives the ActivityLink as first argument
|
|
303
|
+
async onLinkClicked(link: ActivityLink, context: string): Promise<void> {
|
|
304
|
+
// Handle link click
|
|
305
|
+
}
|
|
253
306
|
```
|
|
254
307
|
|
|
255
308
|
## Authentication Pattern
|
|
256
309
|
|
|
257
|
-
|
|
310
|
+
Auth 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.**
|
|
258
311
|
|
|
259
312
|
```typescript
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
links: [authLink],
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Store for later use
|
|
280
|
-
await this.set("auth_activity_id", activity.id);
|
|
313
|
+
// In your tool's build() method:
|
|
314
|
+
build(build: ToolBuilder) {
|
|
315
|
+
return {
|
|
316
|
+
integrations: build(Integrations, {
|
|
317
|
+
providers: [{
|
|
318
|
+
provider: AuthProvider.Google,
|
|
319
|
+
scopes: ["https://www.googleapis.com/auth/calendar"],
|
|
320
|
+
getSyncables: this.getSyncables, // List available resources after auth
|
|
321
|
+
onSyncEnabled: this.onSyncEnabled, // User enabled a resource
|
|
322
|
+
onSyncDisabled: this.onSyncDisabled, // User disabled a resource
|
|
323
|
+
}],
|
|
324
|
+
}),
|
|
325
|
+
// ...
|
|
326
|
+
};
|
|
281
327
|
}
|
|
282
328
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
329
|
+
// Get a token for API calls:
|
|
330
|
+
const token = await this.tools.integrations.get(AuthProvider.Google, syncableId);
|
|
331
|
+
if (!token) throw new Error("No auth token available");
|
|
332
|
+
const client = new ApiClient({ accessToken: token.token });
|
|
333
|
+
```
|
|
286
334
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
335
|
+
For per-user write-backs (e.g., RSVP, comments attributed to the acting user):
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
await this.tools.integrations.actAs(
|
|
339
|
+
AuthProvider.Google,
|
|
340
|
+
actorId, // The user who performed the action
|
|
341
|
+
activityId, // Activity to prompt for auth if needed
|
|
342
|
+
this.performWriteBack,
|
|
343
|
+
...extraArgs
|
|
344
|
+
);
|
|
290
345
|
```
|
|
291
346
|
|
|
292
347
|
## Sync Pattern
|
|
293
348
|
|
|
294
|
-
### Recommended:
|
|
349
|
+
### Recommended: Using External Tools with SyncToolOptions
|
|
295
350
|
|
|
296
|
-
|
|
351
|
+
Most twists use external tools (CalendarTool, ProjectTool, etc.) that handle sync internally. The twist just receives `NewActivityWithNotes` objects and saves them:
|
|
297
352
|
|
|
298
353
|
```typescript
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
);
|
|
354
|
+
build(build: ToolBuilder) {
|
|
355
|
+
return {
|
|
356
|
+
calendarTool: build(GoogleCalendar, {
|
|
357
|
+
onItem: this.handleEvent, // Receives synced items
|
|
358
|
+
onSyncableDisabled: this.onSyncableDisabled, // Clean up when disabled
|
|
359
|
+
}),
|
|
360
|
+
plot: build(Plot, { activity: { access: ActivityAccess.Create } }),
|
|
361
|
+
};
|
|
308
362
|
}
|
|
309
363
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
// Tools deliver NewActivityWithNotes — twist saves them
|
|
365
|
+
async handleEvent(activity: NewActivityWithNotes): Promise<void> {
|
|
366
|
+
await this.tools.plot.createActivity(activity);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
|
|
370
|
+
await this.tools.plot.updateActivity({ match: filter, archived: true });
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Custom Sync: Upsert via Source/Key (Strategy 2)
|
|
375
|
+
|
|
376
|
+
For direct API integration without an external tool, use source/key for automatic upserts:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
async handleEvent(event: ExternalEvent): Promise<void> {
|
|
315
380
|
const activity: NewActivityWithNotes = {
|
|
316
|
-
source: event.htmlLink,
|
|
381
|
+
source: event.htmlLink, // Canonical URL for automatic deduplication
|
|
317
382
|
type: ActivityType.Event,
|
|
318
383
|
title: event.summary || "(No title)",
|
|
319
384
|
start: event.start?.dateTime || event.start?.date || null,
|
|
@@ -321,36 +386,17 @@ async handleEvent(
|
|
|
321
386
|
notes: [],
|
|
322
387
|
};
|
|
323
388
|
|
|
324
|
-
// Add description as an upsertable note
|
|
325
389
|
if (event.description) {
|
|
326
390
|
activity.notes.push({
|
|
327
391
|
activity: { source: event.htmlLink },
|
|
328
|
-
key: "description",
|
|
392
|
+
key: "description", // This key enables note-level upserts
|
|
329
393
|
content: event.description,
|
|
330
394
|
});
|
|
331
395
|
}
|
|
332
396
|
|
|
333
|
-
//
|
|
334
|
-
if (event.attendees?.length) {
|
|
335
|
-
const attendeeList = event.attendees
|
|
336
|
-
.map(a => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ''}`)
|
|
337
|
-
.join('\n');
|
|
338
|
-
|
|
339
|
-
activity.notes.push({
|
|
340
|
-
activity: { source: event.htmlLink },
|
|
341
|
-
key: "attendees", // Different key for different note types
|
|
342
|
-
content: `## Attendees\n${attendeeList}`,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Create or update - Plot automatically handles deduplication based on source
|
|
397
|
+
// Create or update — Plot handles deduplication automatically
|
|
347
398
|
await this.tools.plot.createActivity(activity);
|
|
348
399
|
}
|
|
349
|
-
|
|
350
|
-
async stopSync(calendarId: string): Promise<void> {
|
|
351
|
-
const authToken = await this.get<string>("auth_token");
|
|
352
|
-
await this.tools.calendarTool.stopSync(authToken, calendarId);
|
|
353
|
-
}
|
|
354
400
|
```
|
|
355
401
|
|
|
356
402
|
### Advanced: Generate and Store IDs (Strategy 3)
|
|
@@ -397,61 +443,20 @@ async handleEventAdvanced(
|
|
|
397
443
|
}
|
|
398
444
|
```
|
|
399
445
|
|
|
400
|
-
##
|
|
446
|
+
## Resource Selection
|
|
401
447
|
|
|
402
|
-
|
|
448
|
+
Resource 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.
|
|
403
449
|
|
|
404
450
|
```typescript
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.onCalendarSelected,
|
|
415
|
-
provider,
|
|
416
|
-
calendar.id,
|
|
417
|
-
calendar.name,
|
|
418
|
-
authToken
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
links.push({
|
|
422
|
-
title: `📅 ${calendar.name}${calendar.primary ? " (Primary)" : ""}`,
|
|
423
|
-
type: ActivityLinkType.callback,
|
|
424
|
-
token: token,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
await this.tools.plot.createActivity({
|
|
429
|
-
type: ActivityType.Note,
|
|
430
|
-
title: "Which calendars would you like to connect?",
|
|
431
|
-
notes: [
|
|
432
|
-
{
|
|
433
|
-
content: "Select the calendars you want to sync:",
|
|
434
|
-
links,
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
async onCalendarSelected(
|
|
441
|
-
link: ActivityLink,
|
|
442
|
-
provider: string,
|
|
443
|
-
calendarId: string,
|
|
444
|
-
calendarName: string,
|
|
445
|
-
authToken: string
|
|
446
|
-
): Promise<void> {
|
|
447
|
-
// Start sync for selected calendar
|
|
448
|
-
await this.tools.tool.startSync(
|
|
449
|
-
authToken,
|
|
450
|
-
calendarId,
|
|
451
|
-
this.handleEvent,
|
|
452
|
-
provider,
|
|
453
|
-
calendarId
|
|
454
|
-
);
|
|
451
|
+
// In your tool:
|
|
452
|
+
async getSyncables(_auth: Authorization, token: AuthToken): Promise<Syncable[]> {
|
|
453
|
+
const client = new ApiClient({ accessToken: token.token });
|
|
454
|
+
const calendars = await client.listCalendars();
|
|
455
|
+
return calendars.map(c => ({
|
|
456
|
+
id: c.id,
|
|
457
|
+
title: c.name,
|
|
458
|
+
children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),
|
|
459
|
+
}));
|
|
455
460
|
}
|
|
456
461
|
```
|
|
457
462
|
|
|
@@ -486,7 +491,7 @@ async startSync(resourceId: string): Promise<void> {
|
|
|
486
491
|
await this.runTask(callback);
|
|
487
492
|
}
|
|
488
493
|
|
|
489
|
-
async syncBatch(
|
|
494
|
+
async syncBatch(resourceId: string): Promise<void> {
|
|
490
495
|
// Load state from Store (set by previous execution)
|
|
491
496
|
const state = await this.get(`sync_state_${resourceId}`);
|
|
492
497
|
|
|
@@ -506,7 +511,7 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
|
|
|
506
511
|
key: "description", // Use key for upsertable notes
|
|
507
512
|
content: item.description,
|
|
508
513
|
}],
|
|
509
|
-
unread:
|
|
514
|
+
...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental
|
|
510
515
|
...(state.initialSync ? { archived: false } : {}), // unarchive on initial only
|
|
511
516
|
});
|
|
512
517
|
}
|
|
@@ -551,7 +556,7 @@ All sync-based tools should distinguish between initial sync (first import) and
|
|
|
551
556
|
|
|
552
557
|
| Field | Initial Sync | Incremental Sync | Reason |
|
|
553
558
|
|-------|--------------|------------------|---------|
|
|
554
|
-
| `unread` | `false` |
|
|
559
|
+
| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |
|
|
555
560
|
| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |
|
|
556
561
|
|
|
557
562
|
**Example:**
|
|
@@ -560,14 +565,14 @@ const activity: NewActivity = {
|
|
|
560
565
|
type: ActivityType.Event,
|
|
561
566
|
source: event.url,
|
|
562
567
|
title: event.title,
|
|
563
|
-
unread:
|
|
564
|
-
...(initialSync ? { archived: false } : {}),
|
|
568
|
+
...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental
|
|
569
|
+
...(initialSync ? { archived: false } : {}), // unarchive on initial only
|
|
565
570
|
};
|
|
566
571
|
```
|
|
567
572
|
|
|
568
573
|
**Why this matters:**
|
|
569
|
-
- **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports
|
|
570
|
-
- **Incremental sync**:
|
|
574
|
+
- **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports
|
|
575
|
+
- **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved
|
|
571
576
|
- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)
|
|
572
577
|
|
|
573
578
|
### Two-Way Sync: Avoiding Race Conditions
|