@plotday/twister 0.56.0 → 0.58.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 (331) hide show
  1. package/README.md +53 -44
  2. package/bin/commands/create.js +9 -14
  3. package/bin/commands/create.js.map +1 -1
  4. package/bin/commands/deploy.js +2 -0
  5. package/bin/commands/deploy.js.map +1 -1
  6. package/bin/commands/generate.js +8 -5
  7. package/bin/commands/generate.js.map +1 -1
  8. package/bin/index.js +2 -2
  9. package/bin/index.js.map +1 -1
  10. package/bin/templates/AGENTS.template.md +110 -94
  11. package/bin/templates/README.template.md +36 -33
  12. package/cli/templates/AGENTS.template.md +110 -94
  13. package/cli/templates/README.template.md +36 -33
  14. package/dist/connector.d.ts +24 -17
  15. package/dist/connector.d.ts.map +1 -1
  16. package/dist/connector.js +19 -12
  17. package/dist/connector.js.map +1 -1
  18. package/dist/docs/assets/hierarchy.js +1 -1
  19. package/dist/docs/assets/navigation.js +1 -1
  20. package/dist/docs/assets/search.js +1 -1
  21. package/dist/docs/classes/index.Connector.html +66 -60
  22. package/dist/docs/classes/index.FileNotFoundError.html +2 -2
  23. package/dist/docs/classes/index.Files.html +4 -4
  24. package/dist/docs/classes/index.Imap.html +10 -10
  25. package/dist/docs/classes/index.Options.html +2 -2
  26. package/dist/docs/classes/index.Smtp.html +6 -6
  27. package/dist/docs/classes/tool.ITool.html +2 -2
  28. package/dist/docs/classes/tool.Tool.html +23 -23
  29. package/dist/docs/classes/tools_ai.AI.html +5 -5
  30. package/dist/docs/classes/tools_callbacks.Callbacks.html +8 -8
  31. package/dist/docs/classes/tools_integrations.Integrations.html +26 -12
  32. package/dist/docs/classes/tools_network.Network.html +9 -9
  33. package/dist/docs/classes/tools_plot.Plot.html +34 -33
  34. package/dist/docs/classes/tools_store.Store.html +8 -8
  35. package/dist/docs/classes/tools_tasks.Tasks.html +6 -6
  36. package/dist/docs/classes/tools_twists.Twists.html +12 -11
  37. package/dist/docs/classes/twist.Twist.html +28 -28
  38. package/dist/docs/documents/Building_Connectors.html +42 -28
  39. package/dist/docs/documents/Built-in_Tools.html +170 -67
  40. package/dist/docs/documents/CLI_Reference.html +68 -47
  41. package/dist/docs/documents/Core_Concepts.html +52 -81
  42. package/dist/docs/documents/Getting_Started.html +28 -31
  43. package/dist/docs/documents/MULTI_USER_AUTH.html +45 -0
  44. package/dist/docs/documents/Runtime_Environment.html +13 -12
  45. package/dist/docs/documents/SYNC_STRATEGIES.html +373 -0
  46. package/dist/docs/enums/plot.ActionType.html +9 -9
  47. package/dist/docs/enums/plot.ActorType.html +4 -4
  48. package/dist/docs/enums/plot.ConferencingProvider.html +6 -6
  49. package/dist/docs/enums/plot.ThemeColor.html +9 -9
  50. package/dist/docs/enums/tag.Tag.html +3 -3
  51. package/dist/docs/enums/tools_ai.AIModel.html +3 -3
  52. package/dist/docs/enums/tools_integrations.AuthProvider.html +13 -13
  53. package/dist/docs/enums/tools_plot.ContactAccess.html +2 -2
  54. package/dist/docs/enums/tools_plot.FocusAccess.html +3 -3
  55. package/dist/docs/enums/tools_plot.LinkAccess.html +3 -3
  56. package/dist/docs/enums/tools_plot.ThreadAccess.html +4 -4
  57. package/dist/docs/functions/index.Uuid.Generate.html +1 -1
  58. package/dist/docs/functions/utils_hash.quickHash.html +1 -1
  59. package/dist/docs/hierarchy.html +1 -1
  60. package/dist/docs/index.html +7 -8
  61. package/dist/docs/interfaces/tools_ai.AIRequest.html +13 -13
  62. package/dist/docs/interfaces/tools_ai.AIResponse.html +9 -9
  63. package/dist/docs/interfaces/tools_ai.FilePart.html +5 -5
  64. package/dist/docs/interfaces/tools_ai.ImagePart.html +4 -4
  65. package/dist/docs/interfaces/tools_ai.ReasoningPart.html +4 -4
  66. package/dist/docs/interfaces/tools_ai.RedactedReasoningPart.html +3 -3
  67. package/dist/docs/interfaces/tools_ai.TextPart.html +3 -3
  68. package/dist/docs/interfaces/tools_ai.ToolCallPart.html +5 -5
  69. package/dist/docs/interfaces/tools_ai.ToolExecutionOptions.html +4 -4
  70. package/dist/docs/interfaces/tools_ai.ToolResultPart.html +5 -5
  71. package/dist/docs/interfaces/tools_twists.TwistSource.html +3 -3
  72. package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
  73. package/dist/docs/media/AGENTS.md +101 -74
  74. package/dist/docs/modules/index.html +1 -1
  75. package/dist/docs/modules/tools_integrations.html +1 -1
  76. package/dist/docs/modules.html +1 -1
  77. package/dist/docs/types/index.BooleanDef.html +2 -2
  78. package/dist/docs/types/index.CreateLinkDraft.html +9 -9
  79. package/dist/docs/types/index.ImapAddress.html +3 -3
  80. package/dist/docs/types/index.ImapConnectOptions.html +6 -6
  81. package/dist/docs/types/index.ImapFetchOptions.html +4 -4
  82. package/dist/docs/types/index.ImapFlagOperation.html +1 -1
  83. package/dist/docs/types/index.ImapMailbox.html +5 -5
  84. package/dist/docs/types/index.ImapMailboxStatus.html +7 -7
  85. package/dist/docs/types/index.ImapMessage.html +14 -14
  86. package/dist/docs/types/index.ImapSearchCriteria.html +9 -9
  87. package/dist/docs/types/index.ImapSession.html +1 -1
  88. package/dist/docs/types/index.NewSchedule.html +13 -13
  89. package/dist/docs/types/index.NewScheduleContact.html +2 -2
  90. package/dist/docs/types/index.NewScheduleOccurrence.html +1 -1
  91. package/dist/docs/types/index.NoteWriteBackResult.html +3 -3
  92. package/dist/docs/types/index.NumberDef.html +2 -2
  93. package/dist/docs/types/index.OptionDef.html +1 -1
  94. package/dist/docs/types/index.OptionalScopeGroup.html +6 -6
  95. package/dist/docs/types/index.OptionsSchema.html +1 -1
  96. package/dist/docs/types/index.ReactionCapabilities.html +1 -1
  97. package/dist/docs/types/index.ResolvedOptions.html +1 -1
  98. package/dist/docs/types/index.ResolvedRecipient.html +5 -5
  99. package/dist/docs/types/index.Schedule.html +12 -12
  100. package/dist/docs/types/index.ScheduleContact.html +2 -2
  101. package/dist/docs/types/index.ScheduleContactRole.html +1 -1
  102. package/dist/docs/types/index.ScheduleContactStatus.html +1 -1
  103. package/dist/docs/types/index.ScheduleOccurrence.html +6 -6
  104. package/dist/docs/types/index.ScheduleOccurrenceUpdate.html +1 -1
  105. package/dist/docs/types/index.ScopeConfig.html +3 -3
  106. package/dist/docs/types/index.SelectDef.html +2 -2
  107. package/dist/docs/types/index.Serializable.html +1 -1
  108. package/dist/docs/types/index.SmtpAddress.html +3 -3
  109. package/dist/docs/types/index.SmtpConnectOptions.html +7 -7
  110. package/dist/docs/types/index.SmtpMessage.html +12 -12
  111. package/dist/docs/types/index.SmtpSendResult.html +4 -4
  112. package/dist/docs/types/index.SmtpSession.html +1 -1
  113. package/dist/docs/types/index.TextDef.html +2 -2
  114. package/dist/docs/types/index.Uuid.html +1 -1
  115. package/dist/docs/types/plot.Action.html +1 -1
  116. package/dist/docs/types/plot.Actor.html +5 -5
  117. package/dist/docs/types/plot.ActorId.html +4 -4
  118. package/dist/docs/types/plot.Contact.html +4 -4
  119. package/dist/docs/types/plot.ContentType.html +1 -1
  120. package/dist/docs/types/plot.Focus.html +8 -8
  121. package/dist/docs/types/plot.FocusUpdate.html +1 -1
  122. package/dist/docs/types/plot.Link.html +17 -17
  123. package/dist/docs/types/plot.LinkUpdate.html +1 -1
  124. package/dist/docs/types/plot.NewActor.html +1 -1
  125. package/dist/docs/types/plot.NewContact.html +1 -1
  126. package/dist/docs/types/plot.NewFocus.html +1 -1
  127. package/dist/docs/types/plot.NewLink.html +5 -2
  128. package/dist/docs/types/plot.NewLinkWithNotes.html +1 -1
  129. package/dist/docs/types/plot.NewNote.html +1 -1
  130. package/dist/docs/types/plot.NewReactions.html +1 -1
  131. package/dist/docs/types/plot.NewTags.html +1 -1
  132. package/dist/docs/types/plot.NewThread.html +1 -1
  133. package/dist/docs/types/plot.NewThreadWithNotes.html +1 -1
  134. package/dist/docs/types/plot.Note.html +1 -1
  135. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  136. package/dist/docs/types/plot.PlanOperation.html +1 -1
  137. package/dist/docs/types/plot.Reaction.html +3 -3
  138. package/dist/docs/types/plot.Reactions.html +1 -1
  139. package/dist/docs/types/plot.Tags.html +1 -1
  140. package/dist/docs/types/plot.Thread.html +1 -1
  141. package/dist/docs/types/plot.ThreadAccessLevel.html +1 -1
  142. package/dist/docs/types/plot.ThreadCommon.html +6 -6
  143. package/dist/docs/types/plot.ThreadFilter.html +2 -2
  144. package/dist/docs/types/plot.ThreadMeta.html +1 -1
  145. package/dist/docs/types/plot.ThreadType.html +1 -1
  146. package/dist/docs/types/plot.ThreadUpdate.html +1 -1
  147. package/dist/docs/types/plot.ThreadWithNotes.html +1 -1
  148. package/dist/docs/types/tools_ai.AIAssistantMessage.html +2 -2
  149. package/dist/docs/types/tools_ai.AICapabilities.html +4 -4
  150. package/dist/docs/types/tools_ai.AIMessage.html +1 -1
  151. package/dist/docs/types/tools_ai.AIOptions.html +2 -2
  152. package/dist/docs/types/tools_ai.AISource.html +1 -1
  153. package/dist/docs/types/tools_ai.AISystemMessage.html +2 -2
  154. package/dist/docs/types/tools_ai.AITool.html +1 -1
  155. package/dist/docs/types/tools_ai.AIToolMessage.html +2 -2
  156. package/dist/docs/types/tools_ai.AIToolSet.html +1 -1
  157. package/dist/docs/types/tools_ai.AIUsage.html +5 -5
  158. package/dist/docs/types/tools_ai.AIUserMessage.html +2 -2
  159. package/dist/docs/types/tools_ai.DataContent.html +1 -1
  160. package/dist/docs/types/tools_ai.ModelPreferences.html +5 -5
  161. package/dist/docs/types/tools_callbacks.Callback.html +2 -2
  162. package/dist/docs/types/tools_integrations.ArchiveLinkFilter.html +5 -5
  163. package/dist/docs/types/tools_integrations.ArchiveNotesFilter.html +5 -0
  164. package/dist/docs/types/tools_integrations.AuthToken.html +6 -5
  165. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  166. package/dist/docs/types/tools_integrations.Channel.html +6 -6
  167. package/dist/docs/types/tools_integrations.ComposeConfig.html +4 -4
  168. package/dist/docs/types/tools_integrations.ContactRoleConfig.html +5 -5
  169. package/dist/docs/types/tools_integrations.LinkTypeConfig.html +21 -21
  170. package/dist/docs/types/tools_integrations.NewCustomEmoji.html +8 -8
  171. package/dist/docs/types/tools_integrations.StatusIcon.html +1 -1
  172. package/dist/docs/types/tools_integrations.SyncContext.html +4 -4
  173. package/dist/docs/types/tools_network.WebhookRequest.html +6 -6
  174. package/dist/docs/types/tools_plot.LinkFilter.html +5 -5
  175. package/dist/docs/types/tools_plot.LinkSearchResult.html +1 -1
  176. package/dist/docs/types/tools_plot.NoteIntentHandler.html +4 -4
  177. package/dist/docs/types/tools_plot.NoteSearchResult.html +1 -1
  178. package/dist/docs/types/tools_plot.SearchOptions.html +4 -4
  179. package/dist/docs/types/tools_plot.SearchResult.html +1 -1
  180. package/dist/docs/types/tools_twists.Log.html +2 -2
  181. package/dist/docs/types/tools_twists.TwistPermissions.html +1 -1
  182. package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
  183. package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
  184. package/dist/docs/types/utils_types.InferOptions.html +1 -1
  185. package/dist/docs/types/utils_types.InferTools.html +1 -1
  186. package/dist/docs/types/utils_types.JSONValue.html +1 -1
  187. package/dist/docs/types/utils_types.PromiseValues.html +1 -1
  188. package/dist/docs/types/utils_types.ToolBuilder.html +1 -1
  189. package/dist/docs/variables/tools_plot.SEARCH_DEFAULT_LIMIT.html +1 -1
  190. package/dist/docs/variables/tools_plot.SEARCH_MAX_LIMIT.html +1 -1
  191. package/dist/facets.d.ts +30 -0
  192. package/dist/facets.d.ts.map +1 -0
  193. package/dist/facets.js +16 -0
  194. package/dist/facets.js.map +1 -0
  195. package/dist/llm-docs/connector.d.ts +1 -1
  196. package/dist/llm-docs/connector.d.ts.map +1 -1
  197. package/dist/llm-docs/connector.js +1 -1
  198. package/dist/llm-docs/connector.js.map +1 -1
  199. package/dist/llm-docs/facets.d.ts +9 -0
  200. package/dist/llm-docs/facets.d.ts.map +1 -0
  201. package/dist/llm-docs/facets.js +8 -0
  202. package/dist/llm-docs/facets.js.map +1 -0
  203. package/dist/llm-docs/index.d.ts.map +1 -1
  204. package/dist/llm-docs/index.js +2 -0
  205. package/dist/llm-docs/index.js.map +1 -1
  206. package/dist/llm-docs/plot.d.ts +1 -1
  207. package/dist/llm-docs/plot.d.ts.map +1 -1
  208. package/dist/llm-docs/plot.js +1 -1
  209. package/dist/llm-docs/plot.js.map +1 -1
  210. package/dist/llm-docs/tool.d.ts +1 -1
  211. package/dist/llm-docs/tool.d.ts.map +1 -1
  212. package/dist/llm-docs/tool.js +1 -1
  213. package/dist/llm-docs/tool.js.map +1 -1
  214. package/dist/llm-docs/tools/ai.d.ts +1 -1
  215. package/dist/llm-docs/tools/ai.d.ts.map +1 -1
  216. package/dist/llm-docs/tools/ai.js +1 -1
  217. package/dist/llm-docs/tools/ai.js.map +1 -1
  218. package/dist/llm-docs/tools/callbacks.d.ts +1 -1
  219. package/dist/llm-docs/tools/callbacks.d.ts.map +1 -1
  220. package/dist/llm-docs/tools/callbacks.js +1 -1
  221. package/dist/llm-docs/tools/callbacks.js.map +1 -1
  222. package/dist/llm-docs/tools/files.d.ts +1 -1
  223. package/dist/llm-docs/tools/files.d.ts.map +1 -1
  224. package/dist/llm-docs/tools/files.js +1 -1
  225. package/dist/llm-docs/tools/files.js.map +1 -1
  226. package/dist/llm-docs/tools/imap.d.ts +1 -1
  227. package/dist/llm-docs/tools/imap.d.ts.map +1 -1
  228. package/dist/llm-docs/tools/imap.js +1 -1
  229. package/dist/llm-docs/tools/imap.js.map +1 -1
  230. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  231. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  232. package/dist/llm-docs/tools/integrations.js +1 -1
  233. package/dist/llm-docs/tools/integrations.js.map +1 -1
  234. package/dist/llm-docs/tools/network.d.ts +1 -1
  235. package/dist/llm-docs/tools/network.d.ts.map +1 -1
  236. package/dist/llm-docs/tools/network.js +1 -1
  237. package/dist/llm-docs/tools/network.js.map +1 -1
  238. package/dist/llm-docs/tools/plot.d.ts +1 -1
  239. package/dist/llm-docs/tools/plot.d.ts.map +1 -1
  240. package/dist/llm-docs/tools/plot.js +1 -1
  241. package/dist/llm-docs/tools/plot.js.map +1 -1
  242. package/dist/llm-docs/tools/smtp.d.ts +1 -1
  243. package/dist/llm-docs/tools/smtp.d.ts.map +1 -1
  244. package/dist/llm-docs/tools/smtp.js +1 -1
  245. package/dist/llm-docs/tools/smtp.js.map +1 -1
  246. package/dist/llm-docs/tools/tasks.d.ts +1 -1
  247. package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
  248. package/dist/llm-docs/tools/tasks.js +1 -1
  249. package/dist/llm-docs/tools/tasks.js.map +1 -1
  250. package/dist/llm-docs/tools/twists.d.ts +1 -1
  251. package/dist/llm-docs/tools/twists.d.ts.map +1 -1
  252. package/dist/llm-docs/tools/twists.js +1 -1
  253. package/dist/llm-docs/tools/twists.js.map +1 -1
  254. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  255. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  256. package/dist/llm-docs/twist-guide-template.js +1 -1
  257. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  258. package/dist/llm-docs/twist.d.ts +1 -1
  259. package/dist/llm-docs/twist.d.ts.map +1 -1
  260. package/dist/llm-docs/twist.js +1 -1
  261. package/dist/llm-docs/twist.js.map +1 -1
  262. package/dist/plot.d.ts +15 -8
  263. package/dist/plot.d.ts.map +1 -1
  264. package/dist/plot.js.map +1 -1
  265. package/dist/tool.d.ts +4 -4
  266. package/dist/tool.js +4 -4
  267. package/dist/tools/ai.d.ts +12 -13
  268. package/dist/tools/ai.d.ts.map +1 -1
  269. package/dist/tools/ai.js +8 -9
  270. package/dist/tools/ai.js.map +1 -1
  271. package/dist/tools/callbacks.d.ts +1 -1
  272. package/dist/tools/files.d.ts +2 -2
  273. package/dist/tools/imap.d.ts +1 -1
  274. package/dist/tools/imap.js +1 -1
  275. package/dist/tools/integrations.d.ts +29 -2
  276. package/dist/tools/integrations.d.ts.map +1 -1
  277. package/dist/tools/integrations.js.map +1 -1
  278. package/dist/tools/network.d.ts +5 -5
  279. package/dist/tools/plot.d.ts +42 -37
  280. package/dist/tools/plot.d.ts.map +1 -1
  281. package/dist/tools/plot.js +16 -12
  282. package/dist/tools/plot.js.map +1 -1
  283. package/dist/tools/smtp.d.ts +1 -1
  284. package/dist/tools/smtp.js +1 -1
  285. package/dist/tools/tasks.d.ts +6 -8
  286. package/dist/tools/tasks.d.ts.map +1 -1
  287. package/dist/tools/tasks.js +5 -7
  288. package/dist/tools/tasks.js.map +1 -1
  289. package/dist/tools/twists.d.ts +15 -14
  290. package/dist/tools/twists.d.ts.map +1 -1
  291. package/dist/tools/twists.js +2 -2
  292. package/dist/tools/twists.js.map +1 -1
  293. package/dist/twist-guide.d.ts +1 -1
  294. package/dist/twist-guide.d.ts.map +1 -1
  295. package/dist/twist.d.ts +2 -2
  296. package/dist/twist.js +2 -2
  297. package/package.json +6 -1
  298. package/src/connector.ts +23 -16
  299. package/src/facets.ts +40 -0
  300. package/src/llm-docs/connector.ts +1 -1
  301. package/src/llm-docs/facets.ts +8 -0
  302. package/src/llm-docs/index.ts +2 -0
  303. package/src/llm-docs/plot.ts +1 -1
  304. package/src/llm-docs/tool.ts +1 -1
  305. package/src/llm-docs/tools/ai.ts +1 -1
  306. package/src/llm-docs/tools/callbacks.ts +1 -1
  307. package/src/llm-docs/tools/files.ts +1 -1
  308. package/src/llm-docs/tools/imap.ts +1 -1
  309. package/src/llm-docs/tools/integrations.ts +1 -1
  310. package/src/llm-docs/tools/network.ts +1 -1
  311. package/src/llm-docs/tools/plot.ts +1 -1
  312. package/src/llm-docs/tools/smtp.ts +1 -1
  313. package/src/llm-docs/tools/tasks.ts +1 -1
  314. package/src/llm-docs/tools/twists.ts +1 -1
  315. package/src/llm-docs/twist-guide-template.ts +1 -1
  316. package/src/llm-docs/twist.ts +1 -1
  317. package/src/plot.ts +15 -8
  318. package/src/tool.ts +4 -4
  319. package/src/tools/ai.ts +12 -13
  320. package/src/tools/callbacks.ts +1 -1
  321. package/src/tools/files.ts +2 -2
  322. package/src/tools/imap.ts +1 -1
  323. package/src/tools/integrations.ts +34 -1
  324. package/src/tools/network.ts +5 -5
  325. package/src/tools/plot.ts +42 -37
  326. package/src/tools/smtp.ts +1 -1
  327. package/src/tools/tasks.ts +6 -8
  328. package/src/tools/twists.ts +15 -14
  329. package/src/twist.ts +2 -2
  330. package/dist/docs/media/MULTI_USER_AUTH.md +0 -116
  331. package/dist/docs/media/SYNC_STRATEGIES.md +0 -818
@@ -29,18 +29,18 @@ import type { Callback } from "./callbacks";
29
29
  *
30
30
  * @example
31
31
  * ```typescript
32
- * class SyncTool extends Tool {
32
+ * class SyncTool extends Tool<SyncTool> {
33
33
  * async startBatchSync(totalItems: number) {
34
34
  * // Store initial state using built-in set method
35
35
  * await this.set("sync_progress", { processed: 0, total: totalItems });
36
36
  *
37
37
  * // Create callback and queue first batch
38
- * const callback = await this.callback("processBatch", { batchNumber: 1 });
38
+ * const callback = await this.callback(this.processBatch, 1);
39
39
  * // runTask creates NEW execution with fresh ~1000 request limit
40
40
  * await this.runTask(callback);
41
41
  * }
42
42
  *
43
- * async processBatch(args: any, context: { batchNumber: number }) {
43
+ * async processBatch(batchNumber: number) {
44
44
  * // Process one batch of items (sized to stay under request limit)
45
45
  * const progress = await this.get("sync_progress");
46
46
  *
@@ -60,9 +60,7 @@ import type { Callback } from "./callbacks";
60
60
  *
61
61
  * if (progress.processed < progress.total) {
62
62
  * // Queue next batch - creates NEW execution with fresh request limit
63
- * const callback = await this.callback("processBatch", {
64
- * batchNumber: context.batchNumber + 1
65
- * });
63
+ * const callback = await this.callback(this.processBatch, batchNumber + 1);
66
64
  * await this.runTask(callback);
67
65
  * }
68
66
  * }
@@ -71,7 +69,7 @@ import type { Callback } from "./callbacks";
71
69
  * const tomorrow = new Date();
72
70
  * tomorrow.setDate(tomorrow.getDate() + 1);
73
71
  *
74
- * const callback = await this.callback("cleanupOldData");
72
+ * const callback = await this.callback(this.cleanupOldData);
75
73
  * // Schedule for future execution
76
74
  * return await this.runTask(callback, { runAt: tomorrow });
77
75
  * }
@@ -102,7 +100,7 @@ export declare abstract class Tasks extends ITool {
102
100
  * @example
103
101
  * ```typescript
104
102
  * // Break large loop into batches to stay under request limit
105
- * const callback = await this.callback("syncBatch", { page: 1 });
103
+ * const callback = await this.callback(this.syncBatch, 1);
106
104
  * await this.runTask(callback); // Fresh execution with ~1000 requests
107
105
  * ```
108
106
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/tools/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAC3B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,8BAAsB,KAAM,SAAQ,KAAK;IACvC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IAEH,QAAQ,CAAC,OAAO,CACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,IAAI,CAAA;KAAE,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAEzB;;;;;;;;OAQG;IAEH,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEjD;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CACzC"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/tools/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAC3B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,8BAAsB,KAAM,SAAQ,KAAK;IACvC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IAEH,QAAQ,CAAC,OAAO,CACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,IAAI,CAAA;KAAE,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAEzB;;;;;;;;OAQG;IAEH,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEjD;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CACzC"}
@@ -28,18 +28,18 @@ import { ITool } from "..";
28
28
  *
29
29
  * @example
30
30
  * ```typescript
31
- * class SyncTool extends Tool {
31
+ * class SyncTool extends Tool<SyncTool> {
32
32
  * async startBatchSync(totalItems: number) {
33
33
  * // Store initial state using built-in set method
34
34
  * await this.set("sync_progress", { processed: 0, total: totalItems });
35
35
  *
36
36
  * // Create callback and queue first batch
37
- * const callback = await this.callback("processBatch", { batchNumber: 1 });
37
+ * const callback = await this.callback(this.processBatch, 1);
38
38
  * // runTask creates NEW execution with fresh ~1000 request limit
39
39
  * await this.runTask(callback);
40
40
  * }
41
41
  *
42
- * async processBatch(args: any, context: { batchNumber: number }) {
42
+ * async processBatch(batchNumber: number) {
43
43
  * // Process one batch of items (sized to stay under request limit)
44
44
  * const progress = await this.get("sync_progress");
45
45
  *
@@ -59,9 +59,7 @@ import { ITool } from "..";
59
59
  *
60
60
  * if (progress.processed < progress.total) {
61
61
  * // Queue next batch - creates NEW execution with fresh request limit
62
- * const callback = await this.callback("processBatch", {
63
- * batchNumber: context.batchNumber + 1
64
- * });
62
+ * const callback = await this.callback(this.processBatch, batchNumber + 1);
65
63
  * await this.runTask(callback);
66
64
  * }
67
65
  * }
@@ -70,7 +68,7 @@ import { ITool } from "..";
70
68
  * const tomorrow = new Date();
71
69
  * tomorrow.setDate(tomorrow.getDate() + 1);
72
70
  *
73
- * const callback = await this.callback("cleanupOldData");
71
+ * const callback = await this.callback(this.cleanupOldData);
74
72
  * // Schedule for future execution
75
73
  * return await this.runTask(callback, { runAt: tomorrow });
76
74
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/tools/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAG3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,MAAM,OAAgB,KAAM,SAAQ,KAAK;CAuDxC"}
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/tools/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAG3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,MAAM,OAAgB,KAAM,SAAQ,KAAK;CAuDxC"}
@@ -56,10 +56,10 @@ export type TwistPermissions = Record<string, Record<string, string[]>>;
56
56
  *
57
57
  * @example
58
58
  * ```typescript
59
- * class TwistBuilderTwist extends Twist {
59
+ * class TwistBuilderTwist extends Twist<TwistBuilderTwist> {
60
60
  * build(build: ToolBuilder) {
61
61
  * return {
62
- * twists: build.get(Twists)
62
+ * twists: build(Twists)
63
63
  * }
64
64
  * }
65
65
  *
@@ -72,14 +72,15 @@ export type TwistPermissions = Record<string, Record<string, string[]>>;
72
72
  */
73
73
  export declare abstract class Twists extends ITool {
74
74
  /**
75
- * Creates a new twist ID and grants access to people in the current focus.
75
+ * Creates a new twist package ID. Ownership of the ID is claimed lazily
76
+ * on first deploy — no upfront registration happens beyond generating it.
76
77
  *
77
78
  * @returns Promise resolving to the generated twist ID
78
79
  * @throws When twist creation fails
79
80
  *
80
81
  * @example
81
82
  * ```typescript
82
- * const twistId = await twist.create();
83
+ * const twistId = await this.tools.twists.create();
83
84
  * console.log(`Your twist ID: ${twistId}`);
84
85
  * ```
85
86
  */
@@ -97,15 +98,15 @@ export declare abstract class Twists extends ITool {
97
98
  *
98
99
  * @example
99
100
  * ```typescript
100
- * const source = await twist.generate(`
101
+ * const source = await this.tools.twists.generate(`
101
102
  * # Calendar Sync Twist
102
103
  *
103
- * This twist syncs Google Calendar events to Plot activities.
104
+ * This twist syncs Google Calendar events to Plot threads.
104
105
  *
105
106
  * ## Features
106
107
  * - Authenticate with Google
107
108
  * - Sync calendar events
108
- * - Create activities from events
109
+ * - Create threads from events
109
110
  * `);
110
111
  *
111
112
  * // source.dependencies: { "@plotday/twister": "workspace:^", ... }
@@ -135,7 +136,7 @@ export declare abstract class Twists extends ITool {
135
136
  * @example
136
137
  * ```typescript
137
138
  * // Deploy with a module
138
- * const result = await twist.deploy({
139
+ * const result = await this.tools.twists.deploy({
139
140
  * twistId: 'abc-123-...',
140
141
  * module: 'export default class MyTwist extends Twist {...}',
141
142
  * environment: 'personal',
@@ -145,8 +146,8 @@ export declare abstract class Twists extends ITool {
145
146
  * console.log(`Deployed version ${result.version}`);
146
147
  *
147
148
  * // Deploy with source
148
- * const source = await twist.generate(spec);
149
- * const result = await twist.deploy({
149
+ * const source = await this.tools.twists.generate(spec);
150
+ * const result = await this.tools.twists.deploy({
150
151
  * twistId: 'abc-123-...',
151
152
  * source,
152
153
  * environment: 'personal',
@@ -154,7 +155,7 @@ export declare abstract class Twists extends ITool {
154
155
  * });
155
156
  *
156
157
  * // Validate with dryRun
157
- * const result = await twist.deploy({
158
+ * const result = await this.tools.twists.deploy({
158
159
  * twistId: 'abc-123-...',
159
160
  * source,
160
161
  * dryRun: true,
@@ -194,11 +195,11 @@ export declare abstract class Twists extends ITool {
194
195
  * @example
195
196
  * ```typescript
196
197
  * // Create twist and callback
197
- * const twistId = await this.twist.create();
198
- * const callback = await this.callback.create("onLogs");
198
+ * const twistId = await this.tools.twists.create();
199
+ * const callback = await this.callback(this.onLogs);
199
200
  *
200
201
  * // Subscribe to logs
201
- * await this.twist.watchLogs(twistId, callback);
202
+ * await this.tools.twists.watchLogs(twistId, callback);
202
203
  *
203
204
  * // Implement handler
204
205
  * async onLogs(logs: Log[]) {
@@ -1 +1 @@
1
- {"version":3,"file":"twists.d.ts","sourceRoot":"","sources":["../../src/tools/twists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAErC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,MAAM,GAAG,GAAG;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC1D,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8BAAsB,MAAO,SAAQ,KAAK;IACxC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IAEH,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IAEH,QAAQ,CAAC,MAAM,CACb,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,GAAG,CACA;QACE,MAAM,EAAE,MAAM,CAAC;KAChB,GACD;QACE,MAAM,EAAE,WAAW,CAAC;KACrB,CACJ,GACA,OAAO,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IAEH,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CACvE"}
1
+ {"version":3,"file":"twists.d.ts","sourceRoot":"","sources":["../../src/tools/twists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAErC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,MAAM,GAAG,GAAG;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC1D,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8BAAsB,MAAO,SAAQ,KAAK;IACxC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IAEH,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IAEH,QAAQ,CAAC,MAAM,CACb,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,GAAG,CACA;QACE,MAAM,EAAE,MAAM,CAAC;KAChB,GACD;QACE,MAAM,EAAE,WAAW,CAAC;KACrB,CACJ,GACA,OAAO,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IAEH,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CACvE"}
@@ -7,10 +7,10 @@ import { ITool } from "..";
7
7
  *
8
8
  * @example
9
9
  * ```typescript
10
- * class TwistBuilderTwist extends Twist {
10
+ * class TwistBuilderTwist extends Twist<TwistBuilderTwist> {
11
11
  * build(build: ToolBuilder) {
12
12
  * return {
13
- * twists: build.get(Twists)
13
+ * twists: build(Twists)
14
14
  * }
15
15
  * }
16
16
  *
@@ -1 +1 @@
1
- {"version":3,"file":"twists.js","sourceRoot":"","sources":["../../src/tools/twists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,MAAM,IAAI,CAAC;AAuD1C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAgB,MAAO,SAAQ,KAAK;CAsJzC"}
1
+ {"version":3,"file":"twists.js","sourceRoot":"","sources":["../../src/tools/twists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,MAAM,IAAI,CAAC;AAuD1C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAgB,MAAO,SAAQ,KAAK;CAuJzC"}
@@ -1,2 +1,2 @@
1
- export declare const TWIST_GUIDE = "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Threads and Notes\n\n**CRITICAL CONCEPT**: A **Thread** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that thread.\n\n**Think of a Thread as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Threads with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Threads for updates** - Don't create a new Thread for each related message\n3. **Use Thread.source and Note.key for automatic upserts (Recommended)** - Set Thread.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot threads per external item (see SYNC_STRATEGIES.md)\n5. **Most Threads should be `ThreadType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n \u251C\u2500 Has stable URL or ID?\n \u2502 \u2514\u2500 Yes \u2192 Set Thread.source to the canonical URL/ID\n \u2502 Create Thread (Plot handles deduplication automatically)\n \u2502 Use Note.key for different note types:\n \u2502 - \"description\" for main content\n \u2502 - \"metadata\" for status/priority/assignee\n \u2502 - \"comment-{id}\" for individual comments\n \u2502\n \u2514\u2500 No stable identifier OR need multiple Plot threads per external item?\n \u2514\u2500 Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple threads from one external item):\n\n```\nNew event/task/conversation?\n \u251C\u2500 Yes \u2192 Generate UUID with Uuid.Generate()\n \u2502 Create new Thread with that UUID\n \u2502 Store mapping: external_id \u2192 thread_uuid\n \u2502\n \u2514\u2500 No (update/reply/comment) \u2192 Look up mapping by external_id\n \u251C\u2500 Found \u2192 Add Note to existing Thread using stored UUID\n \u2514\u2500 Not found \u2192 Create new Thread with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Thread,\n type NewThreadWithNotes,\n type ThreadFilter,\n type Priority,\n type ToolBuilder,\n Twist,\n ThreadType,\n} from \"@plotday/twister\";\nimport { ThreadAccess, Plot } from \"@plotday/twister/tools/plot\";\n// Import your sources or tools as needed\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n plot: build(Plot, {\n thread: { access: ThreadAccess.Create },\n }),\n };\n }\n\n async activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection handled in the twist edit modal.\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.\n\nMost twists have an empty or minimal `activate()`:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection are handled in the twist edit modal.\n // Only add custom initialization here if needed.\n}\n```\n\n**Store Parent Thread for Later (optional):**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const threadId = await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Setup complete\",\n notes: [{\n content: \"Your twist is ready. Threads will appear as they sync.\",\n }],\n });\n await this.set(\"setup_thread_id\", threadId);\n}\n```\n\n### Event Callbacks (via build options)\n\nTwists respond to events through callbacks declared in `build()`:\n\n**React to thread changes (for two-way sync):**\n\n```typescript\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n updated: this.onThreadUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\nasync onThreadUpdated(thread: Thread, changes: { tagsAdded, tagsRemoved }): Promise<void> {\n const tool = this.getToolForThread(thread);\n if (tool?.updateIssue) await tool.updateIssue(thread);\n}\n\nasync onNoteCreated(note: Note, thread: Thread): Promise<NoteWriteBackResult | void> {\n if (note.author.type === ActorType.Twist) return; // Prevent loops\n // Sync note to external service as a comment. Return the external\n // system's id + stored content so the runtime can set note.key AND\n // record a sync baseline that preserves Plot's content on round-trip.\n // See connectors/AGENTS.md \u2192 \"Sync baseline preservation\".\n const comment = await externalApi.createComment(thread.meta.externalId, { body: note.content ?? \"\" });\n if (!comment?.id) return;\n return { key: `comment-${comment.id}`, externalContent: comment.body };\n}\n```\n\n**Respond to mentions (AI twist pattern):**\n\n```typescript\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n intents: [{\n description: \"Respond to general questions\",\n examples: [\"What's the weather?\", \"Help me plan my week\"],\n handler: this.respond,\n }],\n },\n}),\n```\n\n**Default mention on replies:**\n\nWhen your twist processes replies (two-way comment sync or conversational AI), set `defaultMention: true` so the user doesn't have to manually re-mention your twist on every note:\n\n- `thread.defaultMention` \u2014 Auto-mention on replies to threads your twist created (e.g., synced issues, emails)\n- `note.defaultMention` \u2014 Auto-mention on follow-up notes in threads where your twist was @-mentioned (e.g., conversational agents)\n\n```typescript\n// Connector with two-way comment sync\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n defaultMention: true, // Users replying to synced threads will mention this twist by default\n updated: this.onThreadUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\n// Conversational AI twist\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n defaultMention: true, // Follow-up notes auto-mention this twist\n intents: [{ description: \"...\", examples: [...], handler: this.respond }],\n },\n}),\n```\n\nWithout `defaultMention`, the twist chip appears toggled OFF by default \u2014 the user can still enable it manually per-note.\n\n## Actions\n\nActions enable user interaction:\n\n```typescript\nimport { type Action, ActionType } from \"@plotday/twister\";\n\n// External URL action\nconst urlAction: Action = {\n title: \"Open website\",\n type: ActionType.external,\n url: \"https://example.com\",\n};\n\n// Callback action (uses Callbacks tool \u2014 use linkCallback, not callback)\nconst token = await this.linkCallback(this.onActionClicked, \"context\");\nconst callbackAction: Action = {\n title: \"Click me\",\n type: ActionType.callback,\n callback: token,\n};\n\n// Add to thread note\nawait this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Task with actions\",\n notes: [\n {\n content: \"Click the actions below to take action.\",\n actions: [urlAction, callbackAction],\n },\n ],\n});\n\n// Callback handler receives the Action as first argument\nasync onActionClicked(action: Action, context: string): Promise<void> {\n // Handle action click\n}\n```\n\n## Authentication Pattern\n\nAuth is handled automatically via the Integrations tool. Tools declare their OAuth provider in `build()`, and users connect in the twist edit modal. **You do not need to create auth activities manually.**\n\n```typescript\n// In your tool's build() method:\nbuild(build: ToolBuilder) {\n return {\n integrations: build(Integrations, {\n providers: [{\n provider: AuthProvider.Google,\n scopes: [\"https://www.googleapis.com/auth/calendar\"],\n getChannels: this.getChannels, // List available resources after auth\n onChannelEnabled: this.onChannelEnabled, // User enabled a resource\n onChannelDisabled: this.onChannelDisabled, // User disabled a resource\n }],\n }),\n // ...\n };\n}\n\n// Get a token for API calls:\nconst token = await this.tools.integrations.get(AuthProvider.Google, channelId);\nif (!token) throw new Error(\"No auth token available\");\nconst client = new ApiClient({ accessToken: token.token });\n```\n\nFor per-user write-backs (e.g., RSVP, comments attributed to the acting user):\nthe dispatch runtime routes the change to the acting user's own connector\ninstance, so your callback (`onScheduleContactUpdated`, etc.) already runs\nunder that user's auth. Just call `this.tools.integrations.get(channelId)`\nto fetch the token and the write-back is attributed correctly. If the\nacting user has no connection of this type, the change lives in Plot but\nis not dispatched.\n\n## Sync Pattern\n\n### Upsert via Source/Key (Strategy 2)\n\nUse source/key for automatic upserts:\n\n```typescript\nasync handleEvent(event: ExternalEvent): Promise<void> {\n const thread: NewThreadWithNotes = {\n source: event.htmlLink, // Canonical URL for automatic deduplication\n type: ThreadType.Event,\n title: event.summary || \"(No title)\",\n notes: [],\n };\n\n if (event.description) {\n thread.notes.push({\n thread: { source: event.htmlLink },\n key: \"description\", // This key enables note-level upserts\n content: event.description,\n });\n }\n\n // Create or update \u2014 Plot handles deduplication automatically\n await this.tools.plot.createThread(thread);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot threads from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingThread: NewThreadWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingThread.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingThreadId = await this.get<Uuid>(mappingKey);\n\n if (existingThreadId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingThread.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n thread: { id: existingThreadId },\n content: incomingThread.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const threadId = Uuid.Generate();\n await this.set(mappingKey, threadId);\n\n // Create new Thread with initial Note (new thread with first message)\n await this.tools.plot.createThread({\n ...incomingThread,\n id: threadId,\n });\n}\n```\n\n## Resource Selection\n\nResource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your tool's `getChannels()` method and toggle them on/off. You do **not** need to build custom selection UI.\n\n```typescript\n// In your tool:\nasync getChannels(_auth: Authorization, token: AuthToken): Promise<Channel[]> {\n const client = new ApiClient({ accessToken: token.token });\n const calendars = await client.listCalendars();\n return calendars.map(c => ({\n id: c.id,\n title: c.name,\n children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),\n }));\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size \u2264 100 items to stay under limit\n for (const item of result.items) {\n // Each createThread may make ~5-10 requests depending on notes/links\n await this.tools.plot.createThread({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ThreadType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Thread Sync Best Practices\n\nWhen syncing threads from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst thread: NewThread = {\n type: ThreadType.Event,\n source: event.url,\n title: event.title,\n ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports\n- **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Thread/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Thread.id` / `Note.id` in the external item's metadata when creating it, and update `Thread.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes \u2014 that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created threads** - Other users may change a Thread created by the twist, resulting in a callback. Be sure to check the `changes === null` and/or `thread.author.id !== this.id` to avoid re-processing.\n- **Always create Threads with Notes** - See \"Understanding Threads and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Thread types** - Most should be `ThreadType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Thread.source and Note.key for automatic upserts (Recommended)** - Set Thread.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Threads** - For source/key pattern, reference threads by source. For UUID pattern, look up stored mappings before creating new Threads. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
1
+ export declare const TWIST_GUIDE = "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Threads and Notes\n\n**CRITICAL CONCEPT**: A **Thread** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that thread.\n\n**Think of a Thread as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Threads with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Threads for updates** - Don't create a new Thread for each related message\n3. **Use source/key for automatic upserts (Recommended)** - External items are saved as links keyed by `source` (connectors call `integrations.saveLink()` with the item's canonical URL/ID), and `Note.key` enables upsertable note content. Reference an already-synced thread with `thread: { source }`. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot threads per external item (see SYNC_STRATEGIES.md)\n5. **Thread `type` is an optional display sub-type** - One of `\"action\"`, `\"notes\"`, `\"idea\"`, `\"goal\"`, `\"decision\"`, `\"discussion\"`, `\"announcement\"`, `\"ask\"`. Omit it for the default (`\"notes\"` in private focuses, `\"discussion\"` in shared ones). Use `\"action\"` for tasks; events are threads with `schedules`.\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n \u251C\u2500 Has stable URL or ID?\n \u2502 \u2514\u2500 Yes \u2192 Save a link with `source` set to the canonical URL/ID\n \u2502 (connectors: integrations.saveLink() \u2014 Plot handles\n \u2502 deduplication automatically)\n \u2502 Use Note.key for different note types:\n \u2502 - \"description\" for main content\n \u2502 - \"metadata\" for status/priority/assignee\n \u2502 - \"comment-{id}\" for individual comments\n \u2502\n \u2514\u2500 No stable identifier OR need multiple Plot threads per external item?\n \u2514\u2500 Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple threads from one external item):\n\n```\nNew event/task/conversation?\n \u251C\u2500 Yes \u2192 Generate UUID with Uuid.Generate()\n \u2502 Create new Thread with that UUID\n \u2502 Store mapping: external_id \u2192 thread_uuid\n \u2502\n \u2514\u2500 No (update/reply/comment) \u2192 Look up mapping by external_id\n \u251C\u2500 Found \u2192 Add Note to existing Thread using stored UUID\n \u2514\u2500 Not found \u2192 Create new Thread with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Thread,\n type NewThreadWithNotes,\n type Note,\n type ToolBuilder,\n Twist,\n} from \"@plotday/twister\";\nimport { ThreadAccess, Plot } from \"@plotday/twister/tools/plot\";\n// Import your sources or tools as needed\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n plot: build(Plot, {\n thread: { access: ThreadAccess.Create },\n }),\n };\n }\n\n async activate() {\n // Auth and resource selection handled in the twist edit modal.\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update threads, notes, focuses, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.runTask()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n## Lifecycle Methods\n\n### activate(context?: { actor: Actor })\n\nCalled when the twist is installed by a user. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.\n\nMost twists have an empty or minimal `activate()`:\n\n```typescript\nasync activate() {\n // Auth and resource selection are handled in the twist edit modal.\n // Only add custom initialization here if needed.\n}\n```\n\n**Store Setup Thread for Later (optional):**\n\n```typescript\nasync activate() {\n const threadId = await this.tools.plot.createThread({\n title: \"Setup complete\",\n notes: [{\n content: \"Your twist is ready. Threads will appear as they sync.\",\n }],\n });\n await this.set(\"setup_thread_id\", threadId);\n}\n```\n\n### Event Callbacks (lifecycle overrides)\n\nTwists respond to events by overriding lifecycle methods inherited from the `Twist` base class. No registration is needed \u2014 declare Plot access in `build()` and the runtime routes events for threads your twist created:\n\n**React to thread changes (for two-way sync):**\n\n```typescript\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n },\n}),\n\n// Called when a thread created by this twist is updated\nasync onThreadUpdated(thread: Thread, changes: { tagsAdded: Record<Tag, ActorId[]>; tagsRemoved: Record<Tag, ActorId[]> }): Promise<void> {\n // Push the change to the external system\n await externalApi.updateItem(thread.meta?.externalId, { title: thread.title });\n}\n\n// Called when a note is created on a thread created by this twist.\n// Notes created by the twist itself are filtered out automatically.\nasync onNoteCreated(note: Note, thread: Thread): Promise<NoteWriteBackResult | void> {\n if (note.author.type === ActorType.Twist) return; // Prevent loops with other twists\n // Sync note to external service as a comment. Return the external\n // system's id + stored content so the runtime can set note.key AND\n // record a sync baseline that preserves Plot's content on round-trip.\n // See connectors/AGENTS.md \u2192 \"Sync baseline preservation\".\n const comment = await externalApi.createComment(thread.meta.externalId, { body: note.content ?? \"\" });\n if (!comment?.id) return;\n return { key: `comment-${comment.id}`, externalContent: comment.body };\n}\n```\n\n**Respond to mentions (AI twist pattern):**\n\n```typescript\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n intents: [{\n description: \"Respond to general questions\",\n examples: [\"What's the weather?\", \"Help me plan my week\"],\n handler: this.respond,\n }],\n },\n}),\n```\n\nFor a fully conversational twist, set `note.handler` instead of `intents` \u2014 every mention is then routed directly to that one method (`(note: Note) => Promise<void>`), skipping intent matching. `handler` and `intents` are mutually exclusive.\n\n**Default mention on replies:**\n\nWhen your twist processes replies (two-way comment sync or conversational AI), set `defaultMention: true` so the user doesn't have to manually re-mention your twist on every note:\n\n- `thread.defaultMention` \u2014 Auto-mention on replies to threads your twist created (e.g., synced issues, emails)\n- `note.defaultMention` \u2014 Auto-mention on follow-up notes in threads where your twist was @-mentioned (e.g., conversational agents)\n\n```typescript\n// Connector with two-way comment sync (override onThreadUpdated / onNoteCreated)\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n defaultMention: true, // Users replying to synced threads will mention this twist by default\n },\n}),\n\n// Conversational AI twist\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n defaultMention: true, // Follow-up notes auto-mention this twist\n intents: [{ description: \"...\", examples: [...], handler: this.respond }],\n },\n}),\n```\n\nWithout `defaultMention`, the twist chip appears toggled OFF by default \u2014 the user can still enable it manually per-note.\n\n## Actions\n\nActions enable user interaction:\n\n```typescript\nimport { type Action, ActionType } from \"@plotday/twister\";\n\n// External URL action\nconst urlAction: Action = {\n title: \"Open website\",\n type: ActionType.external,\n url: \"https://example.com\",\n};\n\n// Callback action (uses Callbacks tool \u2014 use actionCallback, not callback)\nconst token = await this.actionCallback(this.onActionClicked, \"context\");\nconst callbackAction: Action = {\n title: \"Click me\",\n type: ActionType.callback,\n callback: token,\n};\n\n// Add to thread note\nawait this.tools.plot.createThread({\n title: \"Task with actions\",\n notes: [\n {\n content: \"Click the actions below to take action.\",\n actions: [urlAction, callbackAction],\n },\n ],\n});\n\n// Callback handler receives the Action as first argument\nasync onActionClicked(action: Action, context: string): Promise<void> {\n // Handle action click\n}\n```\n\n## Authentication Pattern\n\nAuth is handled automatically via the Integrations tool. Connectors (twists that extend `Connector`) declare their OAuth provider and scopes as class properties, and users connect in the twist edit modal. **You do not need to create auth activities manually.**\n\n```typescript\nimport { Connector, type ToolBuilder } from \"@plotday/twister\";\nimport {\n AuthProvider,\n type AuthToken,\n type Authorization,\n type Channel,\n Integrations,\n} from \"@plotday/twister/tools/integrations\";\n\nexport default class MyConnector extends Connector<MyConnector> {\n readonly provider = AuthProvider.Google;\n readonly scopes = [\"https://www.googleapis.com/auth/calendar\"];\n\n build(build: ToolBuilder) {\n return {\n integrations: build(Integrations),\n };\n }\n\n // List available resources after auth\n async getChannels(auth: Authorization, token: AuthToken): Promise<Channel[]> { ... }\n\n // User enabled a resource\n async onChannelEnabled(channel: Channel): Promise<void> { ... }\n\n // User disabled a resource\n async onChannelDisabled(channel: Channel): Promise<void> { ... }\n}\n\n// Get a token for API calls:\nconst token = await this.tools.integrations.get(channelId);\nif (!token) throw new Error(\"No auth token available\");\nconst client = new ApiClient({ accessToken: token.token });\n```\n\nFor per-user write-backs (e.g., RSVP, comments attributed to the acting user):\nthe dispatch runtime routes the change to the acting user's own connector\ninstance, so your callback (`onScheduleContactUpdated`, etc.) already runs\nunder that user's auth. Just call `this.tools.integrations.get(channelId)`\nto fetch the token and the write-back is attributed correctly. If the\nacting user has no connection of this type, the change lives in Plot but\nis not dispatched.\n\n## Sync Pattern\n\n### Upsert via Source/Key (Strategy 2)\n\nUse source/key for automatic upserts. Connectors save external items as links \u2014 `source` is the upsert key:\n\n```typescript\nasync handleEvent(event: ExternalEvent): Promise<void> {\n // Create or update \u2014 Plot handles deduplication automatically\n await this.tools.integrations.saveLink({\n source: event.htmlLink, // Canonical URL/ID for automatic deduplication\n type: \"event\",\n title: event.summary || \"(No title)\",\n notes: event.description\n ? [{\n key: \"description\", // This key enables note-level upserts\n content: event.description,\n }]\n : [],\n });\n}\n```\n\n`saveLink()` is available to Connectors only. A regular twist adding notes to an already-synced thread references it by source instead:\n\n```typescript\nawait this.tools.plot.createNote({\n thread: { source: event.htmlLink }, // Reference the synced thread\n key: \"summary\",\n content: \"...\",\n});\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot threads from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingThread: NewThreadWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingThread.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingThreadId = await this.get<Uuid>(mappingKey);\n\n if (existingThreadId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingThread.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n thread: { id: existingThreadId },\n content: incomingThread.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const threadId = Uuid.Generate();\n await this.set(mappingKey, threadId);\n\n // Create new Thread with initial Note (new thread with first message)\n await this.tools.plot.createThread({\n ...incomingThread,\n id: threadId,\n });\n}\n```\n\n## Resource Selection\n\nResource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your connector's `getChannels()` method and toggle them on/off. You do **not** need to build custom selection UI.\n\n```typescript\n// In your connector:\nasync getChannels(_auth: Authorization, token: AuthToken): Promise<Channel[]> {\n const client = new ApiClient({ accessToken: token.token });\n const calendars = await client.listCalendars();\n return calendars.map(c => ({\n id: c.id,\n title: c.name,\n children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),\n }));\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size \u2264 100 items to stay under limit\n for (const item of result.items) {\n await this.tools.integrations.saveLink({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: \"item\",\n title: item.title,\n notes: [{\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n // Tip: integrations.saveLinks([...]) saves a whole page in one call,\n // which counts as a single request against the execution budget.\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createThread({\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Thread Sync Best Practices\n\nWhen syncing threads from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nawait this.tools.integrations.saveLink({\n source: event.url,\n type: \"event\",\n title: event.title,\n ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n});\n```\n\n**Why this matters:**\n- **Initial sync**: Threads are unarchived and marked as read for all users, preventing spam from bulk historical imports\n- **Incremental sync**: Threads are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved\n- **Reinstall**: Acts as initial sync, so previously archived threads are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Thread/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Thread.id` / `Note.id` in the external item's metadata when creating it, and update `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes \u2014 that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n if (comment.metadata?.plotNoteId) {\n // Plot ID in metadata: the note originated in Plot (handles the race)\n await this.tools.plot.updateNote({\n id: comment.metadata.plotNoteId,\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n } else {\n // Otherwise upsert by thread source and note key\n await this.tools.plot.createNote({\n thread: { source: payload.itemUrl },\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n }\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createThread({\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created content** - `onNoteCreated` filters out notes created by the twist itself, but still guard against reacting to other automated actors (`note.author.type === ActorType.Twist`) to avoid loops.\n- **Always create Threads with Notes** - See \"Understanding Threads and Notes\" section above for the thread/message pattern and decision tree.\n- **Thread `type` is optional** - It's a display sub-type (`\"action\"`, `\"notes\"`, `\"idea\"`, `\"goal\"`, `\"decision\"`, `\"discussion\"`, `\"announcement\"`, `\"ask\"`). Omit it for the default; use `\"action\"` for tasks.\n- **Use source/key for automatic upserts (Recommended)** - Save external items as links keyed by their canonical URL/ID (connectors: `integrations.saveLink()`) for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Threads** - For source/key pattern, reference threads by source. For UUID pattern, look up stored mappings before creating new Threads. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Don't cache auth tokens** - Fetch tokens with `this.tools.integrations.get(channelId)` when needed; the Integrations tool manages storage and refresh.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
2
2
  //# sourceMappingURL=twist-guide.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"twist-guide.d.ts","sourceRoot":"","sources":["../src/twist-guide.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,y6wBAAsB,CAAC"}
1
+ {"version":3,"file":"twist-guide.d.ts","sourceRoot":"","sources":["../src/twist-guide.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,gi0BAAsB,CAAC"}
package/dist/twist.d.ts CHANGED
@@ -185,8 +185,8 @@ export declare abstract class Twist<TSelf> {
185
185
  * await this.set("handler_token", token);
186
186
  *
187
187
  * // Later, execute the callback
188
- * const token = await this.get<string>("handler_token");
189
- * await this.run(token, args);
188
+ * const token = await this.get<Callback>("handler_token");
189
+ * await this.run(token);
190
190
  * ```
191
191
  *
192
192
  * @template T - The type of value being stored (must be Serializable)
package/dist/twist.js CHANGED
@@ -162,8 +162,8 @@ export class Twist {
162
162
  * await this.set("handler_token", token);
163
163
  *
164
164
  * // Later, execute the callback
165
- * const token = await this.get<string>("handler_token");
166
- * await this.run(token, args);
165
+ * const token = await this.get<Callback>("handler_token");
166
+ * await this.run(token);
167
167
  * ```
168
168
  *
169
169
  * @template T - The type of value being stored (must be Serializable)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plotday/twister",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "Plot Twist Creator - Build intelligent extensions that integrate and automate",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,6 +44,11 @@
44
44
  "types": "./dist/tag.d.ts",
45
45
  "default": "./dist/tag.js"
46
46
  },
47
+ "./facets": {
48
+ "@plotday/connector": "./src/facets.ts",
49
+ "types": "./dist/facets.d.ts",
50
+ "default": "./dist/facets.js"
51
+ },
47
52
  "./options": {
48
53
  "@plotday/connector": "./src/options.ts",
49
54
  "types": "./dist/options.d.ts",
package/src/connector.ts CHANGED
@@ -216,8 +216,8 @@ export type ScopeConfig = {
216
216
  * type: "issue",
217
217
  * label: "Issue",
218
218
  * statuses: [
219
- * { status: "open", label: "Open" },
220
- * { status: "done", label: "Done" },
219
+ * { status: "open", label: "Open", icon: "todo" },
220
+ * { status: "done", label: "Done", icon: "done", done: true },
221
221
  * ],
222
222
  * }];
223
223
  *
@@ -547,17 +547,24 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
547
547
  }
548
548
 
549
549
  /**
550
- * Called when a user adds, removes, or changes the role of contacts on a
551
- * thread owned by this connector. Override on connectors whose source
552
- * supports mid-thread recipient changes (Gmail, IMAP, etc.). Connectors
553
- * that can't change recipients per-message (Slack, Linear) leave this as
554
- * the default no-op and should also declare
555
- * `LinkTypeConfig.supportsContactChanges: false`.
556
- *
557
- * The dispatch fires after Plot has persisted the change. Connectors are
558
- * expected to reflect it on the next outbound note (e.g. building To/Cc/Bcc
559
- * headers from the current `thread.contacts` × `thread.contactMeta`) this
560
- * callback is not the right place to send a standalone notification.
550
+ * Called when a user changes the **thread-level sharing** of a thread owned
551
+ * by this connector adding or removing a contact, or (for connectors with
552
+ * roles) changing a contact's role. Override on connectors whose external
553
+ * source can reflect that membership change, e.g. a group DM / multi-party
554
+ * chat (`LinkTypeConfig.sharingModel: "thread"`) or an email's To/Cc/Bcc
555
+ * recipients (`sharingModel: "message"`). Connectors backed by an immutable
556
+ * roster (most group DMs today) or by channel-level membership
557
+ * (`sharingModel: "channel"`) leave this as the default no-op.
558
+ *
559
+ * `role`/`from`/`to` are **null** for connectors without roles (group DMs
560
+ * have no roles); they carry a `contactRoles` id only for connectors that
561
+ * declare roles (e.g. email `to`/`cc`/`bcc`).
562
+ *
563
+ * The dispatch fires after Plot has persisted the change. A connector may
564
+ * reflect it actively (e.g. add/remove a participant on the external chat)
565
+ * or passively on the next outbound note (e.g. building To/Cc/Bcc headers
566
+ * from the current `thread.contacts` × `thread.contactMeta`) — this callback
567
+ * is not the right place to send a standalone notification.
561
568
  *
562
569
  * @param thread - The thread whose contacts changed
563
570
  * @param changes - The added/removed contacts and any role transitions on existing contacts
@@ -566,9 +573,9 @@ export abstract class Connector<TSelf> extends Twist<TSelf> {
566
573
  onContactsChanged(
567
574
  thread: Thread,
568
575
  changes: {
569
- added: Array<{ contact: Contact; role: string }>;
570
- removed: Array<{ contact: Contact; role: string }>;
571
- changed: Array<{ contact: Contact; from: string; to: string }>;
576
+ added: Array<{ contact: Contact; role: string | null }>;
577
+ removed: Array<{ contact: Contact; role: string | null }>;
578
+ changed: Array<{ contact: Contact; from: string | null; to: string | null }>;
572
579
  },
573
580
  ): Promise<void> {
574
581
  return Promise.resolve();
package/src/facets.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Thread facets — heuristic, message-derived attributes used as internal
3
+ * classifier signal (never user-facing). A connector emits the intrinsic
4
+ * facets on the link it saves; the server stores them on the thread and the
5
+ * focus classifier filters on them.
6
+ *
7
+ * Facets are best-effort: a connector sets a dimension only when a heuristic
8
+ * is confident, leaving it `null` otherwise. The classifier never excludes a
9
+ * thread on a `null` facet.
10
+ *
11
+ * `relationship` (a sender's relationship to the viewing user) is intentionally
12
+ * NOT here: it is recipient-relative and evaluated live by the server, never
13
+ * emitted by a connector.
14
+ */
15
+
16
+ /** The kind of content. Single-valued. */
17
+ export type Format =
18
+ | "chat"
19
+ | "message"
20
+ | "reading"
21
+ | "notification"
22
+ | "receipt"
23
+ | "invoice"
24
+ | "promotion";
25
+
26
+ /** Whether a person or a system produced the message. */
27
+ export type Automation = "human" | "automated";
28
+
29
+ /** How the user was addressed. */
30
+ export type Reach = "direct" | "list";
31
+
32
+ /**
33
+ * Intrinsic facets a connector may set on a `NewLink`. Each is nullable —
34
+ * omit (or set `null`) when no heuristic is confident.
35
+ */
36
+ export type ThreadFacets = {
37
+ format: Format | null;
38
+ automation: Automation | null;
39
+ reach: Reach | null;
40
+ };