@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.
- package/bin/commands/deploy.js +40 -51
- package/bin/commands/deploy.js.map +1 -1
- package/bin/commands/generate.js +13 -19
- package/bin/commands/generate.js.map +1 -1
- package/bin/commands/login.js +16 -20
- package/bin/commands/login.js.map +1 -1
- package/bin/commands/priority-create.js +5 -2
- package/bin/commands/priority-create.js.map +1 -1
- package/bin/commands/priority-list.js +5 -2
- package/bin/commands/priority-list.js.map +1 -1
- package/bin/commands/twist-logs.js +7 -19
- package/bin/commands/twist-logs.js.map +1 -1
- package/bin/templates/AGENTS.template.md +52 -11
- package/bin/utils/token.js +52 -24
- package/bin/utils/token.js.map +1 -1
- package/bin/utils/url-normalize.js +43 -0
- package/bin/utils/url-normalize.js.map +1 -0
- package/cli/templates/AGENTS.template.md +52 -11
- package/dist/common/calendar.d.ts +19 -4
- package/dist/common/calendar.d.ts.map +1 -1
- package/dist/docs/assets/hierarchy.js +1 -1
- package/dist/docs/assets/search.js +1 -1
- package/dist/docs/classes/tool.Tool.html +18 -10
- package/dist/docs/classes/tools_ai.AI.html +1 -1
- package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
- package/dist/docs/classes/tools_integrations.Integrations.html +1 -1
- package/dist/docs/classes/tools_network.Network.html +1 -1
- package/dist/docs/classes/tools_plot.Plot.html +1 -1
- package/dist/docs/classes/tools_store.Store.html +1 -1
- package/dist/docs/classes/tools_tasks.Tasks.html +36 -13
- package/dist/docs/classes/tools_twists.Twists.html +1 -1
- package/dist/docs/documents/Building_Custom_Tools.html +10 -0
- package/dist/docs/documents/Built-in_Tools.html +39 -4
- package/dist/docs/documents/Core_Concepts.html +22 -1
- package/dist/docs/documents/Runtime_Environment.html +32 -19
- package/dist/docs/enums/plot.ActorType.html +4 -4
- package/dist/docs/enums/tag.Tag.html +1 -1
- package/dist/docs/hierarchy.html +1 -1
- package/dist/docs/media/SYNC_STRATEGIES.md +118 -0
- package/dist/docs/types/common_calendar.CalendarTool.html +23 -7
- package/dist/docs/types/common_calendar.SyncOptions.html +20 -4
- package/dist/docs/types/plot.Activity.html +15 -3
- package/dist/docs/types/plot.ActivityOccurrence.html +7 -7
- package/dist/docs/types/plot.ActivityOccurrenceUpdate.html +1 -1
- package/dist/docs/types/plot.ActivityUpdate.html +10 -2
- package/dist/docs/types/plot.ActivityWithNotes.html +1 -1
- package/dist/docs/types/plot.Actor.html +5 -5
- package/dist/docs/types/plot.ContentType.html +1 -1
- package/dist/docs/types/plot.NewActivity.html +21 -2
- package/dist/docs/types/plot.NewActivityOccurrence.html +5 -2
- package/dist/docs/types/plot.NewActivityWithNotes.html +1 -1
- package/dist/docs/types/plot.NewActor.html +1 -1
- package/dist/docs/types/plot.NewContact.html +4 -4
- package/dist/docs/types/plot.NewNote.html +4 -1
- package/dist/docs/types/plot.Note.html +15 -4
- package/dist/docs/types/plot.NoteUpdate.html +1 -1
- package/dist/docs/types/plot.PickPriorityConfig.html +2 -2
- package/dist/llm-docs/common/calendar.d.ts +1 -1
- package/dist/llm-docs/common/calendar.d.ts.map +1 -1
- package/dist/llm-docs/common/calendar.js +1 -1
- package/dist/llm-docs/common/calendar.js.map +1 -1
- package/dist/llm-docs/plot.d.ts +1 -1
- package/dist/llm-docs/plot.d.ts.map +1 -1
- package/dist/llm-docs/plot.js +1 -1
- package/dist/llm-docs/plot.js.map +1 -1
- package/dist/llm-docs/tag.d.ts +1 -1
- package/dist/llm-docs/tag.d.ts.map +1 -1
- package/dist/llm-docs/tag.js +1 -1
- package/dist/llm-docs/tag.js.map +1 -1
- package/dist/llm-docs/tool.d.ts +1 -1
- package/dist/llm-docs/tool.d.ts.map +1 -1
- package/dist/llm-docs/tool.js +1 -1
- package/dist/llm-docs/tool.js.map +1 -1
- package/dist/llm-docs/tools/tasks.d.ts +1 -1
- package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
- package/dist/llm-docs/tools/tasks.js +1 -1
- package/dist/llm-docs/tools/tasks.js.map +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
- package/dist/llm-docs/twist-guide-template.js +1 -1
- package/dist/llm-docs/twist-guide-template.js.map +1 -1
- package/dist/plot.d.ts +72 -6
- package/dist/plot.d.ts.map +1 -1
- package/dist/plot.js.map +1 -1
- package/dist/tag.d.ts.map +1 -1
- package/dist/tag.js +2 -0
- package/dist/tag.js.map +1 -1
- package/dist/tool.d.ts +15 -1
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +15 -1
- package/dist/tool.js.map +1 -1
- package/dist/tools/tasks.d.ts +52 -13
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +34 -10
- package/dist/tools/tasks.js.map +1 -1
- package/dist/twist-guide.d.ts +1 -1
- package/dist/twist-guide.d.ts.map +1 -1
- 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
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
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
|
|
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. **
|
|
463
|
-
2. **
|
|
464
|
-
3. **
|
|
465
|
-
4. **
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
package/bin/utils/token.js
CHANGED
|
@@ -33,43 +33,71 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
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
|
|
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
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
60
|
+
* Returns undefined if no token found (caller handles prompting).
|
|
62
61
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
// Invalid API URL or file read error
|
|
88
|
+
console.error(`Warning: Could not read namespaced token: ${error}`);
|
|
71
89
|
}
|
|
72
90
|
}
|
|
73
|
-
|
|
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
|
package/bin/utils/token.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../cli/utils/token.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
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
|
|
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. **
|
|
463
|
-
2. **
|
|
464
|
-
3. **
|
|
465
|
-
4. **
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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/
|
|
1
|
+
window.hierarchyData = "eJyNkrFuwyAQht/l5gslDTHgLerkparUblVUUYc0Voip4KoMkd+9wm4rOsHCAN//f4d0NwjeU4T2VW/3CMEene1p8GOE9gZ6m87RXCy00L147wDhPIwHaNf3CuErOGihdyZGG+/Ie8dmip3oktD5BVqgeFil2Gq5QOhPgzsEOyavQiEkCimxaTjKNUfZbFBxgUpp1FzuJwStskmqBqmYY0IQQubF1yFSLFXHN5o5tuAVEplLdl1ZYAa268rFTcOz4gfj3LvpzxUf6H9R9hcq2+Q6t3Uj2Y9gll0pCoeMZnm0QttsMu2jpasP57JxXED2Eyh7FBeZ58l5Kks+nSeW0Ip6pbP6Z/LBlvtjwtgMlw2a/9tlE2s2gRLGZrhkmKZvf4JclQ=="
|