diff --git a/.changeset/heavy-steaks-fall.md b/.changeset/heavy-steaks-fall.md new file mode 100644 index 0000000..0d06f59 --- /dev/null +++ b/.changeset/heavy-steaks-fall.md @@ -0,0 +1,6 @@ +--- +"@plotday/tool-outlook-calendar": patch +"@plotday/tool-google-calendar": patch +--- + +Changed: Remove defunct static tool id diff --git a/.changeset/silent-otters-follow.md b/.changeset/silent-otters-follow.md new file mode 100644 index 0000000..fa196ba --- /dev/null +++ b/.changeset/silent-otters-follow.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": patch +--- + +Fixed: Improper use of tools in Agent and Tool base classes causing "Tool not found" errors diff --git a/.changeset/wise-baths-smell.md b/.changeset/wise-baths-smell.md new file mode 100644 index 0000000..b846ed8 --- /dev/null +++ b/.changeset/wise-baths-smell.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": minor +--- + +Added: plot agent logs diff --git a/sdk/cli/commands/agent-logs.ts b/sdk/cli/commands/agent-logs.ts new file mode 100644 index 0000000..11546ed --- /dev/null +++ b/sdk/cli/commands/agent-logs.ts @@ -0,0 +1,126 @@ +import * as fs from "fs"; + +import * as out from "../utils/output"; +import { getGlobalTokenPath } from "../utils/token"; +import { handleSSEStream } from "../utils/sse"; + +interface AgentLogsOptions { + agentId: string; + environment?: string; + deployToken?: string; + apiUrl: string; +} + +/** + * Stream agent logs in real-time + */ +export async function agentLogsCommand(options: AgentLogsOptions) { + const { agentId, environment = "personal", apiUrl } = options; + + // Load deploy token + let deployToken = options.deployToken; + + if (!deployToken) { + // Try to load from PLOT_DEPLOY_TOKEN environment variable + deployToken = process.env.PLOT_DEPLOY_TOKEN; + } + + if (!deployToken) { + // Try to load from global token file + const globalTokenPath = getGlobalTokenPath(); + if (fs.existsSync(globalTokenPath)) { + try { + deployToken = fs.readFileSync(globalTokenPath, "utf-8").trim(); + } catch (error) { + console.warn( + `Warning: Failed to read global token file: ${globalTokenPath}` + ); + } + } + } + + if (!deployToken) { + out.error( + "Authentication required", + "Run 'plot login' or provide token via --deploy-token or PLOT_DEPLOY_TOKEN env var" + ); + process.exit(1); + } + + // Construct API URL + const url = new URL(`/v1/agent/${agentId}/logs`, apiUrl); + if (environment !== "personal") { + url.searchParams.set("environment", environment); + } + + out.info(`Streaming logs for agent ${agentId}`, [ + `Environment: ${environment}`, + "Press Ctrl+C to stop", + ]); + out.blank(); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Accept: "text/event-stream", + Authorization: `Bearer ${deployToken}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + out.error( + `Failed to connect: ${response.status} ${response.statusText}`, + errorText + ); + process.exit(1); + } + + // Handle SSE stream with custom log formatting + await handleSSEStream(response, { + onProgress: (message) => { + // Initial connection message + out.plain(message); + }, + onEvent: (event, data) => { + if (event === "log") { + // Format log entry + const { timestamp, severity, message } = data; + const time = new Date(timestamp).toLocaleTimeString(); + + // Color-code by severity + let severityColor = ""; + let severityLabel = severity.toUpperCase().padEnd(5); + + switch (severity) { + case "error": + severityColor = "\x1b[31m"; // Red + break; + case "warn": + severityColor = "\x1b[33m"; // Yellow + break; + case "info": + severityColor = "\x1b[36m"; // Cyan + break; + default: + severityColor = "\x1b[37m"; // White + } + + const reset = "\x1b[0m"; + const gray = "\x1b[90m"; + + console.log( + `${gray}[${time}]${reset} ${severityColor}${severityLabel}${reset} ${message}` + ); + } + }, + onError: (error) => { + out.error("Stream error", error); + }, + }); + } catch (error) { + out.error("Connection failed", String(error)); + process.exit(1); + } +} diff --git a/sdk/cli/commands/generate.ts b/sdk/cli/commands/generate.ts index cfc0ca9..7eee917 100644 --- a/sdk/cli/commands/generate.ts +++ b/sdk/cli/commands/generate.ts @@ -6,8 +6,8 @@ import prompts from "prompts"; import * as out from "../utils/output"; import { detectPackageManager } from "../utils/packageManager"; -import { getGlobalTokenPath } from "../utils/token"; import { handleSSEStream } from "../utils/sse"; +import { getGlobalTokenPath } from "../utils/token"; interface GenerateOptions { dir: string; @@ -311,32 +311,38 @@ export async function generateCommand(options: GenerateOptions) { // Update @plotday/sdk to latest and install packages try { - out.progress("Updating @plotday/sdk to latest version..."); + out.progress("Updating SDK to latest version..."); const updateCommand = - packageManager === "npm" ? "npm install @plotday/sdk@latest" : - packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : - "yarn add @plotday/sdk@latest"; + packageManager === "npm" + ? "npm install @plotday/sdk@latest" + : packageManager === "pnpm" + ? "pnpm add @plotday/sdk@latest" + : "yarn add @plotday/sdk@latest"; execSync(updateCommand, { cwd: agentPath, stdio: "ignore" }); out.progress("Installing dependencies..."); const installCommand = - packageManager === "yarn" ? "yarn" : - `${packageManager} install`; + packageManager === "yarn" ? "yarn" : `${packageManager} install`; execSync(installCommand, { cwd: agentPath, stdio: "ignore" }); - out.success("Dependencies installed successfully!"); + out.success("Dependencies installed."); } catch (error) { - out.warning( - "Couldn't install dependencies", - [ - `Run '${packageManager === "npm" ? "npm install @plotday/sdk@latest" : packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : "yarn add @plotday/sdk@latest"}' in ${options.dir}`, - `Then run '${packageManager === "yarn" ? "yarn" : `${packageManager} install`}'` - ] - ); + out.warning("Couldn't install dependencies", [ + `Run '${ + packageManager === "npm" + ? "npm install @plotday/sdk@latest" + : packageManager === "pnpm" + ? "pnpm add @plotday/sdk@latest" + : "yarn add @plotday/sdk@latest" + }' in ${options.dir}`, + `Then run '${ + packageManager === "yarn" ? "yarn" : `${packageManager} install` + }'`, + ]); } out.blank(); diff --git a/sdk/cli/index.ts b/sdk/cli/index.ts index e56c026..8c27733 100644 --- a/sdk/cli/index.ts +++ b/sdk/cli/index.ts @@ -3,6 +3,7 @@ import { Command, Option } from "commander"; import { readFileSync } from "fs"; import { join } from "path"; +import { agentLogsCommand } from "./commands/agent-logs"; import { buildCommand } from "./commands/build"; import { createCommand } from "./commands/create"; import { deployCommand } from "./commands/deploy"; @@ -116,6 +117,29 @@ agent return deployCommand(opts); }); +agent + .command("logs ") + .description("Stream real-time logs from an agent") + .option( + "-e, --environment ", + "Agent environment (personal, private, review)", + "personal" + ) + .option("--deploy-token ", "Authentication token") + .action(function (this: Command, agentId: string) { + const opts = this.optsWithGlobals() as { + environment?: string; + deployToken?: string; + apiUrl: string; + }; + return agentLogsCommand({ + agentId, + environment: opts.environment, + deployToken: opts.deployToken, + apiUrl: opts.apiUrl, + }); + }); + // Priority subcommand group const priority = program .command("priority") diff --git a/sdk/cli/templates/AGENTS.template.md b/sdk/cli/templates/AGENTS.template.md index 3c3ba04..4aa496c 100644 --- a/sdk/cli/templates/AGENTS.template.md +++ b/sdk/cli/templates/AGENTS.template.md @@ -374,16 +374,15 @@ try { ## Common Pitfalls -1. **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. -2. **Don't forget timeout limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool. -3. **Don't assume execution order** - Batches may run on different workers. Store all necessary state between executions. -4. **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts. -5. **Store auth tokens** - Don't re-request authentication unnecessarily. -6. **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed. -7. **Handle missing auth gracefully** - Check for stored auth before operations. -8. **Batch size matters** - Process enough items per batch to be efficient, but few enough to stay under time limits. -9. **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing. -10. Activity with type ActivityType.Note typically do not have a start or end set, unless they're a note about a specific day or time that shouldn't be shown until then. +- **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. +- **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing. +- Activity with type ActivityType.Note typically do not have a start or end set, unless they're a note about a specific day or time that shouldn't be shown until then. +- Don't add the Tools instance as an instance variable. Any tools needed must bet rieved via \`this.tools.get(ToolClass)\` in the constructor and assigned to instance variables. +- **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool. Process enough items per batch to be efficient, but few enough to stay under time limits. +- **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts. +- **Store auth tokens** - Don't re-request authentication unnecessarily. +- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed. +- **Handle missing auth gracefully** - Check for stored auth before operations. ## Type Patterns diff --git a/sdk/cli/utils/sse.ts b/sdk/cli/utils/sse.ts index eeaf439..9effeaa 100644 --- a/sdk/cli/utils/sse.ts +++ b/sdk/cli/utils/sse.ts @@ -12,6 +12,7 @@ export interface SSEHandlers { onProgress?: (message: string) => void; onResult?: (data: any) => void; onError?: (error: string) => void; + onEvent?: (event: string, data: any) => void; } /** @@ -73,6 +74,11 @@ export async function handleSSEStream( case "error": handlers.onError?.(parsedData.error); throw new Error(parsedData.error); + default: + // Handle custom events via onEvent handler + if (handlers.onEvent) { + handlers.onEvent(currentEvent.event, parsedData); + } } } diff --git a/sdk/src/agent.ts b/sdk/src/agent.ts index f19d941..a863303 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -3,7 +3,10 @@ import type { Callback, CallbackContext, CallbackMethods, + CallbackTool, } from "./tools/callback"; +import type { Run } from "./tools/run"; +import type { Store } from "./tools/store"; /** * Base class for all agents. @@ -39,11 +42,20 @@ import type { */ export abstract class Agent { protected id: string; - protected tools: Tools; + private _callbackTool: CallbackTool; + private _store: Store; + private _runTool: Run; constructor(id: string, tools: Tools) { this.id = id; - this.tools = tools; + // Get specific tools in constructor for dependency detection + // Use dynamic imports to avoid circular dependencies with tool files + const { CallbackTool } = require("./tools/callback"); + const { Store } = require("./tools/store"); + const { Run } = require("./tools/run"); + this._callbackTool = tools.get(CallbackTool); + this._store = tools.get(Store); + this._runTool = tools.get(Run); } /** @@ -57,9 +69,7 @@ export abstract class Agent { functionName: K, context?: CallbackContext ): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.create(functionName, context); + return this._callbackTool.create(functionName, context); } /** @@ -69,9 +79,7 @@ export abstract class Agent { * @returns Promise that resolves when the callback is deleted */ protected async deleteCallback(token: Callback): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.delete(token); + return this._callbackTool.delete(token); } /** @@ -80,9 +88,7 @@ export abstract class Agent { * @returns Promise that resolves when all callbacks are deleted */ protected async deleteAllCallbacks(): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.deleteAll(); + return this._callbackTool.deleteAll(); } /** @@ -93,9 +99,7 @@ export abstract class Agent { * @returns Promise resolving to the callback result */ protected async call(token: Callback, args?: any): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.call(token, args); + return this._callbackTool.call(token, args); } /** @@ -106,9 +110,7 @@ export abstract class Agent { * @returns Promise resolving to the stored value or null */ protected async get(key: string): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.get(key); + return this._store.get(key); } /** @@ -120,9 +122,7 @@ export abstract class Agent { * @returns Promise that resolves when the value is stored */ protected async set(key: string, value: T): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.set(key, value); + return this._store.set(key, value); } /** @@ -132,9 +132,7 @@ export abstract class Agent { * @returns Promise that resolves when the key is removed */ protected async clear(key: string): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.clear(key); + return this._store.clear(key); } /** @@ -143,9 +141,7 @@ export abstract class Agent { * @returns Promise that resolves when all keys are removed */ protected async clearAll(): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.clearAll(); + return this._store.clearAll(); } /** @@ -160,9 +156,7 @@ export abstract class Agent { callback: Callback, options?: { runAt?: Date } ): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.run(callback, options); + return this._runTool.run(callback, options); } /** @@ -172,9 +166,7 @@ export abstract class Agent { * @returns Promise that resolves when the cancellation is processed */ protected async cancel(token: string): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.cancel(token); + return this._runTool.cancel(token); } /** @@ -183,9 +175,7 @@ export abstract class Agent { * @returns Promise that resolves when all cancellations are processed */ protected async cancelAll(): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.cancelAll(); + return this._runTool.cancelAll(); } /** @@ -231,7 +221,9 @@ export abstract class Agent { */ export abstract class ITool {} -export type ToolConstructor = (abstract new (id: string, tools: Tools) => T) | (new (id: string, tools: Tools) => T); +export type ToolConstructor = + | (abstract new (id: string, tools: Tools) => T) + | (new (id: string, tools: Tools) => T); /** * Base class for regular tools. @@ -256,11 +248,20 @@ export type ToolConstructor = (abstract new (id: string, tools: */ export abstract class Tool implements ITool { protected id: string; - protected tools: Tools; + private _callbackTool: CallbackTool; + private _store: Store; + private _runTool: Run; constructor(id: string, tools: Tools) { this.id = id; - this.tools = tools; + // Get specific tools in constructor for dependency detection + // Use dynamic imports to avoid circular dependencies with tool files + const { CallbackTool } = require("./tools/callback"); + const { Store } = require("./tools/store"); + const { Run } = require("./tools/run"); + this._callbackTool = tools.get(CallbackTool); + this._store = tools.get(Store); + this._runTool = tools.get(Run); } /** @@ -274,9 +275,7 @@ export abstract class Tool implements ITool { functionName: K, context?: CallbackContext ): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.create(functionName, context); + return this._callbackTool.create(functionName, context); } /** @@ -286,9 +285,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the callback is deleted */ protected async deleteCallback(token: Callback): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.delete(token); + return this._callbackTool.delete(token); } /** @@ -297,9 +294,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all callbacks are deleted */ protected async deleteAllCallbacks(): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.deleteAll(); + return this._callbackTool.deleteAll(); } /** @@ -310,9 +305,7 @@ export abstract class Tool implements ITool { * @returns Promise resolving to the callback result */ protected async call(token: Callback, args?: any): Promise { - const { CallbackTool } = await import("./tools/callback"); - const callbackTool = this.tools.get(CallbackTool) as any; - return callbackTool.call(token, args); + return this._callbackTool.call(token, args); } /** @@ -323,9 +316,7 @@ export abstract class Tool implements ITool { * @returns Promise resolving to the stored value or null */ protected async get(key: string): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.get(key); + return this._store.get(key); } /** @@ -337,9 +328,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the value is stored */ protected async set(key: string, value: T): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.set(key, value); + return this._store.set(key, value); } /** @@ -349,9 +338,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the key is removed */ protected async clear(key: string): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.clear(key); + return this._store.clear(key); } /** @@ -360,9 +347,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all keys are removed */ protected async clearAll(): Promise { - const { Store } = await import("./tools/store"); - const store = this.tools.get(Store); - return store.clearAll(); + return this._store.clearAll(); } /** @@ -377,9 +362,7 @@ export abstract class Tool implements ITool { callback: Callback, options?: { runAt?: Date } ): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.run(callback, options); + return this._runTool.run(callback, options); } /** @@ -389,9 +372,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the cancellation is processed */ protected async cancel(token: string): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.cancel(token); + return this._runTool.cancel(token); } /** @@ -400,9 +381,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all cancellations are processed */ protected async cancelAll(): Promise { - const { Run } = await import("./tools/run"); - const runTool = this.tools.get(Run); - return runTool.cancelAll(); + return this._runTool.cancelAll(); } } diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index b76cc77..a3d5875 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -4,20 +4,21 @@ import { Tool, type Tools, } from "@plotday/sdk"; -import { - Auth, - AuthLevel, - AuthProvider, - type Authorization, -} from "@plotday/sdk/tools/auth"; import { type Calendar, type CalendarAuth, type CalendarTool, type SyncOptions, } from "@plotday/sdk/common/calendar"; +import { + Auth, + AuthLevel, + AuthProvider, + type Authorization, +} from "@plotday/sdk/tools/auth"; import { type Callback } from "@plotday/sdk/tools/callback"; import { Webhook, type WebhookRequest } from "@plotday/sdk/tools/webhook"; + import { GoogleApi, type GoogleEvent, @@ -99,8 +100,6 @@ type AuthSuccessContext = { * ``` */ export class GoogleCalendar extends Tool implements CalendarTool { - static readonly id = "google-calendar"; - private auth: Auth; private webhook: Webhook; @@ -135,13 +134,13 @@ export class GoogleCalendar extends Tool implements CalendarTool { level: AuthLevel.User, scopes: calendarScopes, }, - authCallback, + authCallback ); } private async getApi(authToken: string): Promise { const authorization = await this.get( - `authorization:${authToken}`, + `authorization:${authToken}` ); if (!authorization) { throw new Error("Authorization no longer available"); @@ -159,7 +158,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { const api = await this.getApi(authToken); const data = (await api.call( "GET", - "https://www.googleapis.com/calendar/v3/users/me/calendarList", + "https://www.googleapis.com/calendar/v3/users/me/calendarList" )) as { items: Array<{ id: string; @@ -181,7 +180,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { authToken: string, calendarId: string, callback: Callback, - options?: SyncOptions, + options?: SyncOptions ): Promise { // Store the callback token await this.set("event_callback_token", callback); @@ -228,7 +227,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { private async setupCalendarWatch( authToken: string, calendarId: string, - opaqueAuthToken: string, + opaqueAuthToken: string ): Promise { const webhookUrl = await this.webhook.create("onCalendarWebhook", { calendarId, @@ -256,7 +255,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { type: "web_hook", address: webhookUrl, token: new URLSearchParams({ secret }).toString(), - }, + } )) as { expiration: string }; await this.set(`calendar_watch_${calendarId}`, { @@ -281,7 +280,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { authToken: string; }): Promise { console.log( - `Starting Google Calendar sync batch ${batchNumber} (${mode}) for calendar ${calendarId}`, + `Starting Google Calendar sync batch ${batchNumber} (${mode}) for calendar ${calendarId}` ); try { @@ -304,7 +303,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { if (result.events.length > 0) { await this.processCalendarEvents(result.events, calendarId); console.log( - `Synced ${result.events.length} events in batch ${batchNumber} for calendar ${calendarId}`, + `Synced ${result.events.length} events in batch ${batchNumber} for calendar ${calendarId}` ); } @@ -320,7 +319,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { await this.run(syncCallback); } else { console.log( - `Google Calendar ${mode} sync completed after ${batchNumber} batches for calendar ${calendarId}`, + `Google Calendar ${mode} sync completed after ${batchNumber} batches for calendar ${calendarId}` ); if (mode === "full") { await this.clear(`sync_state_${calendarId}`); @@ -329,7 +328,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { } catch (error) { console.error( `Error in sync batch ${batchNumber} for calendar ${calendarId}:`, - error, + error ); throw error; @@ -338,7 +337,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { private async processCalendarEvents( events: GoogleEvent[], - calendarId: string, + calendarId: string ): Promise { for (const event of events) { try { @@ -356,7 +355,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { // Convert to full Activity and call callback const callbackToken = await this.get( - "event_callback_token", + "event_callback_token" ); if (callbackToken && activityData.type) { const activity: NewActivity = { @@ -390,7 +389,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { private async processEventException( event: GoogleEvent, - calendarId: string, + calendarId: string ): Promise { // Similar to processCalendarEvents but for exceptions // This would find the master recurring activity and create an exception @@ -403,9 +402,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { const activityData = transformGoogleEvent(event, calendarId); - const callbackToken = await this.get( - "event_callback_token", - ); + const callbackToken = await this.get("event_callback_token"); if (callbackToken && activityData.type) { const activity: NewActivity = { type: activityData.type, @@ -432,7 +429,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { async onCalendarWebhook( request: WebhookRequest, - context: any, + context: any ): Promise { console.log("Received calendar webhook notification", { headers: request.headers, @@ -449,7 +446,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { } const watchData = await this.get( - `calendar_watch_${context.calendarId}`, + `calendar_watch_${context.calendarId}` ); if (!watchData || watchData.watchId !== channelId) { @@ -471,7 +468,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { private async startIncrementalSync( calendarId: string, - authToken: string, + authToken: string ): Promise { const watchData = await this.get(`calendar_watch_${calendarId}`); if (!watchData) { @@ -482,8 +479,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { const incrementalState: SyncState = { calendarId: watchData.calendarId, state: - (await this.get(`last_sync_token_${calendarId}`)) || - undefined, + (await this.get(`last_sync_token_${calendarId}`)) || undefined, }; await this.set(`sync_state_${calendarId}`, incrementalState); @@ -498,7 +494,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { async onAuthSuccess( authResult: Authorization, - context: AuthSuccessContext, + context: AuthSuccessContext ): Promise { // Store the actual auth token using opaque token as key await this.set(`authorization:${context.authToken}`, authResult); diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index 037af27..a787e80 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -5,18 +5,18 @@ import { Tool, type Tools, } from "@plotday/sdk"; -import { - Auth, - AuthLevel, - AuthProvider, - type Authorization, -} from "@plotday/sdk/tools/auth"; import type { Calendar, CalendarAuth, CalendarTool, SyncOptions, } from "@plotday/sdk/common/calendar"; +import { + Auth, + AuthLevel, + AuthProvider, + type Authorization, +} from "@plotday/sdk/tools/auth"; import { type Callback } from "@plotday/sdk/tools/callback"; import { Webhook, type WebhookRequest } from "@plotday/sdk/tools/webhook"; @@ -114,7 +114,7 @@ const outlookApi = { config: CalendarConfig, credentials: CalendarCredentials, syncState: OutlookSyncState, - limit: number, + limit: number ) => { // This would contain the actual Outlook API sync logic // For now, return empty result @@ -144,7 +144,7 @@ const outlookApi = { config: CalendarConfig, credentials: CalendarCredentials, calendarId: string, - existingWatch?: { watchId: string; watchSecret: string }, + existingWatch?: { watchId: string; watchSecret: string } ) => { // This would contain the webhook setup logic return { @@ -223,8 +223,6 @@ const outlookApi = { * ``` */ export class OutlookCalendar extends Tool implements CalendarTool { - static readonly id = "outlook-calendar"; - private auth: Auth; private webhook: Webhook; @@ -253,7 +251,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { level: AuthLevel.User, scopes: ["https://graph.microsoft.com/calendars.readwrite"], }, - authCallback, + authCallback ); } @@ -262,7 +260,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { credentials: CalendarCredentials; }> { const authorization = await this.get( - `authorization:${authToken}`, + `authorization:${authToken}` ); if (!authorization) { throw new Error("Authorization no longer available"); @@ -308,7 +306,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { authToken: string, calendarId: string, callback: Callback, - options?: SyncOptions, + options?: SyncOptions ): Promise { // Store the callback token await this.set("event_callback_token", callback); @@ -317,15 +315,16 @@ export class OutlookCalendar extends Tool implements CalendarTool { await this.setupOutlookWatch(authToken, calendarId, authToken); // Start sync batch using run tool for long-running operation - const syncCallback = await this.callback("syncOutlookBatch", { calendarId, authToken }); + const syncCallback = await this.callback("syncOutlookBatch", { + calendarId, + authToken, + }); await this.run(syncCallback); } async stopSync(authToken: string, calendarId: string): Promise { // Stop webhook - const watchData = await this.get( - `outlook_watch_${calendarId}`, - ); + const watchData = await this.get(`outlook_watch_${calendarId}`); if (watchData) { // Cancel the watch (would need Microsoft Graph API call) await this.clear(`outlook_watch_${calendarId}`); @@ -338,7 +337,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { private async setupOutlookWatch( authToken: string, calendarId: string, - opaqueAuthToken: string, + opaqueAuthToken: string ): Promise { const { config, credentials } = await this.getApi(authToken); @@ -350,10 +349,10 @@ export class OutlookCalendar extends Tool implements CalendarTool { config.webhookUrl = webhookUrl; const existingWatchId = await this.get( - `outlook_watch_id_${calendarId}`, + `outlook_watch_id_${calendarId}` ); const existingSecret = await this.get( - `outlook_watch_secret_${calendarId}`, + `outlook_watch_secret_${calendarId}` ); const watchResult = await outlookApi.watch( @@ -362,16 +361,13 @@ export class OutlookCalendar extends Tool implements CalendarTool { calendarId, existingWatchId && existingSecret ? { watchId: existingWatchId, watchSecret: existingSecret } - : undefined, + : undefined ); - await this.set( - `outlook_watch_id_${calendarId}`, - watchResult.state.watchId, - ); + await this.set(`outlook_watch_id_${calendarId}`, watchResult.state.watchId); await this.set( `outlook_watch_secret_${calendarId}`, - watchResult.state.secret, + watchResult.state.secret ); console.log("Outlook Calendar webhook configured", { @@ -396,14 +392,13 @@ export class OutlookCalendar extends Tool implements CalendarTool { } catch (error) { console.error( "No Microsoft credentials found for the provided authToken:", - error, + error ); return; } const lastSyncToken = - (await this.get(`last_sync_token_${calendarId}`)) || - undefined; + (await this.get(`last_sync_token_${calendarId}`)) || undefined; const syncState: OutlookSyncState = { calendarId, @@ -510,14 +505,14 @@ export class OutlookCalendar extends Tool implements CalendarTool { if (recurrenceRule && event.data?.recurrence) { // Parse recurrence count (takes precedence over end date) const recurrenceCount = parseOutlookRecurrenceCount( - event.data.recurrence, + event.data.recurrence ); if (recurrenceCount) { activity.recurrenceCount = recurrenceCount; } else { // Parse recurrence end date if no count const recurrenceUntil = parseOutlookRecurrenceEnd( - event.data.recurrence, + event.data.recurrence ); if (recurrenceUntil) { activity.recurrenceUntil = recurrenceUntil; @@ -567,9 +562,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { } // Call the event callback - const callbackToken = await this.get( - "event_callback_token", - ); + const callbackToken = await this.get("event_callback_token"); if (callbackToken) { await this.call(callbackToken, activity); } @@ -582,12 +575,12 @@ export class OutlookCalendar extends Tool implements CalendarTool { syncState.state = result.state.state; console.log( - `Synced ${result.events.length} events, total: ${eventCount} for calendar ${calendarId}`, + `Synced ${result.events.length} events, total: ${eventCount} for calendar ${calendarId}` ); } console.log( - `Outlook Calendar sync completed: ${eventCount} events for calendar ${calendarId}`, + `Outlook Calendar sync completed: ${eventCount} events for calendar ${calendarId}` ); } @@ -610,7 +603,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { for (const notification of notifications) { if (notification.changeType) { console.log( - `Calendar ${notification.changeType} notification for ${context.calendarId}`, + `Calendar ${notification.changeType} notification for ${context.calendarId}` ); // Trigger incremental sync @@ -621,7 +614,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { private async startIncrementalSync( calendarId: string, - authToken: string, + authToken: string ): Promise { console.log("Starting incremental Outlook Calendar sync for", calendarId); @@ -630,18 +623,21 @@ export class OutlookCalendar extends Tool implements CalendarTool { } catch (error) { console.error( "No Microsoft credentials found for the provided authToken:", - error, + error ); return; } - const callback = await this.callback("syncOutlookBatch", { calendarId, authToken }); + const callback = await this.callback("syncOutlookBatch", { + calendarId, + authToken, + }); await this.run(callback); } async onAuthSuccess( authResult: Authorization, - context: AuthSuccessContext, + context: AuthSuccessContext ): Promise { console.log("Outlook Calendar authentication successful", authResult); @@ -650,7 +646,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { // Retrieve and call the stored callback const callbackToken = await this.get( - `auth_callback_token:${context.token}`, + `auth_callback_token:${context.token}` ); if (callbackToken) { const authSuccessResult: CalendarAuth = {