@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,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tool
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import {\n type Actor,\n} from \"./plot\";\nimport type { Callback } from \"./tools/callbacks\";\nimport type {\n InferOptions,\n InferTools,\n Serializable,\n ToolBuilder,\n ToolShed,\n} from \"./utils/types\";\n\nexport type { ToolBuilder };\n\n/**\n * Abstrtact parent for both built-in tools and regular Tools.\n * Regular tools extend Tool.\n */\nexport abstract class ITool {}\n\n/**\n * Base class for regular tools.\n *\n * Regular tools run in isolation and can only access other tools declared\n * in their build method. They are ideal for external API integrations\n * and reusable functionality that doesn't require Plot's internal infrastructure.\n *\n * @example\n * ```typescript\n * class GoogleCalendarTool extends Tool<GoogleCalendarTool> {\n * constructor(id: string, options: { clientId: string }) {\n * super(id, options);\n * }\n *\n * build(tools: ToolBuilder) {\n * return {\n * auth: tools.build(Integrations),\n * network: tools.build(Network),\n * };\n * }\n *\n * async getCalendars() {\n * const token = await this.tools.auth.get(...);\n * // Implementation\n * }\n * }\n * ```\n */\nexport abstract class Tool<TSelf> implements ITool {\n constructor(\n protected id: string,\n protected options: InferOptions<TSelf>,\n private toolShed: ToolShed\n ) {}\n\n /**\n * Gets the initialized tools for this tool.\n * @throws Error if called before initialization is complete\n */\n protected get tools() {\n return this.toolShed.getTools<InferTools<TSelf>>();\n }\n\n /**\n * Declares tool dependencies for this tool.\n * Return an object mapping tool names to build() promises.\n * Default implementation returns empty object (no custom tools).\n *\n * @param build - The build function to use for declaring dependencies\n * @returns Object mapping tool names to tool promises\n *\n * @example\n * ```typescript\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, { urls: [\"https://api.example.com/*\"] }),\n * };\n * }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n build(build: ToolBuilder): Record<string, Promise<ITool>> {\n return {};\n }\n\n /**\n * Creates a persistent callback to a method on this tool.\n *\n * ExtraArgs are strongly typed to match the method's signature.\n *\n * @param fn - The method to callback\n * @param extraArgs - Additional arguments to pass (type-checked, must be serializable)\n * @returns Promise resolving to a persistent callback token\n *\n * @example\n * ```typescript\n * const callback = await this.callback(this.onWebhook, \"calendar\", 123);\n * ```\n */\n protected async callback<\n TArgs extends Serializable[],\n Fn extends (...args: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback> {\n return this.tools.callbacks.create(fn, ...extraArgs);\n }\n\n /**\n * Deletes a specific callback by its token.\n *\n * @param token - The callback token to delete\n * @returns Promise that resolves when the callback is deleted\n */\n protected async deleteCallback(token: Callback): Promise<void> {\n return this.tools.callbacks.delete(token);\n }\n\n /**\n * Deletes all callbacks for this tool.\n *\n * @returns Promise that resolves when all callbacks are deleted\n */\n protected async deleteAllCallbacks(): Promise<void> {\n return this.tools.callbacks.deleteAll();\n }\n\n /**\n * Executes a callback by its token inline in the current execution.\n *\n * **Use `this.runTask()` instead for batch continuations and long-running work.**\n * `this.run()` executes inline, sharing the current request count (~1000 limit)\n * and blocking the HTTP response. This causes timeouts when used in lifecycle\n * methods like `onChannelEnabled` or `syncBatch` continuations.\n *\n * `this.run()` is appropriate when you need the callback's **return value** —\n * e.g., running a parent callback token that returns data. For fire-and-forget\n * work, always prefer `this.runTask()`.\n *\n * @param token - The callback token to execute\n * @param args - Optional arguments to pass to the callback\n * @returns Promise resolving to the callback result\n */\n protected async run(token: Callback, ...args: any[]): Promise<any> {\n return this.tools.callbacks.run(token, ...args);\n }\n\n /**\n * Retrieves a value from persistent storage by key.\n *\n * Values are automatically deserialized using SuperJSON, which\n * properly restores Date objects, Maps, Sets, and other complex types.\n *\n * @template T - The expected type of the stored value (must be Serializable)\n * @param key - The storage key to retrieve\n * @returns Promise resolving to the stored value or null\n */\n protected async get<T extends Serializable>(key: string): Promise<T | null> {\n return this.tools.store.get(key);\n }\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\n *\n * **Important**: Functions and Symbols cannot be stored.\n * **For function references**: Use callbacks instead of storing functions directly.\n *\n * @example\n * ```typescript\n * // ✅ Date objects are preserved\n * await this.set(\"sync_state\", {\n * lastSync: new Date(),\n * minDate: new Date(2024, 0, 1)\n * });\n *\n * // ✅ undefined is now supported\n * await this.set(\"data\", { name: \"test\", optional: undefined });\n *\n * // ✅ Arrays with undefined are supported\n * await this.set(\"items\", [1, undefined, 3]);\n * await this.set(\"items\", [1, null, 3]); // Also works\n *\n * // ✅ Maps and Sets are supported\n * await this.set(\"mapping\", new Map([[\"key\", \"value\"]]));\n * await this.set(\"tags\", new Set([\"tag1\", \"tag2\"]));\n *\n * // ❌ WRONG: Cannot store functions directly\n * await this.set(\"handler\", this.myHandler);\n *\n * // ✅ CORRECT: Create a callback token first\n * const token = await this.callback(this.myHandler, \"arg1\", \"arg2\");\n * await this.set(\"handler_token\", token);\n *\n * // Later, execute the callback\n * const token = await this.get<string>(\"handler_token\");\n * await this.run(token, args);\n * ```\n *\n * @template T - The type of value being stored (must be Serializable)\n * @param key - The storage key to use\n * @param value - The value to store (must be SuperJSON-serializable)\n * @returns Promise that resolves when the value is stored\n */\n protected async set<T extends Serializable>(\n key: string,\n value: T\n ): Promise<void> {\n return this.tools.store.set(key, value);\n }\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n protected async list(prefix: string): Promise<string[]> {\n return this.tools.store.list(prefix);\n }\n\n /**\n * Removes a specific key from persistent storage.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n protected async clear(key: string): Promise<void> {\n return this.tools.store.clear(key);\n }\n\n /**\n * Removes all keys from this tool's storage.\n *\n * @returns Promise that resolves when all keys are removed\n */\n protected async clearAll(): Promise<void> {\n return this.tools.store.clearAll();\n }\n\n /**\n * Queues a callback to execute in a separate worker context with a fresh request limit.\n *\n * **Creates a NEW execution** with its own request limit of ~1000 requests (HTTP requests,\n * tool calls, database operations). This is the primary way to stay under request limits\n * when processing large datasets or making many API calls.\n *\n * Use this to break long loops into chunks that each stay under the ~1000 request limit.\n * Each task runs in an isolated execution environment with ~1000 requests and ~60 seconds CPU time.\n *\n * @param callback - The callback token created with `this.callback()`\n * @param options - Optional configuration for the execution\n * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately\n * @returns Promise resolving to a cancellation token (only for scheduled executions)\n *\n * @example\n * ```typescript\n * // Break large loop into batches\n * const callback = await this.callback(\"processBatch\", { page: 1 });\n * await this.runTask(callback); // New execution with fresh request limit\n * ```\n */\n protected async runTask(\n callback: Callback,\n options?: { runAt?: Date }\n ): Promise<string | void> {\n return this.tools.tasks.runTask(callback, options);\n }\n\n /**\n * Cancels a previously scheduled execution.\n *\n * @param token - The cancellation token returned by runTask() with runAt option\n * @returns Promise that resolves when the cancellation is processed\n */\n protected async cancelTask(token: string): Promise<void> {\n return this.tools.tasks.cancelTask(token);\n }\n\n /**\n * Cancels all scheduled executions for this tool.\n *\n * @returns Promise that resolves when all cancellations are processed\n */\n protected async cancelAllTasks(): Promise<void> {\n return this.tools.tasks.cancelAllTasks();\n }\n\n /**\n * Called before the twist's activate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * activate method is called.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when pre-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n preActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's activate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when post-activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n postActivate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's upgrade method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * upgrade method is called.\n *\n * @returns Promise that resolves when pre-upgrade is complete\n */\n preUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's upgrade method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-upgrade is complete\n */\n postUpgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called before the twist's deactivate method, starting from the deepest tool dependencies.\n *\n * This method is called in a depth-first manner, with the deepest dependencies\n * being called first, bubbling up to the top-level tools before the twist's\n * deactivate method is called.\n *\n * @returns Promise that resolves when pre-deactivation is complete\n */\n preDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called after the twist's deactivate method, starting from the top-level tools.\n *\n * This method is called in reverse order, with top-level tools being called\n * first, then cascading down to the deepest dependencies.\n *\n * @returns Promise that resolves when post-deactivation is complete\n */\n postDeactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Waits for tool initialization to complete.\n * Called automatically by the entrypoint before lifecycle methods.\n * @internal\n */\n async waitForReady(): Promise<void> {\n await this.toolShed.waitForReady();\n }\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/ai
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import type { Static, TSchema } from \"typebox\";\n\nimport { ITool } from \"..\";\n\n/**\n * Built-in tool for prompting Large Language Models (LLMs).\n *\n * The AI tool provides twists and tools with access to LLM capabilities\n * for natural language processing, text generation, data extraction,\n * and intelligent decision making within their workflows.\n *\n * **Features:**\n * - Access to multiple AI providers (OpenAI, Anthropic, Google, Workers AI)\n * - Multi-turn conversation support with `messages`\n * - Tool calling with automatic execution\n * - Structured output with Typebox schemas via `outputSchema`\n * - Unified API across all models via Vercel AI SDK\n * - Automatic response parsing and validation with full type inference\n *\n * @example\n * ```typescript\n * import { Type } from \"typebox\";\n *\n * class SmartEmailTool extends Tool {\n * private ai: AI;\n *\n * constructor(id: string, tools: ToolBuilder) {\n * super();\n * this.ai = tools.get(AI);\n * }\n *\n * async categorizeEmail(emailContent: string) {\n * // Define the output schema using Typebox\n * const schema = Type.Object({\n * category: Type.Union([\n * Type.Literal(\"work\"),\n * Type.Literal(\"personal\"),\n * Type.Literal(\"spam\"),\n * Type.Literal(\"promotional\")\n * ]),\n * confidence: Type.Number({ minimum: 0, maximum: 1 }),\n * reasoning: Type.Optional(Type.String())\n * });\n *\n * const response = await this.ai.prompt({\n * model: { speed: \"fast\", cost: \"medium\" },\n * system: \"Classify emails into categories: work, personal, spam, or promotional.\",\n * prompt: `Categorize this email: ${emailContent}`,\n * outputSchema: schema\n * });\n *\n * return response.output;\n * }\n *\n * async generateResponse(emailContent: string) {\n * const response = await this.ai.prompt({\n * model: { speed: \"fast\", cost: \"medium\" },\n * system: \"Generate professional email responses that are helpful and concise.\",\n * prompt: `Write a response to: ${emailContent}`\n * });\n *\n * return response.text;\n * }\n * }\n * ```\n */\nexport abstract class AI extends ITool {\n /**\n * Returns which AI capabilities are currently available.\n * Check this before calling prompt() or embed() to gracefully\n * handle cases where AI is disabled by the user.\n */\n abstract available(): AICapabilities;\n\n /**\n * Sends a request to an AI model and returns the response using the Vercel AI SDK.\n *\n * Supports text generation, multi-turn conversations, structured outputs,\n * and tool calling across multiple AI providers via Cloudflare AI Gateway.\n *\n * @param request - AI request with model, prompt/messages, and optional configuration\n * @returns Promise resolving to the AI response with generated text and metadata\n *\n * @example\n * ```typescript\n * // Simple text generation\n * const response = await ai.prompt({\n * model: { speed: \"fast\", cost: \"medium\" },\n * prompt: \"Explain quantum computing in simple terms\"\n * });\n * console.log(response.text);\n *\n * // Fast and cheap for simple tasks\n * const response = await ai.prompt({\n * model: { speed: \"fast\", cost: \"low\" },\n * prompt: \"Summarize this text...\"\n * });\n * console.log(response.text);\n *\n * // With system instructions for complex reasoning\n * const response = await ai.prompt({\n * model: { speed: \"capable\", cost: \"high\" },\n * system: \"You are a helpful physics tutor.\",\n * prompt: \"Explain quantum entanglement\"\n * });\n * console.log(response.text);\n *\n * // Multi-turn conversation\n * const response = await ai.prompt({\n * model: { speed: \"balanced\", cost: \"medium\" },\n * messages: [\n * { role: \"user\", content: \"What is 2+2?\" },\n * { role: \"assistant\", content: \"2+2 equals 4.\" },\n * { role: \"user\", content: \"What about 3+3?\" }\n * ]\n * });\n * console.log(response.text);\n *\n * // Structured output with Typebox schema\n * const response = await ai.prompt({\n * model: { speed: \"fast\", cost: \"medium\" },\n * prompt: \"Extract information: John is 30 years old\",\n * outputSchema: Type.Object({\n * name: Type.String(),\n * age: Type.Number()\n * })\n * });\n * console.log(response.output); // { name: \"John\", age: 30 }\n *\n * // Tool calling\n * const response = await ai.prompt({\n * model: { speed: \"balanced\", cost: \"medium\" },\n * prompt: \"What's the weather in San Francisco?\",\n * tools: {\n * getWeather: {\n * description: \"Get weather for a city\",\n * parameters: Type.Object({\n * city: Type.String()\n * }),\n * execute: async ({ city }) => {\n * return { temp: 72, condition: \"sunny\" };\n * }\n * }\n * }\n * });\n * console.log(response.text); // Model's response using tool results\n * console.log(response.toolCalls); // Array of tool calls made\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract prompt<TOOLS extends AIToolSet, SCHEMA extends TSchema = never>(\n request: AIRequest<TOOLS, SCHEMA>\n ): Promise<AIResponse<TOOLS, SCHEMA>>;\n}\n\n/** Options for configuring AI tool usage in a twist. */\nexport type AIOptions = {\n /** Whether AI is required for this twist to function. Default: true */\n required?: boolean;\n};\n\n/** Describes which AI capabilities are currently available to this twist. */\nexport type AICapabilities = {\n /** Whether AI prompting (text generation) is available. */\n prompt: boolean;\n /** Whether AI embedding generation is available. */\n embed: boolean;\n};\n\n/**\n * Model preferences for selecting an AI model based on performance and cost requirements.\n * This allows Plot to match those preferences with user preferences (such as preferred or\n * disallowed providers), as well as availability of newer and better models.\n *\n * @example\n * ```typescript\n * // Fast and cheap - uses Workers AI models like Llama 3.2 1B\n * const response = await ai.prompt({\n * model: { speed: \"fast\", cost: \"low\" },\n * prompt: \"Summarize this in one sentence: ...\"\n * });\n *\n * // Balanced performance - uses GPT-5 Mini or Gemini 2.5 Flash\n * const response = await ai.prompt({\n * model: { speed: \"balanced\", cost: \"medium\" },\n * prompt: \"Analyze this data...\"\n * });\n *\n * // Most capable - uses Claude Sonnet 4.5 or Opus 4.1\n * const response = await ai.prompt({\n * model: { speed: \"capable\", cost: \"high\" },\n * prompt: \"Solve this complex reasoning problem...\"\n * });\n *\n * // Request a specific model with a hint\n * const response = await ai.prompt({\n * model: { speed: \"balanced\", cost: \"medium\", hint: AIModel.CLAUDE_SONNET_45 },\n * prompt: \"...\"\n * });\n * ```\n */\nexport type ModelPreferences = {\n /**\n * Desired speed tier:\n * - \"fast\": Optimized for low latency and quick responses\n * - \"balanced\": Good balance of speed and capability\n * - \"capable\": Maximum reasoning and problem-solving ability\n */\n speed: \"fast\" | \"balanced\" | \"capable\";\n\n /**\n * Desired cost tier:\n * - \"low\": Minimal cost, often using Workers AI models (free/very cheap)\n * - \"medium\": Moderate pricing for good performance\n * - \"high\": Premium pricing for best-in-class models\n */\n cost: \"low\" | \"medium\" | \"high\";\n\n /**\n * Optional hint to suggest a specific model. The system will use this\n * model if possible, but may override it based on user preferences.\n */\n hint?: AIModel;\n};\n\n/**\n * Supported AI models available through Cloudflare AI Gateway and Workers AI.\n *\n * Models are organized by provider:\n * - **OpenAI**: Latest GPT models via AI Gateway\n * - **Anthropic**: Claude models via AI Gateway (prefix with \"anthropic/\")\n * - **Google**: Gemini models via AI Gateway (prefix with \"google-ai-studio/\")\n * - **Workers AI**: Models running on Cloudflare's network (free/low cost)\n */\nexport enum AIModel {\n // OpenAI models - Latest GPT and reasoning models\n GPT_5 = \"openai/gpt-5\",\n GPT_5_PRO = \"openai/gpt-5-pro\",\n GPT_5_MINI = \"openai/gpt-5-mini\",\n GPT_5_NANO = \"openai/gpt-5-nano\",\n GPT_4O = \"openai/gpt-4o\",\n GPT_4O_MINI = \"openai/gpt-4o-mini\",\n O3 = \"openai/o3\",\n O3_MINI = \"openai/o3-mini\",\n\n // Anthropic models - Claude 4.x series\n CLAUDE_OPUS_46 = \"anthropic/claude-opus-4-6\",\n CLAUDE_SONNET_46 = \"anthropic/claude-sonnet-4-6\",\n CLAUDE_HAIKU_45 = \"anthropic/claude-haiku-4-5\",\n\n // Google models - Gemini 2.x series\n GEMINI_25_PRO = \"google/gemini-2.5-pro\",\n GEMINI_25_FLASH = \"google/gemini-2.5-flash\",\n GEMINI_25_FLASH_LITE = \"google/gemini-2.5-flash-lite\",\n GEMINI_20_FLASH = \"google/gemini-2.0-flash\",\n GEMINI_20_FLASH_LITE = \"google/gemini-2.0-flash-lite\",\n\n // Cloudflare Workers AI models - Free/low-cost models running on Cloudflare's network\n LLAMA_4_SCOUT_17B = \"meta/llama-4-scout-17b-16e-instruct\",\n LLAMA_33_70B = \"meta/llama-3.3-70b-instruct-fp8-fast\",\n LLAMA_31_8B = \"meta/llama-3.1-8b-instruct-fp8\",\n LLAMA_32_1B = \"meta/llama-3.2-1b-instruct\",\n DEEPSEEK_R1_32B = \"deepseek-ai/deepseek-r1-distill-qwen-32b\",\n}\n\n/**\n * Request parameters for AI text generation, matching Vercel AI SDK's generateText() function.\n */\nexport interface AIRequest<\n TOOLS extends AIToolSet,\n SCHEMA extends TSchema = never\n> {\n /**\n * Model selection preferences based on desired speed and cost characteristics.\n * Plot will automatically select the best available model matching these preferences.\n *\n * @example\n * // Fast and cheap - good for simple tasks\n * model: { speed: \"fast\", cost: \"low\" }\n *\n * @example\n * // Balanced performance - general purpose\n * model: { speed: \"balanced\", cost: \"medium\" }\n *\n * @example\n * // Maximum capability - complex reasoning\n * model: { speed: \"capable\", cost: \"high\" }\n *\n * @example\n * // With a specific model hint\n * model: { speed: \"balanced\", cost: \"medium\", hint: \"anthropic/claude-sonnet-4-6\" }\n */\n model: ModelPreferences;\n\n /**\n * System instructions to guide the model's behavior.\n */\n system?: string;\n\n /**\n * The user's input prompt. Can be a simple string or an array of messages for multi-turn conversations.\n */\n prompt?: string;\n\n /**\n * Conversation messages for multi-turn interactions.\n * Replaces 'prompt' for more complex conversations.\n */\n messages?: AIMessage[];\n\n /**\n * Tools that the model can call during generation.\n * Each tool definition includes a description, input schema, and optional execute function.\n */\n tools?: TOOLS;\n\n /**\n * Controls which tools the model can use.\n * - \"auto\": Model decides whether to use tools\n * - \"none\": Model cannot use tools\n * - \"required\": Model must use at least one tool\n * - { type: \"tool\", toolName: string }: Model must use specific tool\n */\n toolChoice?: ToolChoice<TOOLS>;\n\n /**\n * Structured output schema using Typebox.\n * Typebox schemas are JSON Schema objects that provide full TypeScript type inference.\n */\n outputSchema?: SCHEMA;\n\n /**\n * Maximum number of tokens to generate.\n */\n maxOutputTokens?: number;\n\n /**\n * Temperature for controlling randomness (0-2).\n * Higher values make output more random, lower values more deterministic.\n */\n temperature?: number;\n\n /**\n * Top P sampling parameter (0-1).\n * Controls diversity by limiting to top probability tokens.\n */\n topP?: number;\n}\n\n/**\n * Response from AI text generation, matching Vercel AI SDK's GenerateTextResult.\n */\nexport interface AIResponse<\n TOOLS extends AIToolSet,\n SCHEMA extends TSchema = never\n> {\n /**\n * The generated text.\n */\n text: string;\n\n /**\n * Tool calls made by the model during generation.\n */\n toolCalls?: ToolCallArray<TOOLS>;\n\n /**\n * Results from tool executions.\n */\n toolResults?: ToolResultArray<TOOLS>;\n\n /**\n * Reason why the model stopped generating.\n */\n finishReason:\n | \"stop\"\n | \"length\"\n | \"content-filter\"\n | \"tool-calls\"\n | \"error\"\n | \"other\"\n | \"unknown\";\n\n /**\n * Token usage information for this generation.\n */\n usage: AIUsage;\n\n /**\n * Sources used by the model (if supported).\n */\n sources?: Array<AISource>;\n\n /**\n * Structured output when using outputSchema.\n * Type is automatically inferred from the Typebox schema.\n */\n output?: Static<SCHEMA>;\n\n /**\n * Response metadata including messages.\n */\n response?: {\n id?: string;\n timestamp?: Date;\n modelId?: string;\n messages?: AIMessage[];\n };\n}\n\n// ============================================================================\n// Message Types\n// ============================================================================\n\n/**\n * A system message. It can contain system information.\n *\n * Note: using the \"system\" part of the prompt is strongly preferred\n * to increase the resilience against prompt injection attacks,\n * and because not all providers support several system messages.\n */\nexport type AISystemMessage = {\n role: \"system\";\n content: string;\n};\n\n/**\n * A user message. It can contain text or a combination of text and images.\n */\nexport type AIUserMessage = {\n role: \"user\";\n content: string | Array<TextPart | ImagePart | FilePart>;\n};\n\n/**\n * An assistant message. It can contain text, tool calls, or a combination of text and tool calls.\n */\nexport type AIAssistantMessage = {\n role: \"assistant\";\n content:\n | string\n | Array<\n TextPart | FilePart | ReasoningPart | ToolCallPart | ToolResultPart\n >;\n};\n\n/**\n * A tool message. It contains the result of one or more tool calls.\n */\nexport type AIToolMessage = {\n role: \"tool\";\n content: Array<ToolResultPart>;\n};\n\n/**\n * A message that can be used in the `messages` field of a prompt.\n * It can be a user message, an assistant message, or a tool message.\n */\nexport type AIMessage =\n | AISystemMessage\n | AIUserMessage\n | AIAssistantMessage\n | AIToolMessage;\n\n// ============================================================================\n// Usage & Sources\n// ============================================================================\n\n/**\n * Represents the number of tokens used in a prompt and completion.\n */\nexport type AIUsage = {\n /**\n * The number of tokens used in the prompt.\n */\n inputTokens?: number;\n /**\n * The number of tokens used in the completion.\n */\n outputTokens?: number;\n /**\n * The total number of tokens used (promptTokens + completionTokens).\n */\n totalTokens?: number;\n /**\n * The number of reasoning tokens used in the completion.\n */\n reasoningTokens?: number;\n};\n\n/**\n * A source that has been used as input to generate the response.\n */\nexport type AISource =\n | {\n type: \"source\";\n /**\n * A URL source. This is returned by web search RAG models.\n */\n sourceType: \"url\";\n /**\n * The ID of the source.\n */\n id: string;\n /**\n * The URL of the source.\n */\n url: string;\n /**\n * The title of the source.\n */\n title?: string;\n }\n | {\n type: \"source\";\n /**\n * The type of source - document sources reference files/documents.\n */\n sourceType: \"document\";\n /**\n * The ID of the source.\n */\n id: string;\n /**\n * IANA media type of the document (e.g., 'application/pdf').\n */\n mediaType: string;\n /**\n * The title of the document.\n */\n title: string;\n /**\n * Optional filename of the document.\n */\n filename?: string;\n };\n\n// ============================================================================\n// Content Parts\n// ============================================================================\n\n/**\n * Text content part of a prompt. It contains a string of text.\n */\nexport interface TextPart {\n type: \"text\";\n /**\n * The text content.\n */\n text: string;\n}\n\n/**\n * Data content. Can either be a base64-encoded string, a Uint8Array, or an ArrayBuffer.\n */\nexport type DataContent = string | Uint8Array | ArrayBuffer;\n\n/**\n * Image content part of a prompt. It contains an image.\n */\nexport interface ImagePart {\n type: \"image\";\n /**\n * Image data. Can either be:\n *\n * - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer\n * - URL: a URL that points to the image\n */\n image: DataContent | URL;\n /**\n * Optional mime type of the image.\n */\n mimeType?: string;\n}\n\n/**\n * File content part of a prompt. It contains a file.\n */\nexport interface FilePart {\n type: \"file\";\n /**\n * File data. Can either be:\n *\n * - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer\n * - URL: a URL that points to the file\n */\n data: DataContent | URL;\n /**\n * Optional filename of the file.\n */\n filename?: string;\n /**\n * IANA media type of the file.\n *\n * @see https://www.iana.org/assignments/media-types/media-types.xhtml\n */\n mediaType: string;\n}\n\n/**\n * Reasoning content part of a prompt. It contains a reasoning.\n */\nexport interface ReasoningPart {\n type: \"reasoning\";\n /**\n * The reasoning text.\n */\n text: string;\n /**\n * An optional signature for verifying that the reasoning originated from the model.\n */\n signature?: string;\n}\n\n/**\n * Redacted reasoning content part of a prompt.\n */\nexport interface RedactedReasoningPart {\n type: \"redacted-reasoning\";\n /**\n * Redacted reasoning data.\n */\n data: string;\n}\n\n/**\n * Tool call content part of a prompt. It contains a tool call (usually generated by the AI model).\n */\nexport interface ToolCallPart {\n type: \"tool-call\";\n /**\n * ID of the tool call. This ID is used to match the tool call with the tool result.\n */\n toolCallId: string;\n /**\n * Name of the tool that is being called.\n */\n toolName: string;\n /**\n * Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema.\n */\n input: unknown;\n}\n\ntype JSONValue = null | string | number | boolean | JSONObject | JSONArray;\ntype JSONObject = {\n [key: string]: JSONValue;\n};\ntype JSONArray = JSONValue[];\n\n/**\n * Tool result content part of a prompt. It contains the result of the tool call with the matching ID.\n */\nexport interface ToolResultPart {\n type: \"tool-result\";\n /**\n * ID of the tool call that this result is associated with.\n */\n toolCallId: string;\n /**\n * Name of the tool that generated this result.\n */\n toolName: string;\n /**\n * Result of the tool call. This is a JSON-serializable object.\n */\n output:\n | {\n type: \"text\";\n value: string;\n }\n | {\n type: \"json\";\n value: JSONValue;\n }\n | {\n type: \"error-text\";\n value: string;\n }\n | {\n type: \"error-json\";\n value: JSONValue;\n }\n | {\n type: \"content\";\n value: Array<\n | {\n type: \"text\";\n /**\nText content.\n*/\n text: string;\n }\n | {\n type: \"media\";\n /**\nBase-64 encoded media data.\n*/\n data: string;\n /**\nIANA media type.\n@see https://www.iana.org/assignments/media-types/media-types.xhtml\n*/\n mediaType: string;\n }\n >;\n };\n}\n\n// ============================================================================\n// Tool Types\n// ============================================================================\n\ntype ToolParameters = TSchema;\n\ntype inferParameters<PARAMETERS extends ToolParameters> = Static<PARAMETERS>;\n\n/**\n * Options passed to tool execution functions.\n */\nexport interface ToolExecutionOptions {\n /**\n * The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data.\n */\n toolCallId: string;\n /**\n * Messages that were sent to the language model to initiate the response that contained the tool call.\n * The messages **do not** include the system prompt nor the assistant response that contained the tool call.\n */\n messages: AIMessage[];\n /**\n * An optional abort signal that indicates that the overall operation should be aborted.\n */\n abortSignal?: AbortSignal;\n}\n\n/**\n * A tool contains the description and the schema of the input that the tool expects.\n * This enables the language model to generate the input.\n *\n * The tool can also contain an optional execute function for the actual execution function of the tool.\n */\nexport type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {\n /**\n * The schema of the input that the tool expects. The language model will use this to generate the input.\n * It is also used to validate the output of the language model.\n * Use descriptions to make the input understandable for the language model.\n */\n parameters: PARAMETERS;\n /**\n * The schema of the input that the tool expects. The language model will use this to generate the input.\n * It is also used to validate the output of the language model.\n * Use descriptions to make the input understandable for the language model.\n */\n inputSchema: TSchema;\n /**\n * An optional description of what the tool does.\n * Will be used by the language model to decide whether to use the tool.\n * Not used for provider-defined tools.\n */\n description?: string;\n /**\n * An async function that is called with the arguments from the tool call and produces a result.\n * If not provided, the tool will not be executed automatically.\n *\n * @param args - The input of the tool call\n * @param options - Execution options including abort signal and messages\n */\n execute?: (\n args: inferParameters<PARAMETERS>,\n options: ToolExecutionOptions\n ) => PromiseLike<RESULT>;\n} & (\n | {\n /**\n * Function tool.\n */\n type?: undefined | \"function\";\n }\n | {\n /**\n * Provider-defined tool.\n */\n type: \"provider-defined\";\n /**\n * The ID of the tool. Should follow the format `<provider-name>.<tool-name>`.\n */\n id: `${string}.${string}`;\n /**\n * The arguments for configuring the tool. Must match the expected arguments defined by the provider for this tool.\n */\n args: Record<string, unknown>;\n }\n);\n\n/**\n * Tool choice for the generation. It supports the following settings:\n *\n * - `auto` (default): the model can choose whether and which tools to call.\n * - `required`: the model must call a tool. It can choose which tool to call.\n * - `none`: the model must not call tools\n * - `{ type: 'tool', toolName: string (typed) }`: the model must call the specified tool\n */\ntype ToolChoice<TOOLS extends Record<string, unknown>> =\n | \"auto\"\n | \"none\"\n | \"required\"\n | {\n type: \"tool\";\n toolName: Extract<keyof TOOLS, string>;\n };\n\nexport type AIToolSet = Record<\n string,\n (\n | AITool<never, never>\n | AITool<any, any>\n | AITool<any, never>\n | AITool<never, any>\n ) &\n Pick<AITool<any, any>, \"execute\">\n>;\n\n// ============================================================================\n// Internal Helper Types\n// ============================================================================\n\ntype ToolCallUnion<_TOOLS extends AIToolSet> = {\n type: \"tool-call\";\n toolCallId: string;\n toolName: string;\n args?: unknown;\n};\n\ntype ToolCallArray<TOOLS extends AIToolSet> = Array<ToolCallUnion<TOOLS>>;\n\ntype ToolResultUnion<_TOOLS extends AIToolSet> = {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n args?: unknown;\n result?: unknown;\n};\n\ntype ToolResultArray<TOOLS extends AIToolSet> = Array<ToolResultUnion<TOOLS>>;\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/callbacks
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool } from \"..\";\nimport { Serializable } from \"../utils/types\";\n\n/**\n * Represents a callback token for persistent function references.\n *\n * Callbacks enable tools and twists to create persistent references to functions\n * that can survive worker restarts and be invoked across different execution contexts.\n *\n * This is a branded strin\n * type to prevent mixing callback tokens with regular strings.\n *\n * @example\n * ```typescript\n * const callback = await this.callback(this.onCalendarSelected, \"primary\", \"google\");\n * ```\n */\nexport type Callback = string & { readonly __brand: \"Callback\" };\n\n/**\n * Built-in tool for creating and managing persistent callback references.\n *\n * The Callbacks tool enables twists and tools to create callback links that persist\n * across worker invocations and restarts. This is essential for webhook handlers,\n * scheduled operations, and user interaction flows that need to survive runtime\n * boundaries.\n *\n * **Note:** Callback methods are also available directly on Twist and Tool classes\n * via `this.callback()`, `this.deleteCallback()`, `this.deleteAllCallbacks()`, and\n * `this.run()`. This is the recommended approach for most use cases.\n *\n * **When to use callbacks:**\n * - Webhook handlers that need persistent function references\n * - Scheduled operations that run after worker timeouts\n * - User interaction actions (ActionType.callback)\n * - Cross-tool communication that survives restarts\n *\n * **Type Safety:**\n * Callbacks are fully type-safe - extraArgs are type-checked against the function signature.\n *\n * @example\n * ```typescript\n * class MyTool extends Tool {\n * async setupWebhook() {\n * // Using built-in callback method (recommended)\n * const callback = await this.callback(this.handleWebhook, \"calendar\");\n * return `https://api.plot.day/webhook/${callback}`;\n * }\n *\n * async handleWebhook(data: any, webhookType: string) {\n * console.log(\"Webhook received:\", data, webhookType);\n * }\n * }\n * ```\n */\nexport abstract class Callbacks extends ITool {\n /**\n * Creates a persistent callback to a method on the current class.\n *\n * @param fn - The function to callback\n * @param extraArgs - Additional arguments to pass to the function after any passed by the caller\n * @returns Promise resolving to a persistent callback token\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract create<\n TArgs extends Serializable[],\n F extends (...extraArgs: TArgs) => any\n >(fn: F, ...extraArgs: TArgs): Promise<Callback>;\n // Variation when called provides the first argument\n abstract create<\n TArgs extends Serializable[],\n F extends (arg1: any, ...extraArgs: TArgs) => any\n >(fn: F, ...extraArgs: TArgs): Promise<Callback>;\n\n /**\n * Creates a persistent callback to a function from the parent twist/tool.\n * Use this when the callback function is passed in from outside this class.\n *\n * @param fn - The function to callback\n * @param extraArgs - Additional arguments to pass to the function after any passed by the caller\n * @returns Promise resolving to a callback token\n */\n abstract createFromParent<\n TArgs extends Serializable[],\n F extends (...extraArgs: TArgs) => any\n >(fn: F, ...extraArgs: TArgs): Promise<Callback>;\n // Variation when called provides the first argument\n abstract createFromParent<\n TArgs extends Serializable[],\n F extends (arg1: any, ...extraArgs: TArgs) => any\n >(fn: F, ...extraArgs: TArgs): Promise<Callback>;\n\n // <\n // DeferredArgs extends any[],\n // Fn extends (...args: any[]) => any,\n // CurriedArgs extends Serializable[],\n // AllArgs extends any[] = Parameters<Fn>,\n // R = ReturnType<Fn>,\n // DeferredArgs extends any[] = AllArgs extends [...infer D, ...CurriedArgs]\n // ? D extends any[]\n // ? D\n // : never\n // : never\n // >(\n // fn: (...args: [...DeferredArgs, ...CurriedArgs]) => R,\n // ...curriedArgs: CurriedArgs\n // ): Promise<Callback>;\n\n /**\n * Deletes a specific callback by its token.\n *\n * @param callback - The callback token to delete\n * @returns Promise that resolves when the callback is deleted\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract delete(callback: Callback): Promise<void>;\n\n /**\n * Deletes all callbacks for the tool's parent.\n *\n * @returns Promise that resolves when all callbacks are deleted\n */\n abstract deleteAll(): Promise<void>;\n\n /**\n * Executes a callback by its token.\n *\n * @param callback - The callback token returned by create()\n * @param args - Optional arguments to pass to the callback function\n * @returns Promise resolving to the callback result\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract run<T = unknown>(callback: Callback, ...args: any[]): Promise<T>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/imap
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool } from \"..\";\n\n/** Opaque session handle returned by connect(). */\nexport type ImapSession = string;\n\n/** Credentials and server info for connecting to an IMAP server. */\nexport type ImapConnectOptions = {\n /** IMAP server hostname (e.g. \"imap.mail.me.com\") */\n host: string;\n /** IMAP server port (e.g. 993 for TLS) */\n port: number;\n /** Whether to use TLS (true for port 993) */\n tls: boolean;\n /** IMAP username (typically the email address) */\n username: string;\n /** IMAP password (app-specific password for Apple) */\n password: string;\n};\n\n/** A mailbox returned by listMailboxes(). */\nexport type ImapMailbox = {\n /** Mailbox name (e.g. \"INBOX\", \"Sent Messages\") */\n name: string;\n /** Hierarchy delimiter (e.g. \"/\") */\n delimiter: string;\n /** Mailbox flags (e.g. [\"\\\\HasNoChildren\"]) */\n flags: string[];\n /** Special-use attribute if present (e.g. \"\\\\Sent\", \"\\\\Drafts\", \"\\\\Trash\") */\n specialUse?: string;\n};\n\n/** Status of a selected mailbox. */\nexport type ImapMailboxStatus = {\n /** Mailbox name */\n name: string;\n /** Total number of messages */\n exists: number;\n /** Number of recent messages */\n recent: number;\n /** UID validity value (changes if UIDs are reassigned) */\n uidValidity: number;\n /** Next UID to be assigned */\n uidNext: number;\n /** Number of unseen messages (may be absent) */\n unseen?: number;\n};\n\n/** Criteria for searching messages. All fields are optional; they are ANDed together. */\nexport type ImapSearchCriteria = {\n /** Messages with internal date on or after this date */\n since?: Date | string;\n /** Messages with internal date before this date */\n before?: Date | string;\n /** Messages with From header containing this string */\n from?: string;\n /** Messages with To header containing this string */\n to?: string;\n /** Messages with Subject containing this string */\n subject?: string;\n /** If true, only unseen messages; if false, only seen messages */\n unseen?: boolean;\n /** If true, only flagged messages; if false, only unflagged messages */\n flagged?: boolean;\n /** Specific UIDs to match */\n uid?: number[];\n};\n\n/** An email address with optional display name. */\nexport type ImapAddress = {\n /** Display name (e.g. \"John Doe\") */\n name?: string;\n /** Email address (e.g. \"john@example.com\") */\n address: string;\n};\n\n/** A fetched message. Fields are populated based on ImapFetchOptions. */\nexport type ImapMessage = {\n /** Message UID (stable across sessions if uidValidity hasn't changed) */\n uid: number;\n /** Message flags (e.g. [\"\\\\Seen\", \"\\\\Flagged\"]) */\n flags: string[];\n /** Internal date of the message */\n date?: Date;\n /** Subject header */\n subject?: string;\n /** From addresses */\n from?: ImapAddress[];\n /** To addresses */\n to?: ImapAddress[];\n /** CC addresses */\n cc?: ImapAddress[];\n /** Message-ID header */\n messageId?: string;\n /** In-Reply-To header (for threading) */\n inReplyTo?: string;\n /** References header (for threading) */\n references?: string[];\n /** Plain text body (when requested) */\n bodyText?: string;\n /** HTML body (when requested) */\n bodyHtml?: string;\n /** Message size in bytes */\n size?: number;\n};\n\n/** Options for fetchMessages(). */\nexport type ImapFetchOptions = {\n /** Fetch envelope/headers. Default: true. */\n headers?: boolean;\n /** Fetch body content. Default: false. */\n body?: boolean;\n /** Which body parts to fetch when body is true. Default: \"both\". */\n bodyType?: \"text\" | \"html\" | \"both\";\n};\n\n/** How to modify flags. */\nexport type ImapFlagOperation = \"add\" | \"remove\" | \"set\";\n\n/**\n * Built-in tool for IMAP email access.\n *\n * Provides high-level IMAP operations for reading email and managing flags.\n * Handles TCP/TLS connections, IMAP protocol details, and MIME decoding\n * internally.\n *\n * **Permission model:** Connectors declare which IMAP hosts they need access\n * to. Connections to undeclared hosts are rejected.\n *\n * @example\n * ```typescript\n * class AppleMailConnector extends Connector<AppleMailConnector> {\n * build(build: ConnectorBuilder) {\n * return {\n * options: build(Options, {\n * email: { type: \"text\", label: \"Apple ID Email\", default: \"\" },\n * password: { type: \"text\", label: \"App-Specific Password\", secure: true, default: \"\" },\n * }),\n *\n * imap: build(Imap, { hosts: [\"imap.mail.me.com\"] }),\n * integrations: build(Integrations),\n * };\n * }\n *\n * async syncInbox() {\n * const session = await this.tools.imap.connect({\n * host: \"imap.mail.me.com\",\n * port: 993,\n * tls: true,\n * username: this.tools.options.email,\n * password: this.tools.options.password,\n * });\n *\n * try {\n * await this.tools.imap.selectMailbox(session, \"INBOX\");\n * const uids = await this.tools.imap.search(session, { unseen: true });\n * const messages = await this.tools.imap.fetchMessages(session, uids, {\n * body: true,\n * bodyType: \"html\",\n * });\n *\n * for (const msg of messages) {\n * await this.tools.integrations.saveLink({\n * source: `apple-mail:${msg.messageId}`,\n * title: msg.subject ?? \"(no subject)\",\n * // ...\n * });\n * }\n * } finally {\n * await this.tools.imap.disconnect(session);\n * }\n * }\n * }\n * ```\n */\nexport abstract class Imap extends ITool {\n static readonly Options: {\n /** IMAP server hostnames this tool is allowed to connect to. */\n hosts: string[];\n };\n\n /**\n * Opens a connection to an IMAP server and authenticates.\n *\n * @param options - Server address, port, TLS setting, and credentials\n * @returns An opaque session handle for subsequent operations\n * @throws If the host is not in the declared hosts list, connection fails, or auth fails\n */\n abstract connect(options: ImapConnectOptions): Promise<ImapSession>;\n\n /**\n * Lists all mailboxes (folders) on the server.\n *\n * @param session - Session handle from connect()\n * @returns Array of mailbox descriptors\n */\n abstract listMailboxes(session: ImapSession): Promise<ImapMailbox[]>;\n\n /**\n * Selects a mailbox for subsequent search/fetch/flag operations.\n *\n * @param session - Session handle from connect()\n * @param mailbox - Mailbox name (e.g. \"INBOX\")\n * @returns Mailbox status including message count and UID validity\n */\n abstract selectMailbox(\n session: ImapSession,\n mailbox: string\n ): Promise<ImapMailboxStatus>;\n\n /**\n * Searches for messages matching the given criteria in the selected mailbox.\n *\n * All criteria fields are ANDed together. Returns UIDs (not sequence numbers).\n *\n * @param session - Session handle from connect()\n * @param criteria - Search criteria (all optional, ANDed)\n * @returns Array of matching message UIDs\n */\n abstract search(\n session: ImapSession,\n criteria: ImapSearchCriteria\n ): Promise<number[]>;\n\n /**\n * Fetches message data for the given UIDs.\n *\n * By default fetches headers only. Set `body: true` in options to include\n * message body content. The implementation handles MIME decoding internally.\n *\n * @param session - Session handle from connect()\n * @param uids - Array of message UIDs to fetch\n * @param options - What to fetch (headers, body, body type)\n * @returns Array of message objects with requested fields populated\n */\n abstract fetchMessages(\n session: ImapSession,\n uids: number[],\n options?: ImapFetchOptions\n ): Promise<ImapMessage[]>;\n\n /**\n * Modifies flags on messages.\n *\n * Common flags: \"\\\\Seen\" (read), \"\\\\Flagged\" (starred), \"\\\\Deleted\" (marked for deletion).\n *\n * @param session - Session handle from connect()\n * @param uids - Array of message UIDs to modify\n * @param flags - Flags to add/remove/set (e.g. [\"\\\\Seen\"])\n * @param operation - \"add\", \"remove\", or \"set\" (replace all flags)\n */\n abstract setFlags(\n session: ImapSession,\n uids: number[],\n flags: string[],\n operation: ImapFlagOperation\n ): Promise<void>;\n\n /**\n * Closes the IMAP connection.\n *\n * Always call this when done, preferably in a finally block.\n *\n * @param session - Session handle from connect()\n */\n abstract disconnect(session: ImapSession): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/integrations
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import {\n type Actor,\n type ActorId,\n type NewContact,\n type NewLinkWithNotes,\n ITool,\n Serializable,\n} from \"..\";\nimport { Tag } from \"../tag\";\nimport type { JSONValue } from \"../utils/types\";\nimport type { Uuid } from \"../utils/uuid\";\n\n/**\n * A resource that can be synced (e.g., a calendar, project, channel).\n * Returned by getChannels() and managed by users in the twist setup/edit modal.\n */\nexport type Channel = {\n /** External ID shared across users (e.g., Google calendar ID) */\n id: string;\n /** Display name shown in the UI */\n title: string;\n /** Optional nested channel resources (e.g., subfolders) */\n children?: Channel[];\n /** Per-channel link type configs. Overrides twist-level linkTypes when present. */\n linkTypes?: LinkTypeConfig[];\n};\n\n/**\n * Describes a link type that a connector creates.\n * Used for display in the UI (icons, labels).\n */\nexport type LinkTypeConfig = {\n /** Machine-readable type identifier (e.g., \"issue\", \"pull_request\") */\n type: string;\n /** Human-readable label (e.g., \"Issue\", \"Pull Request\") */\n label: string;\n /** URL to an icon for this link type (light mode). Prefer Iconify `logos/*` URLs. */\n logo?: string;\n /** URL to an icon for dark mode. Use when the default logo is invisible on dark backgrounds (e.g., Iconify `simple-icons/*` with `?color=`). */\n logoDark?: string;\n /** URL to a monochrome icon (uses `currentColor`). Prefer Iconify `simple-icons/*` URLs without a `?color=` param. */\n logoMono?: string;\n /** Possible status values for this type */\n statuses?: Array<{\n /** Machine-readable status (e.g., \"open\", \"done\") */\n status: string;\n /** Human-readable label (e.g., \"Open\", \"Done\") */\n label: string;\n /** Tag to propagate to thread when this status is active (e.g., Tag.Done) */\n tag?: Tag;\n /** Whether this status represents completion (done, closed, merged, cancelled, etc.) */\n done?: boolean;\n /**\n * Whether this status represents the connector's \"to-do\" / active state.\n * When a user adds a thread to Plot's agenda, done-status links flip to\n * the status marked `todo: true` (e.g., Gmail's \"starred\", Linear's\n * \"todo\") so the link widget and thread tags reflect the active state.\n * At most one status per type should set this.\n */\n todo?: boolean;\n /**\n * Default status applied when Plot asks the connector to create a new\n * item of this type via `Connector.onCreateLink`. Declaring at least one\n * status with `createDefault: true` is how a link type opts in to\n * Plot-initiated creation. At most one status per type should set this.\n */\n createDefault?: boolean;\n }>;\n /** Whether this link type supports displaying and changing the assignee */\n supportsAssignee?: boolean;\n /** Default thread creation mode for this link type: 'all' | 'actionable' | 'manual' */\n defaultCreateThreads?: string;\n};\n\n/**\n * Context passed to onChannelEnabled with plan-based sync hints.\n * Connectors can use these hints to limit initial sync scope.\n */\nexport type SyncContext = {\n /**\n * Earliest date to include in initial sync, based on the user's plan.\n *\n * Non-calendar connectors should use this as their date filter (timeMin,\n * created.gte, etc.) during initial sync. Calendar connectors should\n * ignore this for API queries (to avoid missing recurring events) — the\n * API layer filters non-recurring items automatically.\n *\n * Undefined when no limit applies.\n */\n syncHistoryMin?: Date;\n};\n\n/**\n * Built-in tool for managing OAuth authentication and channel resources.\n *\n * The Integrations tool:\n * 1. Manages channel resources (calendars, projects, etc.) per actor\n * 2. Returns tokens for the user who enabled sync on a channel\n * 3. Supports per-actor auth via actAs() for write-back operations\n * 4. Provides saveLink/saveContacts for Connectors to save data directly\n *\n * Connectors declare their provider, scopes, and channel lifecycle methods as\n * class properties and methods. The Integrations tool reads these automatically.\n * Auth and channel management is handled in the twist edit modal in Flutter.\n *\n * @example\n * ```typescript\n * class CalendarConnector extends Connector<CalendarConnector> {\n * readonly provider = AuthProvider.Google;\n * readonly scopes = [\"https://www.googleapis.com/auth/calendar\"];\n *\n * build(build: ToolBuilder) {\n * return {\n * integrations: build(Integrations),\n * };\n * }\n *\n * async getChannels(auth: Authorization, token: AuthToken): Promise<Channel[]> {\n * const calendars = await this.listCalendars(token);\n * return calendars.map(c => ({ id: c.id, title: c.name }));\n * }\n *\n * async onChannelEnabled(channel: Channel) {\n * // Start syncing\n * }\n *\n * async onChannelDisabled(channel: Channel) {\n * // Stop syncing\n * }\n * }\n * ```\n */\nexport abstract class Integrations extends ITool {\n /**\n * Merge scopes from multiple tools, deduplicating.\n *\n * @param scopeArrays - Arrays of scopes to merge\n * @returns Deduplicated array of scopes\n */\n static MergeScopes(...scopeArrays: string[][]): string[] {\n return Array.from(new Set(scopeArrays.flat()));\n }\n\n /**\n * Retrieves an access token for a channel resource.\n *\n * Returns the token of the user who enabled sync on the given channel.\n * If the channel is not enabled or the token is expired/invalid, returns null.\n *\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n */\n abstract get(channelId: string): Promise<AuthToken | null>;\n /**\n * Retrieves an access token for a channel resource.\n *\n * @param provider - The OAuth provider (deprecated, ignored for single-provider connectors)\n * @param channelId - The channel resource ID (e.g., calendar ID)\n * @returns Promise resolving to the access token or null\n * @deprecated Use get(channelId) instead. The provider is implicit from the connector.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get(provider: AuthProvider, channelId: string): Promise<AuthToken | null>;\n\n /**\n * Execute a callback as a specific actor, requesting auth if needed.\n *\n * If the actor has a valid token, calls the callback immediately with it.\n * If the actor has no token, creates a private auth note in the specified\n * activity prompting them to connect. Once they authorize, this callback fires.\n *\n * @param provider - The OAuth provider\n * @param actorId - The actor to act as\n * @param activityId - The activity to create an auth note in (if needed)\n * @param callback - Function to call with the token\n * @param extraArgs - Additional arguments to pass to the callback\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract actAs<\n TArgs extends Serializable[],\n TCallback extends (token: AuthToken, ...args: TArgs) => any\n >(\n provider: AuthProvider,\n actorId: ActorId,\n activityId: Uuid,\n callback: TCallback,\n ...extraArgs: TArgs\n ): Promise<void>;\n\n /**\n * Saves a link with notes to the connector's priority.\n *\n * Creates a thread+link pair. The thread is a lightweight container;\n * the link holds the external entity data (source, meta, type, status, etc.).\n *\n * This method is available only to Connectors (not regular Twists).\n *\n * @param link - The link with notes to save\n * @returns Promise resolving to the saved thread's UUID, or null if the\n * link was filtered out (e.g. older than the plan's sync history limit)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveLink(link: NewLinkWithNotes): Promise<Uuid | null>;\n\n /**\n * Saves contacts to the connector's priority.\n *\n * @param contacts - Array of contacts to save\n * @returns Promise resolving to the saved actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract saveContacts(contacts: NewContact[]): Promise<Actor[]>;\n\n /**\n * Archives links matching the given filter that were created by this connector.\n *\n * For each archived link's thread, if no other non-archived links remain,\n * the thread is also archived.\n *\n * @param filter - Filter criteria for which links to archive\n * @returns Promise that resolves when archiving is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract archiveLinks(filter: ArchiveLinkFilter): Promise<void>;\n\n /**\n * Sets or clears todo status on a thread owned by this connector.\n *\n * @param source - The link source URL identifying the thread\n * @param actorId - The user to set the todo for\n * @param todo - true to mark as todo, false to clear/complete\n * @param options - Additional options\n * @param options.date - The todo date (when todo=true). Defaults to today.\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract setThreadToDo(\n source: string,\n actorId: ActorId,\n todo: boolean,\n options?: { date?: Date | string }\n ): Promise<void>;\n\n}\n\n/**\n * Filter criteria for archiving links.\n * All fields are optional; only provided fields are used for matching.\n */\nexport type ArchiveLinkFilter = {\n /** Filter by channel ID */\n channelId?: string;\n /** Filter by link type (e.g., \"issue\", \"pull_request\") */\n type?: string;\n /** Filter by link status (e.g., \"open\", \"closed\") */\n status?: string;\n /** Filter by metadata fields (uses containment matching) */\n meta?: Record<string, JSONValue>;\n};\n\n/**\n * Enumeration of supported OAuth providers.\n *\n * Each provider has different OAuth endpoints, scopes, and token formats.\n * The Integrations tool handles the provider-specific implementation details.\n */\nexport enum AuthProvider {\n /** Google OAuth provider for Google Workspace services */\n Google = \"google\",\n /** Microsoft OAuth provider for Microsoft 365 services */\n Microsoft = \"microsoft\",\n /** Notion OAuth provider for Notion workspaces */\n Notion = \"notion\",\n /** Slack OAuth provider for Slack workspaces */\n Slack = \"slack\",\n /** Atlassian OAuth provider for Jira and Confluence */\n Atlassian = \"atlassian\",\n /** Linear OAuth provider for Linear workspaces */\n Linear = \"linear\",\n /** Monday.com OAuth provider */\n Monday = \"monday\",\n /** GitHub OAuth provider for GitHub repositories and organizations */\n GitHub = \"github\",\n /** Asana OAuth provider for Asana workspaces */\n Asana = \"asana\",\n /** HubSpot OAuth provider for HubSpot CRM */\n HubSpot = \"hubspot\",\n /** Todoist OAuth provider for Todoist task management */\n Todoist = \"todoist\",\n /** Airtable OAuth provider for Airtable bases */\n Airtable = \"airtable\",\n}\n\n/**\n * Represents a completed authorization from an OAuth flow.\n *\n * Contains the provider, granted scopes, and the actor (contact) that was authorized.\n * Tokens are looked up by (provider, actorId) rather than a random ID.\n */\nexport type Authorization = {\n /** The OAuth provider this authorization is for */\n provider: AuthProvider;\n /** Array of OAuth scopes this authorization covers */\n scopes: string[];\n /** The external account that was authorized (e.g., the Google account) */\n actor: Actor;\n};\n\n/**\n * Represents a stored OAuth authentication token.\n *\n * Contains the actual access token and the scopes it was granted,\n * which may be a subset of the originally requested scopes.\n */\nexport type AuthToken = {\n /** The OAuth access token */\n token: string;\n /** Array of granted OAuth scopes */\n scopes: string[];\n /**\n * Provider-specific metadata as key-value pairs.\n *\n * For Slack (AuthProvider.Slack):\n * - authed_user_id: The authenticated user's Slack ID\n * - bot_user_id: The bot user's Slack ID\n * - team_name: The Slack workspace/team name\n */\n provider?: Record<string, string>;\n};\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/network
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool } from \"..\";\nimport { type JSONValue, Serializable } from \"../utils/types\";\nimport { type AuthProvider, type Authorization } from \"./integrations\";\n\n/**\n * Represents an incoming webhook request.\n *\n * This object is passed to webhook callback functions and contains all\n * the information about the HTTP request that triggered the webhook.\n *\n * @example\n * ```typescript\n * async onWebhookReceived(request: WebhookRequest, context: any) {\n * console.log(`${request.method} request received`);\n * console.log(\"Headers:\", request.headers);\n * console.log(\"Query params:\", request.params);\n * console.log(\"Body:\", request.body);\n * console.log(\"Context:\", context);\n * }\n * ```\n */\nexport type WebhookRequest = {\n /** HTTP method of the request (GET, POST, etc.) */\n method: string;\n /** HTTP headers from the request */\n headers: Record<string, string>;\n /** Query string parameters from the request URL */\n params: Record<string, string>;\n /** Request body (parsed as JSON if applicable) */\n body: JSONValue;\n /** Raw request body (for signature verification) */\n rawBody?: string;\n};\n\n/**\n * Built-in tool for requesting HTTP access permissions and managing webhooks.\n *\n * The Network tool serves two purposes:\n * 1. Declares which URLs a twist or tool is allowed to access via HTTP/HTTPS\n * 2. Provides webhook creation and management for receiving HTTP callbacks\n *\n * **IMPORTANT**: Must be requested in the Twist or Tool Init method to declare\n * HTTP access permissions. Without requesting this tool with the appropriate URLs,\n * all outbound HTTP requests (fetch, etc.) will be blocked.\n *\n * **Permission Patterns:**\n * - `*` - Allow access to all URLs\n * - `https://*.example.com` - Allow access to all subdomains\n * - `https://api.example.com/*` - Allow access to all paths on the domain\n * - `https://api.example.com/v1/*` - Allow access to specific path prefix\n *\n * **Webhook Characteristics:**\n * - Persistent across worker restarts\n * - Automatic callback routing to parent tool/twist\n * - Support for all HTTP methods\n * - Context preservation for callback execution\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist<MyTwist> {\n * build(build: ToolBuilder) {\n * return {\n * // Request HTTP access to specific APIs\n * network: build(Network, {\n * urls: [\n * 'https://api.github.com/*',\n * 'https://api.openai.com/*'\n * ]\n * })\n * };\n * }\n * }\n * ```\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool<CalendarTool> {\n * build(build: ToolBuilder) {\n * return {\n * network: build(Network, {\n * urls: ['https://www.googleapis.com/calendar/*']\n * })\n * };\n * }\n *\n * async setupCalendarWebhook(calendarId: string) {\n * // Create webhook URL that will call onCalendarEvent\n * const webhookUrl = await this.tools.network.createWebhook(\n * {},\n * this.onCalendarEvent,\n * calendarId,\n * \"google\"\n * );\n *\n * // Register webhook with Google Calendar API\n * await this.registerWithGoogleCalendar(calendarId, webhookUrl);\n *\n * return webhookUrl;\n * }\n *\n * async onCalendarEvent(request: WebhookRequest, calendarId: string, provider: string) {\n * console.log(\"Calendar event received:\", {\n * method: request.method,\n * calendarId,\n * provider,\n * body: request.body\n * });\n *\n * // Process the calendar event change\n * await this.processCalendarChange(request.body);\n * }\n *\n * async cleanup(webhookUrl: string) {\n * await this.tools.network.deleteWebhook(webhookUrl);\n * }\n * }\n * ```\n */\nexport abstract class Network extends ITool {\n static readonly Options: {\n /**\n * All network access is blocked except the specified URLs.\n * Wildcards (*) are supported for domains and paths.\n */\n urls: string[];\n };\n\n /**\n * Creates a new webhook endpoint.\n *\n * Generates a unique HTTP endpoint that will invoke the callback function\n * when requests are received. The callback receives the WebhookRequest plus any extraArgs.\n *\n * **Provider-Specific Behavior:**\n * - **Slack**: Uses provider-specific routing via team_id. Requires `authorization` parameter.\n * - **Gmail** (Google with Gmail scopes): Returns a Google Pub/Sub topic name instead of a webhook URL.\n * The topic name (e.g., \"projects/plot-prod/topics/gmail-webhook-abc123\") should be passed\n * to the Gmail API's `users.watch` endpoint. Requires `authorization` parameter with Gmail scopes.\n * - **Pub/Sub** (`pubsub: true`): Returns a Google Pub/Sub topic name instead of a webhook URL.\n * Use this for services that deliver events via Pub/Sub (e.g., Google Workspace Events API).\n * A Pub/Sub topic and push subscription are created automatically; the returned topic name\n * can be passed to any Google API that accepts a Pub/Sub notification endpoint.\n * - **Default**: Returns a standard webhook URL for all other cases.\n *\n * @param options - Webhook creation options\n * @param options.provider - Optional provider for provider-specific webhook routing\n * @param options.authorization - Optional authorization for provider-specific webhooks (required for Slack and Gmail)\n * @param callback - Function receiving (request, ...extraArgs)\n * @param extraArgs - Additional arguments to pass to the callback (type-checked, no functions allowed)\n * @returns Promise resolving to the webhook URL, or for Gmail/Pub/Sub, a Pub/Sub topic name\n *\n * @example\n * ```typescript\n * // Pub/Sub webhook for Workspace Events API\n * const topicName = await this.tools.network.createWebhook(\n * { pubsub: true },\n * this.onEventReceived,\n * channelId\n * );\n * // topicName: \"projects/plot-prod/topics/ps-abc123\"\n *\n * // Pass topic name to Workspace Events API\n * await api.createSubscription(targetResource, topicName, eventTypes);\n * ```\n *\n * @example\n * ```typescript\n * // Gmail webhook - auto-detected from scopes, returns Pub/Sub topic name\n * const topicName = await this.tools.network.createWebhook(\n * {},\n * this.onGmailNotification,\n * \"inbox\"\n * );\n * ```\n */\n abstract createWebhook<\n TArgs extends Serializable[],\n TCallback extends (request: WebhookRequest, ...args: TArgs) => any\n >(\n options: {\n provider?: AuthProvider;\n authorization?: Authorization;\n /** When true, creates a Google Pub/Sub topic instead of a webhook URL. */\n pubsub?: boolean;\n /**\n * Controls whether the returned webhook URL runs callbacks synchronously\n * or asynchronously.\n *\n * **Async (default, `async: true`)** — Plot enqueues each incoming\n * request and immediately returns `200 { queued: true }`. A background\n * queue consumer runs the callback with bounded concurrency. The\n * sender never sees the callback's return value or any error thrown\n * by it, and delivery is at-least-once (the callback must be\n * idempotent). This is the right default for the vast majority of\n * webhooks — service event notifications, bulk-import fan-out, etc. —\n * because it removes ingress-path database pressure and prevents\n * sender-side retry storms when callbacks are slow.\n *\n * **Sync (`async: false`)** — Plot runs the callback inline and\n * responds with the callback's return value. Required when:\n * - The sender reads the response body (e.g. Microsoft Graph\n * subscription validation, which POSTs with a `validationToken` and\n * expects the token echoed as `text/plain`).\n * - The sender uses the HTTP status code to decide whether to retry\n * (e.g. to surface 4xx for permanent failures).\n * - The handler must observe throws before the sender times out.\n *\n * When `async: false`, a callback returning a `string` is sent back\n * with `Content-Type: text/plain`; any other value is serialized as\n * JSON. `undefined` / `void` yields a plain `200 OK` body.\n *\n * Defaults to `true`.\n */\n async?: boolean;\n },\n callback: TCallback,\n ...extraArgs: TArgs\n ): Promise<string>;\n\n /**\n * Deletes an existing webhook endpoint.\n *\n * Removes the webhook endpoint and stops processing requests.\n * Works with all webhook types (standard, Slack, and Gmail).\n *\n * **For Gmail webhooks:** Also deletes the associated Google Pub/Sub topic and subscription.\n *\n * **For Slack webhooks:** Removes the callback registration for the specific team.\n *\n * **For standard webhooks:** Removes the webhook endpoint. Any subsequent requests\n * to the deleted webhook will return 404.\n *\n * @param url - The webhook identifier returned from `createWebhook()`.\n * This can be a URL (standard webhooks), a Pub/Sub topic name (Gmail),\n * or an opaque identifier (Slack). Always pass the exact value returned\n * from `createWebhook()`.\n * @returns Promise that resolves when the webhook is deleted\n */\n abstract deleteWebhook(url: string): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/plot
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import {\n type Action,\n type Thread,\n type ThreadUpdate,\n type Actor,\n type ActorId,\n ITool,\n type Link,\n type LinkUpdate,\n type NewThread,\n type NewThreadWithNotes,\n type NewNote,\n type NewPriority,\n type Note,\n type NoteUpdate,\n type PlanOperation,\n type Priority,\n type PriorityUpdate,\n Uuid,\n} from \"..\";\nimport {\n type Schedule,\n type NewSchedule,\n} from \"../schedule\";\nimport type { Callback } from \"./callbacks\";\n\nexport enum ThreadAccess {\n /**\n * Create new Note on a Thread where the twist was mentioned.\n * Add/remove tags on Thread or Note where the twist was mentioned.\n */\n Respond,\n /**\n * Create new Thread.\n * Create new Note in a Thread the twist created.\n * All Respond permissions.\n */\n Create,\n /**\n * List/query all Threads owned by the twist's user.\n * Update any Thread (title, tags, archived, type, priority) regardless of creator.\n * Create Notes on any Thread (not just own or mentioned).\n * All Create permissions.\n */\n Full,\n}\n\nexport enum PriorityAccess {\n /**\n * Create new Priorities under the twist owner's priority tree.\n * Update Priorities created by the twist.\n */\n Create,\n /**\n * Read all Priorities owned by the twist's user.\n * Create new Priorities under the twist owner's priority tree.\n * Update and archive any Priority owned by the twist's user.\n */\n Full,\n}\n\nexport enum ContactAccess {\n /** Read existing contact details. Without this, only the ID will be provided. */\n Read,\n}\n\nexport enum LinkAccess {\n /** Read links on any thread owned by the twist's user. */\n Read,\n /** Read + update links, including moving links between threads owned by the twist's user. */\n Full,\n}\n\n/**\n * Intent handler for thread mentions.\n * Defines how the twist should respond when mentioned in a thread.\n */\nexport type NoteIntentHandler = {\n /** Human-readable description of what this intent handles */\n description: string;\n /** Example phrases or activity content that would match this intent */\n examples: string[];\n /** The function to call when this intent is matched */\n handler: (note: Note) => Promise<void>;\n};\n\n/**\n * Filter for querying links from connected source channels.\n */\nexport type LinkFilter = {\n /** Only return links from these channel IDs. */\n channelIds?: string[];\n /** Only return links created/updated after this date. */\n since?: Date;\n /** Only return links of this type. */\n type?: string;\n /** Maximum number of links to return. */\n limit?: number;\n};\n\ntype SearchResultBase = {\n thread: { id: string; title: string | null };\n priority: { id: string; title: string | null };\n similarity: number;\n};\n\nexport type NoteSearchResult = SearchResultBase & {\n type: 'note';\n id: string;\n content: string | null;\n};\n\nexport type LinkSearchResult = SearchResultBase & {\n type: 'link';\n id: string;\n title: string | null;\n sourceUrl: string | null;\n content: string | null;\n};\n\nexport type SearchResult = NoteSearchResult | LinkSearchResult;\n\n/** Default number of search results returned */\nexport const SEARCH_DEFAULT_LIMIT = 10;\n/** Maximum number of search results allowed */\nexport const SEARCH_MAX_LIMIT = 30;\n\nexport type SearchOptions = {\n /** Max results to return (default: 10, max: 30) */\n limit?: number;\n /** Minimum similarity score 0-1 (default: 0.3) */\n threshold?: number;\n /**\n * Scope search to this priority + descendants. Must be owned by the twist\n * owner. When omitted, the server scopes the search to the owner's entire\n * priority tree.\n */\n priorityId?: string;\n};\n\n/**\n * Built-in tool for interacting with the core Plot data layer.\n *\n * The Plot tool provides twists with the ability to create and manage threads,\n * priorities, and contacts within the Plot system. This is the primary interface\n * for twists to persist data and interact with the Plot database.\n *\n * @example\n * ```typescript\n * class MyTwist extends Twist {\n * private plot: Plot;\n *\n * constructor(id: string, tools: ToolBuilder) {\n * super();\n * this.plot = tools.get(Plot);\n * }\n *\n * async activate() {\n * // Create a welcome thread\n * await this.plot.createThread({\n * title: \"Welcome to Plot!\",\n * actions: [{\n * title: \"Get Started\",\n * type: ActionType.external,\n * url: \"https://plot.day/docs\"\n * }]\n * });\n * }\n * }\n * ```\n */\nexport abstract class Plot extends ITool {\n /**\n * Configuration options for the Plot tool.\n *\n * **Important**: All permissions must be explicitly requested. There are no default permissions.\n *\n * @example\n * ```typescript\n * // Minimal configuration with required permissions\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * thread: {\n * access: ThreadAccess.Create\n * }\n * })\n * };\n * }\n *\n * // Full configuration with callbacks\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot, {\n * thread: {\n * access: ThreadAccess.Create,\n * },\n * note: {\n * intents: [{\n * description: \"Schedule meetings\",\n * examples: [\"Schedule a meeting tomorrow\"],\n * handler: this.onSchedulingIntent\n * }],\n * },\n * link: true,\n * priority: {\n * access: PriorityAccess.Full\n * },\n * contact: {\n * access: ContactAccess.Read\n * }\n * })\n * };\n * }\n * ```\n */\n static readonly Options: {\n thread?: {\n /**\n * Capability to create Notes and modify tags.\n * Must be explicitly set to grant permissions.\n */\n access?: ThreadAccess;\n /** When true, auto-mention this twist on new notes in threads where it authored content. */\n defaultMention?: boolean;\n };\n note?: {\n /** When true, auto-mention this twist on new notes in threads where it was @-mentioned. */\n defaultMention?: boolean;\n /**\n * Respond to mentions in notes.\n *\n * When a note mentions this twist, the system will match the note\n * content against these intents and call the matching handler.\n *\n * @example\n * ```typescript\n * intents: [{\n * description: \"Schedule or reschedule calendar events\",\n * examples: [\"Schedule a meeting tomorrow at 2pm\", \"Move my 3pm meeting to 4pm\"],\n * handler: this.onSchedulingRequest\n * }, {\n * description: \"Find available meeting times\",\n * examples: [\"When am I free this week?\", \"Find time for a 1 hour meeting\"],\n * handler: this.onAvailabilityRequest\n * }]\n * ```\n */\n intents?: NoteIntentHandler[];\n };\n /** Enable link processing from connected source channels. */\n link?: true | {\n /** Access level for links. When omitted with `link: true`, only source channel links are accessible. */\n access?: LinkAccess;\n };\n priority?: {\n access?: PriorityAccess;\n };\n contact?: {\n access?: ContactAccess;\n };\n /** Enable semantic search across notes and links owned by the twist's user. */\n search?: true;\n /**\n * When true, admin write operations (on threads/notes/links/priorities not created by this twist)\n * require user approval via plan actions instead of executing immediately.\n * Read operations and operations on the twist's own content still work directly.\n */\n requireApproval?: boolean;\n };\n\n /**\n * Creates a new thread in the Plot system.\n *\n * The thread will be automatically assigned an ID and author information\n * based on the current execution context. All other fields from NewThread\n * will be preserved in the created thread.\n *\n * @param thread - The thread data to create\n * @returns Promise resolving to the created thread's ID\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createThread(\n thread: NewThread | NewThreadWithNotes\n ): Promise<Uuid>;\n\n /**\n * Creates multiple threads in a single batch operation.\n *\n * This method efficiently creates multiple threads at once, which is\n * more performant than calling createThread() multiple times individually.\n * All threads are created with the same author and access control rules.\n *\n * @param threads - Array of thread data to create\n * @returns Promise resolving to array of created thread IDs\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createThreads(\n threads: (NewThread | NewThreadWithNotes)[]\n ): Promise<Uuid[]>;\n\n /**\n * Updates an existing thread in the Plot system.\n *\n * **Important**: This method only updates existing threads. It will throw an error\n * if the thread does not exist. Use `createThread()` to create or update (upsert)\n * threads.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire thread object.\n *\n * For tags, provide a Record<number, boolean> where true adds a tag and false removes it.\n * Tags not included in the update remain unchanged.\n *\n * When updating the parent, the thread's path will be automatically recalculated to\n * maintain the correct hierarchical structure.\n *\n * Scheduling is handled separately via `createSchedule()` / `updateSchedule()`.\n *\n * @param thread - The thread update containing the ID or source and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the thread does not exist\n *\n * @example\n * ```typescript\n * // Mark a task as complete\n * await this.plot.updateThread({\n * id: \"task-123\",\n * done: new Date()\n * });\n *\n * // Add and remove tags\n * await this.plot.updateThread({\n * id: \"thread-789\",\n * tags: {\n * 1: true, // Add tag with ID 1\n * 2: false // Remove tag with ID 2\n * }\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateThread(thread: ThreadUpdate): Promise<void>;\n\n /**\n * Retrieves all notes within a thread.\n *\n * Notes are detailed entries within a thread, ordered by creation time.\n * Each note can contain markdown content, actions, and other detailed information\n * related to the parent thread.\n *\n * @param thread - The thread whose notes to retrieve\n * @returns Promise resolving to array of notes in the thread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNotes(thread: Thread): Promise<Note[]>;\n\n /**\n * Creates a new note in a thread.\n *\n * Notes provide detailed content within a thread, supporting markdown,\n * actions, and other rich content. The note will be automatically assigned\n * an ID and author information based on the current execution context.\n *\n * @param note - The note data to create\n * @returns Promise resolving to the created note's ID\n *\n * @example\n * ```typescript\n * // Create a note with content\n * await this.plot.createNote({\n * thread: { id: \"thread-123\" },\n * note: \"Discussion notes from the meeting...\",\n * contentType: \"markdown\"\n * });\n *\n * // Create a note with actions\n * await this.plot.createNote({\n * thread: { id: \"thread-456\" },\n * note: \"Meeting recording available\",\n * actions: [{\n * type: ActionType.external,\n * title: \"View Recording\",\n * url: \"https://example.com/recording\"\n * }]\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNote(note: NewNote): Promise<Uuid>;\n\n /**\n * Creates multiple notes in a single batch operation.\n *\n * This method efficiently creates multiple notes at once, which is\n * more performant than calling createNote() multiple times individually.\n * All notes are created with the same author and access control rules.\n *\n * @param notes - Array of note data to create\n * @returns Promise resolving to array of created note IDs\n *\n * @example\n * ```typescript\n * // Create multiple notes in one batch\n * await this.plot.createNotes([\n * {\n * thread: { id: \"thread-123\" },\n * note: \"First message in thread\"\n * },\n * {\n * thread: { id: \"thread-123\" },\n * note: \"Second message in thread\"\n * }\n * ]);\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createNotes(notes: NewNote[]): Promise<Uuid[]>;\n\n /**\n * Updates an existing note in the Plot system.\n *\n * **Important**: This method only updates existing notes. It will throw an error\n * if the note does not exist. Use `createNote()` to create or update (upsert) notes.\n *\n * Only the fields provided in the update object will be modified - all other fields\n * remain unchanged. This enables partial updates without needing to fetch and resend\n * the entire note object.\n *\n * @param note - The note update containing the ID or key and fields to change\n * @returns Promise that resolves when the update is complete\n * @throws Error if the note does not exist\n *\n * @example\n * ```typescript\n * // Update note content\n * await this.plot.updateNote({\n * id: \"note-123\",\n * note: \"Updated content with more details\"\n * });\n *\n * // Add tags to a note\n * await this.plot.updateNote({\n * id: \"note-456\",\n * twistTags: {\n * [Tag.Important]: true\n * }\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateNote(note: NoteUpdate): Promise<void>;\n\n /**\n * Retrieves a thread by ID or source.\n *\n * This method enables lookup of threads either by their unique ID or by their\n * source identifier (canonical URL from an external system). Archived threads\n * are included in the results.\n *\n * @param thread - Thread lookup by ID or source\n * @returns Promise resolving to the matching thread or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getThread(\n thread: { id: Uuid } | { source: string }\n ): Promise<Thread | null>;\n\n /**\n * Retrieves a note by ID or key.\n *\n * This method enables lookup of notes either by their unique ID or by their\n * key (unique identifier within the thread). Archived notes are included\n * in the results.\n *\n * @param note - Note lookup by ID or key\n * @returns Promise resolving to the matching note or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getNote(note: { id: Uuid } | { key: string }): Promise<Note | null>;\n\n /**\n * Creates a new priority in the Plot system.\n *\n * Priorities serve as organizational containers for threads and twists.\n * The created priority will be automatically assigned a unique ID.\n *\n * @param priority - The priority data to create\n * @returns Promise resolving to the complete created priority\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createPriority(priority: NewPriority): Promise<Priority & { created: boolean }>;\n\n /**\n * Retrieves a priority by ID or key.\n *\n * Archived priorities are included in the results.\n *\n * @param priority - Priority lookup by ID or key\n * @returns Promise resolving to the matching priority or null if not found\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getPriority(\n priority: { id: Uuid } | { key: string }\n ): Promise<Priority | null>;\n\n /**\n * Updates an existing priority in the Plot system.\n *\n * The priority is identified by either its ID or key.\n * Only the fields specified in the update will be changed.\n *\n * @param update - Priority update containing ID/key and fields to change\n * @returns Promise that resolves when the update is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updatePriority(update: PriorityUpdate): Promise<void>;\n\n /**\n * Retrieves actors by their IDs.\n *\n * Actors represent users, contacts, or twists in the Plot system.\n * This method requires ContactAccess.Read permission.\n *\n * @param ids - Array of actor IDs to retrieve\n * @returns Promise resolving to array of actors\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getActors(ids: ActorId[]): Promise<Actor[]>;\n\n /**\n * Returns the full Actor for the user who installed this twist.\n * Useful for per-user operations like schedule creation, or when\n * the owner's name or email is needed.\n */\n abstract getOwner(): Promise<Actor>;\n\n /**\n * Returns the user ID (`twist_instance.owner_id`) that installed this\n * twist. This is the same value exposed on Twist via `this.userId`.\n */\n abstract getUserId(): Promise<string>;\n\n /**\n * Returns the owner user's root priority ID. Used as the implicit default\n * when an operation needs a priority but the caller didn't supply one —\n * for example, `plot.createPriority()` without a parent, or\n * `plot.getThreads()` without an explicit `priorityId`.\n *\n * On the server, priority resolution for newly created threads/links\n * happens automatically via `match_priority_for_user`; twists rarely need\n * to call this directly.\n */\n abstract getDefaultPriorityId(): Promise<string>;\n\n /**\n * Creates a new schedule for a thread.\n *\n * Schedules define when a thread occurs in time. A thread can have\n * multiple schedules (shared and per-user).\n *\n * @param schedule - The schedule data to create\n * @returns Promise resolving to the created schedule\n *\n * @example\n * ```typescript\n * // Schedule a timed event\n * const threadId = await this.plot.createThread({\n * title: \"Team standup\"\n * });\n * await this.plot.createSchedule({\n * threadId,\n * start: new Date(\"2025-01-15T10:00:00Z\"),\n * end: new Date(\"2025-01-15T10:30:00Z\"),\n * recurrenceRule: \"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR\"\n * });\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createSchedule(schedule: NewSchedule): Promise<Schedule>;\n\n /**\n * Retrieves all schedules for a thread.\n *\n * @param threadId - The thread whose schedules to retrieve\n * @returns Promise resolving to array of schedules for the thread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getSchedules(threadId: Uuid): Promise<Schedule[]>;\n\n /**\n * Retrieves links from connected source channels.\n *\n * Requires `link: true` in Plot options.\n *\n * @param filter - Optional filter criteria for links\n * @returns Promise resolving to array of links with their notes\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getLinks(filter?: LinkFilter): Promise<Array<{ link: Link; notes: Note[] }>>;\n\n /**\n * Searches notes and links using semantic similarity.\n *\n * Requires `search: true` in Plot options.\n *\n * @param query - The search query text\n * @param options - Optional search configuration\n * @returns Promise resolving to array of search results ordered by similarity\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract search(query: string, options?: SearchOptions): Promise<SearchResult[]>;\n\n /**\n * Lists threads owned by the twist's user.\n *\n * Requires `ThreadAccess.Full`.\n *\n * @param options - Query options for filtering threads\n * @returns Promise resolving to array of threads\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getThreads(options?: {\n /**\n * Priority to list threads from. Must be owned by the twist owner.\n * When omitted, defaults to the owner's root priority.\n */\n priorityId?: Uuid;\n /** Include threads from descendant priorities. Default: true. */\n includeDescendants?: boolean;\n /** Include archived threads. Default: false. */\n includeArchived?: boolean;\n /** Maximum number of threads to return. Default: 50, max: 200. */\n limit?: number;\n /** Number of threads to skip for pagination. Default: 0. */\n offset?: number;\n }): Promise<Thread[]>;\n\n /**\n * Lists priorities owned by the twist's user.\n *\n * Requires `PriorityAccess.Full`.\n *\n * @param options - Query options for filtering priorities\n * @returns Promise resolving to array of priorities\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract getPriorities(options?: {\n /**\n * Parent priority to list children of. Must be owned by the twist\n * owner. When omitted, defaults to the owner's root priority.\n */\n parentId?: Uuid;\n /** Include all descendants, not just direct children. Default: false. */\n includeDescendants?: boolean;\n /** Include archived priorities. Default: false. */\n includeArchived?: boolean;\n }): Promise<Priority[]>;\n\n /**\n * Updates a link.\n *\n * Requires `LinkAccess.Full`. Set `threadId` to move the link to a different thread.\n *\n * @param link - The link update containing the ID and fields to change\n * @returns Promise that resolves when the update is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract updateLink(link: LinkUpdate): Promise<void>;\n\n /**\n * Creates a plan of operations for user approval.\n *\n * Returns an Action that can be attached to a note. The user can approve,\n * deny, or request changes. On approval, operations are executed by the API.\n *\n * Requires `requireApproval: true` in Plot options.\n *\n * @param options - Plan configuration\n * @returns An Action of type `plan` to attach to a note\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract createPlan(options: {\n /** Human-readable title summarizing the plan */\n title: string;\n /** Array of operations to execute on approval */\n operations: PlanOperation[];\n /** Callback invoked with (action, approved: boolean) when the user responds */\n callback: Callback;\n }): Action;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/smtp
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool } from \"..\";\n\n/** Opaque session handle returned by connect(). */\nexport type SmtpSession = string;\n\n/** Credentials and server info for connecting to an SMTP server. */\nexport type SmtpConnectOptions = {\n /** SMTP server hostname (e.g. \"smtp.mail.me.com\") */\n host: string;\n /** SMTP server port (e.g. 465 for TLS, 587 for STARTTLS) */\n port: number;\n /** Whether to use implicit TLS (true for port 465) */\n tls: boolean;\n /** Whether to upgrade to TLS via STARTTLS (true for port 587) */\n starttls: boolean;\n /** SMTP username (typically the email address) */\n username: string;\n /** SMTP password (app-specific password for Apple) */\n password: string;\n};\n\n/** An email address with optional display name. */\nexport type SmtpAddress = {\n /** Display name (e.g. \"John Doe\") */\n name?: string;\n /** Email address (e.g. \"john@example.com\") */\n address: string;\n};\n\n/** An email message to send. */\nexport type SmtpMessage = {\n /** Sender address */\n from: SmtpAddress;\n /** Primary recipients */\n to: SmtpAddress[];\n /** Carbon copy recipients */\n cc?: SmtpAddress[];\n /** Blind carbon copy recipients (not visible in headers) */\n bcc?: SmtpAddress[];\n /** Reply-To address (if different from From) */\n replyTo?: SmtpAddress;\n /** Message-ID of the message being replied to (for threading) */\n inReplyTo?: string;\n /** Message-ID chain for threading */\n references?: string[];\n /** Email subject line */\n subject: string;\n /** Plain text body */\n text?: string;\n /** HTML body */\n html?: string;\n /** Custom Message-ID; auto-generated as <uuid@plot.day> if omitted */\n messageId?: string;\n};\n\n/** Result of sending an email. */\nexport type SmtpSendResult = {\n /** The Message-ID that was used (auto-generated or from SmtpMessage) */\n messageId: string;\n /** Email addresses that were accepted by the server */\n accepted: string[];\n /** Email addresses that were rejected by the server */\n rejected: string[];\n};\n\n/**\n * Built-in tool for SMTP email sending.\n *\n * Provides high-level SMTP operations for composing and sending email.\n * Handles TCP/TLS connections, STARTTLS upgrades, SMTP protocol details,\n * and RFC 2822 message formatting internally.\n *\n * **Permission model:** Connectors declare which SMTP hosts they need access\n * to. Connections to undeclared hosts are rejected.\n *\n * @example\n * ```typescript\n * class AppleMailConnector extends Connector<AppleMailConnector> {\n * build(build: ConnectorBuilder) {\n * return {\n * options: build(Options, {\n * email: { type: \"text\", label: \"Apple ID Email\", default: \"\" },\n * password: { type: \"text\", label: \"App-Specific Password\", secure: true, default: \"\" },\n * }),\n *\n * imap: build(Imap, { hosts: [\"imap.mail.me.com\"] }),\n * smtp: build(Smtp, { hosts: [\"smtp.mail.me.com\"] }),\n * integrations: build(Integrations),\n * };\n * }\n *\n * async sendReply(originalMessage: ImapMessage, replyBody: string) {\n * const session = await this.tools.smtp.connect({\n * host: \"smtp.mail.me.com\",\n * port: 587,\n * tls: false,\n * starttls: true,\n * username: this.tools.options.email,\n * password: this.tools.options.password,\n * });\n *\n * try {\n * const result = await this.tools.smtp.send(session, {\n * from: { address: this.tools.options.email },\n * to: originalMessage.from ?? [],\n * subject: `Re: ${originalMessage.subject ?? \"(no subject)\"}`,\n * text: replyBody,\n * inReplyTo: originalMessage.messageId,\n * references: [\n * ...(originalMessage.references ?? []),\n * ...(originalMessage.messageId ? [originalMessage.messageId] : []),\n * ],\n * });\n *\n * console.log(`Sent reply, Message-ID: ${result.messageId}`);\n * } finally {\n * await this.tools.smtp.disconnect(session);\n * }\n * }\n * }\n * ```\n */\nexport abstract class Smtp extends ITool {\n static readonly Options: {\n /** SMTP server hostnames this tool is allowed to connect to. */\n hosts: string[];\n };\n\n /**\n * Opens a connection to an SMTP server and authenticates.\n *\n * Handles the full SMTP handshake: greeting, EHLO, optional STARTTLS\n * upgrade, and AUTH LOGIN authentication.\n *\n * @param options - Server address, port, TLS/STARTTLS setting, and credentials\n * @returns An opaque session handle for subsequent operations\n * @throws If the host is not in the declared hosts list, connection fails, or auth fails\n */\n abstract connect(options: SmtpConnectOptions): Promise<SmtpSession>;\n\n /**\n * Sends an email message.\n *\n * Constructs a properly formatted RFC 2822 message with MIME support\n * and sends it via the SMTP protocol. Handles multipart messages when\n * both text and HTML bodies are provided.\n *\n * @param session - Session handle from connect()\n * @param message - The email message to send\n * @returns Send result with Message-ID and per-recipient acceptance status\n * @throws If the session is invalid or the server rejects the message entirely\n */\n abstract send(\n session: SmtpSession,\n message: SmtpMessage\n ): Promise<SmtpSendResult>;\n\n /**\n * Closes the SMTP connection.\n *\n * Always call this when done, preferably in a finally block.\n *\n * @param session - Session handle from connect()\n */\n abstract disconnect(session: SmtpSession): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/store
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool, type Serializable } from \"..\";\n\n/**\n * Built-in tool for persistent key-value storage.\n *\n * The Store tool provides twists and tools with a simple, persistent storage\n * mechanism that survives worker restarts and invocations. Each twist/tool\n * instance gets its own isolated storage namespace.\n *\n * **Note:** Store methods are also available directly on Twist and Tool classes\n * via `this.get()`, `this.set()`, `this.clear()`, and `this.clearAll()`.\n * This is the recommended approach for most use cases.\n *\n * **Storage Characteristics:**\n * - Persistent across worker restarts\n * - Isolated per twist/tool instance\n * - Supports SuperJSON-serializable data (see below)\n * - Async operations for scalability\n *\n * **Supported Data Types (via SuperJSON):**\n * - Primitives: string, number, boolean, null, undefined\n * - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt\n * - Collections: Arrays and objects (recursively)\n *\n * **NOT Supported (will throw validation errors):**\n * - Functions (use callback tokens instead - see Callbacks tool)\n * - Symbols\n * - Circular references\n * - Custom class instances\n *\n * **Use Cases:**\n * - Storing authentication tokens\n * - Caching configuration data\n * - Maintaining sync state\n * - Persisting user preferences\n * - Tracking processing checkpoints\n *\n * @example\n * ```typescript\n * class CalendarTool extends Tool {\n * async saveAuthToken(provider: string, token: string) {\n * // Using built-in set method (recommended)\n * await this.set(`auth_token_${provider}`, token);\n * }\n *\n * async getAuthToken(provider: string): Promise<string | null> {\n * // Using built-in get method (recommended)\n * return await this.get<string>(`auth_token_${provider}`);\n * }\n *\n * async clearAllTokens() {\n * // Using built-in clearAll method (recommended)\n * await this.clearAll();\n * }\n * }\n * ```\n */\nexport abstract class Store extends ITool {\n /**\n * Retrieves a value from storage by key.\n *\n * Returns the stored value deserialized to the specified type,\n * or null if the key doesn't exist or the value is null.\n *\n * Values are automatically deserialized using SuperJSON, which\n * properly restores Date objects, Maps, Sets, and other complex types.\n *\n * @template T - The expected type of the stored value (must be Serializable)\n * @param key - The storage key to retrieve\n * @returns Promise resolving to the stored value or null\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract get<T extends Serializable>(key: string): Promise<T | null>;\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * Any existing value at the same key will be overwritten.\n *\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\n *\n * @template T - The type of value being stored (must be Serializable)\n * @param key - The storage key to use\n * @param value - The value to store (must be SuperJSON-serializable)\n * @returns Promise that resolves when the value is stored\n *\n * @example\n * ```typescript\n * // Date objects are preserved\n * await this.set('sync_state', {\n * lastSync: new Date(),\n * minDate: new Date(2024, 0, 1)\n * });\n *\n * // undefined is now supported\n * await this.set('data', { name: 'test', optional: undefined }); // ✅ Works\n *\n * // Arrays with undefined are supported\n * await this.set('items', [1, undefined, 3]); // ✅ Works\n * await this.set('items', [1, null, 3]); // ✅ Also works\n *\n * // Maps and Sets are supported\n * await this.set('mapping', new Map([['key', 'value']])); // ✅ Works\n * await this.set('tags', new Set(['tag1', 'tag2'])); // ✅ Works\n *\n * // Functions are NOT supported - use callback tokens instead\n * const token = await this.callback(this.myFunction);\n * await this.set('callback_ref', token); // ✅ Use callback token\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract set<T extends Serializable>(key: string, value: T): Promise<void>;\n\n /**\n * Lists all storage keys matching a prefix.\n *\n * Returns an array of key strings that start with the given prefix.\n * Useful for finding all keys in a namespace (e.g., all sync locks).\n *\n * @param prefix - The prefix to match keys against\n * @returns Promise resolving to an array of matching key strings\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract list(prefix: string): Promise<string[]>;\n\n /**\n * Removes a specific key from storage.\n *\n * After this operation, get() calls for this key will return null.\n * No error is thrown if the key doesn't exist.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract clear(key: string): Promise<void>;\n\n /**\n * Removes all keys from this storage instance.\n *\n * This operation clears all data stored by this twist/tool instance\n * but does not affect storage for other twists or tools.\n *\n * @returns Promise that resolves when all keys are removed\n */\n abstract clearAll(): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/tasks
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { ITool } from \"..\";\nimport type { Callback } from \"./callbacks\";\n\n/**\n * Run background tasks and scheduled jobs.\n *\n * The Tasks tool enables twists and tools to queue callbacks for execution in separate\n * worker contexts. **This is critical for staying under request limits**: each execution\n * has a limit of ~1000 requests (HTTP requests, tool calls, database operations), and\n * running a task creates a NEW execution with a fresh request limit.\n *\n * **Key distinction:**\n * - **Calling a callback** (via `this.run()`) continues the same execution and shares the request count\n * - **Running a task** (via `this.runTask()`) creates a NEW execution with fresh ~1000 request limit\n *\n * **When to use tasks:**\n * - Processing large datasets that would exceed 1000 requests\n * - Breaking loops into chunks where each chunk stays under the request limit\n * - Scheduling operations for future execution\n *\n * **Note:** Tasks tool methods are also available directly on Twist and Tool classes\n * via `this.runTask()`, `this.cancelTask()`, and `this.cancelAllTasks()`.\n * This is the recommended approach for most use cases.\n *\n * **Best Practices:**\n * - Size batches to stay under ~1000 requests per execution\n * - Calculate requests per item to determine safe batch size\n * - Create callbacks first using `this.callback()`\n * - Store intermediate state using the Store tool\n *\n * @example\n * ```typescript\n * class SyncTool extends Tool {\n * async startBatchSync(totalItems: number) {\n * // Store initial state using built-in set method\n * await this.set(\"sync_progress\", { processed: 0, total: totalItems });\n *\n * // Create callback and queue first batch\n * const callback = await this.callback(\"processBatch\", { batchNumber: 1 });\n * // runTask creates NEW execution with fresh ~1000 request limit\n * await this.runTask(callback);\n * }\n *\n * async processBatch(args: any, context: { batchNumber: number }) {\n * // Process one batch of items (sized to stay under request limit)\n * const progress = await this.get(\"sync_progress\");\n *\n * // If each item makes ~10 requests, process ~100 items per batch\n * // 100 items × 10 requests = 1000 requests (at limit)\n * const batchSize = 100;\n * const items = await this.fetchItems(progress.processed, batchSize);\n *\n * for (const item of items) {\n * await this.processItem(item); // Makes ~10 requests per item\n * }\n *\n * await this.set(\"sync_progress\", {\n * processed: progress.processed + batchSize,\n * total: progress.total\n * });\n *\n * if (progress.processed < progress.total) {\n * // Queue next batch - creates NEW execution with fresh request limit\n * const callback = await this.callback(\"processBatch\", {\n * batchNumber: context.batchNumber + 1\n * });\n * await this.runTask(callback);\n * }\n * }\n *\n * async scheduleCleanup() {\n * const tomorrow = new Date();\n * tomorrow.setDate(tomorrow.getDate() + 1);\n *\n * const callback = await this.callback(\"cleanupOldData\");\n * // Schedule for future execution\n * return await this.runTask(callback, { runAt: tomorrow });\n * }\n * }\n * ```\n */\nexport abstract class Tasks extends ITool {\n /**\n * Queues a callback to execute in a separate worker context with a fresh request limit.\n *\n * **Creates a NEW execution** with its own request limit of ~1000 requests (HTTP requests,\n * tool calls, database operations). This is the primary way to stay under request limits\n * when processing large datasets or making many API calls.\n *\n * The callback will be invoked either immediately or at a scheduled time\n * in an isolated execution environment. Each execution has ~1000 requests and ~60 seconds\n * CPU time. Use this for breaking loops into chunks that stay under the request limit.\n *\n * **Key distinction:**\n * - `this.run(callback)` - Continues same execution, shares request count\n * - `this.runTask(callback)` - NEW execution, fresh request limit\n *\n * @param callback - Callback created with `this.callback()`\n * @param options - Optional configuration for the execution\n * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately\n * @returns Promise resolving to a cancellation token (only for scheduled executions)\n *\n * @example\n * ```typescript\n * // Break large loop into batches to stay under request limit\n * const callback = await this.callback(\"syncBatch\", { page: 1 });\n * await this.runTask(callback); // Fresh execution with ~1000 requests\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract runTask(\n callback: Callback,\n options?: { runAt?: Date }\n ): Promise<string | void>;\n\n /**\n * Cancels a previously scheduled execution.\n *\n * Prevents a scheduled function from executing. No error is thrown\n * if the token is invalid or the execution has already completed.\n *\n * @param token - The cancellation token returned by runTask() with runAt option\n * @returns Promise that resolves when the cancellation is processed\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract cancelTask(token: string): Promise<void>;\n\n /**\n * Cancels all scheduled executions for this tool/twist.\n *\n * Cancels all pending scheduled executions created by this tool or twist\n * instance. Immediate executions cannot be cancelled.\n *\n * @returns Promise that resolves when all cancellations are processed\n */\n abstract cancelAllTasks(): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/tools/twists
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { type Callback, ITool } from \"..\";\n\n/**\n * Twist source code structure containing dependencies and source files.\n */\nexport interface TwistSource {\n /**\n * Package dependencies with version specifiers\n * @example { \"@plotday/twister\": \"workspace:^\", \"@plotday/tool-google-calendar\": \"^1.0.0\" }\n */\n dependencies: Record<string, string>;\n\n /**\n * Source files with their content\n * Must include \"index.ts\" as the entry point\n * @example { \"index.ts\": \"export default class MyTwist extends Twist {...}\" }\n */\n files: Record<string, string>;\n}\n\n/**\n * Represents a log entry from a twist execution.\n */\nexport type Log = {\n timestamp: Date;\n environment: \"personal\" | \"private\" | \"review\" | \"public\";\n severity: \"log\" | \"error\" | \"warn\" | \"info\";\n message: string;\n};\n\n/**\n * Twist permissions returned after deployment.\n * Nested structure mapping domains to entities to permission flags.\n *\n * Format: { domain: { entity: flags[] } }\n * - domain: Tool name (e.g., \"network\", \"plot\")\n * - entity: Domain-specific identifier (e.g., URL pattern, resource type)\n * - flags: Array of permission flags (\"read\", \"write\", \"update\", \"use\")\n *\n * @example\n * ```typescript\n * {\n * \"network\": {\n * \"https://api.example.com/*\": [\"use\"],\n * \"https://googleapis.com/*\": [\"use\"]\n * },\n * \"plot\": {\n * \"thread:mentioned\": [\"read\", \"write\", \"update\"],\n * \"priority\": [\"read\", \"write\", \"update\"]\n * }\n * }\n * ```\n */\nexport type TwistPermissions = Record<string, Record<string, string[]>>;\n\n/**\n * Built-in tool for managing twists and deployments.\n *\n * The Twists tool provides twists with the ability to create twist IDs\n * and programmatically deploy twists.\n *\n * @example\n * ```typescript\n * class TwistBuilderTwist extends Twist {\n * build(build: ToolBuilder) {\n * return {\n * twists: build.get(Twists)\n * }\n * }\n *\n * async activate() {\n * const twistId = await this.tools.twists.create();\n * // Display twist ID to user\n * }\n * }\n * ```\n */\nexport abstract class Twists extends ITool {\n /**\n * Creates a new twist ID and grants access to people in the current priority.\n *\n * @returns Promise resolving to the generated twist ID\n * @throws When twist creation fails\n *\n * @example\n * ```typescript\n * const twistId = await twist.create();\n * console.log(`Your twist ID: ${twistId}`);\n * ```\n */\n abstract create(): Promise<string>;\n\n /**\n * Generates twist source code from a specification using AI.\n *\n * This method uses Claude AI to generate TypeScript source code and dependencies\n * from a markdown specification. The generated source is validated by attempting\n * to build it, with iterative error correction (up to 3 attempts).\n *\n * @param spec - Markdown specification describing the twist functionality\n * @returns Promise resolving to twist source (dependencies and files)\n * @throws When generation fails after maximum attempts\n *\n * @example\n * ```typescript\n * const source = await twist.generate(`\n * # Calendar Sync Twist\n *\n * This twist syncs Google Calendar events to Plot activities.\n *\n * ## Features\n * - Authenticate with Google\n * - Sync calendar events\n * - Create activities from events\n * `);\n *\n * // source.dependencies: { \"@plotday/twister\": \"workspace:^\", ... }\n * // source.files: { \"index.ts\": \"export default class...\" }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract generate(spec: string): Promise<TwistSource>;\n\n /**\n * Deploys a twist programmatically.\n *\n * This method provides the same functionality as the plot deploy CLI\n * command, but can be called from within a twist. Accepts either:\n * - A pre-bundled module (JavaScript code)\n * - A source object (dependencies + files) which is built in a sandbox\n *\n * @param options - Deployment configuration\n * @param options.twistId - Twist ID for deployment\n * @param options.module - Pre-bundled twist module code (mutually exclusive with source)\n * @param options.source - Twist source code with dependencies (mutually exclusive with module)\n * @param options.environment - Target environment (defaults to \"personal\")\n * @param options.name - Optional twist name (required for first deploy)\n * @param options.description - Optional twist description (required for first deploy)\n * @param options.dryRun - If true, validates without deploying (returns errors if any)\n * @returns Promise resolving to deployment result with version and optional errors\n * @throws When deployment fails or user lacks access\n *\n * @example\n * ```typescript\n * // Deploy with a module\n * const result = await twist.deploy({\n * twistId: 'abc-123-...',\n * module: 'export default class MyTwist extends Twist {...}',\n * environment: 'personal',\n * name: 'My Twist',\n * description: 'Does something cool'\n * });\n * console.log(`Deployed version ${result.version}`);\n *\n * // Deploy with source\n * const source = await twist.generate(spec);\n * const result = await twist.deploy({\n * twistId: 'abc-123-...',\n * source,\n * environment: 'personal',\n * name: 'My Twist',\n * });\n *\n * // Validate with dryRun\n * const result = await twist.deploy({\n * twistId: 'abc-123-...',\n * source,\n * dryRun: true,\n * });\n * if (result.errors?.length) {\n * console.error('Build errors:', result.errors);\n * }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract deploy(\n options: {\n twistId: string;\n environment?: \"personal\" | \"private\" | \"review\";\n name?: string;\n description?: string;\n dryRun?: boolean;\n } & (\n | {\n module: string;\n }\n | {\n source: TwistSource;\n }\n )\n ): Promise<{\n version: string;\n permissions: TwistPermissions;\n errors?: string[];\n }>;\n\n /**\n * Subscribes to logs from a twist.\n *\n * This method registers a callback to receive batches of logs from twist executions.\n * The callback will be invoked with an array of logs whenever new logs are captured\n * from the twist's console output.\n *\n * @param twistId - Twist ID (root ID) to watch logs for\n * @param callback - Callback token created via CallbackTool that will receive log batches\n * @returns Promise that resolves when the subscription is created\n * @throws When subscription fails\n *\n * @example\n * ```typescript\n * // Create twist and callback\n * const twistId = await this.twist.create();\n * const callback = await this.callback.create(\"onLogs\");\n *\n * // Subscribe to logs\n * await this.twist.watchLogs(twistId, callback);\n *\n * // Implement handler\n * async onLogs(logs: Log[]) {\n * for (const log of logs) {\n * console.log(`[${log.environment}] ${log.severity}: ${log.message}`);\n * }\n * }\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n abstract watchLogs(twistId: string, callback: Callback): Promise<void>;\n}\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated twists guide template
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: cli/templates/AGENTS.template.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "# Twist Implementation Guide for LLMs\n\nThis document provides context for AI assistants generating or modifying twists.\n\n## Architecture Overview\n\nPlot Twists are TypeScript classes that extend the `Twist` base class. Twists interact with external services and Plot's core functionality through a tool-based architecture.\n\n### Runtime Environment\n\n**Critical**: All Twists and tool functions are executed in a sandboxed, ephemeral environment with limited resources:\n\n- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the twist/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching.\n- **Limited requests per execution**: Each execution has ~1000 requests (HTTP requests, tool calls, database operations)\n- **Limited CPU time**: Each execution has limited CPU time (typically ~60 seconds) and memory (128MB)\n- **Use tasks to get fresh request limits**: `this.runTask()` creates a NEW execution with a fresh ~1000 request limit\n- **Calling callbacks continues same execution**: `this.run()` continues the same execution and shares the request count\n- **Break long loops**: Split large operations into batches that each stay under the ~1000 request limit\n- **Store intermediate state**: Use the Store tool to persist state between batches\n- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations\n\n## Understanding Threads and Notes\n\n**CRITICAL CONCEPT**: A **Thread** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that thread.\n\n**Think of a Thread as a thread** on a messaging platform, and **Notes as the messages in that thread**.\n\n### Key Guidelines\n\n1. **Always create Threads with an initial Note** - The title is just a summary; detailed content goes in Notes\n2. **Add Notes to existing Threads for updates** - Don't create a new Thread for each related message\n3. **Use Thread.source and Note.key for automatic upserts (Recommended)** - Set Thread.source to the external item's URL for deduplication, and use Note.key for upsertable note content. No manual ID tracking needed.\n4. **For advanced cases, use generated UUIDs** - Only when you need multiple Plot threads per external item (see SYNC_STRATEGIES.md)\n5. **Most Threads should be `ThreadType.Note`** - Use `Action` only for tasks with `done`, use `Event` only for items with `start`/`end`\n\n### Recommended Decision Tree (Strategy 2: Upsert via Source/Key)\n\n```\nNew event/task/conversation from external system?\n ├─ Has stable URL or ID?\n │ └─ Yes → Set Thread.source to the canonical URL/ID\n │ Create Thread (Plot handles deduplication automatically)\n │ Use Note.key for different note types:\n │ - \"description\" for main content\n │ - \"metadata\" for status/priority/assignee\n │ - \"comment-{id}\" for individual comments\n │\n └─ No stable identifier OR need multiple Plot threads per external item?\n └─ Use Advanced Pattern (Strategy 3: Generate and Store IDs)\n See SYNC_STRATEGIES.md for details\n```\n\n### Advanced Decision Tree (Strategy 3: Generate and Store IDs)\n\nOnly use when source/key upserts aren't sufficient (e.g., creating multiple threads from one external item):\n\n```\nNew event/task/conversation?\n ├─ Yes → Generate UUID with Uuid.Generate()\n │ Create new Thread with that UUID\n │ Store mapping: external_id → thread_uuid\n │\n └─ No (update/reply/comment) → Look up mapping by external_id\n ├─ Found → Add Note to existing Thread using stored UUID\n └─ Not found → Create new Thread with UUID + store mapping\n```\n\n## Twist Structure Pattern\n\n```typescript\nimport {\n type Thread,\n type NewThreadWithNotes,\n type ThreadFilter,\n type Priority,\n type ToolBuilder,\n Twist,\n ThreadType,\n} from \"@plotday/twister\";\nimport { ThreadAccess, Plot } from \"@plotday/twister/tools/plot\";\n// Import your sources or tools as needed\n\nexport default class MyTwist extends Twist<MyTwist> {\n build(build: ToolBuilder) {\n return {\n plot: build(Plot, {\n thread: { access: ThreadAccess.Create },\n }),\n };\n }\n\n async activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection handled in the twist edit modal.\n }\n}\n```\n\n## Tool System\n\n### Accessing Tools\n\nAll tools are declared in the `build` method:\n\n```typescript\nbuild(build: ToolBuilder) {\n return {\n toolName: build(ToolClass),\n };\n}\n```\n\nAll `build()` calls must occur in the `build` method as they are used for dependency analysis.\n\nIMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed.\n\n### Built-in Tools (Always Available)\n\nFor complete API documentation of built-in tools including all methods, types, and detailed examples, see the TypeScript definitions in your installed package at `node_modules/@plotday/twister/src/tools/*.ts`. Each tool file contains comprehensive JSDoc documentation.\n\n**Quick reference - Available tools:**\n\n- `@plotday/twister/tools/plot` - Core data layer (create/update activities, priorities, contacts)\n- `@plotday/twister/tools/ai` - LLM integration (text generation, structured output, reasoning)\n - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high)\n- `@plotday/twister/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`)\n- `@plotday/twister/tools/tasks` - Queue batched work (also via `this.run()`)\n- `@plotday/twister/tools/callbacks` - Persistent function references (also via `this.callback()`)\n- `@plotday/twister/tools/integrations` - OAuth2 authentication flows\n- `@plotday/twister/tools/network` - HTTP access permissions and webhook management\n- `@plotday/twister/tools/twists` - Manage other Twists\n\n**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods.\n\n## Lifecycle Methods\n\n### activate(priority: Pick<Priority, \"id\">)\n\nCalled when the twist is enabled for a priority. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.\n\nMost twists have an empty or minimal `activate()`:\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n // Auth and resource selection are handled in the twist edit modal.\n // Only add custom initialization here if needed.\n}\n```\n\n**Store Parent Thread for Later (optional):**\n\n```typescript\nasync activate(_priority: Pick<Priority, \"id\">) {\n const threadId = await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Setup complete\",\n notes: [{\n content: \"Your twist is ready. Threads will appear as they sync.\",\n }],\n });\n await this.set(\"setup_thread_id\", threadId);\n}\n```\n\n### Event Callbacks (via build options)\n\nTwists respond to events through callbacks declared in `build()`:\n\n**React to thread changes (for two-way sync):**\n\n```typescript\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n updated: this.onThreadUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\nasync onThreadUpdated(thread: Thread, changes: { tagsAdded, tagsRemoved }): Promise<void> {\n const tool = this.getToolForThread(thread);\n if (tool?.updateIssue) await tool.updateIssue(thread);\n}\n\nasync onNoteCreated(note: Note): Promise<void> {\n if (note.author.type === ActorType.Twist) return; // Prevent loops\n // Sync note to external service as a comment\n}\n```\n\n**Respond to mentions (AI twist pattern):**\n\n```typescript\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n intents: [{\n description: \"Respond to general questions\",\n examples: [\"What's the weather?\", \"Help me plan my week\"],\n handler: this.respond,\n }],\n },\n}),\n```\n\n**Default mention on replies:**\n\nWhen your twist processes replies (two-way comment sync or conversational AI), set `defaultMention: true` so the user doesn't have to manually re-mention your twist on every note:\n\n- `thread.defaultMention` — Auto-mention on replies to threads your twist created (e.g., synced issues, emails)\n- `note.defaultMention` — Auto-mention on follow-up notes in threads where your twist was @-mentioned (e.g., conversational agents)\n\n```typescript\n// Connector with two-way comment sync\nplot: build(Plot, {\n thread: {\n access: ThreadAccess.Create,\n defaultMention: true, // Users replying to synced threads will mention this twist by default\n updated: this.onThreadUpdated,\n },\n note: {\n created: this.onNoteCreated,\n },\n}),\n\n// Conversational AI twist\nplot: build(Plot, {\n thread: { access: ThreadAccess.Respond },\n note: {\n defaultMention: true, // Follow-up notes auto-mention this twist\n intents: [{ description: \"...\", examples: [...], handler: this.respond }],\n },\n}),\n```\n\nWithout `defaultMention`, the twist chip appears toggled OFF by default — the user can still enable it manually per-note.\n\n## Actions\n\nActions enable user interaction:\n\n```typescript\nimport { type Action, ActionType } from \"@plotday/twister\";\n\n// External URL action\nconst urlAction: Action = {\n title: \"Open website\",\n type: ActionType.external,\n url: \"https://example.com\",\n};\n\n// Callback action (uses Callbacks tool — use linkCallback, not callback)\nconst token = await this.linkCallback(this.onActionClicked, \"context\");\nconst callbackAction: Action = {\n title: \"Click me\",\n type: ActionType.callback,\n callback: token,\n};\n\n// Add to thread note\nawait this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Task with actions\",\n notes: [\n {\n content: \"Click the actions below to take action.\",\n actions: [urlAction, callbackAction],\n },\n ],\n});\n\n// Callback handler receives the Action as first argument\nasync onActionClicked(action: Action, context: string): Promise<void> {\n // Handle action click\n}\n```\n\n## Authentication Pattern\n\nAuth is handled automatically via the Integrations tool. Tools declare their OAuth provider in `build()`, and users connect in the twist edit modal. **You do not need to create auth activities manually.**\n\n```typescript\n// In your tool's build() method:\nbuild(build: ToolBuilder) {\n return {\n integrations: build(Integrations, {\n providers: [{\n provider: AuthProvider.Google,\n scopes: [\"https://www.googleapis.com/auth/calendar\"],\n getChannels: this.getChannels, // List available resources after auth\n onChannelEnabled: this.onChannelEnabled, // User enabled a resource\n onChannelDisabled: this.onChannelDisabled, // User disabled a resource\n }],\n }),\n // ...\n };\n}\n\n// Get a token for API calls:\nconst token = await this.tools.integrations.get(AuthProvider.Google, channelId);\nif (!token) throw new Error(\"No auth token available\");\nconst client = new ApiClient({ accessToken: token.token });\n```\n\nFor per-user write-backs (e.g., RSVP, comments attributed to the acting user):\n\n```typescript\nawait this.tools.integrations.actAs(\n AuthProvider.Google,\n actorId, // The user who performed the action\n threadId, // Thread to prompt for auth if needed\n this.performWriteBack,\n ...extraArgs\n);\n```\n\n## Sync Pattern\n\n### Upsert via Source/Key (Strategy 2)\n\nUse source/key for automatic upserts:\n\n```typescript\nasync handleEvent(event: ExternalEvent): Promise<void> {\n const thread: NewThreadWithNotes = {\n source: event.htmlLink, // Canonical URL for automatic deduplication\n type: ThreadType.Event,\n title: event.summary || \"(No title)\",\n notes: [],\n };\n\n if (event.description) {\n thread.notes.push({\n thread: { source: event.htmlLink },\n key: \"description\", // This key enables note-level upserts\n content: event.description,\n });\n }\n\n // Create or update — Plot handles deduplication automatically\n await this.tools.plot.createThread(thread);\n}\n```\n\n### Advanced: Generate and Store IDs (Strategy 3)\n\nOnly use this pattern when you need to create multiple Plot threads from a single external item, or when the external system doesn't provide stable identifiers. See SYNC_STRATEGIES.md for details.\n\n```typescript\nasync handleEventAdvanced(\n incomingThread: NewThreadWithNotes,\n calendarId: string\n): Promise<void> {\n // Extract external event ID from meta (adapt based on your tool's data)\n const externalId = incomingThread.meta?.eventId;\n\n if (!externalId) {\n console.error(\"Event missing external ID\");\n return;\n }\n\n // Check if we've already synced this event\n const mappingKey = `event_mapping:${calendarId}:${externalId}`;\n const existingThreadId = await this.get<Uuid>(mappingKey);\n\n if (existingThreadId) {\n // Event already exists - add update as a Note (add message to thread)\n if (incomingThread.notes?.[0]?.content) {\n await this.tools.plot.createNote({\n thread: { id: existingThreadId },\n content: incomingThread.notes[0].content,\n });\n }\n return;\n }\n\n // New event - generate UUID and store mapping\n const threadId = Uuid.Generate();\n await this.set(mappingKey, threadId);\n\n // Create new Thread with initial Note (new thread with first message)\n await this.tools.plot.createThread({\n ...incomingThread,\n id: threadId,\n });\n}\n```\n\n## Resource Selection\n\nResource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your tool's `getChannels()` method and toggle them on/off. You do **not** need to build custom selection UI.\n\n```typescript\n// In your tool:\nasync getChannels(_auth: Authorization, token: AuthToken): Promise<Channel[]> {\n const client = new ApiClient({ accessToken: token.token });\n const calendars = await client.listCalendars();\n return calendars.map(c => ({\n id: c.id,\n title: c.name,\n children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),\n }));\n}\n```\n\n## Batch Processing Pattern\n\n**Important**: Because Twists run in an ephemeral environment with limited requests per execution (~1000 requests), you must break long operations into batches. Each batch runs independently in a new execution context with its own fresh request limit.\n\n### Key Principles\n\n1. **Stay under request limits**: Each execution has ~1000 requests. Size batches accordingly.\n2. **Use runTask() for fresh limits**: Each call to `this.runTask()` creates a NEW execution with fresh ~1000 requests\n3. **Store state between batches**: Use the Store tool to persist progress\n4. **Calculate safe batch sizes**: Determine requests per item to size batches (e.g., ~10 requests per item = ~100 items per batch)\n5. **Clean up when done**: Delete stored state after completion\n6. **Handle failures**: Store enough state to resume if a batch fails\n\n### Example Implementation\n\n```typescript\nasync startSync(resourceId: string): Promise<void> {\n // Initialize state in Store (persists between executions)\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: null,\n batchNumber: 1,\n itemsProcessed: 0,\n initialSync: true, // Track whether this is the first sync\n });\n\n // Queue first batch using runTask method\n const callback = await this.callback(this.syncBatch, resourceId);\n // runTask creates NEW execution with fresh ~1000 request limit\n await this.runTask(callback);\n}\n\nasync syncBatch(resourceId: string): Promise<void> {\n // Load state from Store (set by previous execution)\n const state = await this.get(`sync_state_${resourceId}`);\n\n // Process one batch (size to stay under ~1000 request limit)\n const result = await this.fetchBatch(state.nextPageToken);\n\n // Process results using source/key pattern (automatic upserts, no manual tracking)\n // If each item makes ~10 requests, keep batch size ≤ 100 items to stay under limit\n for (const item of result.items) {\n // Each createThread may make ~5-10 requests depending on notes/links\n await this.tools.plot.createThread({\n source: item.url, // Use item's canonical URL for automatic deduplication\n type: ThreadType.Note,\n title: item.title,\n notes: [{\n activity: { source: item.url },\n key: \"description\", // Use key for upsertable notes\n content: item.description,\n }],\n ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only\n });\n }\n\n if (result.nextPageToken) {\n // Update state in Store for next batch\n await this.set(`sync_state_${resourceId}`, {\n nextPageToken: result.nextPageToken,\n batchNumber: state.batchNumber + 1,\n itemsProcessed: state.itemsProcessed + result.items.length,\n initialSync: state.initialSync, // Preserve initialSync flag across batches\n });\n\n // Queue next batch - creates NEW execution with fresh request limit\n const nextCallback = await this.callback(this.syncBatch, resourceId);\n await this.runTask(nextCallback);\n } else {\n // Cleanup when complete\n await this.clear(`sync_state_${resourceId}`);\n\n // Optionally notify user of completion\n await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Sync complete\",\n notes: [\n {\n content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,\n },\n ],\n });\n }\n}\n```\n\n## Thread Sync Best Practices\n\nWhen syncing threads from external systems, follow these patterns for optimal user experience:\n\n### The `initialSync` Flag\n\nAll sync-based tools should distinguish between initial sync (first import) and incremental sync (ongoing updates):\n\n| Field | Initial Sync | Incremental Sync | Reason |\n|-------|--------------|------------------|---------|\n| `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |\n| `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |\n\n**Example:**\n```typescript\nconst thread: NewThread = {\n type: ThreadType.Event,\n source: event.url,\n title: event.title,\n ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental\n ...(initialSync ? { archived: false } : {}), // unarchive on initial only\n};\n```\n\n**Why this matters:**\n- **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports\n- **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved\n- **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)\n\n### Two-Way Sync: Avoiding Race Conditions\n\nWhen implementing two-way sync where items created in Plot are pushed to an external system (e.g. Notes becoming comments), a race condition can occur: the external system may send a webhook for the newly created item before you've updated the Thread/Note with the external key. The webhook handler won't find the item by external key and may create a duplicate.\n\n**Solution:** Embed the Plot `Thread.id` / `Note.id` in the external item's metadata when creating it, and update `Thread.source` / `Note.key` after creation. When processing webhooks, check for the Plot ID in metadata first.\n\n```typescript\nasync pushNoteAsComment(note: Note, externalItemId: string): Promise<void> {\n // Create external item with Plot ID in metadata for webhook correlation\n const externalComment = await externalApi.createComment(externalItemId, {\n body: note.content,\n metadata: { plotNoteId: note.id },\n });\n\n // Update Note with external key AFTER creation\n // A webhook may arrive before this completes — that's OK (see onWebhook below)\n await this.tools.plot.updateNote({\n id: note.id,\n key: `comment-${externalComment.id}`,\n });\n}\n\nasync onWebhook(payload: WebhookPayload): Promise<void> {\n const comment = payload.comment;\n\n // Use Plot ID from metadata if present (handles race condition),\n // otherwise fall back to upserting by activity source and key\n await this.tools.plot.createNote({\n ...(comment.metadata?.plotNoteId\n ? { id: comment.metadata.plotNoteId }\n : { activity: { source: payload.itemUrl } }),\n key: `comment-${comment.id}`,\n content: comment.body,\n });\n}\n```\n\n## Error Handling\n\nAlways handle errors gracefully and communicate them to users:\n\n```typescript\ntry {\n await this.externalOperation();\n} catch (error) {\n console.error(\"Operation failed:\", error);\n\n await this.tools.plot.createThread({\n type: ThreadType.Note,\n title: \"Operation failed\",\n notes: [\n {\n content: `Failed to complete operation: ${error.message}`,\n },\n ],\n });\n}\n```\n\n## Common Pitfalls\n\n- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.\n- **Processing self-created threads** - Other users may change a Thread created by the twist, resulting in a callback. Be sure to check the `changes === null` and/or `thread.author.id !== this.id` to avoid re-processing.\n- **Always create Threads with Notes** - See \"Understanding Threads and Notes\" section above for the thread/message pattern and decision tree.\n- **Use correct Thread types** - Most should be `ThreadType.Note`. Only use `Action` for tasks with `done`, and `Event` for items with `start`/`end`.\n- **Use Thread.source and Note.key for automatic upserts (Recommended)** - Set Thread.source to the external item's URL for automatic deduplication. Only use UUID generation and storage for advanced cases (see SYNC_STRATEGIES.md).\n- **Add Notes to existing Threads** - For source/key pattern, reference threads by source. For UUID pattern, look up stored mappings before creating new Threads. Think thread replies, not new threads.\n- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.\n- **Don't forget request limits** - Each execution has ~1000 requests (HTTP requests, tool calls). Break long loops into batches with `this.runTask()` to get fresh request limits. Calculate requests per item to determine safe batch size (e.g., if each item needs ~10 requests, batch size = ~100 items).\n- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.\n- **Store auth tokens** - Don't re-request authentication unnecessarily.\n- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.\n- **Handle missing auth gracefully** - Check for stored auth before operations.\n- **CRITICAL: Maintain callback backward compatibility** - All callbacks (webhooks, tasks, batch operations) automatically upgrade to new twist versions. You **must** maintain backward compatibility in callback method signatures. Only add optional parameters at the end, never remove or reorder parameters. For breaking changes, implement migration logic in the `upgrade()` lifecycle method to recreate affected callbacks.\n\n## Testing\n\nBefore deploying, verify:\n\n1. Linting passes: `{{packageManager}} lint`\n2. All dependencies are in package.json\n3. Authentication flow works end-to-end\n4. Batch operations handle pagination correctly\n5. Error cases are handled gracefully\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated LLM documentation for @plotday/twister/twist
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated during build. Do not edit manually.
|
|
5
|
+
* Generated from: prebuild.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default "import { type Action, type Actor, type ActorId, type Link, type Note, type Thread, Uuid } from \"./plot\";\nimport type { Tag } from \"./tag\";\nimport { type ITool } from \"./tool\";\nimport type { Callback } from \"./tools/callbacks\";\nimport type { Serializable } from \"./utils/serializable\";\nimport type { InferTools, ToolBuilder, ToolShed } from \"./utils/types\";\n\n/**\n * Base class for all twists.\n *\n * A twist is installed at the workspace level and is owned by a single user\n * (see `this.userId`). It has no inherent priority scope: threads, notes, and\n * links it creates are filed against the owner's priorities, with automatic\n * priority matching when no explicit target is provided.\n *\n * Override `build()` to declare tool dependencies and lifecycle methods to\n * handle events.\n *\n * @example\n * ```typescript\n * class FlatteringTwist extends Twist<FlatteringTwist> {\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot),\n * };\n * }\n *\n * async activate() {\n * await this.tools.plot.createThread({\n * title: \"Hello, good looking!\",\n * });\n * }\n * }\n * ```\n */\nexport abstract class Twist<TSelf> {\n /**\n * When `true`, users may install multiple instances of this twist within\n * the same scope (personal workspace or team). Each instance must have a\n * distinct name.\n *\n * Defaults to `false` (single instance per scope).\n *\n * @example\n * ```typescript\n * class WorkflowTwist extends Twist<WorkflowTwist> {\n * static readonly multipleInstances = true;\n * // ...\n * }\n * ```\n */\n static readonly multipleInstances?: boolean;\n\n /**\n * The user ID (`twist_instance.owner_id`) that installed this twist.\n * Populated by the runtime before any lifecycle method runs.\n */\n protected userId!: Uuid;\n\n constructor(protected id: Uuid, private toolShed: ToolShed) {}\n\n /**\n * Gets the initialized tools for this twist.\n * @throws Error if called before initialization is complete\n */\n protected get tools(): InferTools<TSelf> {\n return this.toolShed.getTools<InferTools<TSelf>>();\n }\n\n /**\n * Declares tool dependencies for this twist.\n * Return an object mapping tool names to build() promises.\n *\n * @param build - The build function to use for declaring dependencies\n * @returns Object mapping tool names to tool promises\n *\n * @example\n * ```typescript\n * build(build: ToolBuilder) {\n * return {\n * plot: build(Plot),\n * calendar: build(GoogleCalendar, { apiKey: \"...\" }),\n * };\n * }\n * ```\n */\n abstract build(build: ToolBuilder): Record<string, Promise<ITool>>;\n\n /**\n * Creates a persistent callback to a method on this twist.\n *\n * ExtraArgs are strongly typed to match the method's signature. They must be serializable.\n *\n * @param fn - The method to callback\n * @param extraArgs - Additional arguments to pass (type-checked, must be serializable)\n * @returns Promise resolving to a persistent callback token\n *\n * @example\n * ```typescript\n * const callback = await this.callback(this.onWebhook, \"calendar\", 123);\n * ```\n */\n protected callback<\n TArgs extends Serializable[],\n Fn extends (...args: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback>;\n // Overload when caller provides the first argument\n protected callback<\n TArgs extends Serializable[],\n Fn extends (arg1: any, ...extraArgs: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback>;\n protected async callback<\n TArgs extends Serializable[],\n Fn extends (...args: any[]) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback> {\n return this.tools.callbacks.create(fn, ...extraArgs);\n }\n\n /**\n * Like callback(), but for an Action, which receives the action as the first argument.\n *\n * @param fn - The method to callback\n * @param extraArgs - Additional arguments to pass after the action\n * @returns Promise resolving to a persistent callback token\n *\n * @example\n * ```typescript\n * const callback = await this.actionCallback(this.doSomething, 123);\n * const action: Action = {\n * type: ActionType.callback,\n * title: \"Do Something\",\n * callback,\n * };\n * ```\n */\n protected async actionCallback<\n TArgs extends Serializable[],\n Fn extends (action: Action, ...extraArgs: TArgs) => any\n >(fn: Fn, ...extraArgs: TArgs): Promise<Callback> {\n return this.tools.callbacks.create(fn, ...extraArgs);\n }\n\n /**\n * Deletes a specific callback by its token.\n *\n * @param token - The callback token to delete\n * @returns Promise that resolves when the callback is deleted\n */\n protected async deleteCallback(token: Callback): Promise<void> {\n return this.tools.callbacks.delete(token);\n }\n\n /**\n * Deletes all callbacks for this twist.\n *\n * @returns Promise that resolves when all callbacks are deleted\n */\n protected async deleteAllCallbacks(): Promise<void> {\n return this.tools.callbacks.deleteAll();\n }\n\n /**\n * Executes a callback by its token inline in the current execution.\n *\n * **Use `this.runTask()` instead for batch continuations and long-running work.**\n * `this.run()` executes inline, sharing the current request count (~1000 limit)\n * and blocking the HTTP response. This causes timeouts when used in lifecycle\n * methods like `onChannelEnabled` or `syncBatch` continuations.\n *\n * `this.run()` is appropriate when you need the callback's **return value** —\n * e.g., running a parent callback token that returns data. For fire-and-forget\n * work, always prefer `this.runTask()`.\n *\n * @param token - The callback token to execute\n * @param args - Optional arguments to pass to the callback\n * @returns Promise resolving to the callback result\n */\n protected async run(token: Callback, ...args: []): Promise<any> {\n return this.tools.callbacks.run(token, ...args);\n }\n\n /**\n * Retrieves a value from persistent storage by key.\n *\n * Values are automatically deserialized using SuperJSON, which\n * properly restores Date objects, Maps, Sets, and other complex types.\n *\n * @template T - The expected type of the stored value (must be Serializable)\n * @param key - The storage key to retrieve\n * @returns Promise resolving to the stored value or null\n */\n protected async get<T extends import(\"./index\").Serializable>(\n key: string\n ): Promise<T | null> {\n return this.tools.store.get(key);\n }\n\n /**\n * Stores a value in persistent storage.\n *\n * The value will be serialized using SuperJSON and stored persistently.\n * SuperJSON automatically handles Date objects, Maps, Sets, undefined values,\n * and other complex types that standard JSON doesn't support.\n *\n * **Important**: Functions and Symbols cannot be stored.\n * **For function references**: Use callbacks instead of storing functions directly.\n *\n * @example\n * ```typescript\n * // ✅ Date objects are preserved\n * await this.set(\"sync_state\", {\n * lastSync: new Date(),\n * minDate: new Date(2024, 0, 1)\n * });\n *\n * // ✅ undefined is now supported\n * await this.set(\"data\", { name: \"test\", optional: undefined });\n *\n * // ❌ WRONG: Cannot store functions directly\n * await this.set(\"handler\", this.myHandler);\n *\n * // ✅ CORRECT: Create a callback token first\n * const token = await this.callback(this.myHandler, \"arg1\", \"arg2\");\n * await this.set(\"handler_token\", token);\n *\n * // Later, execute the callback\n * const token = await this.get<string>(\"handler_token\");\n * await this.run(token, args);\n * ```\n *\n * @template T - The type of value being stored (must be Serializable)\n * @param key - The storage key to use\n * @param value - The value to store (must be SuperJSON-serializable)\n * @returns Promise that resolves when the value is stored\n */\n protected async set<T extends import(\"./index\").Serializable>(\n key: string,\n value: T\n ): Promise<void> {\n return this.tools.store.set(key, value);\n }\n\n /**\n * Removes a specific key from persistent storage.\n *\n * @param key - The storage key to remove\n * @returns Promise that resolves when the key is removed\n */\n protected async clear(key: string): Promise<void> {\n return this.tools.store.clear(key);\n }\n\n /**\n * Removes all keys from this twist's storage.\n *\n * @returns Promise that resolves when all keys are removed\n */\n protected async clearAll(): Promise<void> {\n return this.tools.store.clearAll();\n }\n\n /**\n * Queues a callback to execute in a separate worker context.\n *\n * @param callback - The callback token created with `this.callback()`\n * @param options - Optional configuration for the execution\n * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately\n * @returns Promise resolving to a cancellation token (only for scheduled executions)\n */\n protected async runTask(\n callback: Callback,\n options?: { runAt?: Date }\n ): Promise<string | void> {\n return this.tools.tasks.runTask(callback, options);\n }\n\n /**\n * Cancels a previously scheduled execution.\n *\n * @param token - The cancellation token returned by runTask() with runAt option\n * @returns Promise that resolves when the cancellation is processed\n */\n protected async cancelTask(token: string): Promise<void> {\n return this.tools.tasks.cancelTask(token);\n }\n\n /**\n * Cancels all scheduled executions for this twist.\n *\n * @returns Promise that resolves when all cancellations are processed\n */\n protected async cancelAllTasks(): Promise<void> {\n return this.tools.tasks.cancelAllTasks();\n }\n\n /**\n * Called when the twist is installed by a user.\n *\n * This method should contain initialization logic such as seeding\n * initial threads, configuring webhooks, or establishing external\n * connections. When it runs, `this.userId` is already populated with\n * the installing user's ID.\n *\n * @param context - Optional context containing the actor who triggered activation\n * @returns Promise that resolves when activation is complete\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n activate(context?: { actor: Actor }): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a new version of the twist is deployed.\n *\n * This method should contain migration logic for updating old data structures\n * or setting up new resources that weren't needed by the previous version.\n * It is called once per active twist_instance with the new version.\n *\n * @returns Promise that resolves when upgrade is complete\n */\n upgrade(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when the twist's options configuration changes.\n *\n * Override to react to option changes, e.g. archiving items when a sync\n * type is toggled off, or starting sync when a type is toggled on.\n *\n * @param oldOptions - The previously resolved options\n * @param newOptions - The newly resolved options\n * @returns Promise that resolves when the change is handled\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onOptionsChanged(\n oldOptions: Record<string, any>,\n newOptions: Record<string, any>\n ): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when the twist is uninstalled.\n *\n * This method should contain cleanup logic such as removing webhooks,\n * cleaning up external resources, or performing final data operations.\n *\n * @returns Promise that resolves when deactivation is complete\n */\n deactivate(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a thread created by this twist is updated.\n * Override to implement two-way sync with an external system.\n *\n * @param thread - The updated thread\n * @param changes - Tag additions and removals on the thread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onThreadUpdated(\n thread: Thread,\n changes: {\n tagsAdded: Record<Tag, ActorId[]>;\n tagsRemoved: Record<Tag, ActorId[]>;\n }\n ): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a note is created on a thread created by this twist.\n * Override to implement two-way sync (e.g. syncing notes as comments).\n *\n * Notes created by the twist itself are filtered out to prevent loops.\n *\n * Returning a string sets the note's `key` for future upsert matching,\n * linking the Plot note to its external counterpart so that subsequent\n * syncs (reactions, edits) update the existing note instead of creating duplicates.\n *\n * @param note - The newly created note\n * @returns Optional note key for external deduplication\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onNoteCreated(note: Note, ...args: any[]): Promise<string | void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a link is created in a connected source channel.\n * Requires `link: true` in Plot options.\n *\n * @param link - The newly created link\n * @param notes - Notes on the link's thread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkCreated(link: Link, notes: Note[]): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a link in a connected source channel is updated.\n * Requires `link: true` in Plot options.\n *\n * @param link - The updated link\n * @param notes - Notes on the link's thread (optional)\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkUpdated(link: Link, notes?: Note[]): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Called when a note is created on a thread with a link from a connected channel.\n * Requires `link: true` in Plot options.\n *\n * @param note - The newly created note\n * @param link - The link associated with the thread\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkNoteCreated(note: Note, link: Link): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Waits for tool initialization to complete.\n * Called automatically by the entrypoint before lifecycle methods.\n * @internal\n */\n async waitForReady(): Promise<void> {\n await this.toolShed.waitForReady();\n }\n}\n";
|