@plotday/twister 0.31.2 → 0.32.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 (98) hide show
  1. package/bin/commands/deploy.js +40 -51
  2. package/bin/commands/deploy.js.map +1 -1
  3. package/bin/commands/generate.js +13 -19
  4. package/bin/commands/generate.js.map +1 -1
  5. package/bin/commands/login.js +16 -20
  6. package/bin/commands/login.js.map +1 -1
  7. package/bin/commands/priority-create.js +5 -2
  8. package/bin/commands/priority-create.js.map +1 -1
  9. package/bin/commands/priority-list.js +5 -2
  10. package/bin/commands/priority-list.js.map +1 -1
  11. package/bin/commands/twist-logs.js +7 -19
  12. package/bin/commands/twist-logs.js.map +1 -1
  13. package/bin/templates/AGENTS.template.md +52 -11
  14. package/bin/utils/token.js +52 -24
  15. package/bin/utils/token.js.map +1 -1
  16. package/bin/utils/url-normalize.js +43 -0
  17. package/bin/utils/url-normalize.js.map +1 -0
  18. package/cli/templates/AGENTS.template.md +52 -11
  19. package/dist/common/calendar.d.ts +19 -4
  20. package/dist/common/calendar.d.ts.map +1 -1
  21. package/dist/docs/assets/hierarchy.js +1 -1
  22. package/dist/docs/assets/search.js +1 -1
  23. package/dist/docs/classes/tool.Tool.html +18 -10
  24. package/dist/docs/classes/tools_ai.AI.html +1 -1
  25. package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
  26. package/dist/docs/classes/tools_integrations.Integrations.html +1 -1
  27. package/dist/docs/classes/tools_network.Network.html +1 -1
  28. package/dist/docs/classes/tools_plot.Plot.html +1 -1
  29. package/dist/docs/classes/tools_store.Store.html +1 -1
  30. package/dist/docs/classes/tools_tasks.Tasks.html +36 -13
  31. package/dist/docs/classes/tools_twists.Twists.html +1 -1
  32. package/dist/docs/documents/Building_Custom_Tools.html +10 -0
  33. package/dist/docs/documents/Built-in_Tools.html +39 -4
  34. package/dist/docs/documents/Core_Concepts.html +22 -1
  35. package/dist/docs/documents/Runtime_Environment.html +32 -19
  36. package/dist/docs/enums/plot.ActorType.html +4 -4
  37. package/dist/docs/enums/tag.Tag.html +1 -1
  38. package/dist/docs/hierarchy.html +1 -1
  39. package/dist/docs/media/SYNC_STRATEGIES.md +118 -0
  40. package/dist/docs/types/common_calendar.CalendarTool.html +23 -7
  41. package/dist/docs/types/common_calendar.SyncOptions.html +20 -4
  42. package/dist/docs/types/plot.Activity.html +15 -3
  43. package/dist/docs/types/plot.ActivityOccurrence.html +7 -7
  44. package/dist/docs/types/plot.ActivityOccurrenceUpdate.html +1 -1
  45. package/dist/docs/types/plot.ActivityUpdate.html +10 -2
  46. package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
  47. package/dist/docs/types/plot.Actor.html +5 -5
  48. package/dist/docs/types/plot.ContentType.html +1 -1
  49. package/dist/docs/types/plot.NewActivity.html +21 -2
  50. package/dist/docs/types/plot.NewActivityOccurrence.html +5 -2
  51. package/dist/docs/types/plot.NewActivityWithNotes.html +1 -1
  52. package/dist/docs/types/plot.NewActor.html +1 -1
  53. package/dist/docs/types/plot.NewContact.html +4 -4
  54. package/dist/docs/types/plot.NewNote.html +4 -1
  55. package/dist/docs/types/plot.Note.html +15 -4
  56. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  57. package/dist/docs/types/plot.PickPriorityConfig.html +2 -2
  58. package/dist/llm-docs/common/calendar.d.ts +1 -1
  59. package/dist/llm-docs/common/calendar.d.ts.map +1 -1
  60. package/dist/llm-docs/common/calendar.js +1 -1
  61. package/dist/llm-docs/common/calendar.js.map +1 -1
  62. package/dist/llm-docs/plot.d.ts +1 -1
  63. package/dist/llm-docs/plot.d.ts.map +1 -1
  64. package/dist/llm-docs/plot.js +1 -1
  65. package/dist/llm-docs/plot.js.map +1 -1
  66. package/dist/llm-docs/tag.d.ts +1 -1
  67. package/dist/llm-docs/tag.d.ts.map +1 -1
  68. package/dist/llm-docs/tag.js +1 -1
  69. package/dist/llm-docs/tag.js.map +1 -1
  70. package/dist/llm-docs/tool.d.ts +1 -1
  71. package/dist/llm-docs/tool.d.ts.map +1 -1
  72. package/dist/llm-docs/tool.js +1 -1
  73. package/dist/llm-docs/tool.js.map +1 -1
  74. package/dist/llm-docs/tools/tasks.d.ts +1 -1
  75. package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
  76. package/dist/llm-docs/tools/tasks.js +1 -1
  77. package/dist/llm-docs/tools/tasks.js.map +1 -1
  78. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  79. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  80. package/dist/llm-docs/twist-guide-template.js +1 -1
  81. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  82. package/dist/plot.d.ts +72 -6
  83. package/dist/plot.d.ts.map +1 -1
  84. package/dist/plot.js.map +1 -1
  85. package/dist/tag.d.ts.map +1 -1
  86. package/dist/tag.js +2 -0
  87. package/dist/tag.js.map +1 -1
  88. package/dist/tool.d.ts +15 -1
  89. package/dist/tool.d.ts.map +1 -1
  90. package/dist/tool.js +15 -1
  91. package/dist/tool.js.map +1 -1
  92. package/dist/tools/tasks.d.ts +52 -13
  93. package/dist/tools/tasks.d.ts.map +1 -1
  94. package/dist/tools/tasks.js +34 -10
  95. package/dist/tools/tasks.js.map +1 -1
  96. package/dist/twist-guide.d.ts +1 -1
  97. package/dist/twist-guide.d.ts.map +1 -1
  98. package/package.json +1 -1
@@ -11,9 +11,11 @@ Plot Twists are TypeScript classes that extend the `Twist` base class. Twists in
11
11
  **Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:
12
12
 
13
13
  - **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.
14
- - **Limited CPU time**: Each execution has limited CPU time (typically 10 seconds) and memory (128MB)
15
- - **Use the Run tool**: Queue separate chunks of work with `run.now(functionName, context)`
16
- - **Break long operations**: Split large operations into smaller batches that can be processed independently
14
+ - **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)
15
+ - **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)
16
+ - **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit
17
+ - **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count
18
+ - **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit
17
19
  - **Store intermediate state**: Use the Store tool to persist state between batches
18
20
  - **Examples**: Syncing large datasets, processing many API calls, or performing batch operations
19
21
 
@@ -455,14 +457,16 @@ async onCalendarSelected(
455
457
 
456
458
  ## Batch Processing Pattern
457
459
 
458
- **Important**: Because Twists run in an ephemeral environment with limited execution time, you must break long operations into batches. Each batch runs independently in a new execution context.
460
+ **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.
459
461
 
460
462
  ### Key Principles
461
463
 
462
- 1. **Store state between batches**: Use the Store tool to persist progress
463
- 2. **Queue next batch**: Use the Run tool to schedule the next chunk
464
- 3. **Clean up when done**: Delete stored state after completion
465
- 4. **Handle failures**: Store enough state to resume if a batch fails
464
+ 1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.
465
+ 2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests
466
+ 3. **Store state between batches**: Use the Store tool to persist progress
467
+ 4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)
468
+ 5. **Clean up when done**: Delete stored state after completion
469
+ 6. **Handle failures**: Store enough state to resume if a batch fails
466
470
 
467
471
  ### Example Implementation
468
472
 
@@ -473,10 +477,12 @@ async startSync(resourceId: string): Promise<void> {
473
477
  nextPageToken: null,
474
478
  batchNumber: 1,
475
479
  itemsProcessed: 0,
480
+ initialSync: true, // Track whether this is the first sync
476
481
  });
477
482
 
478
483
  // Queue first batch using runTask method
479
484
  const callback = await this.callback(this.syncBatch, resourceId);
485
+ // runTask creates NEW execution with fresh ~1000 request limit
480
486
  await this.runTask(callback);
481
487
  }
482
488
 
@@ -484,11 +490,13 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
484
490
  // Load state from Store (set by previous execution)
485
491
  const state = await this.get(`sync_state_${resourceId}`);
486
492
 
487
- // Process one batch (keep under time limit)
493
+ // Process one batch (size to stay under ~1000 request limit)
488
494
  const result = await this.fetchBatch(state.nextPageToken);
489
495
 
490
496
  // Process results using source/key pattern (automatic upserts, no manual tracking)
497
+ // If each item makes ~10 requests, keep batch size ≤ 100 items to stay under limit
491
498
  for (const item of result.items) {
499
+ // Each createActivity may make ~5-10 requests depending on notes/links
492
500
  await this.tools.plot.createActivity({
493
501
  source: item.url, // Use item's canonical URL for automatic deduplication
494
502
  type: ActivityType.Note,
@@ -498,6 +506,8 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
498
506
  key: "description", // Use key for upsertable notes
499
507
  content: item.description,
500
508
  }],
509
+ unread: !state.initialSync, // false for initial sync, true for incremental
510
+ ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only
501
511
  });
502
512
  }
503
513
 
@@ -507,9 +517,10 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
507
517
  nextPageToken: result.nextPageToken,
508
518
  batchNumber: state.batchNumber + 1,
509
519
  itemsProcessed: state.itemsProcessed + result.items.length,
520
+ initialSync: state.initialSync, // Preserve initialSync flag across batches
510
521
  });
511
522
 
512
- // Queue next batch (runs in new execution context)
523
+ // Queue next batch - creates NEW execution with fresh request limit
513
524
  const nextCallback = await this.callback(this.syncBatch, resourceId);
514
525
  await this.runTask(nextCallback);
515
526
  } else {
@@ -530,6 +541,35 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
530
541
  }
531
542
  ```
532
543
 
544
+ ## Activity Sync Best Practices
545
+
546
+ When syncing activities from external systems, follow these patterns for optimal user experience:
547
+
548
+ ### The `initialSync` Flag
549
+
550
+ All sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):
551
+
552
+ | Field | Initial Sync | Incremental Sync | Reason |
553
+ |-------|--------------|------------------|---------|
554
+ | `unread` | `false` | `true` | Avoid notification overload from historical items |
555
+ | `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |
556
+
557
+ **Example:**
558
+ ```typescript
559
+ const activity: NewActivity = {
560
+ type: ActivityType.Event,
561
+ source: event.url,
562
+ title: event.title,
563
+ unread: !initialSync, // false for initial, true for incremental
564
+ ...(initialSync ? { archived: false } : {}), // unarchive on initial only
565
+ };
566
+ ```
567
+
568
+ **Why this matters:**
569
+ - **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports
570
+ - **Incremental sync**: New activities appear as unread, and archived state is preserved (respects user's archiving decisions)
571
+ - **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)
572
+
533
573
  ## Error Handling
534
574
 
535
575
  Always handle errors gracefully and communicate them to users:
@@ -561,11 +601,12 @@ try {
561
601
  - **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).
562
602
  - **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.
563
603
  - Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.
564
- - **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Tasks tool. Process enough items per batch to be efficient, but few enough to stay under time limits.
604
+ - **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).
565
605
  - **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.
566
606
  - **Store auth tokens** - Don't re-request authentication unnecessarily.
567
607
  - **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.
568
608
  - **Handle missing auth gracefully** - Check for stored auth before operations.
609
+ - **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.
569
610
 
570
611
  ## Testing
571
612
 
@@ -33,43 +33,71 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.getGlobalTokenPath = getGlobalTokenPath;
36
+ exports.getNamespacedTokenPath = getNamespacedTokenPath;
37
+ exports.resolveToken = resolveToken;
37
38
  exports.getToken = getToken;
38
39
  const fs = __importStar(require("fs"));
39
- const os = __importStar(require("os"));
40
40
  const path = __importStar(require("path"));
41
+ const os = __importStar(require("os"));
42
+ const url_normalize_js_1 = require("./url-normalize.js");
41
43
  /**
42
- * Get the path to the global token file.
43
- *
44
- * @returns The path to the global token file
44
+ * Get the namespaced token path for a specific API URL.
45
45
  */
46
- function getGlobalTokenPath() {
47
- const homeDir = os.homedir();
48
- if (process.platform === "win32") {
49
- // Windows: Use APPDATA
50
- const appData = process.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
51
- return path.join(appData, "plot", "token");
52
- }
53
- else {
54
- // Unix-like: Use ~/.config/plot/token
55
- return path.join(homeDir, ".config", "plot", "token");
56
- }
46
+ function getNamespacedTokenPath(apiUrl) {
47
+ const namespace = (0, url_normalize_js_1.normalizeApiUrl)(apiUrl);
48
+ const configDir = process.platform === "win32"
49
+ ? process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming")
50
+ : path.join(os.homedir(), ".config");
51
+ return path.join(configDir, "plot", "credentials", namespace, "token");
57
52
  }
58
53
  /**
59
- * Read the authentication token from the global token file.
54
+ * Resolve token using the complete resolution chain:
55
+ * 1. --deploy-token CLI flag
56
+ * 2. PLOT_DEPLOY_TOKEN env var
57
+ * 3. .env file DEPLOY_TOKEN
58
+ * 4. Namespaced token file
60
59
  *
61
- * @returns The token string if found, null otherwise
60
+ * Returns undefined if no token found (caller handles prompting).
62
61
  */
63
- async function getToken() {
64
- const globalTokenPath = getGlobalTokenPath();
65
- if (fs.existsSync(globalTokenPath)) {
62
+ function resolveToken(options) {
63
+ // Step 1: CLI flag
64
+ if (options.deployToken) {
65
+ return options.deployToken;
66
+ }
67
+ // Step 2: PLOT_DEPLOY_TOKEN env var
68
+ if (options.envToken) {
69
+ return options.envToken;
70
+ }
71
+ // Step 3: .env file DEPLOY_TOKEN
72
+ if (options.dotEnvToken) {
73
+ return options.dotEnvToken;
74
+ }
75
+ // Step 4: Namespaced token file
76
+ if (options.apiUrl) {
66
77
  try {
67
- return fs.readFileSync(globalTokenPath, "utf-8").trim();
78
+ const namespacedPath = getNamespacedTokenPath(options.apiUrl);
79
+ if (fs.existsSync(namespacedPath)) {
80
+ const token = fs.readFileSync(namespacedPath, "utf-8").trim();
81
+ if (token) {
82
+ return token;
83
+ }
84
+ }
68
85
  }
69
86
  catch (error) {
70
- console.warn(`Warning: Failed to read global token file: ${globalTokenPath}`);
87
+ // Invalid API URL or file read error
88
+ console.error(`Warning: Could not read namespaced token: ${error}`);
71
89
  }
72
90
  }
73
- return null;
91
+ // Step 5: Prompt (handled by caller)
92
+ return undefined;
93
+ }
94
+ /**
95
+ * Legacy getToken() function updated to use resolveToken().
96
+ */
97
+ async function getToken() {
98
+ const token = resolveToken({
99
+ apiUrl: process.env.PLOT_API_URL || "https://api.plot.day",
100
+ });
101
+ return token || null;
74
102
  }
75
103
  //# sourceMappingURL=token.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","sourceRoot":"","sources":["../../cli/utils/token.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASA,gDAUC;AAOD,4BAYC;AAtCD,uCAAyB;AACzB,uCAAyB;AACzB,2CAA6B;AAE7B;;;;GAIG;AACH,SAAgB,kBAAkB;IAChC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,uBAAuB;QACvB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAChF,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,sCAAsC;QACtC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,QAAQ;IAC5B,MAAM,eAAe,GAAG,kBAAkB,EAAE,CAAC;IAC7C,IAAI,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,OAAO,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CACV,8CAA8C,eAAe,EAAE,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../../cli/utils/token.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,wDAQC;AA4BD,oCAoCC;AAKD,4BAKC;AA1FD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AACzB,yDAAqD;AAErD;;GAEG;AACH,SAAgB,sBAAsB,CAAC,MAAc;IACnD,MAAM,SAAS,GAAG,IAAA,kCAAe,EAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,SAAS,GACb,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC1B,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC;QACtE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IAEzC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC;AAmBD;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAC1B,OAA+B;IAE/B,mBAAmB;IACnB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,WAAW,CAAC;IAC7B,CAAC;IAED,oCAAoC;IACpC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,OAAO,OAAO,CAAC,QAAQ,CAAC;IAC1B,CAAC;IAED,iCAAiC;IACjC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,WAAW,CAAC;IAC7B,CAAC;IAED,gCAAgC;IAChC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,sBAAsB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC9D,IAAI,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;gBAClC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC9D,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qCAAqC;YACrC,OAAO,CAAC,KAAK,CAAC,6CAA6C,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,QAAQ;IAC5B,MAAM,KAAK,GAAG,YAAY,CAAC;QACzB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,sBAAsB;KAC3D,CAAC,CAAC;IACH,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeApiUrl = normalizeApiUrl;
4
+ /**
5
+ * Normalize API URL to a filesystem-safe namespace identifier.
6
+ *
7
+ * Examples:
8
+ * https://api.plot.day → api.plot.day
9
+ * http://localhost:8787 → localhost-8787
10
+ * https://api.plot.day/ → api.plot.day (trailing slash removed)
11
+ * http://[::1]:8787 → ::1-8787 (IPv6 supported)
12
+ * https://api.plot.day/v2 → api.plot.day (path ignored)
13
+ *
14
+ * @throws {Error} If URL is malformed or invalid
15
+ */
16
+ function normalizeApiUrl(apiUrl) {
17
+ let url;
18
+ try {
19
+ url = new URL(apiUrl);
20
+ }
21
+ catch (error) {
22
+ throw new Error(`Invalid API URL: ${apiUrl}`);
23
+ }
24
+ // Reject non-http(s) protocols
25
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
26
+ throw new Error(`Invalid API URL protocol: ${url.protocol} (must be http or https)`);
27
+ }
28
+ // Extract hostname (handles IPv6 by removing brackets)
29
+ let hostname = url.hostname;
30
+ // Get port
31
+ const port = url.port;
32
+ // Omit standard ports (80 for http, 443 for https)
33
+ const isStandardPort = (url.protocol === "http:" && port === "80") ||
34
+ (url.protocol === "https:" && port === "443") ||
35
+ !port;
36
+ if (isStandardPort) {
37
+ return hostname;
38
+ }
39
+ // For non-standard ports, append with dash separator
40
+ // (works for both IPv4/IPv6 since hostname has brackets removed)
41
+ return `${hostname}-${port}`;
42
+ }
43
+ //# sourceMappingURL=url-normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-normalize.js","sourceRoot":"","sources":["../../cli/utils/url-normalize.ts"],"names":[],"mappings":";;AAYA,0CAmCC;AA/CD;;;;;;;;;;;GAWG;AACH,SAAgB,eAAe,CAAC,MAAc;IAC5C,IAAI,GAAQ,CAAC;IAEb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,+BAA+B;IAC/B,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,6BAA6B,GAAG,CAAC,QAAQ,0BAA0B,CACpE,CAAC;IACJ,CAAC;IAED,uDAAuD;IACvD,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAE5B,WAAW;IACX,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IAEtB,mDAAmD;IACnD,MAAM,cAAc,GAClB,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,KAAK,IAAI,CAAC;QAC3C,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,IAAI,KAAK,KAAK,CAAC;QAC7C,CAAC,IAAI,CAAC;IAER,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,qDAAqD;IACrD,iEAAiE;IACjE,OAAO,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC;AAC/B,CAAC"}
@@ -11,9 +11,11 @@ Plot Twists are TypeScript classes that extend the `Twist` base class. Twists in
11
11
  **Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:
12
12
 
13
13
  - **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.
14
- - **Limited CPU time**: Each execution has limited CPU time (typically 10 seconds) and memory (128MB)
15
- - **Use the Run tool**: Queue separate chunks of work with `run.now(functionName, context)`
16
- - **Break long operations**: Split large operations into smaller batches that can be processed independently
14
+ - **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)
15
+ - **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)
16
+ - **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit
17
+ - **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count
18
+ - **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit
17
19
  - **Store intermediate state**: Use the Store tool to persist state between batches
18
20
  - **Examples**: Syncing large datasets, processing many API calls, or performing batch operations
19
21
 
@@ -455,14 +457,16 @@ async onCalendarSelected(
455
457
 
456
458
  ## Batch Processing Pattern
457
459
 
458
- **Important**: Because Twists run in an ephemeral environment with limited execution time, you must break long operations into batches. Each batch runs independently in a new execution context.
460
+ **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.
459
461
 
460
462
  ### Key Principles
461
463
 
462
- 1. **Store state between batches**: Use the Store tool to persist progress
463
- 2. **Queue next batch**: Use the Run tool to schedule the next chunk
464
- 3. **Clean up when done**: Delete stored state after completion
465
- 4. **Handle failures**: Store enough state to resume if a batch fails
464
+ 1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.
465
+ 2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests
466
+ 3. **Store state between batches**: Use the Store tool to persist progress
467
+ 4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)
468
+ 5. **Clean up when done**: Delete stored state after completion
469
+ 6. **Handle failures**: Store enough state to resume if a batch fails
466
470
 
467
471
  ### Example Implementation
468
472
 
@@ -473,10 +477,12 @@ async startSync(resourceId: string): Promise<void> {
473
477
  nextPageToken: null,
474
478
  batchNumber: 1,
475
479
  itemsProcessed: 0,
480
+ initialSync: true, // Track whether this is the first sync
476
481
  });
477
482
 
478
483
  // Queue first batch using runTask method
479
484
  const callback = await this.callback(this.syncBatch, resourceId);
485
+ // runTask creates NEW execution with fresh ~1000 request limit
480
486
  await this.runTask(callback);
481
487
  }
482
488
 
@@ -484,11 +490,13 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
484
490
  // Load state from Store (set by previous execution)
485
491
  const state = await this.get(`sync_state_${resourceId}`);
486
492
 
487
- // Process one batch (keep under time limit)
493
+ // Process one batch (size to stay under ~1000 request limit)
488
494
  const result = await this.fetchBatch(state.nextPageToken);
489
495
 
490
496
  // Process results using source/key pattern (automatic upserts, no manual tracking)
497
+ // If each item makes ~10 requests, keep batch size ≤ 100 items to stay under limit
491
498
  for (const item of result.items) {
499
+ // Each createActivity may make ~5-10 requests depending on notes/links
492
500
  await this.tools.plot.createActivity({
493
501
  source: item.url, // Use item's canonical URL for automatic deduplication
494
502
  type: ActivityType.Note,
@@ -498,6 +506,8 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
498
506
  key: "description", // Use key for upsertable notes
499
507
  content: item.description,
500
508
  }],
509
+ unread: !state.initialSync, // false for initial sync, true for incremental
510
+ ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only
501
511
  });
502
512
  }
503
513
 
@@ -507,9 +517,10 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
507
517
  nextPageToken: result.nextPageToken,
508
518
  batchNumber: state.batchNumber + 1,
509
519
  itemsProcessed: state.itemsProcessed + result.items.length,
520
+ initialSync: state.initialSync, // Preserve initialSync flag across batches
510
521
  });
511
522
 
512
- // Queue next batch (runs in new execution context)
523
+ // Queue next batch - creates NEW execution with fresh request limit
513
524
  const nextCallback = await this.callback(this.syncBatch, resourceId);
514
525
  await this.runTask(nextCallback);
515
526
  } else {
@@ -530,6 +541,35 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
530
541
  }
531
542
  ```
532
543
 
544
+ ## Activity Sync Best Practices
545
+
546
+ When syncing activities from external systems, follow these patterns for optimal user experience:
547
+
548
+ ### The `initialSync` Flag
549
+
550
+ All sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):
551
+
552
+ | Field | Initial Sync | Incremental Sync | Reason |
553
+ |-------|--------------|------------------|---------|
554
+ | `unread` | `false` | `true` | Avoid notification overload from historical items |
555
+ | `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |
556
+
557
+ **Example:**
558
+ ```typescript
559
+ const activity: NewActivity = {
560
+ type: ActivityType.Event,
561
+ source: event.url,
562
+ title: event.title,
563
+ unread: !initialSync, // false for initial, true for incremental
564
+ ...(initialSync ? { archived: false } : {}), // unarchive on initial only
565
+ };
566
+ ```
567
+
568
+ **Why this matters:**
569
+ - **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports
570
+ - **Incremental sync**: New activities appear as unread, and archived state is preserved (respects user's archiving decisions)
571
+ - **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)
572
+
533
573
  ## Error Handling
534
574
 
535
575
  Always handle errors gracefully and communicate them to users:
@@ -561,11 +601,12 @@ try {
561
601
  - **Use Activity.source and Note.key for automatic upserts (Recommended)** - Set Activity.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).
562
602
  - **Add Notes to existing Activities** - For source/key pattern, reference activities by source. For UUID pattern, look up stored mappings before creating new Activities. Think thread replies, not new threads.
563
603
  - Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.
564
- - **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Tasks tool. Process enough items per batch to be efficient, but few enough to stay under time limits.
604
+ - **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).
565
605
  - **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.
566
606
  - **Store auth tokens** - Don't re-request authentication unnecessarily.
567
607
  - **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.
568
608
  - **Handle missing auth gracefully** - Check for stored auth before operations.
609
+ - **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.
569
610
 
570
611
  ## Testing
571
612
 
@@ -34,10 +34,25 @@ export type Calendar = {
34
34
  * Used to limit sync scope and optimize performance.
35
35
  */
36
36
  export type SyncOptions = {
37
- /** Earliest date to sync events from (inclusive) */
38
- timeMin?: Date;
39
- /** Latest date to sync events to (exclusive) */
40
- timeMax?: Date;
37
+ /**
38
+ * Earliest date to sync events from (inclusive).
39
+ * - If undefined: defaults to 2 years in the past
40
+ * - If null: syncs all history from the beginning of time
41
+ * - If Date: syncs from the specified date
42
+ */
43
+ timeMin?: Date | null;
44
+ /**
45
+ * Latest date to sync events to (exclusive).
46
+ * - If undefined: no limit (syncs all future events)
47
+ * - If null: no limit (syncs all future events)
48
+ * - If Date: syncs up to but not including the specified date
49
+ *
50
+ * Use cases:
51
+ * - Daily digest: Set to end of today
52
+ * - Week view: Set to end of current week
53
+ * - Performance: Limit initial sync range
54
+ */
55
+ timeMax?: Date | null;
41
56
  };
42
57
  /**
43
58
  * Base interface for calendar integration tools.
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../src/common/calendar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,oDAAoD;IACpD,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,gDAAgD;IAChD,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,WAAW,CACT,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,GAAG,EAE7D,QAAQ,EAAE,SAAS,EACnB,GAAG,SAAS,EAAE,KAAK,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,SAAS,CACP,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,SAAS,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,GAAG,EAEzE,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,WAAW,EACf,QAAQ,EAAE,SAAS,EACnB,GAAG,SAAS,EAAE,KAAK,GAClB,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE,CAAC"}
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../src/common/calendar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,WAAW,CACT,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,GAAG,EAE7D,QAAQ,EAAE,SAAS,EACnB,GAAG,SAAS,EAAE,KAAK,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,SAAS,CACP,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,SAAS,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,GAAG,EAEzE,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,WAAW,EACf,QAAQ,EAAE,SAAS,EACnB,GAAG,SAAS,EAAE,KAAK,GAClB,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE,CAAC"}
@@ -1 +1 @@
1
- window.hierarchyData = "eJyNkrFuwyAQht/l5gslFTbYW9TJS1Wp3aqoojZNLBNTwVUZIr97hd1WdIKFAb7//w7pbuCdowDta1MdEbz5sKan0c0B2hs0VTxnfTHQQvfinAWEaZwHaPf3CuHLW2ihtzoEE+7IOctWip3pEtH1BVqgMOxibLddIPTn0Q7ezNGrUAiBQgqsK4mSS5Q1R8X3qFSNDRfHBaFRySRFgxTMsSAIIdLi6xgo5KrDG60c2/ACiUwlhy4v0CM7dPniupJJ8YO29l33U8EH+l+U/YXyNslTWzeTOXm97UpWOCY0S6MF2pon2kdDV+envHHeQPYTyHsU3yeeJ+soL/m0jlhEC+pVndQ/k/Mm3x8ixlY4b2j4v13WoWQTKGJshXOGZfkGZ4dciQ=="
1
+ window.hierarchyData = "eJyNkrFuwyAQht/l5gslDTHgLerkparUblVUUYc0Voip4KoMkd+9wm4rOsHCAN//f4d0NwjeU4T2VW/3CMEene1p8GOE9gZ6m87RXCy00L147wDhPIwHaNf3CuErOGihdyZGG+/Ie8dmip3oktD5BVqgeFil2Gq5QOhPgzsEOyavQiEkCimxaTjKNUfZbFBxgUpp1FzuJwStskmqBqmYY0IQQubF1yFSLFXHN5o5tuAVEplLdl1ZYAa268rFTcOz4gfj3LvpzxUf6H9R9hcq2+Q6t3Uj2Y9gll0pCoeMZnm0QttsMu2jpasP57JxXED2Eyh7FBeZ58l5Kks+nSeW0Ip6pbP6Z/LBlvtjwtgMlw2a/9tlE2s2gRLGZrhkmKZvf4JclQ=="