@plotday/twister 0.47.0 → 0.48.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/generate.js +5 -5
- package/bin/commands/generate.js.map +1 -1
- package/bin/utils/bundle.js +14 -0
- package/bin/utils/bundle.js.map +1 -1
- package/dist/docs/assets/hierarchy.js +1 -1
- package/dist/docs/assets/search.js +1 -1
- package/dist/docs/classes/index.Connector.html +1 -1
- package/dist/docs/classes/index.Imap.html +1 -1
- package/dist/docs/classes/index.Options.html +1 -1
- package/dist/docs/classes/index.Smtp.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 +1 -1
- package/dist/docs/documents/CLI_Reference.html +6 -4
- package/dist/docs/enums/tools_integrations.AuthProvider.html +3 -1
- package/dist/docs/hierarchy.html +1 -1
- package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
- package/dist/docs/types/tools_integrations.Authorization.html +4 -4
- package/dist/llm-docs/tools/integrations.d.ts +1 -1
- package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
- package/dist/llm-docs/tools/integrations.js +1 -1
- package/dist/llm-docs/tools/integrations.js.map +1 -1
- package/dist/tools/integrations.d.ts +3 -1
- package/dist/tools/integrations.d.ts.map +1 -1
- package/dist/tools/integrations.js +2 -0
- package/dist/tools/integrations.js.map +1 -1
- package/package.json +2 -1
- package/src/connector.ts +366 -0
- package/src/creator-docs.ts +29 -0
- package/src/index.ts +10 -0
- package/src/llm-docs/connector.ts +8 -0
- package/src/llm-docs/index.ts +48 -0
- package/src/llm-docs/options.ts +8 -0
- package/src/llm-docs/plot.ts +8 -0
- package/src/llm-docs/schedule.ts +8 -0
- package/src/llm-docs/tag.ts +8 -0
- package/src/llm-docs/tool.ts +8 -0
- package/src/llm-docs/tools/ai.ts +8 -0
- package/src/llm-docs/tools/callbacks.ts +8 -0
- package/src/llm-docs/tools/imap.ts +8 -0
- package/src/llm-docs/tools/integrations.ts +8 -0
- package/src/llm-docs/tools/network.ts +8 -0
- package/src/llm-docs/tools/plot.ts +8 -0
- package/src/llm-docs/tools/smtp.ts +8 -0
- package/src/llm-docs/tools/store.ts +8 -0
- package/src/llm-docs/tools/tasks.ts +8 -0
- package/src/llm-docs/tools/twists.ts +8 -0
- package/src/llm-docs/twist-guide-template.ts +8 -0
- package/src/llm-docs/twist.ts +8 -0
- package/src/options.ts +115 -0
- package/src/plot.ts +1068 -0
- package/src/schedule.ts +203 -0
- package/src/tag.ts +44 -0
- package/src/tool.ts +377 -0
- package/src/tools/ai.ts +845 -0
- package/src/tools/callbacks.ts +134 -0
- package/src/tools/imap.ts +266 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/integrations.ts +328 -0
- package/src/tools/network.ts +240 -0
- package/src/tools/plot.ts +692 -0
- package/src/tools/smtp.ts +166 -0
- package/src/tools/store.ts +149 -0
- package/src/tools/tasks.ts +137 -0
- package/src/tools/twists.ts +228 -0
- package/src/twist-guide.ts +9 -0
- package/src/twist.ts +435 -0
- package/src/utils/hash.ts +8 -0
- package/src/utils/serializable.ts +54 -0
- package/src/utils/types.ts +130 -0
- 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;
|