@plotday/twister 0.47.0 → 0.49.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 (123) hide show
  1. package/bin/commands/generate.js +5 -5
  2. package/bin/commands/generate.js.map +1 -1
  3. package/bin/templates/AGENTS.template.md +8 -2
  4. package/bin/utils/bundle.js +14 -0
  5. package/bin/utils/bundle.js.map +1 -1
  6. package/cli/templates/AGENTS.template.md +8 -2
  7. package/dist/connector.d.ts +67 -7
  8. package/dist/connector.d.ts.map +1 -1
  9. package/dist/connector.js +15 -5
  10. package/dist/connector.js.map +1 -1
  11. package/dist/docs/assets/hierarchy.js +1 -1
  12. package/dist/docs/assets/navigation.js +1 -1
  13. package/dist/docs/assets/search.js +1 -1
  14. package/dist/docs/classes/index.Connector.html +58 -49
  15. package/dist/docs/classes/index.Imap.html +1 -1
  16. package/dist/docs/classes/index.Options.html +1 -1
  17. package/dist/docs/classes/index.Smtp.html +1 -1
  18. package/dist/docs/classes/tools_ai.AI.html +1 -1
  19. package/dist/docs/classes/tools_callbacks.Callbacks.html +1 -1
  20. package/dist/docs/classes/tools_integrations.Integrations.html +21 -5
  21. package/dist/docs/classes/tools_network.Network.html +1 -1
  22. package/dist/docs/classes/tools_plot.Plot.html +1 -1
  23. package/dist/docs/classes/tools_store.Store.html +1 -1
  24. package/dist/docs/classes/tools_tasks.Tasks.html +1 -1
  25. package/dist/docs/classes/tools_twists.Twists.html +1 -1
  26. package/dist/docs/classes/twist.Twist.html +28 -28
  27. package/dist/docs/documents/Building_Connectors.html +8 -1
  28. package/dist/docs/documents/CLI_Reference.html +6 -4
  29. package/dist/docs/enums/tag.Tag.html +11 -1
  30. package/dist/docs/enums/tools_integrations.AuthProvider.html +14 -12
  31. package/dist/docs/hierarchy.html +1 -1
  32. package/dist/docs/media/AGENTS.md +298 -775
  33. package/dist/docs/media/MULTI_USER_AUTH.md +6 -4
  34. package/dist/docs/media/SYNC_STRATEGIES.md +20 -14
  35. package/dist/docs/modules/index.html +1 -1
  36. package/dist/docs/types/index.CreateLinkDraft.html +7 -12
  37. package/dist/docs/types/index.NoteWriteBackResult.html +38 -0
  38. package/dist/docs/types/tools_integrations.ArchiveLinkFilter.html +5 -5
  39. package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
  40. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  41. package/dist/llm-docs/connector.d.ts +1 -1
  42. package/dist/llm-docs/connector.d.ts.map +1 -1
  43. package/dist/llm-docs/connector.js +1 -1
  44. package/dist/llm-docs/connector.js.map +1 -1
  45. package/dist/llm-docs/tag.d.ts +1 -1
  46. package/dist/llm-docs/tag.d.ts.map +1 -1
  47. package/dist/llm-docs/tag.js +1 -1
  48. package/dist/llm-docs/tag.js.map +1 -1
  49. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  50. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  51. package/dist/llm-docs/tools/integrations.js +1 -1
  52. package/dist/llm-docs/tools/integrations.js.map +1 -1
  53. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  54. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  55. package/dist/llm-docs/twist-guide-template.js +1 -1
  56. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  57. package/dist/llm-docs/twist.d.ts +1 -1
  58. package/dist/llm-docs/twist.d.ts.map +1 -1
  59. package/dist/llm-docs/twist.js +1 -1
  60. package/dist/llm-docs/twist.js.map +1 -1
  61. package/dist/tag.d.ts +11 -1
  62. package/dist/tag.d.ts.map +1 -1
  63. package/dist/tag.js +10 -0
  64. package/dist/tag.js.map +1 -1
  65. package/dist/tools/integrations.d.ts +25 -1
  66. package/dist/tools/integrations.d.ts.map +1 -1
  67. package/dist/tools/integrations.js +2 -0
  68. package/dist/tools/integrations.js.map +1 -1
  69. package/dist/twist-guide.d.ts +1 -1
  70. package/dist/twist-guide.d.ts.map +1 -1
  71. package/dist/twist.d.ts +2 -1
  72. package/dist/twist.d.ts.map +1 -1
  73. package/dist/twist.js.map +1 -1
  74. package/dist/utils/markdown.d.ts +27 -0
  75. package/dist/utils/markdown.d.ts.map +1 -0
  76. package/dist/utils/markdown.js +82 -0
  77. package/dist/utils/markdown.js.map +1 -0
  78. package/package.json +7 -1
  79. package/src/connector.ts +427 -0
  80. package/src/creator-docs.ts +29 -0
  81. package/src/index.ts +10 -0
  82. package/src/llm-docs/connector.ts +8 -0
  83. package/src/llm-docs/index.ts +48 -0
  84. package/src/llm-docs/options.ts +8 -0
  85. package/src/llm-docs/plot.ts +8 -0
  86. package/src/llm-docs/schedule.ts +8 -0
  87. package/src/llm-docs/tag.ts +8 -0
  88. package/src/llm-docs/tool.ts +8 -0
  89. package/src/llm-docs/tools/ai.ts +8 -0
  90. package/src/llm-docs/tools/callbacks.ts +8 -0
  91. package/src/llm-docs/tools/imap.ts +8 -0
  92. package/src/llm-docs/tools/integrations.ts +8 -0
  93. package/src/llm-docs/tools/network.ts +8 -0
  94. package/src/llm-docs/tools/plot.ts +8 -0
  95. package/src/llm-docs/tools/smtp.ts +8 -0
  96. package/src/llm-docs/tools/store.ts +8 -0
  97. package/src/llm-docs/tools/tasks.ts +8 -0
  98. package/src/llm-docs/tools/twists.ts +8 -0
  99. package/src/llm-docs/twist-guide-template.ts +8 -0
  100. package/src/llm-docs/twist.ts +8 -0
  101. package/src/options.ts +115 -0
  102. package/src/plot.ts +1068 -0
  103. package/src/schedule.ts +203 -0
  104. package/src/tag.ts +54 -0
  105. package/src/tool.ts +377 -0
  106. package/src/tools/ai.ts +845 -0
  107. package/src/tools/callbacks.ts +134 -0
  108. package/src/tools/imap.ts +266 -0
  109. package/src/tools/index.ts +10 -0
  110. package/src/tools/integrations.ts +352 -0
  111. package/src/tools/network.ts +240 -0
  112. package/src/tools/plot.ts +692 -0
  113. package/src/tools/smtp.ts +166 -0
  114. package/src/tools/store.ts +149 -0
  115. package/src/tools/tasks.ts +137 -0
  116. package/src/tools/twists.ts +228 -0
  117. package/src/twist-guide.ts +9 -0
  118. package/src/twist.ts +436 -0
  119. package/src/utils/hash.ts +8 -0
  120. package/src/utils/markdown.ts +94 -0
  121. package/src/utils/serializable.ts +54 -0
  122. package/src/utils/types.ts +130 -0
  123. package/src/utils/uuid.ts +9 -0
@@ -0,0 +1,166 @@
1
+ import { ITool } from "..";
2
+
3
+ /** Opaque session handle returned by connect(). */
4
+ export type SmtpSession = string;
5
+
6
+ /** Credentials and server info for connecting to an SMTP server. */
7
+ export type SmtpConnectOptions = {
8
+ /** SMTP server hostname (e.g. "smtp.mail.me.com") */
9
+ host: string;
10
+ /** SMTP server port (e.g. 465 for TLS, 587 for STARTTLS) */
11
+ port: number;
12
+ /** Whether to use implicit TLS (true for port 465) */
13
+ tls: boolean;
14
+ /** Whether to upgrade to TLS via STARTTLS (true for port 587) */
15
+ starttls: boolean;
16
+ /** SMTP username (typically the email address) */
17
+ username: string;
18
+ /** SMTP password (app-specific password for Apple) */
19
+ password: string;
20
+ };
21
+
22
+ /** An email address with optional display name. */
23
+ export type SmtpAddress = {
24
+ /** Display name (e.g. "John Doe") */
25
+ name?: string;
26
+ /** Email address (e.g. "john@example.com") */
27
+ address: string;
28
+ };
29
+
30
+ /** An email message to send. */
31
+ export type SmtpMessage = {
32
+ /** Sender address */
33
+ from: SmtpAddress;
34
+ /** Primary recipients */
35
+ to: SmtpAddress[];
36
+ /** Carbon copy recipients */
37
+ cc?: SmtpAddress[];
38
+ /** Blind carbon copy recipients (not visible in headers) */
39
+ bcc?: SmtpAddress[];
40
+ /** Reply-To address (if different from From) */
41
+ replyTo?: SmtpAddress;
42
+ /** Message-ID of the message being replied to (for threading) */
43
+ inReplyTo?: string;
44
+ /** Message-ID chain for threading */
45
+ references?: string[];
46
+ /** Email subject line */
47
+ subject: string;
48
+ /** Plain text body */
49
+ text?: string;
50
+ /** HTML body */
51
+ html?: string;
52
+ /** Custom Message-ID; auto-generated as <uuid@plot.day> if omitted */
53
+ messageId?: string;
54
+ };
55
+
56
+ /** Result of sending an email. */
57
+ export type SmtpSendResult = {
58
+ /** The Message-ID that was used (auto-generated or from SmtpMessage) */
59
+ messageId: string;
60
+ /** Email addresses that were accepted by the server */
61
+ accepted: string[];
62
+ /** Email addresses that were rejected by the server */
63
+ rejected: string[];
64
+ };
65
+
66
+ /**
67
+ * Built-in tool for SMTP email sending.
68
+ *
69
+ * Provides high-level SMTP operations for composing and sending email.
70
+ * Handles TCP/TLS connections, STARTTLS upgrades, SMTP protocol details,
71
+ * and RFC 2822 message formatting internally.
72
+ *
73
+ * **Permission model:** Connectors declare which SMTP hosts they need access
74
+ * to. Connections to undeclared hosts are rejected.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * class AppleMailConnector extends Connector<AppleMailConnector> {
79
+ * build(build: ConnectorBuilder) {
80
+ * return {
81
+ * options: build(Options, {
82
+ * email: { type: "text", label: "Apple ID Email", default: "" },
83
+ * password: { type: "text", label: "App-Specific Password", secure: true, default: "" },
84
+ * }),
85
+ *
86
+ * imap: build(Imap, { hosts: ["imap.mail.me.com"] }),
87
+ * smtp: build(Smtp, { hosts: ["smtp.mail.me.com"] }),
88
+ * integrations: build(Integrations),
89
+ * };
90
+ * }
91
+ *
92
+ * async sendReply(originalMessage: ImapMessage, replyBody: string) {
93
+ * const session = await this.tools.smtp.connect({
94
+ * host: "smtp.mail.me.com",
95
+ * port: 587,
96
+ * tls: false,
97
+ * starttls: true,
98
+ * username: this.tools.options.email,
99
+ * password: this.tools.options.password,
100
+ * });
101
+ *
102
+ * try {
103
+ * const result = await this.tools.smtp.send(session, {
104
+ * from: { address: this.tools.options.email },
105
+ * to: originalMessage.from ?? [],
106
+ * subject: `Re: ${originalMessage.subject ?? "(no subject)"}`,
107
+ * text: replyBody,
108
+ * inReplyTo: originalMessage.messageId,
109
+ * references: [
110
+ * ...(originalMessage.references ?? []),
111
+ * ...(originalMessage.messageId ? [originalMessage.messageId] : []),
112
+ * ],
113
+ * });
114
+ *
115
+ * console.log(`Sent reply, Message-ID: ${result.messageId}`);
116
+ * } finally {
117
+ * await this.tools.smtp.disconnect(session);
118
+ * }
119
+ * }
120
+ * }
121
+ * ```
122
+ */
123
+ export abstract class Smtp extends ITool {
124
+ static readonly Options: {
125
+ /** SMTP server hostnames this tool is allowed to connect to. */
126
+ hosts: string[];
127
+ };
128
+
129
+ /**
130
+ * Opens a connection to an SMTP server and authenticates.
131
+ *
132
+ * Handles the full SMTP handshake: greeting, EHLO, optional STARTTLS
133
+ * upgrade, and AUTH LOGIN authentication.
134
+ *
135
+ * @param options - Server address, port, TLS/STARTTLS setting, and credentials
136
+ * @returns An opaque session handle for subsequent operations
137
+ * @throws If the host is not in the declared hosts list, connection fails, or auth fails
138
+ */
139
+ abstract connect(options: SmtpConnectOptions): Promise<SmtpSession>;
140
+
141
+ /**
142
+ * Sends an email message.
143
+ *
144
+ * Constructs a properly formatted RFC 2822 message with MIME support
145
+ * and sends it via the SMTP protocol. Handles multipart messages when
146
+ * both text and HTML bodies are provided.
147
+ *
148
+ * @param session - Session handle from connect()
149
+ * @param message - The email message to send
150
+ * @returns Send result with Message-ID and per-recipient acceptance status
151
+ * @throws If the session is invalid or the server rejects the message entirely
152
+ */
153
+ abstract send(
154
+ session: SmtpSession,
155
+ message: SmtpMessage
156
+ ): Promise<SmtpSendResult>;
157
+
158
+ /**
159
+ * Closes the SMTP connection.
160
+ *
161
+ * Always call this when done, preferably in a finally block.
162
+ *
163
+ * @param session - Session handle from connect()
164
+ */
165
+ abstract disconnect(session: SmtpSession): Promise<void>;
166
+ }
@@ -0,0 +1,149 @@
1
+ import { ITool, type Serializable } from "..";
2
+
3
+ /**
4
+ * Built-in tool for persistent key-value storage.
5
+ *
6
+ * The Store tool provides twists and tools with a simple, persistent storage
7
+ * mechanism that survives worker restarts and invocations. Each twist/tool
8
+ * instance gets its own isolated storage namespace.
9
+ *
10
+ * **Note:** Store methods are also available directly on Twist and Tool classes
11
+ * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.
12
+ * This is the recommended approach for most use cases.
13
+ *
14
+ * **Storage Characteristics:**
15
+ * - Persistent across worker restarts
16
+ * - Isolated per twist/tool instance
17
+ * - Supports SuperJSON-serializable data (see below)
18
+ * - Async operations for scalability
19
+ *
20
+ * **Supported Data Types (via SuperJSON):**
21
+ * - Primitives: string, number, boolean, null, undefined
22
+ * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt
23
+ * - Collections: Arrays and objects (recursively)
24
+ *
25
+ * **NOT Supported (will throw validation errors):**
26
+ * - Functions (use callback tokens instead - see Callbacks tool)
27
+ * - Symbols
28
+ * - Circular references
29
+ * - Custom class instances
30
+ *
31
+ * **Use Cases:**
32
+ * - Storing authentication tokens
33
+ * - Caching configuration data
34
+ * - Maintaining sync state
35
+ * - Persisting user preferences
36
+ * - Tracking processing checkpoints
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * class CalendarTool extends Tool {
41
+ * async saveAuthToken(provider: string, token: string) {
42
+ * // Using built-in set method (recommended)
43
+ * await this.set(`auth_token_${provider}`, token);
44
+ * }
45
+ *
46
+ * async getAuthToken(provider: string): Promise<string | null> {
47
+ * // Using built-in get method (recommended)
48
+ * return await this.get<string>(`auth_token_${provider}`);
49
+ * }
50
+ *
51
+ * async clearAllTokens() {
52
+ * // Using built-in clearAll method (recommended)
53
+ * await this.clearAll();
54
+ * }
55
+ * }
56
+ * ```
57
+ */
58
+ export abstract class Store extends ITool {
59
+ /**
60
+ * Retrieves a value from storage by key.
61
+ *
62
+ * Returns the stored value deserialized to the specified type,
63
+ * or null if the key doesn't exist or the value is null.
64
+ *
65
+ * Values are automatically deserialized using SuperJSON, which
66
+ * properly restores Date objects, Maps, Sets, and other complex types.
67
+ *
68
+ * @template T - The expected type of the stored value (must be Serializable)
69
+ * @param key - The storage key to retrieve
70
+ * @returns Promise resolving to the stored value or null
71
+ */
72
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
+ abstract get<T extends Serializable>(key: string): Promise<T | null>;
74
+
75
+ /**
76
+ * Stores a value in persistent storage.
77
+ *
78
+ * The value will be serialized using SuperJSON and stored persistently.
79
+ * Any existing value at the same key will be overwritten.
80
+ *
81
+ * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,
82
+ * and other complex types that standard JSON doesn't support.
83
+ *
84
+ * @template T - The type of value being stored (must be Serializable)
85
+ * @param key - The storage key to use
86
+ * @param value - The value to store (must be SuperJSON-serializable)
87
+ * @returns Promise that resolves when the value is stored
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Date objects are preserved
92
+ * await this.set('sync_state', {
93
+ * lastSync: new Date(),
94
+ * minDate: new Date(2024, 0, 1)
95
+ * });
96
+ *
97
+ * // undefined is now supported
98
+ * await this.set('data', { name: 'test', optional: undefined }); // ✅ Works
99
+ *
100
+ * // Arrays with undefined are supported
101
+ * await this.set('items', [1, undefined, 3]); // ✅ Works
102
+ * await this.set('items', [1, null, 3]); // ✅ Also works
103
+ *
104
+ * // Maps and Sets are supported
105
+ * await this.set('mapping', new Map([['key', 'value']])); // ✅ Works
106
+ * await this.set('tags', new Set(['tag1', 'tag2'])); // ✅ Works
107
+ *
108
+ * // Functions are NOT supported - use callback tokens instead
109
+ * const token = await this.callback(this.myFunction);
110
+ * await this.set('callback_ref', token); // ✅ Use callback token
111
+ * ```
112
+ */
113
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
114
+ abstract set<T extends Serializable>(key: string, value: T): Promise<void>;
115
+
116
+ /**
117
+ * Lists all storage keys matching a prefix.
118
+ *
119
+ * Returns an array of key strings that start with the given prefix.
120
+ * Useful for finding all keys in a namespace (e.g., all sync locks).
121
+ *
122
+ * @param prefix - The prefix to match keys against
123
+ * @returns Promise resolving to an array of matching key strings
124
+ */
125
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
126
+ abstract list(prefix: string): Promise<string[]>;
127
+
128
+ /**
129
+ * Removes a specific key from storage.
130
+ *
131
+ * After this operation, get() calls for this key will return null.
132
+ * No error is thrown if the key doesn't exist.
133
+ *
134
+ * @param key - The storage key to remove
135
+ * @returns Promise that resolves when the key is removed
136
+ */
137
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
138
+ abstract clear(key: string): Promise<void>;
139
+
140
+ /**
141
+ * Removes all keys from this storage instance.
142
+ *
143
+ * This operation clears all data stored by this twist/tool instance
144
+ * but does not affect storage for other twists or tools.
145
+ *
146
+ * @returns Promise that resolves when all keys are removed
147
+ */
148
+ abstract clearAll(): Promise<void>;
149
+ }
@@ -0,0 +1,137 @@
1
+ import { ITool } from "..";
2
+ import type { Callback } from "./callbacks";
3
+
4
+ /**
5
+ * Run background tasks and scheduled jobs.
6
+ *
7
+ * The Tasks tool enables twists and tools to queue callbacks for execution in separate
8
+ * worker contexts. **This is critical for staying under request limits**: each execution
9
+ * has a limit of ~1000 requests (HTTP requests, tool calls, database operations), and
10
+ * running a task creates a NEW execution with a fresh request limit.
11
+ *
12
+ * **Key distinction:**
13
+ * - **Calling a callback** (via `this.run()`) continues the same execution and shares the request count
14
+ * - **Running a task** (via `this.runTask()`) creates a NEW execution with fresh ~1000 request limit
15
+ *
16
+ * **When to use tasks:**
17
+ * - Processing large datasets that would exceed 1000 requests
18
+ * - Breaking loops into chunks where each chunk stays under the request limit
19
+ * - Scheduling operations for future execution
20
+ *
21
+ * **Note:** Tasks tool methods are also available directly on Twist and Tool classes
22
+ * via `this.runTask()`, `this.cancelTask()`, and `this.cancelAllTasks()`.
23
+ * This is the recommended approach for most use cases.
24
+ *
25
+ * **Best Practices:**
26
+ * - Size batches to stay under ~1000 requests per execution
27
+ * - Calculate requests per item to determine safe batch size
28
+ * - Create callbacks first using `this.callback()`
29
+ * - Store intermediate state using the Store tool
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * class SyncTool extends Tool {
34
+ * async startBatchSync(totalItems: number) {
35
+ * // Store initial state using built-in set method
36
+ * await this.set("sync_progress", { processed: 0, total: totalItems });
37
+ *
38
+ * // Create callback and queue first batch
39
+ * const callback = await this.callback("processBatch", { batchNumber: 1 });
40
+ * // runTask creates NEW execution with fresh ~1000 request limit
41
+ * await this.runTask(callback);
42
+ * }
43
+ *
44
+ * async processBatch(args: any, context: { batchNumber: number }) {
45
+ * // Process one batch of items (sized to stay under request limit)
46
+ * const progress = await this.get("sync_progress");
47
+ *
48
+ * // If each item makes ~10 requests, process ~100 items per batch
49
+ * // 100 items × 10 requests = 1000 requests (at limit)
50
+ * const batchSize = 100;
51
+ * const items = await this.fetchItems(progress.processed, batchSize);
52
+ *
53
+ * for (const item of items) {
54
+ * await this.processItem(item); // Makes ~10 requests per item
55
+ * }
56
+ *
57
+ * await this.set("sync_progress", {
58
+ * processed: progress.processed + batchSize,
59
+ * total: progress.total
60
+ * });
61
+ *
62
+ * if (progress.processed < progress.total) {
63
+ * // Queue next batch - creates NEW execution with fresh request limit
64
+ * const callback = await this.callback("processBatch", {
65
+ * batchNumber: context.batchNumber + 1
66
+ * });
67
+ * await this.runTask(callback);
68
+ * }
69
+ * }
70
+ *
71
+ * async scheduleCleanup() {
72
+ * const tomorrow = new Date();
73
+ * tomorrow.setDate(tomorrow.getDate() + 1);
74
+ *
75
+ * const callback = await this.callback("cleanupOldData");
76
+ * // Schedule for future execution
77
+ * return await this.runTask(callback, { runAt: tomorrow });
78
+ * }
79
+ * }
80
+ * ```
81
+ */
82
+ export abstract class Tasks extends ITool {
83
+ /**
84
+ * Queues a callback to execute in a separate worker context with a fresh request limit.
85
+ *
86
+ * **Creates a NEW execution** with its own request limit of ~1000 requests (HTTP requests,
87
+ * tool calls, database operations). This is the primary way to stay under request limits
88
+ * when processing large datasets or making many API calls.
89
+ *
90
+ * The callback will be invoked either immediately or at a scheduled time
91
+ * in an isolated execution environment. Each execution has ~1000 requests and ~60 seconds
92
+ * CPU time. Use this for breaking loops into chunks that stay under the request limit.
93
+ *
94
+ * **Key distinction:**
95
+ * - `this.run(callback)` - Continues same execution, shares request count
96
+ * - `this.runTask(callback)` - NEW execution, fresh request limit
97
+ *
98
+ * @param callback - Callback created with `this.callback()`
99
+ * @param options - Optional configuration for the execution
100
+ * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately
101
+ * @returns Promise resolving to a cancellation token (only for scheduled executions)
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Break large loop into batches to stay under request limit
106
+ * const callback = await this.callback("syncBatch", { page: 1 });
107
+ * await this.runTask(callback); // Fresh execution with ~1000 requests
108
+ * ```
109
+ */
110
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
111
+ abstract runTask(
112
+ callback: Callback,
113
+ options?: { runAt?: Date }
114
+ ): Promise<string | void>;
115
+
116
+ /**
117
+ * Cancels a previously scheduled execution.
118
+ *
119
+ * Prevents a scheduled function from executing. No error is thrown
120
+ * if the token is invalid or the execution has already completed.
121
+ *
122
+ * @param token - The cancellation token returned by runTask() with runAt option
123
+ * @returns Promise that resolves when the cancellation is processed
124
+ */
125
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
126
+ abstract cancelTask(token: string): Promise<void>;
127
+
128
+ /**
129
+ * Cancels all scheduled executions for this tool/twist.
130
+ *
131
+ * Cancels all pending scheduled executions created by this tool or twist
132
+ * instance. Immediate executions cannot be cancelled.
133
+ *
134
+ * @returns Promise that resolves when all cancellations are processed
135
+ */
136
+ abstract cancelAllTasks(): Promise<void>;
137
+ }
@@ -0,0 +1,228 @@
1
+ import { type Callback, ITool } from "..";
2
+
3
+ /**
4
+ * Twist source code structure containing dependencies and source files.
5
+ */
6
+ export interface TwistSource {
7
+ /**
8
+ * Package dependencies with version specifiers
9
+ * @example { "@plotday/twister": "workspace:^", "@plotday/tool-google-calendar": "^1.0.0" }
10
+ */
11
+ dependencies: Record<string, string>;
12
+
13
+ /**
14
+ * Source files with their content
15
+ * Must include "index.ts" as the entry point
16
+ * @example { "index.ts": "export default class MyTwist extends Twist {...}" }
17
+ */
18
+ files: Record<string, string>;
19
+ }
20
+
21
+ /**
22
+ * Represents a log entry from a twist execution.
23
+ */
24
+ export type Log = {
25
+ timestamp: Date;
26
+ environment: "personal" | "private" | "review" | "public";
27
+ severity: "log" | "error" | "warn" | "info";
28
+ message: string;
29
+ };
30
+
31
+ /**
32
+ * Twist permissions returned after deployment.
33
+ * Nested structure mapping domains to entities to permission flags.
34
+ *
35
+ * Format: { domain: { entity: flags[] } }
36
+ * - domain: Tool name (e.g., "network", "plot")
37
+ * - entity: Domain-specific identifier (e.g., URL pattern, resource type)
38
+ * - flags: Array of permission flags ("read", "write", "update", "use")
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * {
43
+ * "network": {
44
+ * "https://api.example.com/*": ["use"],
45
+ * "https://googleapis.com/*": ["use"]
46
+ * },
47
+ * "plot": {
48
+ * "thread:mentioned": ["read", "write", "update"],
49
+ * "priority": ["read", "write", "update"]
50
+ * }
51
+ * }
52
+ * ```
53
+ */
54
+ export type TwistPermissions = Record<string, Record<string, string[]>>;
55
+
56
+ /**
57
+ * Built-in tool for managing twists and deployments.
58
+ *
59
+ * The Twists tool provides twists with the ability to create twist IDs
60
+ * and programmatically deploy twists.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * class TwistBuilderTwist extends Twist {
65
+ * build(build: ToolBuilder) {
66
+ * return {
67
+ * twists: build.get(Twists)
68
+ * }
69
+ * }
70
+ *
71
+ * async activate() {
72
+ * const twistId = await this.tools.twists.create();
73
+ * // Display twist ID to user
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ export abstract class Twists extends ITool {
79
+ /**
80
+ * Creates a new twist ID and grants access to people in the current priority.
81
+ *
82
+ * @returns Promise resolving to the generated twist ID
83
+ * @throws When twist creation fails
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const twistId = await twist.create();
88
+ * console.log(`Your twist ID: ${twistId}`);
89
+ * ```
90
+ */
91
+ abstract create(): Promise<string>;
92
+
93
+ /**
94
+ * Generates twist source code from a specification using AI.
95
+ *
96
+ * This method uses Claude AI to generate TypeScript source code and dependencies
97
+ * from a markdown specification. The generated source is validated by attempting
98
+ * to build it, with iterative error correction (up to 3 attempts).
99
+ *
100
+ * @param spec - Markdown specification describing the twist functionality
101
+ * @returns Promise resolving to twist source (dependencies and files)
102
+ * @throws When generation fails after maximum attempts
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const source = await twist.generate(`
107
+ * # Calendar Sync Twist
108
+ *
109
+ * This twist syncs Google Calendar events to Plot activities.
110
+ *
111
+ * ## Features
112
+ * - Authenticate with Google
113
+ * - Sync calendar events
114
+ * - Create activities from events
115
+ * `);
116
+ *
117
+ * // source.dependencies: { "@plotday/twister": "workspace:^", ... }
118
+ * // source.files: { "index.ts": "export default class..." }
119
+ * ```
120
+ */
121
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
122
+ abstract generate(spec: string): Promise<TwistSource>;
123
+
124
+ /**
125
+ * Deploys a twist programmatically.
126
+ *
127
+ * This method provides the same functionality as the plot deploy CLI
128
+ * command, but can be called from within a twist. Accepts either:
129
+ * - A pre-bundled module (JavaScript code)
130
+ * - A source object (dependencies + files) which is built in a sandbox
131
+ *
132
+ * @param options - Deployment configuration
133
+ * @param options.twistId - Twist ID for deployment
134
+ * @param options.module - Pre-bundled twist module code (mutually exclusive with source)
135
+ * @param options.source - Twist source code with dependencies (mutually exclusive with module)
136
+ * @param options.environment - Target environment (defaults to "personal")
137
+ * @param options.name - Optional twist name (required for first deploy)
138
+ * @param options.description - Optional twist description (required for first deploy)
139
+ * @param options.dryRun - If true, validates without deploying (returns errors if any)
140
+ * @returns Promise resolving to deployment result with version and optional errors
141
+ * @throws When deployment fails or user lacks access
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * // Deploy with a module
146
+ * const result = await twist.deploy({
147
+ * twistId: 'abc-123-...',
148
+ * module: 'export default class MyTwist extends Twist {...}',
149
+ * environment: 'personal',
150
+ * name: 'My Twist',
151
+ * description: 'Does something cool'
152
+ * });
153
+ * console.log(`Deployed version ${result.version}`);
154
+ *
155
+ * // Deploy with source
156
+ * const source = await twist.generate(spec);
157
+ * const result = await twist.deploy({
158
+ * twistId: 'abc-123-...',
159
+ * source,
160
+ * environment: 'personal',
161
+ * name: 'My Twist',
162
+ * });
163
+ *
164
+ * // Validate with dryRun
165
+ * const result = await twist.deploy({
166
+ * twistId: 'abc-123-...',
167
+ * source,
168
+ * dryRun: true,
169
+ * });
170
+ * if (result.errors?.length) {
171
+ * console.error('Build errors:', result.errors);
172
+ * }
173
+ * ```
174
+ */
175
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
176
+ abstract deploy(
177
+ options: {
178
+ twistId: string;
179
+ environment?: "personal" | "private" | "review";
180
+ name?: string;
181
+ description?: string;
182
+ dryRun?: boolean;
183
+ } & (
184
+ | {
185
+ module: string;
186
+ }
187
+ | {
188
+ source: TwistSource;
189
+ }
190
+ )
191
+ ): Promise<{
192
+ version: string;
193
+ permissions: TwistPermissions;
194
+ errors?: string[];
195
+ }>;
196
+
197
+ /**
198
+ * Subscribes to logs from a twist.
199
+ *
200
+ * This method registers a callback to receive batches of logs from twist executions.
201
+ * The callback will be invoked with an array of logs whenever new logs are captured
202
+ * from the twist's console output.
203
+ *
204
+ * @param twistId - Twist ID (root ID) to watch logs for
205
+ * @param callback - Callback token created via CallbackTool that will receive log batches
206
+ * @returns Promise that resolves when the subscription is created
207
+ * @throws When subscription fails
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * // Create twist and callback
212
+ * const twistId = await this.twist.create();
213
+ * const callback = await this.callback.create("onLogs");
214
+ *
215
+ * // Subscribe to logs
216
+ * await this.twist.watchLogs(twistId, callback);
217
+ *
218
+ * // Implement handler
219
+ * async onLogs(logs: Log[]) {
220
+ * for (const log of logs) {
221
+ * console.log(`[${log.environment}] ${log.severity}: ${log.message}`);
222
+ * }
223
+ * }
224
+ * ```
225
+ */
226
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
227
+ abstract watchLogs(twistId: string, callback: Callback): Promise<void>;
228
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Twist Implementation Guide
3
+ *
4
+ * This guide is used by AI systems to generate Plot twists.
5
+ * Auto-generated from cli/templates/AGENTS.template.md during build.
6
+ */
7
+ import twistsGuideTemplate from "./llm-docs/twist-guide-template.js";
8
+
9
+ export const TWIST_GUIDE = twistsGuideTemplate;