veryfront 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/esm/cli/templates/manifest.js +37 -37
  2. package/esm/deno.d.ts +3 -0
  3. package/esm/deno.js +6 -3
  4. package/esm/src/agent/composition/composition.d.ts.map +1 -1
  5. package/esm/src/agent/composition/composition.js +13 -3
  6. package/esm/src/agent/factory.d.ts.map +1 -1
  7. package/esm/src/agent/factory.js +3 -3
  8. package/esm/src/agent/middleware/security/validator.d.ts +92 -0
  9. package/esm/src/agent/middleware/security/validator.d.ts.map +1 -0
  10. package/esm/src/agent/middleware/security/validator.js +187 -0
  11. package/esm/src/agent/runtime/index.d.ts +3 -2
  12. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  13. package/esm/src/agent/runtime/index.js +16 -8
  14. package/esm/src/agent/types.d.ts +4 -0
  15. package/esm/src/agent/types.d.ts.map +1 -1
  16. package/esm/src/channels/invoke.d.ts +427 -0
  17. package/esm/src/channels/invoke.d.ts.map +1 -0
  18. package/esm/src/channels/invoke.js +401 -0
  19. package/esm/src/embedding/embedding.js +2 -2
  20. package/esm/src/integrations/schema.d.ts +2 -2
  21. package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
  22. package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
  23. package/esm/src/oauth/handlers/init-handler.js +8 -2
  24. package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
  25. package/esm/src/platform/compat/opaque-deps.js +10 -1
  26. package/esm/src/prompt/factory.d.ts.map +1 -1
  27. package/esm/src/prompt/factory.js +9 -1
  28. package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
  29. package/esm/src/react/components/ai/markdown.js +4 -4
  30. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  31. package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
  32. package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
  33. package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
  34. package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
  35. package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
  36. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
  37. package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
  38. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts +11 -0
  39. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts.map +1 -0
  40. package/esm/src/server/handlers/request/channel-invoke.handler.js +72 -0
  41. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  42. package/esm/src/server/runtime-handler/index.js +2 -0
  43. package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
  44. package/esm/src/transforms/md/compiler/md-compiler.js +25 -1
  45. package/package.json +3 -1
  46. package/src/cli/templates/manifest.js +37 -37
  47. package/src/deno.js +6 -3
  48. package/src/src/agent/composition/composition.ts +15 -3
  49. package/src/src/agent/factory.ts +19 -6
  50. package/src/src/agent/middleware/security/validator.ts +288 -0
  51. package/src/src/agent/runtime/index.ts +26 -3
  52. package/src/src/agent/types.ts +4 -0
  53. package/src/src/channels/invoke.ts +521 -0
  54. package/src/src/embedding/embedding.ts +2 -2
  55. package/src/src/oauth/handlers/init-handler.ts +20 -5
  56. package/src/src/platform/compat/opaque-deps.ts +19 -4
  57. package/src/src/prompt/factory.ts +10 -1
  58. package/src/src/react/components/ai/markdown.tsx +5 -4
  59. package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
  60. package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
  61. package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
  62. package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
  63. package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
  64. package/src/src/server/runtime-handler/index.ts +2 -0
  65. package/src/src/transforms/md/compiler/md-compiler.ts +27 -1
@@ -91,7 +91,7 @@ export default {
91
91
  ".env.example": "# =============================================================================\n# Gmail Integration Setup\n# =============================================================================\n#\n# STEP 1: Create a Google Cloud Project\n# Visit: https://console.cloud.google.com/projectcreate\n#\n# STEP 2: Enable the Gmail API\n# Visit: https://console.cloud.google.com/apis/library/gmail.googleapis.com\n# Click \"Enable\" to activate the Gmail API for your project\n#\n# STEP 3: Configure OAuth Consent Screen\n# Visit: https://console.cloud.google.com/apis/credentials/consent\n# - Choose \"External\" user type (or \"Internal\" for Workspace)\n# - Fill in app name, support email\n# - Add scopes: gmail.readonly, gmail.send, gmail.modify\n# - Add your email as a test user (required for development)\n#\n# STEP 4: Create OAuth Credentials\n# Visit: https://console.cloud.google.com/apis/credentials\n# - Click \"Create Credentials\" > \"OAuth client ID\"\n# - Application type: \"Web application\"\n# - Add Authorized redirect URI: http://localhost:3000/api/auth/gmail/callback\n# - Copy the Client ID and Client Secret below\n#\n# =============================================================================\n\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n",
92
92
  "lib/gmail-client.ts": "/**\n * Gmail API Client\n *\n * Provides a type-safe interface to Gmail API operations\n * using the veryfront/oauth module for authentication.\n */\n\nimport { gmailConfig, OAuthService } from \"veryfront/oauth\";\nimport { tokenStore } from \"./token-store.ts\";\n\nexport interface GmailMessage {\n id: string;\n threadId: string;\n labelIds: string[];\n snippet: string;\n payload?: {\n headers: Array<{ name: string; value: string }>;\n body?: { data?: string; size: number };\n parts?: Array<{\n mimeType: string;\n body?: { data?: string; size: number };\n }>;\n };\n internalDate: string;\n}\n\nexport interface GmailMessageList {\n messages: Array<{ id: string; threadId: string }>;\n nextPageToken?: string;\n resultSizeEstimate: number;\n}\n\nexport interface SendEmailOptions {\n to: string | string[];\n subject: string;\n body: string;\n cc?: string | string[];\n bcc?: string | string[];\n replyTo?: string;\n isHtml?: boolean;\n}\n\nconst tokenStoreAdapter = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(\"current-user\", serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(\"current-user\", serviceId, tokens);\n },\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(\"current-user\", serviceId);\n },\n async getState(): Promise<null> {\n return null;\n },\n async setState(): Promise<void> {},\n async clearState(): Promise<void> {},\n};\n\nconst gmailService = new OAuthService(gmailConfig, tokenStoreAdapter);\n\nexport function createGmailClient(): {\n isConnected(): Promise<boolean>;\n listMessages(options?: {\n maxResults?: number;\n query?: string;\n labelIds?: string[];\n pageToken?: string;\n }): Promise<GmailMessageList>;\n getMessage(messageId: string, format?: \"full\" | \"metadata\" | \"minimal\"): Promise<GmailMessage>;\n sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }>;\n searchEmails(query: string, maxResults?: number): Promise<GmailMessage[]>;\n getUnreadEmails(maxResults?: number): Promise<GmailMessage[]>;\n markAsRead(messageId: string): Promise<void>;\n archiveEmail(messageId: string): Promise<void>;\n} {\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return gmailService.fetch<T>(endpoint, options);\n }\n\n function formatAddresses(addresses: string | string[] | undefined): string {\n if (!addresses) return \"\";\n return Array.isArray(addresses) ? addresses.join(\", \") : addresses;\n }\n\n return {\n async isConnected(): Promise<boolean> {\n const token = await gmailService.getAccessToken();\n return token !== null;\n },\n\n listMessages(\n options: {\n maxResults?: number;\n query?: string;\n labelIds?: string[];\n pageToken?: string;\n } = {},\n ): Promise<GmailMessageList> {\n const params = new URLSearchParams();\n\n if (options.maxResults != null) params.set(\"maxResults\", String(options.maxResults));\n if (options.query) params.set(\"q\", options.query);\n if (options.labelIds?.length) params.set(\"labelIds\", options.labelIds.join(\",\"));\n if (options.pageToken) params.set(\"pageToken\", options.pageToken);\n\n const query = params.toString();\n const url = query ? `/users/me/messages?${query}` : \"/users/me/messages\";\n\n return apiRequest<GmailMessageList>(url);\n },\n\n getMessage(messageId: string, format: \"full\" | \"metadata\" | \"minimal\" = \"full\"): Promise<GmailMessage> {\n return apiRequest<GmailMessage>(`/users/me/messages/${messageId}?format=${format}`);\n },\n\n sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }> {\n const toAddresses = formatAddresses(options.to);\n const ccAddresses = formatAddresses(options.cc);\n const bccAddresses = formatAddresses(options.bcc);\n\n const headers = [\n `To: ${toAddresses}`,\n `Subject: ${options.subject}`,\n options.isHtml ? \"Content-Type: text/html; charset=utf-8\" : \"Content-Type: text/plain; charset=utf-8\",\n ];\n\n if (ccAddresses) headers.push(`Cc: ${ccAddresses}`);\n if (bccAddresses) headers.push(`Bcc: ${bccAddresses}`);\n if (options.replyTo) headers.push(`Reply-To: ${options.replyTo}`);\n\n const email = `${headers.join(\"\\r\\n\")}\\r\\n\\r\\n${options.body}`;\n const encodedEmail = btoa(email).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n\n return apiRequest<{ id: string; threadId: string }>(\"/users/me/messages/send\", {\n method: \"POST\",\n body: JSON.stringify({ raw: encodedEmail }),\n });\n },\n\n async searchEmails(query: string, maxResults = 10): Promise<GmailMessage[]> {\n const list = await this.listMessages({ query, maxResults });\n if (!list.messages?.length) return [];\n return Promise.all(list.messages.map((m) => this.getMessage(m.id, \"metadata\")));\n },\n\n getUnreadEmails(maxResults = 10): Promise<GmailMessage[]> {\n return this.searchEmails(\"is:unread\", maxResults);\n },\n\n async markAsRead(messageId: string): Promise<void> {\n await apiRequest(`/users/me/messages/${messageId}/modify`, {\n method: \"POST\",\n body: JSON.stringify({ removeLabelIds: [\"UNREAD\"] }),\n });\n },\n\n async archiveEmail(messageId: string): Promise<void> {\n await apiRequest(`/users/me/messages/${messageId}/modify`, {\n method: \"POST\",\n body: JSON.stringify({ removeLabelIds: [\"INBOX\"] }),\n });\n },\n };\n}\n\nexport function parseEmailHeaders(\n headers: Array<{ name: string; value: string }>,\n): { from: string; to: string; subject: string; date: string } {\n function getHeader(name: string): string {\n return headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? \"\";\n }\n\n return {\n from: getHeader(\"From\"),\n to: getHeader(\"To\"),\n subject: getHeader(\"Subject\"),\n date: getHeader(\"Date\"),\n };\n}\n\nexport type GmailClient = ReturnType<typeof createGmailClient>;\n",
93
93
  "app/api/auth/gmail/route.ts": "import { createOAuthInitHandler, gmailConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(gmailConfig, { tokenStore: oauthMemoryTokenStore });\n",
94
- "app/api/auth/gmail/callback/route.ts": "import { createOAuthCallbackHandler, gmailConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gmailConfig, { tokenStore: hybridTokenStore });\n",
94
+ "app/api/auth/gmail/callback/route.ts": "import { createOAuthCallbackHandler, gmailConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gmailConfig, { tokenStore: hybridTokenStore });\n",
95
95
  "tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGmailClient } from \"../../lib/gmail-client.ts\";\n\nfunction formatRecipients(value?: string | string[]): string | undefined {\n if (!value) return undefined;\n return Array.isArray(value) ? value.join(\", \") : value;\n}\n\nexport default tool({\n id: \"send-email\",\n description: \"Send an email via Gmail. Can send to multiple recipients with CC and BCC support.\",\n inputSchema: z.object({\n to: z.union([z.string().email(), z.array(z.string().email())]).describe(\"Email recipient(s)\"),\n subject: z.string().min(1).describe(\"Email subject line\"),\n body: z.string().min(1).describe(\"Email body content\"),\n cc: z\n .union([z.string().email(), z.array(z.string().email())])\n .optional()\n .describe(\"CC recipient(s)\"),\n bcc: z\n .union([z.string().email(), z.array(z.string().email())])\n .optional()\n .describe(\"BCC recipient(s)\"),\n isHtml: z.boolean().default(false).describe(\"Whether the body contains HTML\"),\n }),\n execute: async ({ to, subject, body, cc, bcc, isHtml }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const gmail = createGmailClient(userId);\n\n const result = await gmail.sendEmail({ to, subject, body, cc, bcc, isHtml });\n\n const toFormatted = formatRecipients(to) ?? \"\";\n\n return {\n success: true,\n messageId: result.id,\n threadId: result.threadId,\n message: `Email sent successfully to ${toFormatted}.`,\n details: {\n to: toFormatted,\n subject,\n cc: formatRecipients(cc),\n bcc: formatRecipients(bcc),\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
96
96
  "tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGmailClient, parseEmailHeaders } from \"../../lib/gmail-client.ts\";\n\nexport default tool({\n id: \"search-emails\",\n description:\n \"Search emails using Gmail's search syntax. Supports queries like 'from:person@email.com', 'subject:meeting', 'after:2024/01/01', etc.\",\n inputSchema: z.object({\n query: z\n .string()\n .min(1)\n .describe(\n \"Search query using Gmail search syntax (e.g., 'from:boss@company.com subject:urgent')\",\n ),\n maxResults: z\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }),\n execute: async ({ query, maxResults }, context) => {\n const userId = context?.userId ?? \"current-user\";\n const gmail = createGmailClient(userId);\n\n try {\n const list = await gmail.listMessages({ query, maxResults });\n\n if (!list.messages?.length) {\n return {\n emails: [],\n query,\n message: `No emails found matching: \"${query}\"`,\n searchTips: [\n \"from:email@example.com - Search by sender\",\n \"to:email@example.com - Search by recipient\",\n \"subject:keywords - Search in subject\",\n \"after:YYYY/MM/DD - Emails after date\",\n \"before:YYYY/MM/DD - Emails before date\",\n \"is:unread - Unread emails only\",\n \"has:attachment - Emails with attachments\",\n ],\n };\n }\n\n const emails = await Promise.all(\n list.messages.map(async ({ id }) => {\n const message = await gmail.getMessage(id, \"metadata\");\n const headers = parseEmailHeaders(message.payload?.headers ?? []);\n\n return {\n id: message.id,\n threadId: message.threadId,\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: headers.date,\n snippet: message.snippet,\n isUnread: message.labelIds?.includes(\"UNREAD\") ?? false,\n labels: message.labelIds,\n };\n }),\n );\n\n return {\n emails,\n query,\n count: emails.length,\n message: `Found ${emails.length} email(s) matching: \"${query}\"`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
97
97
  "tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGmailClient, parseEmailHeaders } from \"../../lib/gmail-client.ts\";\n\nexport default tool({\n id: \"list-emails\",\n description:\n \"List recent emails from Gmail inbox. Returns email subjects, senders, and snippets.\",\n inputSchema: z.object({\n maxResults: z\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of emails to return\"),\n unreadOnly: z.boolean().default(false).describe(\"Only return unread emails\"),\n label: z\n .string()\n .optional()\n .describe(\"Filter by Gmail label (e.g., 'INBOX', 'IMPORTANT', 'STARRED')\"),\n }),\n execute: async ({ maxResults, unreadOnly, label }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const gmail = createGmailClient(userId);\n\n const list = await gmail.listMessages({\n maxResults,\n query: unreadOnly ? \"is:unread\" : undefined,\n labelIds: label ? [label] : undefined,\n });\n\n if (!list.messages?.length) {\n return { emails: [], message: \"No emails found matching your criteria.\" };\n }\n\n const emails = await Promise.all(\n list.messages.map(async ({ id }) => {\n const message = await gmail.getMessage(id, \"metadata\");\n const headers = parseEmailHeaders(message.payload?.headers ?? []);\n const labelIds = message.labelIds ?? [];\n\n return {\n id: message.id,\n threadId: message.threadId,\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: headers.date,\n snippet: message.snippet,\n isUnread: labelIds.includes(\"UNREAD\"),\n isStarred: labelIds.includes(\"STARRED\"),\n isImportant: labelIds.includes(\"IMPORTANT\"),\n };\n }),\n );\n\n return {\n emails,\n count: emails.length,\n message: `Found ${emails.length} email(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n"
@@ -102,7 +102,7 @@ export default {
102
102
  ".env.example": "# Google Sheets Integration\n# Create OAuth credentials at https://console.cloud.google.com/apis/credentials\n# Make sure to enable:\n# - Google Sheets API: https://console.cloud.google.com/apis/library/sheets.googleapis.com\n# - Google Drive API: https://console.cloud.google.com/apis/library/drive.googleapis.com\n\nGOOGLE_CLIENT_ID=your_client_id_here\nGOOGLE_CLIENT_SECRET=your_client_secret_here\n",
103
103
  "lib/sheets-client.ts": "/**\n * Google Sheets API Client\n *\n * Provides a type-safe interface to Google Sheets API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst SHEETS_API_BASE = \"https://sheets.googleapis.com/v4\";\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface Spreadsheet {\n spreadsheetId: string;\n properties: {\n title: string;\n locale: string;\n autoRecalc: string;\n timeZone: string;\n };\n sheets: Sheet[];\n spreadsheetUrl: string;\n}\n\nexport interface Sheet {\n properties: {\n sheetId: number;\n title: string;\n index: number;\n sheetType: \"GRID\" | \"OBJECT\";\n gridProperties?: {\n rowCount: number;\n columnCount: number;\n };\n };\n}\n\nexport interface SpreadsheetFile {\n id: string;\n name: string;\n mimeType: string;\n createdTime: string;\n modifiedTime: string;\n webViewLink: string;\n}\n\nexport interface CellData {\n values: unknown[][];\n range: string;\n}\n\nexport interface CreateSpreadsheetOptions {\n title: string;\n sheets?: Array<{\n title: string;\n rowCount?: number;\n columnCount?: number;\n }>;\n}\n\nexport interface WriteRangeOptions {\n spreadsheetId: string;\n range: string;\n values: unknown[][];\n valueInputOption?: \"RAW\" | \"USER_ENTERED\";\n}\n\nexport const sheetsOAuthProvider = {\n name: \"sheets\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/spreadsheets\",\n \"https://www.googleapis.com/auth/drive.readonly\",\n ],\n callbackPath: \"/api/auth/sheets/callback\",\n};\n\nexport function createSheetsClient(userId: string): {\n listSpreadsheets(options?: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n }): Promise<SpreadsheetFile[]>;\n getSpreadsheet(spreadsheetId: string): Promise<Spreadsheet>;\n readRange(spreadsheetId: string, range: string): Promise<CellData>;\n readRanges(spreadsheetId: string, ranges: string[]): Promise<CellData[]>;\n writeRange(options: WriteRangeOptions): Promise<{\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n }>;\n appendRange(\n spreadsheetId: string,\n range: string,\n values: unknown[][],\n valueInputOption?: \"RAW\" | \"USER_ENTERED\",\n ): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }>;\n clearRange(spreadsheetId: string, range: string): Promise<{ clearedRange: string }>;\n createSpreadsheet(options: CreateSpreadsheetOptions): Promise<Spreadsheet>;\n addSheet(\n spreadsheetId: string,\n title: string,\n options?: { rowCount?: number; columnCount?: number },\n ): Promise<Sheet>;\n deleteSheet(spreadsheetId: string, sheetId: number): Promise<void>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(sheetsOAuthProvider, userId, \"sheets\");\n if (token) return token;\n throw new Error(\"Google Sheets not connected. Please connect your Google account first.\");\n }\n\n async function apiRequest<T>(\n baseUrl: string,\n serviceName: \"Sheets\" | \"Drive\",\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`${serviceName} API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n function sheetsApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(SHEETS_API_BASE, \"Sheets\", endpoint, options);\n }\n\n function driveApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(DRIVE_API_BASE, \"Drive\", endpoint, options);\n }\n\n return {\n async listSpreadsheets(options: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n } = {}): Promise<SpreadsheetFile[]> {\n const params = new URLSearchParams({\n q: \"mimeType='application/vnd.google-apps.spreadsheet' and trashed=false\",\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink)\",\n pageSize: String(options.maxResults ?? 20),\n orderBy: `${options.orderBy ?? \"modifiedTime\"} desc`,\n });\n\n const result = await driveApiRequest<{ files?: SpreadsheetFile[] }>(`/files?${params.toString()}`);\n return result.files ?? [];\n },\n\n getSpreadsheet(spreadsheetId: string): Promise<Spreadsheet> {\n return sheetsApiRequest<Spreadsheet>(`/spreadsheets/${spreadsheetId}`);\n },\n\n async readRange(spreadsheetId: string, range: string): Promise<CellData> {\n const result = await sheetsApiRequest<{ values?: unknown[][]; range: string }>(\n `/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`,\n );\n\n return { values: result.values ?? [], range: result.range };\n },\n\n async readRanges(spreadsheetId: string, ranges: string[]): Promise<CellData[]> {\n const params = new URLSearchParams();\n ranges.forEach((range) => params.append(\"ranges\", range));\n\n const result = await sheetsApiRequest<{\n valueRanges: Array<{ values?: unknown[][]; range: string }>;\n }>(`/spreadsheets/${spreadsheetId}/values:batchGet?${params.toString()}`);\n\n return result.valueRanges.map((vr) => ({ values: vr.values ?? [], range: vr.range }));\n },\n\n writeRange(options: WriteRangeOptions): Promise<{\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n }> {\n const valueInputOption = options.valueInputOption ?? \"USER_ENTERED\";\n\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}/values/${encodeURIComponent(options.range)}?valueInputOption=${valueInputOption}`,\n {\n method: \"PUT\",\n body: JSON.stringify({ values: options.values }),\n },\n );\n },\n\n appendRange(\n spreadsheetId: string,\n range: string,\n values: unknown[][],\n valueInputOption: \"RAW\" | \"USER_ENTERED\" = \"USER_ENTERED\",\n ): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }> {\n return sheetsApiRequest(\n `/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:append?valueInputOption=${valueInputOption}`,\n {\n method: \"POST\",\n body: JSON.stringify({ values }),\n },\n );\n },\n\n clearRange(spreadsheetId: string, range: string): Promise<{ clearedRange: string }> {\n return sheetsApiRequest(`/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:clear`, {\n method: \"POST\",\n });\n },\n\n createSpreadsheet(options: CreateSpreadsheetOptions): Promise<Spreadsheet> {\n const body: {\n properties: { title: string };\n sheets?: Array<{\n properties: {\n title: string;\n gridProperties?: { rowCount: number; columnCount: number };\n };\n }>;\n } = { properties: { title: options.title } };\n\n if (options.sheets?.length) {\n body.sheets = options.sheets.map((sheet) => ({\n properties: {\n title: sheet.title,\n gridProperties: {\n rowCount: sheet.rowCount ?? 1000,\n columnCount: sheet.columnCount ?? 26,\n },\n },\n }));\n }\n\n return sheetsApiRequest(\"/spreadsheets\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n },\n\n async addSheet(\n spreadsheetId: string,\n title: string,\n options?: { rowCount?: number; columnCount?: number },\n ): Promise<Sheet> {\n const result = await sheetsApiRequest<{\n replies: Array<{ addSheet?: { properties: Sheet[\"properties\"] } }>;\n }>(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [\n {\n addSheet: {\n properties: {\n title,\n gridProperties: {\n rowCount: options?.rowCount ?? 1000,\n columnCount: options?.columnCount ?? 26,\n },\n },\n },\n },\n ],\n }),\n });\n\n const properties = result.replies[0]?.addSheet?.properties;\n if (!properties) throw new Error(\"Failed to add sheet\");\n\n return { properties };\n },\n\n async deleteSheet(spreadsheetId: string, sheetId: number): Promise<void> {\n await sheetsApiRequest(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{ deleteSheet: { sheetId } }],\n }),\n });\n },\n };\n}\n\nexport type SheetsClient = ReturnType<typeof createSheetsClient>;\n",
104
104
  "app/api/auth/sheets/route.ts": "import { createOAuthInitHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(sheetsConfig, { tokenStore: oauthMemoryTokenStore });\n",
105
- "app/api/auth/sheets/callback/route.ts": "import { createOAuthCallbackHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sheetsConfig, { tokenStore: hybridTokenStore });\n",
105
+ "app/api/auth/sheets/callback/route.ts": "import { createOAuthCallbackHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sheetsConfig, { tokenStore: hybridTokenStore });\n",
106
106
  "tools/read-range.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"read-range\",\n description:\n \"Read cell data from a Google Sheets range. Returns a 2D array of values. Use A1 notation (e.g., 'Sheet1!A1:D10', 'A1:B', or just 'Sheet1' for entire sheet).\",\n inputSchema: z.object({\n spreadsheetId: z.string().describe(\"The ID of the spreadsheet\"),\n range: z\n .string()\n .describe(\n \"Range in A1 notation (e.g., 'Sheet1!A1:D10', 'A1:B5', or 'Sheet1' for entire sheet)\",\n ),\n }),\n async execute({ spreadsheetId, range }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\n const { range: resultRange, values } = await client.readRange(\n spreadsheetId,\n range,\n );\n\n return {\n range: resultRange,\n values,\n rowCount: values.length,\n columnCount: values[0]?.length ?? 0,\n };\n },\n});\n",
107
107
  "tools/get-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"get-spreadsheet\",\n description:\n \"Get metadata about a Google Sheets spreadsheet including all sheet names, properties, and structure. Use this to discover available sheets and their dimensions.\",\n inputSchema: z.object({\n spreadsheetId: z\n .string()\n .describe(\"The ID of the spreadsheet (from URL or list-spreadsheets)\"),\n }),\n async execute({ spreadsheetId }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\n const spreadsheet = await client.getSpreadsheet(spreadsheetId);\n\n return {\n id: spreadsheet.spreadsheetId,\n title: spreadsheet.properties.title,\n url: spreadsheet.spreadsheetUrl,\n locale: spreadsheet.properties.locale,\n timeZone: spreadsheet.properties.timeZone,\n sheets: spreadsheet.sheets.map(({ properties }) => ({\n id: properties.sheetId,\n title: properties.title,\n index: properties.index,\n type: properties.sheetType,\n rowCount: properties.gridProperties?.rowCount,\n columnCount: properties.gridProperties?.columnCount,\n })),\n };\n },\n});\n",
108
108
  "tools/write-range.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"write-range\",\n description:\n \"Write data to a Google Sheets range. Overwrites existing content in the specified range. Provide data as a 2D array where each inner array is a row.\",\n inputSchema: z.object({\n spreadsheetId: z.string().describe(\"The ID of the spreadsheet\"),\n range: z\n .string()\n .describe(\n \"Range in A1 notation where to write data (e.g., 'Sheet1!A1', 'Sheet1!A1:D5')\",\n ),\n values: z\n .array(z.array(z.any()))\n .describe(\n \"2D array of values to write. Each inner array represents a row. Example: [['Name', 'Age'], ['John', 30], ['Jane', 25]]\",\n ),\n valueInputOption: z\n .enum([\"RAW\", \"USER_ENTERED\"])\n .default(\"USER_ENTERED\")\n .describe(\n \"RAW: Values are stored as-is. USER_ENTERED: Values are parsed as if typed by user (formulas, numbers, dates)\",\n ),\n }),\n async execute({ spreadsheetId, range, values, valueInputOption }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\n return client.writeRange({ spreadsheetId, range, values, valueInputOption });\n },\n});\n",
@@ -115,7 +115,7 @@ export default {
115
115
  ".env.example": "# Dropbox Integration Environment Variables\n\n# Dropbox App Key (Client ID)\n# Get this from https://www.dropbox.com/developers/apps\nDROPBOX_APP_KEY=your_app_key_here\n\n# Dropbox App Secret\n# Get this from https://www.dropbox.com/developers/apps\nDROPBOX_APP_SECRET=your_app_secret_here\n\n# Setup Instructions:\n# 1. Go to https://www.dropbox.com/developers/apps\n# 2. Create a new app or select an existing one\n# 3. Choose \"Scoped access\" as the API type\n# 4. Select \"Full Dropbox\" or \"App folder\" access\n# 5. Copy the App Key and App Secret\n# 6. Add the OAuth2 redirect URI: http://localhost:3000/api/auth/dropbox/callback\n# 7. Enable the following permissions in the Permissions tab:\n# - files.content.read\n# - files.content.write\n# - files.metadata.read\n# - files.metadata.write\n# - account_info.read\n# 8. Submit the app for production use if needed\n",
116
116
  "lib/dropbox-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst DROPBOX_API_URL = \"https://api.dropboxapi.com/2\";\nconst DROPBOX_CONTENT_URL = \"https://content.dropboxapi.com/2\";\n\nexport interface DropboxMetadata {\n \".tag\": \"file\" | \"folder\" | \"deleted\";\n name: string;\n path_lower?: string;\n path_display?: string;\n id: string;\n}\n\nexport interface DropboxFileMetadata extends DropboxMetadata {\n \".tag\": \"file\";\n client_modified: string;\n server_modified: string;\n rev: string;\n size: number;\n is_downloadable: boolean;\n content_hash?: string;\n}\n\nexport interface DropboxFolderMetadata extends DropboxMetadata {\n \".tag\": \"folder\";\n}\n\nexport interface ListFolderResult {\n entries: Array<DropboxFileMetadata | DropboxFolderMetadata>;\n cursor: string;\n has_more: boolean;\n}\n\nexport interface SearchResult {\n matches: Array<{\n match_type: {\n \".tag\": \"filename\" | \"content\" | \"both\";\n };\n metadata: {\n \".tag\": \"metadata\";\n metadata: DropboxFileMetadata | DropboxFolderMetadata;\n };\n }>;\n has_more: boolean;\n cursor?: string;\n}\n\nexport interface AccountInfo {\n account_id: string;\n name: {\n given_name: string;\n surname: string;\n familiar_name: string;\n display_name: string;\n };\n email: string;\n email_verified: boolean;\n disabled: boolean;\n country: string;\n locale: string;\n account_type: {\n \".tag\": \"basic\" | \"pro\" | \"business\";\n };\n}\n\nexport interface SpaceUsage {\n used: number;\n allocation: {\n \".tag\": \"individual\" | \"team\";\n allocated?: number;\n };\n}\n\nexport interface SharedLinkMetadata {\n url: string;\n id: string;\n name: string;\n path_lower?: string;\n link_permissions: {\n can_revoke: boolean;\n resolved_visibility?: {\n \".tag\": \"public\" | \"team_only\" | \"password\";\n };\n };\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (token) return token;\n throw new Error(\"Not authenticated with Dropbox. Please connect your account.\");\n}\n\nasync function parseDropboxError(response: Response): Promise<any> {\n return response.json().catch(() => ({}));\n}\n\nfunction throwDropboxError(response: Response, error: any): never {\n throw new Error(\n `Dropbox API error: ${response.status} ${error?.error_summary ?? response.statusText}`,\n );\n}\n\nasync function dropboxRPC<T>(\n endpoint: string,\n body: Record<string, unknown> = {},\n): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${DROPBOX_API_URL}${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n return response.json();\n}\n\nasync function dropboxContent<T>(\n endpoint: string,\n args: Record<string, unknown>,\n content?: string | Uint8Array,\n): Promise<T> {\n const token = await requireAccessToken();\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n \"Dropbox-API-Arg\": JSON.stringify(args),\n };\n\n if (content != null) {\n headers[\"Content-Type\"] = \"application/octet-stream\";\n }\n\n const response = await fetch(`${DROPBOX_CONTENT_URL}${endpoint}`, {\n method: \"POST\",\n headers,\n body: content,\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n return response.json();\n}\n\nexport function getCurrentAccount(): Promise<AccountInfo> {\n return dropboxRPC<AccountInfo>(\"/users/get_current_account\");\n}\n\nexport function getSpaceUsage(): Promise<SpaceUsage> {\n return dropboxRPC<SpaceUsage>(\"/users/get_space_usage\");\n}\n\nexport function listFolder(\n path: string = \"\",\n options?: {\n recursive?: boolean;\n includeDeleted?: boolean;\n includeHasExplicitSharedMembers?: boolean;\n limit?: number;\n },\n): Promise<ListFolderResult> {\n return dropboxRPC<ListFolderResult>(\"/files/list_folder\", {\n path: path || \"\",\n recursive: options?.recursive ?? false,\n include_deleted: options?.includeDeleted ?? false,\n include_has_explicit_shared_members: options?.includeHasExplicitSharedMembers ?? false,\n limit: options?.limit ?? 100,\n });\n}\n\nexport function listFolderContinue(cursor: string): Promise<ListFolderResult> {\n return dropboxRPC<ListFolderResult>(\"/files/list_folder/continue\", { cursor });\n}\n\nexport function getMetadata(\n path: string,\n options?: {\n includeMediaInfo?: boolean;\n includeDeleted?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<DropboxFileMetadata | DropboxFolderMetadata>(\"/files/get_metadata\", {\n path,\n include_media_info: options?.includeMediaInfo ?? false,\n include_deleted: options?.includeDeleted ?? false,\n });\n}\n\nexport async function downloadFile(path: string): Promise<{\n content: string;\n metadata: DropboxFileMetadata;\n}> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${DROPBOX_CONTENT_URL}/files/download`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Dropbox-API-Arg\": JSON.stringify({ path }),\n },\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n const content = await response.text();\n const metadataHeader = response.headers.get(\"Dropbox-API-Result\");\n const metadata = metadataHeader ? JSON.parse(metadataHeader) : {};\n\n return { content, metadata };\n}\n\nexport function uploadFile(\n path: string,\n content: string | Uint8Array,\n options?: {\n mode?: \"add\" | \"overwrite\" | \"update\";\n autorename?: boolean;\n mute?: boolean;\n },\n): Promise<DropboxFileMetadata> {\n return dropboxContent<DropboxFileMetadata>(\n \"/files/upload\",\n {\n path,\n mode: options?.mode ?? \"add\",\n autorename: options?.autorename ?? false,\n mute: options?.mute ?? false,\n },\n content,\n );\n}\n\nexport function deleteFile(\n path: string,\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/delete_v2\",\n { path },\n ).then((result) => result.metadata);\n}\n\nexport function moveFile(\n fromPath: string,\n toPath: string,\n options?: {\n allowSharedFolder?: boolean;\n autorename?: boolean;\n allowOwnershipTransfer?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/move_v2\",\n {\n from_path: fromPath,\n to_path: toPath,\n allow_shared_folder: options?.allowSharedFolder ?? false,\n autorename: options?.autorename ?? false,\n allow_ownership_transfer: options?.allowOwnershipTransfer ?? false,\n },\n ).then((result) => result.metadata);\n}\n\nexport function copyFile(\n fromPath: string,\n toPath: string,\n options?: {\n allowSharedFolder?: boolean;\n autorename?: boolean;\n allowOwnershipTransfer?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/copy_v2\",\n {\n from_path: fromPath,\n to_path: toPath,\n allow_shared_folder: options?.allowSharedFolder ?? false,\n autorename: options?.autorename ?? false,\n allow_ownership_transfer: options?.allowOwnershipTransfer ?? false,\n },\n ).then((result) => result.metadata);\n}\n\nexport function createFolder(\n path: string,\n autorename?: boolean,\n): Promise<DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFolderMetadata }>(\"/files/create_folder_v2\", {\n path,\n autorename: autorename ?? false,\n }).then((result) => result.metadata);\n}\n\nexport function searchFiles(\n query: string,\n options?: {\n path?: string;\n maxResults?: number;\n fileCategories?: Array<\n | \"image\"\n | \"document\"\n | \"pdf\"\n | \"spreadsheet\"\n | \"presentation\"\n | \"audio\"\n | \"video\"\n | \"folder\"\n | \"paper\"\n | \"others\"\n >;\n fileExtensions?: string[];\n },\n): Promise<SearchResult> {\n return dropboxRPC<SearchResult>(\"/files/search_v2\", {\n query,\n options: {\n path: options?.path ?? \"\",\n max_results: options?.maxResults ?? 20,\n file_categories: options?.fileCategories,\n filename_only: false,\n },\n });\n}\n\nexport async function createSharedLink(\n path: string,\n settings?: {\n requestedVisibility?: \"public\" | \"team_only\" | \"password\";\n linkPassword?: string;\n expires?: string;\n },\n): Promise<SharedLinkMetadata> {\n try {\n return await dropboxRPC<SharedLinkMetadata>(\"/sharing/create_shared_link_with_settings\", {\n path,\n settings: settings ?? {},\n });\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"shared_link_already_exists\")) {\n const links = await listSharedLinks(path);\n if (links.length > 0) return links[0];\n }\n throw error;\n }\n}\n\nexport async function listSharedLinks(path?: string): Promise<SharedLinkMetadata[]> {\n const result = await dropboxRPC<{ links: SharedLinkMetadata[] }>(\"/sharing/list_shared_links\", {\n path: path ?? \"\",\n });\n return result.links;\n}\n\nexport function formatFileSize(bytes: number): string {\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n}\n\nexport function isFile(metadata: DropboxMetadata): metadata is DropboxFileMetadata {\n return metadata[\".tag\"] === \"file\";\n}\n\nexport function isFolder(metadata: DropboxMetadata): metadata is DropboxFolderMetadata {\n return metadata[\".tag\"] === \"folder\";\n}\n",
117
117
  "app/api/auth/dropbox/route.ts": "import { createOAuthInitHandler, dropboxConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(dropboxConfig, { tokenStore: oauthMemoryTokenStore });\n",
118
- "app/api/auth/dropbox/callback/route.ts": "import { createOAuthCallbackHandler, dropboxConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(dropboxConfig, { tokenStore: hybridTokenStore });\n",
118
+ "app/api/auth/dropbox/callback/route.ts": "import { createOAuthCallbackHandler, dropboxConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(dropboxConfig, { tokenStore: hybridTokenStore });\n",
119
119
  "tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport {\n formatFileSize,\n getCurrentAccount,\n getSpaceUsage,\n} from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"get-account\",\n description:\n \"Get current Dropbox account information including user details and storage usage.\",\n inputSchema: z.object({\n includeSpaceUsage: z\n .boolean()\n .default(true)\n .describe(\"Whether to include storage usage information\"),\n }),\n async execute({ includeSpaceUsage }): Promise<Record<string, unknown>> {\n const account = await getCurrentAccount();\n\n const result: Record<string, unknown> = {\n accountId: account.account_id,\n name: {\n displayName: account.name.display_name,\n givenName: account.name.given_name,\n surname: account.name.surname,\n familiarName: account.name.familiar_name,\n },\n email: account.email,\n emailVerified: account.email_verified,\n accountType: account.account_type[\".tag\"],\n country: account.country,\n locale: account.locale,\n disabled: account.disabled,\n };\n\n if (!includeSpaceUsage) return result;\n\n try {\n const spaceUsage = await getSpaceUsage();\n const used = spaceUsage.used;\n const allocated = spaceUsage.allocation.allocated ?? 0;\n const hasAllocated = allocated > 0;\n\n result.storage = {\n used,\n usedFormatted: formatFileSize(used),\n allocated,\n allocatedFormatted: hasAllocated ? formatFileSize(allocated) : \"N/A\",\n allocationType: spaceUsage.allocation[\".tag\"],\n percentUsed: hasAllocated ? Math.round((used / allocated) * 100) : 0,\n available: hasAllocated ? allocated - used : 0,\n availableFormatted: hasAllocated\n ? formatFileSize(allocated - used)\n : \"N/A\",\n };\n } catch (error) {\n result.storageError =\n error instanceof Error ? error.message : \"Failed to get storage usage\";\n }\n\n return result;\n },\n});\n",
120
120
  "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatFileSize, isFile, searchFiles } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in Dropbox by name or content. Returns matching items with their paths and metadata.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query to find files or folders\"),\n path: z.string().optional().describe(\"Optional path to limit search to a specific folder\"),\n maxResults: z.number().min(1).max(100).default(20).describe(\"Maximum number of results to return\"),\n fileCategories: z\n .array(\n z.enum([\n \"image\",\n \"document\",\n \"pdf\",\n \"spreadsheet\",\n \"presentation\",\n \"audio\",\n \"video\",\n \"folder\",\n \"paper\",\n \"others\",\n ]),\n )\n .optional()\n .describe(\"Filter by file categories\"),\n }),\n async execute({ query, path, maxResults, fileCategories }) {\n const result = await searchFiles(query, { path, maxResults, fileCategories });\n\n const matches = result.matches.map((match) => {\n const metadata = match.metadata.metadata;\n const baseInfo = {\n name: metadata.name,\n path: metadata.path_display ?? metadata.path_lower ?? \"\",\n id: metadata.id,\n type: metadata[\".tag\"],\n matchType: match.match_type[\".tag\"],\n };\n\n if (!isFile(metadata)) {\n return baseInfo;\n }\n\n return {\n ...baseInfo,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n modified: metadata.server_modified,\n clientModified: metadata.client_modified,\n isDownloadable: metadata.is_downloadable,\n };\n });\n\n return {\n matches,\n count: matches.length,\n hasMore: result.has_more,\n query,\n };\n },\n});\n",
121
121
  "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { downloadFile, formatFileSize, getMetadata, isFile } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description:\n \"Get file metadata and optionally download file content from Dropbox. Use this to read file information or retrieve file contents.\",\n inputSchema: z.object({\n path: z.string().describe('Path to the file in Dropbox (e.g., \"/Documents/file.txt\")'),\n includeContent: z\n .boolean()\n .default(false)\n .describe(\"Whether to download and return the file content (only works for text files and small files)\"),\n }),\n async execute({ path, includeContent }): Promise<Record<string, unknown>> {\n const metadata = await getMetadata(path);\n\n if (!isFile(metadata)) {\n throw new Error(`Path \"${path}\" is not a file, it's a ${metadata[\".tag\"]}`);\n }\n\n const result: Record<string, unknown> = {\n name: metadata.name,\n path: metadata.path_display ?? metadata.path_lower ?? \"\",\n id: metadata.id,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n modified: metadata.server_modified,\n clientModified: metadata.client_modified,\n isDownloadable: metadata.is_downloadable,\n rev: metadata.rev,\n };\n\n if (!includeContent) {\n return result;\n }\n\n if (!metadata.is_downloadable) {\n throw new Error(`File \"${path}\" is not downloadable`);\n }\n\n const maxSize = 1024 * 1024;\n if (metadata.size > maxSize) {\n throw new Error(\n `File is too large to download content (${formatFileSize(\n metadata.size,\n )}). Maximum size is 1MB. Use includeContent: false to get metadata only.`,\n );\n }\n\n try {\n const { content } = await downloadFile(path);\n result.content = content;\n result.contentLength = content.length;\n } catch (error) {\n result.contentError = error instanceof Error ? error.message : \"Failed to download content\";\n }\n\n return result;\n },\n});\n",
@@ -137,7 +137,7 @@ export default {
137
137
  "files": {
138
138
  "lib/jira-client.ts": "import { getAccessToken, getCloudId } from \"./token-store.ts\";\n\nconst JIRA_API_VERSION = \"3\";\n\ninterface JiraResponse<T> {\n expand?: string;\n startAt?: number;\n maxResults?: number;\n total?: number;\n issues?: T[];\n values?: T[];\n}\n\nexport interface JiraIssue {\n id: string;\n key: string;\n self: string;\n fields: {\n summary: string;\n description?:\n | {\n type: string;\n content: unknown[];\n }\n | string;\n status: {\n name: string;\n statusCategory: {\n key: string;\n name: string;\n };\n };\n issuetype: {\n id: string;\n name: string;\n iconUrl: string;\n };\n priority?: {\n name: string;\n iconUrl: string;\n };\n assignee?: {\n displayName: string;\n emailAddress: string;\n accountId: string;\n };\n reporter?: {\n displayName: string;\n emailAddress: string;\n accountId: string;\n };\n created: string;\n updated: string;\n project: {\n id: string;\n key: string;\n name: string;\n };\n labels?: string[];\n [key: string]: unknown;\n };\n}\n\nexport interface JiraProject {\n id: string;\n key: string;\n name: string;\n projectTypeKey: string;\n self: string;\n avatarUrls?: Record<string, string>;\n lead?: {\n displayName: string;\n accountId: string;\n };\n}\n\nexport interface JiraIssueType {\n id: string;\n name: string;\n description: string;\n iconUrl: string;\n subtask: boolean;\n}\n\nexport interface JiraTransition {\n id: string;\n name: string;\n to: {\n id: string;\n name: string;\n };\n}\n\nfunction buildAdfDescription(text: string): Record<string, unknown> {\n return {\n type: \"doc\",\n version: 1,\n content: [\n {\n type: \"paragraph\",\n content: [\n {\n type: \"text\",\n text,\n },\n ],\n },\n ],\n };\n}\n\nasync function jiraFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Jira. Please connect your account.\");\n }\n\n const cloudId = await getCloudId();\n if (!cloudId) {\n throw new Error(\"Jira cloud ID not found. Please reconnect your account.\");\n }\n\n const baseUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/${JIRA_API_VERSION}`;\n const url = endpoint.startsWith(\"http\") ? endpoint : `${baseUrl}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({} as unknown));\n const message =\n (error as any)?.errorMessages?.join(\", \") ||\n (error as any)?.message ||\n response.statusText;\n\n throw new Error(`Jira API error: ${response.status} ${message}`);\n }\n\n if (response.status === 204) {\n return {} as T;\n }\n\n return response.json();\n}\n\nexport async function searchIssues(\n jql: string,\n options?: {\n fields?: string[];\n maxResults?: number;\n startAt?: number;\n },\n): Promise<{ issues: JiraIssue[]; total: number }> {\n const params = new URLSearchParams({\n jql,\n maxResults: String(options?.maxResults ?? 50),\n startAt: String(options?.startAt ?? 0),\n });\n\n if (options?.fields?.length) {\n params.set(\"fields\", options.fields.join(\",\"));\n }\n\n const response = await jiraFetch<JiraResponse<JiraIssue>>(\n `/search?${params.toString()}`,\n );\n\n return {\n issues: response.issues ?? [],\n total: response.total ?? 0,\n };\n}\n\nexport function getIssue(issueIdOrKey: string): Promise<JiraIssue> {\n return jiraFetch<JiraIssue>(`/issue/${issueIdOrKey}`);\n}\n\nexport async function createIssue(options: {\n projectKey: string;\n summary: string;\n description?: string;\n issueType: string;\n priority?: string;\n assigneeId?: string;\n labels?: string[];\n}): Promise<JiraIssue> {\n const fields: Record<string, unknown> = {\n project: { key: options.projectKey },\n summary: options.summary,\n issuetype: { name: options.issueType },\n };\n\n if (options.description) {\n fields.description = buildAdfDescription(options.description);\n }\n\n if (options.priority) {\n fields.priority = { name: options.priority };\n }\n\n if (options.assigneeId) {\n fields.assignee = { id: options.assigneeId };\n }\n\n if (options.labels?.length) {\n fields.labels = options.labels;\n }\n\n const response = await jiraFetch<{ id: string; key: string; self: string }>(\n \"/issue\",\n {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n },\n );\n\n return getIssue(response.key);\n}\n\nexport function updateIssue(\n issueIdOrKey: string,\n updates: {\n summary?: string;\n description?: string;\n priority?: string;\n assigneeId?: string;\n labels?: string[];\n },\n): Promise<void> {\n const fields: Record<string, unknown> = {};\n\n if (updates.summary) {\n fields.summary = updates.summary;\n }\n\n if (updates.description) {\n fields.description = buildAdfDescription(updates.description);\n }\n\n if (updates.priority) {\n fields.priority = { name: updates.priority };\n }\n\n if (updates.assigneeId) {\n fields.assignee = { id: updates.assigneeId };\n }\n\n if (updates.labels) {\n fields.labels = updates.labels;\n }\n\n return jiraFetch<void>(`/issue/${issueIdOrKey}`, {\n method: \"PUT\",\n body: JSON.stringify({ fields }),\n });\n}\n\nexport async function transitionIssue(\n issueIdOrKey: string,\n transitionId: string,\n): Promise<void> {\n await jiraFetch<void>(`/issue/${issueIdOrKey}/transitions`, {\n method: \"POST\",\n body: JSON.stringify({ transition: { id: transitionId } }),\n });\n}\n\nexport async function getIssueTransitions(\n issueIdOrKey: string,\n): Promise<JiraTransition[]> {\n const response = await jiraFetch<{ transitions: JiraTransition[] }>(\n `/issue/${issueIdOrKey}/transitions`,\n );\n return response.transitions ?? [];\n}\n\nexport async function listProjects(): Promise<JiraProject[]> {\n return jiraFetch<JiraProject[]>(\"/project\");\n}\n\nexport function getProject(projectIdOrKey: string): Promise<JiraProject> {\n return jiraFetch<JiraProject>(`/project/${projectIdOrKey}`);\n}\n\nexport async function getProjectIssueTypes(\n projectIdOrKey: string,\n): Promise<JiraIssueType[]> {\n return jiraFetch<JiraIssueType[]>(`/project/${projectIdOrKey}/statuses`);\n}\n\nexport function extractDescriptionText(description: unknown): string {\n if (typeof description === \"string\") {\n return description;\n }\n\n if (!description || typeof description !== \"object\") {\n return \"\";\n }\n\n const content = (description as { content?: unknown[] }).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n\n const texts: string[] = [];\n\n function extractText(node: any): void {\n if (node?.type === \"text\" && node.text) {\n texts.push(node.text);\n }\n\n if (Array.isArray(node?.content)) {\n node.content.forEach(extractText);\n }\n }\n\n content.forEach(extractText);\n return texts.join(\" \");\n}\n",
139
139
  "app/api/auth/jira/route.ts": "import { createOAuthInitHandler, jiraConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(jiraConfig, { tokenStore: oauthMemoryTokenStore });\n",
140
- "app/api/auth/jira/callback/route.ts": "import { createOAuthCallbackHandler, jiraConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): Promise<void> {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string): Promise<void> {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(jiraConfig, { tokenStore: hybridTokenStore });\n",
140
+ "app/api/auth/jira/callback/route.ts": "import { createOAuthCallbackHandler, jiraConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): Promise<void> {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string): Promise<void> {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(jiraConfig, { tokenStore: hybridTokenStore });\n",
141
141
  "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { extractDescriptionText, searchIssues } from \"../../lib/jira-client.ts\";\n\nexport default tool({\n id: \"search-issues\",\n description:\n 'Search for Jira issues using JQL (Jira Query Language). Returns matching issues with key details. Common JQL examples: \"assignee = currentUser() AND status != Done\", \"project = PROJ AND type = Bug\", \"created >= -7d\".',\n inputSchema: z.object({\n jql: z\n .string()\n .describe(\n 'JQL query string to search issues. Examples: \"assignee = currentUser()\", \"project = PROJ\", \"status = Open\"',\n ),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n 'Specific fields to include (e.g., [\"summary\", \"status\", \"assignee\"])',\n ),\n }),\n async execute({ jql, maxResults, fields }) {\n const result = await searchIssues(jql, { maxResults, fields });\n\n return {\n total: result.total,\n issues: result.issues.map((issue) => {\n const issueFields = issue.fields;\n\n return {\n key: issue.key,\n id: issue.id,\n summary: issueFields.summary,\n description: extractDescriptionText(issueFields.description),\n status: issueFields.status.name,\n statusCategory: issueFields.status.statusCategory.name,\n type: issueFields.issuetype.name,\n priority: issueFields.priority?.name,\n assignee: issueFields.assignee?.displayName,\n reporter: issueFields.reporter?.displayName,\n project: {\n key: issueFields.project.key,\n name: issueFields.project.name,\n },\n created: issueFields.created,\n updated: issueFields.updated,\n labels: issueFields.labels ?? [],\n };\n }),\n };\n },\n});\n",
142
142
  "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createIssue } from \"../../lib/jira-client.ts\";\n\nexport default tool({\n id: \"create-issue\",\n description:\n \"Create a new Jira issue in a project. Requires project key, summary, and issue type. Optionally set description, priority, assignee, and labels.\",\n inputSchema: z.object({\n projectKey: z.string().describe('The project key (e.g., \"PROJ\", \"DEV\")'),\n summary: z.string().describe(\"Brief summary/title of the issue\"),\n issueType: z.string().describe('Type of issue: \"Task\", \"Bug\", \"Story\", \"Epic\", etc.'),\n description: z.string().optional().describe(\"Detailed description of the issue\"),\n priority: z\n .string()\n .optional()\n .describe('Priority: \"Highest\", \"High\", \"Medium\", \"Low\", \"Lowest\"'),\n assigneeId: z.string().optional().describe(\"Atlassian account ID of the assignee (optional)\"),\n labels: z.array(z.string()).optional().describe(\"Array of labels to add to the issue\"),\n }),\n async execute({ projectKey, summary, issueType, description, priority, assigneeId, labels }) {\n const { key, id, fields } = await createIssue({\n projectKey,\n summary,\n issueType,\n description,\n priority,\n assigneeId,\n labels,\n });\n\n return {\n key,\n id,\n summary: fields.summary,\n status: fields.status.name,\n type: fields.issuetype.name,\n priority: fields.priority?.name,\n assignee: fields.assignee?.displayName,\n project: {\n key: fields.project.key,\n name: fields.project.name,\n },\n created: fields.created,\n message: `Issue ${key} created successfully`,\n };\n },\n});\n",
143
143
  "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listProjects } from \"../../lib/jira-client.ts\";\n\nexport default tool({\n id: \"list-projects\",\n description:\n \"List all accessible Jira projects in the connected site. Returns project keys, names, and basic information.\",\n inputSchema: z.object({}),\n async execute() {\n const projects = await listProjects();\n\n return {\n total: projects.length,\n projects: projects.map((project) => {\n const lead = project.lead\n ? {\n displayName: project.lead.displayName,\n accountId: project.lead.accountId,\n }\n : null;\n\n return {\n key: project.key,\n id: project.id,\n name: project.name,\n projectType: project.projectTypeKey,\n lead,\n avatarUrl: project.avatarUrls?.[\"48x48\"],\n };\n }),\n };\n },\n});\n",
@@ -173,7 +173,7 @@ export default {
173
173
  "lib/oauth.ts": "import { type OAuthToken, tokenStore } from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction getExpiresAt(expiresIn: unknown): number | undefined {\n if (typeof expiresIn !== \"number\") return undefined;\n return Date.now() + expiresIn * 1000;\n}\n\nasync function postForm(url: string, body: Record<string, string>): Promise<any> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body),\n });\n\n if (response.ok) return response.json();\n\n throw new Error(\n `Token request failed: ${response.status} - ${await response.text()}`,\n );\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await postForm(provider.tokenUrl, {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n });\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await postForm(provider.tokenUrl, {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n });\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? refreshToken,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired || !token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
174
174
  "lib/docs-client.ts": "/**\n * Google Docs API Client\n *\n * Provides a type-safe interface to Google Docs API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst DOCS_API_BASE = \"https://docs.googleapis.com/v1\";\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface Document {\n documentId: string;\n title: string;\n body: {\n content: StructuralElement[];\n };\n revisionId: string;\n suggestionsViewMode: string;\n documentStyle: DocumentStyle;\n}\n\nexport interface StructuralElement {\n startIndex: number;\n endIndex: number;\n paragraph?: Paragraph;\n table?: Table;\n sectionBreak?: SectionBreak;\n}\n\nexport interface Paragraph {\n elements: ParagraphElement[];\n paragraphStyle?: ParagraphStyle;\n bullet?: Bullet;\n}\n\nexport interface ParagraphElement {\n startIndex: number;\n endIndex: number;\n textRun?: TextRun;\n inlineObjectElement?: InlineObjectElement;\n}\n\nexport interface TextRun {\n content: string;\n textStyle?: TextStyle;\n}\n\nexport interface TextStyle {\n bold?: boolean;\n italic?: boolean;\n underline?: boolean;\n strikethrough?: boolean;\n fontSize?: Dimension;\n foregroundColor?: Color;\n backgroundColor?: Color;\n fontFamily?: string;\n link?: Link;\n}\n\nexport interface Link {\n url?: string;\n bookmarkId?: string;\n headingId?: string;\n}\n\nexport interface Dimension {\n magnitude: number;\n unit: string;\n}\n\nexport interface Color {\n rgbColor?: RgbColor;\n}\n\nexport interface RgbColor {\n red: number;\n green: number;\n blue: number;\n}\n\nexport interface ParagraphStyle {\n headingId?: string;\n namedStyleType?: string;\n alignment?: string;\n lineSpacing?: number;\n direction?: string;\n spacingMode?: string;\n spaceAbove?: Dimension;\n spaceBelow?: Dimension;\n indentFirstLine?: Dimension;\n indentStart?: Dimension;\n indentEnd?: Dimension;\n}\n\nexport interface Bullet {\n listId: string;\n nestingLevel?: number;\n textStyle?: TextStyle;\n}\n\nexport interface Table {\n rows: number;\n columns: number;\n tableRows: TableRow[];\n tableStyle?: TableStyle;\n}\n\nexport interface TableRow {\n startIndex: number;\n endIndex: number;\n tableCells: TableCell[];\n}\n\nexport interface TableCell {\n startIndex: number;\n endIndex: number;\n content: StructuralElement[];\n tableCellStyle?: TableCellStyle;\n}\n\nexport interface TableCellStyle {\n rowSpan?: number;\n columnSpan?: number;\n backgroundColor?: Color;\n borderLeft?: TableCellBorder;\n borderRight?: TableCellBorder;\n borderTop?: TableCellBorder;\n borderBottom?: TableCellBorder;\n paddingLeft?: Dimension;\n paddingRight?: Dimension;\n paddingTop?: Dimension;\n paddingBottom?: Dimension;\n}\n\nexport interface TableCellBorder {\n color?: Color;\n width?: Dimension;\n dashStyle?: string;\n}\n\nexport interface TableStyle {\n tableColumnProperties?: TableColumnProperties[];\n}\n\nexport interface TableColumnProperties {\n width?: Dimension;\n widthType?: string;\n}\n\nexport interface SectionBreak {\n sectionStyle?: SectionStyle;\n}\n\nexport interface SectionStyle {\n columnSeparatorStyle?: string;\n contentDirection?: string;\n marginTop?: Dimension;\n marginBottom?: Dimension;\n marginRight?: Dimension;\n marginLeft?: Dimension;\n pageNumberStart?: number;\n}\n\nexport interface DocumentStyle {\n background?: Background;\n pageNumberStart?: number;\n marginTop?: Dimension;\n marginBottom?: Dimension;\n marginRight?: Dimension;\n marginLeft?: Dimension;\n pageSize?: Size;\n marginHeader?: Dimension;\n marginFooter?: Dimension;\n useFirstPageHeaderFooter?: boolean;\n}\n\nexport interface Background {\n color?: Color;\n}\n\nexport interface Size {\n height?: Dimension;\n width?: Dimension;\n}\n\nexport interface InlineObjectElement {\n inlineObjectId: string;\n textStyle?: TextStyle;\n}\n\nexport interface DocumentFile {\n id: string;\n name: string;\n mimeType: string;\n createdTime: string;\n modifiedTime: string;\n webViewLink: string;\n iconLink?: string;\n thumbnailLink?: string;\n}\n\nexport interface CreateDocumentOptions {\n title: string;\n}\n\nexport interface BatchUpdateRequest {\n requests: Request[];\n}\n\nexport interface Request {\n insertText?: InsertTextRequest;\n deleteContentRange?: DeleteContentRangeRequest;\n replaceAllText?: ReplaceAllTextRequest;\n updateTextStyle?: UpdateTextStyleRequest;\n updateParagraphStyle?: UpdateParagraphStyleRequest;\n insertPageBreak?: InsertPageBreakRequest;\n insertTable?: InsertTableRequest;\n deleteTableRow?: DeleteTableRowRequest;\n deleteTableColumn?: DeleteTableColumnRequest;\n createParagraphBullets?: CreateParagraphBulletsRequest;\n deleteParagraphBullets?: DeleteParagraphBulletsRequest;\n}\n\nexport interface InsertTextRequest {\n text: string;\n location: Location;\n}\n\nexport interface DeleteContentRangeRequest {\n range: Range;\n}\n\nexport interface ReplaceAllTextRequest {\n containsText: ContainsText;\n replaceText: string;\n}\n\nexport interface UpdateTextStyleRequest {\n range: Range;\n textStyle: TextStyle;\n fields: string;\n}\n\nexport interface UpdateParagraphStyleRequest {\n range: Range;\n paragraphStyle: ParagraphStyle;\n fields: string;\n}\n\nexport interface InsertPageBreakRequest {\n location: Location;\n}\n\nexport interface InsertTableRequest {\n rows: number;\n columns: number;\n location: Location;\n}\n\nexport interface DeleteTableRowRequest {\n tableCellLocation: TableCellLocation;\n}\n\nexport interface DeleteTableColumnRequest {\n tableCellLocation: TableCellLocation;\n}\n\nexport interface CreateParagraphBulletsRequest {\n range: Range;\n bulletPreset: string;\n}\n\nexport interface DeleteParagraphBulletsRequest {\n range: Range;\n}\n\nexport interface Location {\n index: number;\n segmentId?: string;\n}\n\nexport interface Range {\n startIndex: number;\n endIndex: number;\n segmentId?: string;\n}\n\nexport interface ContainsText {\n text: string;\n matchCase: boolean;\n}\n\nexport interface TableCellLocation {\n tableStartLocation: Location;\n rowIndex: number;\n columnIndex: number;\n}\n\nexport interface BatchUpdateResponse {\n documentId: string;\n replies: Reply[];\n writeControl?: WriteControl;\n}\n\nexport interface Reply {\n [key: string]: unknown;\n}\n\nexport interface WriteControl {\n requiredRevisionId: string;\n targetRevisionId: string;\n}\n\n/**\n * Google Docs OAuth provider configuration\n */\nexport const docsOAuthProvider = {\n name: \"docs-google\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/documents.readonly\",\n \"https://www.googleapis.com/auth/documents\",\n \"https://www.googleapis.com/auth/drive.readonly\",\n ],\n callbackPath: \"/api/auth/docs-google/callback\",\n};\n\nexport function createDocsClient(userId: string): {\n listDocuments(options?: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n }): Promise<DocumentFile[]>;\n getDocument(documentId: string): Promise<Document>;\n createDocument(options: CreateDocumentOptions): Promise<Document>;\n updateDocument(documentId: string, requests: Request[]): Promise<BatchUpdateResponse>;\n insertText(documentId: string, text: string, index: number): Promise<BatchUpdateResponse>;\n deleteContent(documentId: string, startIndex: number, endIndex: number): Promise<BatchUpdateResponse>;\n replaceAllText(\n documentId: string,\n searchText: string,\n replaceText: string,\n matchCase?: boolean,\n ): Promise<BatchUpdateResponse>;\n searchDocuments(query: string, maxResults?: number): Promise<DocumentFile[]>;\n extractText(document: Document): string;\n createDocumentWithContent(title: string, content: string): Promise<Document>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(docsOAuthProvider, userId, \"docs-google\");\n if (!token) throw new Error(\"Google Docs not connected. Please connect your Google account first.\");\n return token;\n }\n\n async function apiRequest<T>(\n baseUrl: string,\n label: string,\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`${label} API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n function docsApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(DOCS_API_BASE, \"Docs\", endpoint, options);\n }\n\n function driveApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(DRIVE_API_BASE, \"Drive\", endpoint, options);\n }\n\n function extractText(document: Document): string {\n const textParts: string[] = [];\n\n function processElement(element: StructuralElement): void {\n if (element.paragraph) {\n for (const el of element.paragraph.elements) {\n if (el.textRun) textParts.push(el.textRun.content);\n }\n return;\n }\n\n if (!element.table) return;\n\n for (const row of element.table.tableRows) {\n for (const cell of row.tableCells) {\n for (const child of cell.content) processElement(child);\n }\n }\n }\n\n for (const element of document.body.content) processElement(element);\n return textParts.join(\"\");\n }\n\n async function listDocuments(options: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n } = {}): Promise<DocumentFile[]> {\n const params = new URLSearchParams({\n q: \"mimeType='application/vnd.google-apps.document' and trashed=false\",\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink,iconLink,thumbnailLink)\",\n pageSize: String(options.maxResults ?? 20),\n orderBy: `${options.orderBy ?? \"modifiedTime\"} desc`,\n });\n\n const result = await driveApiRequest<{ files: DocumentFile[] }>(`/files?${params.toString()}`);\n return result.files ?? [];\n }\n\n async function searchDocuments(query: string, maxResults = 20): Promise<DocumentFile[]> {\n const params = new URLSearchParams({\n q: `mimeType='application/vnd.google-apps.document' and trashed=false and fullText contains '${query}'`,\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink,iconLink,thumbnailLink)\",\n pageSize: String(maxResults),\n orderBy: \"modifiedTime desc\",\n });\n\n const result = await driveApiRequest<{ files: DocumentFile[] }>(`/files?${params.toString()}`);\n return result.files ?? [];\n }\n\n function getDocument(documentId: string): Promise<Document> {\n return docsApiRequest<Document>(`/documents/${documentId}`);\n }\n\n function createDocument(options: CreateDocumentOptions): Promise<Document> {\n return docsApiRequest<Document>(\"/documents\", {\n method: \"POST\",\n body: JSON.stringify({ title: options.title }),\n });\n }\n\n function updateDocument(documentId: string, requests: Request[]): Promise<BatchUpdateResponse> {\n return docsApiRequest<BatchUpdateResponse>(`/documents/${documentId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({ requests }),\n });\n }\n\n function insertText(documentId: string, text: string, index: number): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n insertText: {\n text,\n location: { index },\n },\n },\n ]);\n }\n\n function deleteContent(documentId: string, startIndex: number, endIndex: number): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n deleteContentRange: {\n range: { startIndex, endIndex },\n },\n },\n ]);\n }\n\n function replaceAllText(\n documentId: string,\n searchText: string,\n replaceText: string,\n matchCase = false,\n ): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n replaceAllText: {\n containsText: {\n text: searchText,\n matchCase,\n },\n replaceText,\n },\n },\n ]);\n }\n\n async function createDocumentWithContent(title: string, content: string): Promise<Document> {\n const doc = await createDocument({ title });\n await insertText(doc.documentId, content, 1);\n return getDocument(doc.documentId);\n }\n\n return {\n listDocuments,\n getDocument,\n createDocument,\n updateDocument,\n insertText,\n deleteContent,\n replaceAllText,\n searchDocuments,\n extractText,\n createDocumentWithContent,\n };\n}\n\nexport type DocsClient = ReturnType<typeof createDocsClient>;\n",
175
175
  "app/api/auth/docs-google/route.ts": "import { createOAuthInitHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(docsGoogleConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
176
- "app/api/auth/docs-google/callback/route.ts": "import { createOAuthCallbackHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(docsGoogleConfig, { tokenStore: hybridTokenStore });\n",
176
+ "app/api/auth/docs-google/callback/route.ts": "import { createOAuthCallbackHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(docsGoogleConfig, { tokenStore: hybridTokenStore });\n",
177
177
  "tools/update-document.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDocsClient, type Request } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"update-document\",\n description:\n \"Update a Google Docs document using batch requests. Supports inserting text, deleting content, replacing text, and more.\",\n inputSchema: z\n .object({\n documentId: z.string().describe(\"The ID of the document to update\"),\n requests: z\n .array(z.any())\n .describe(\n \"Array of batch update requests. See Google Docs API documentation for request types: insertText, deleteContentRange, replaceAllText, etc.\",\n ),\n })\n .or(\n z.object({\n documentId: z.string().describe(\"The ID of the document to update\"),\n operation: z\n .object({\n type: z\n .enum([\"insertText\", \"deleteContent\", \"replaceAllText\"])\n .describe(\"Type of operation to perform\"),\n insertText: z\n .object({\n text: z.string().describe(\"Text to insert\"),\n index: z.number().describe(\"Position to insert at (1 = start of document)\"),\n })\n .optional()\n .describe(\"Parameters for insertText operation\"),\n deleteContent: z\n .object({\n startIndex: z.number().describe(\"Start position of content to delete\"),\n endIndex: z.number().describe(\"End position of content to delete\"),\n })\n .optional()\n .describe(\"Parameters for deleteContent operation\"),\n replaceAllText: z\n .object({\n searchText: z.string().describe(\"Text to search for\"),\n replaceText: z.string().describe(\"Text to replace with\"),\n matchCase: z.boolean().default(false).describe(\"Whether to match case\"),\n })\n .optional()\n .describe(\"Parameters for replaceAllText operation\"),\n })\n .describe(\"Simple operation to perform\"),\n }),\n ),\n async execute(input): Promise<{\n documentId: string;\n success: true;\n replies: unknown;\n writeControl?: unknown;\n }> {\n const client = createDocsClient(DEFAULT_USER_ID);\n\n if (!(\"operation\" in input)) {\n const { documentId, requests } = input;\n const result = await client.updateDocument(documentId, requests as Request[]);\n\n return {\n documentId: result.documentId,\n success: true,\n replies: result.replies,\n writeControl: result.writeControl,\n };\n }\n\n const { documentId, operation } = input;\n\n switch (operation.type) {\n case \"insertText\": {\n const params = operation.insertText;\n if (!params) throw new Error(\"insertText parameters required\");\n\n const result = await client.insertText(documentId, params.text, params.index);\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n\n case \"deleteContent\": {\n const params = operation.deleteContent;\n if (!params) throw new Error(\"deleteContent parameters required\");\n\n const result = await client.deleteContent(documentId, params.startIndex, params.endIndex);\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n\n case \"replaceAllText\": {\n const params = operation.replaceAllText;\n if (!params) throw new Error(\"replaceAllText parameters required\");\n\n const result = await client.replaceAllText(\n documentId,\n params.searchText,\n params.replaceText,\n params.matchCase,\n );\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n\n default:\n throw new Error(`Unknown operation type: ${operation.type}`);\n }\n },\n});\n",
178
178
  "tools/search-documents.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"search-documents\",\n description:\n \"Search for Google Docs documents by query string. Searches document names and content. Returns matching document IDs, names, and metadata.\",\n inputSchema: z.object({\n query: z\n .string()\n .describe(\"Search query to find documents. Searches in document names and content.\"),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n }),\n async execute({ query, maxResults }) {\n const client = createDocsClient(DEFAULT_USER_ID);\n const documents = await client.searchDocuments(query, maxResults);\n\n return documents.map((document) => ({\n id: document.id,\n name: document.name,\n url: document.webViewLink,\n createdTime: document.createdTime,\n modifiedTime: document.modifiedTime,\n thumbnail: document.thumbnailLink,\n }));\n },\n});\n",
179
179
  "tools/create-document.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"create-document\",\n description:\n \"Create a new Google Docs document with optional initial content. Returns the new document ID and URL.\",\n inputSchema: z.object({\n title: z.string().describe(\"Title of the new document\"),\n content: z\n .string()\n .optional()\n .describe(\"Optional initial text content to insert into the document\"),\n }),\n async execute({ title, content }) {\n const client = createDocsClient(DEFAULT_USER_ID);\n\n const document = content\n ? await client.createDocumentWithContent(title, content)\n : await client.createDocument({ title });\n\n const [docMeta] = await client.listDocuments({ maxResults: 1 });\n const webViewLink = docMeta?.id === document.documentId ? docMeta.webViewLink : undefined;\n\n return {\n documentId: document.documentId,\n title: document.title,\n url: webViewLink ?? `https://docs.google.com/document/d/${document.documentId}/edit`,\n revisionId: document.revisionId,\n };\n },\n});\n",
@@ -197,7 +197,7 @@ export default {
197
197
  "files": {
198
198
  "lib/github-client.ts": "/**\n * GitHub API Client\n *\n * Provides a type-safe interface to GitHub API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n\n return undefined;\n}\n\nconst GITHUB_API_BASE = \"https://api.github.com\";\n\nexport interface GitHubRepo {\n id: number;\n name: string;\n full_name: string;\n description: string | null;\n private: boolean;\n html_url: string;\n default_branch: string;\n language: string | null;\n stargazers_count: number;\n forks_count: number;\n open_issues_count: number;\n updated_at: string;\n}\n\nexport interface GitHubPullRequest {\n id: number;\n number: number;\n title: string;\n body: string | null;\n state: \"open\" | \"closed\";\n html_url: string;\n user: { login: string; avatar_url: string };\n created_at: string;\n updated_at: string;\n head: { ref: string; sha: string };\n base: { ref: string };\n mergeable: boolean | null;\n additions: number;\n deletions: number;\n changed_files: number;\n draft: boolean;\n labels: Array<{ name: string; color: string }>;\n}\n\nexport interface GitHubIssue {\n id: number;\n number: number;\n title: string;\n body: string | null;\n state: \"open\" | \"closed\";\n html_url: string;\n user: { login: string };\n created_at: string;\n updated_at: string;\n labels: Array<{ name: string; color: string }>;\n assignees: Array<{ login: string }>;\n}\n\nexport interface GitHubCommit {\n sha: string;\n commit: {\n message: string;\n author: { name: string; date: string };\n };\n html_url: string;\n author: { login: string; avatar_url: string } | null;\n}\n\n/**\n * GitHub OAuth provider configuration\n */\nexport const githubOAuthProvider = {\n name: \"github\",\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n tokenUrl: \"https://github.com/login/oauth/access_token\",\n clientId: getEnv(\"GITHUB_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GITHUB_CLIENT_SECRET\") ?? \"\",\n scopes: [\"repo\", \"read:user\", \"read:org\"],\n callbackPath: \"/api/auth/github/callback\",\n};\n\nexport function createGitHubClient(userId: string): {\n listRepos(options?: {\n sort?: \"created\" | \"updated\" | \"pushed\" | \"full_name\";\n perPage?: number;\n type?: \"all\" | \"owner\" | \"public\" | \"private\" | \"member\";\n }): Promise<GitHubRepo[]>;\n listPullRequests(\n owner: string,\n repo: string,\n options?: { state?: \"open\" | \"closed\" | \"all\"; perPage?: number },\n ): Promise<GitHubPullRequest[]>;\n getPullRequest(owner: string, repo: string, pullNumber: number): Promise<GitHubPullRequest>;\n getPullRequestDiff(owner: string, repo: string, pullNumber: number): Promise<string>;\n createIssue(\n owner: string,\n repo: string,\n options: { title: string; body?: string; labels?: string[]; assignees?: string[] },\n ): Promise<GitHubIssue>;\n listIssues(\n owner: string,\n repo: string,\n options?: { state?: \"open\" | \"closed\" | \"all\"; perPage?: number },\n ): Promise<GitHubIssue[]>;\n listCommits(\n owner: string,\n repo: string,\n options?: { sha?: string; perPage?: number },\n ): Promise<GitHubCommit[]>;\n getUser(): Promise<{ login: string; name: string; email: string }>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(githubOAuthProvider, userId, \"github\");\n if (!token) throw new Error(\"GitHub not connected. Please connect your GitHub account first.\");\n return token;\n }\n\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`GitHub API error: ${response.status} - ${error}`);\n }\n\n return response.json() as Promise<T>;\n }\n\n async function apiTextRequest(endpoint: string, accept: string): Promise<string> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: accept,\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n });\n\n if (!response.ok) throw new Error(`GitHub API error: ${response.status}`);\n\n return response.text();\n }\n\n function toQueryString(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n }\n\n return {\n listRepos(options = {}): Promise<GitHubRepo[]> {\n const params = new URLSearchParams();\n if (options.sort) params.set(\"sort\", options.sort);\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n if (options.type) params.set(\"type\", options.type);\n\n return apiRequest<GitHubRepo[]>(`/user/repos${toQueryString(params)}`);\n },\n\n listPullRequests(owner, repo, options = {}): Promise<GitHubPullRequest[]> {\n const params = new URLSearchParams();\n params.set(\"state\", options.state ?? \"open\");\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubPullRequest[]>(\n `/repos/${owner}/${repo}/pulls${toQueryString(params)}`,\n );\n },\n\n getPullRequest(owner, repo, pullNumber): Promise<GitHubPullRequest> {\n return apiRequest<GitHubPullRequest>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);\n },\n\n getPullRequestDiff(owner, repo, pullNumber): Promise<string> {\n return apiTextRequest(\n `/repos/${owner}/${repo}/pulls/${pullNumber}`,\n \"application/vnd.github.diff\",\n );\n },\n\n createIssue(owner, repo, options): Promise<GitHubIssue> {\n return apiRequest<GitHubIssue>(`/repos/${owner}/${repo}/issues`, {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n },\n\n listIssues(owner, repo, options = {}): Promise<GitHubIssue[]> {\n const params = new URLSearchParams();\n params.set(\"state\", options.state ?? \"open\");\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubIssue[]>(`/repos/${owner}/${repo}/issues${toQueryString(params)}`);\n },\n\n listCommits(owner, repo, options = {}): Promise<GitHubCommit[]> {\n const params = new URLSearchParams();\n if (options.sha) params.set(\"sha\", options.sha);\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubCommit[]>(`/repos/${owner}/${repo}/commits${toQueryString(params)}`);\n },\n\n getUser(): Promise<{ login: string; name: string; email: string }> {\n return apiRequest(\"/user\");\n },\n };\n}\n\nexport type GitHubClient = ReturnType<typeof createGitHubClient>;\n",
199
199
  "app/api/auth/github/route.ts": "import { createOAuthInitHandler, githubConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(githubConfig, { tokenStore: oauthMemoryTokenStore });\n",
200
- "app/api/auth/github/callback/route.ts": "import { createOAuthCallbackHandler, githubConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(githubConfig, { tokenStore: hybridTokenStore });\n",
200
+ "app/api/auth/github/callback/route.ts": "import { createOAuthCallbackHandler, githubConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(githubConfig, { tokenStore: hybridTokenStore });\n",
201
201
  "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGitHubClient } from \"../../lib/github-client.ts\";\n\nexport default tool({\n id: \"create-issue\",\n description: \"Create a new issue in a GitHub repository\",\n inputSchema: z.object({\n repo: z\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n title: z.string().min(1).describe(\"Issue title\"),\n body: z\n .string()\n .optional()\n .describe(\"Issue body/description (supports Markdown)\"),\n labels: z.array(z.string()).optional().describe(\"Labels to add to the issue\"),\n assignees: z\n .array(z.string())\n .optional()\n .describe(\"GitHub usernames to assign to the issue\"),\n }),\n execute: async ({ repo, title, body, labels, assignees }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const issue = await github.createIssue(owner, repoName, {\n title,\n body,\n labels,\n assignees,\n });\n\n return {\n success: true,\n issue: {\n number: issue.number,\n title: issue.title,\n url: issue.html_url,\n state: issue.state,\n labels: issue.labels.map((l: { name: string }) => l.name),\n assignees: issue.assignees.map((a: { login: string }) => a.login),\n },\n message: `Issue #${issue.number} created successfully in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
202
202
  "tools/list-prs.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGitHubClient } from \"../../lib/github-client.ts\";\n\ntype PullRequest = {\n number: number;\n title: string;\n state: string;\n draft: boolean;\n html_url: string;\n user: { login: string };\n created_at: string;\n updated_at: string;\n head: { ref: string };\n base: { ref: string };\n additions: number;\n deletions: number;\n changed_files: number;\n labels: Array<{ name: string }>;\n};\n\nexport default tool({\n id: \"list-prs\",\n description: \"List pull requests for a GitHub repository\",\n inputSchema: z.object({\n repo: z\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n state: z\n .enum([\"open\", \"closed\", \"all\"])\n .default(\"open\")\n .describe(\"State of pull requests to list\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of pull requests to return\"),\n }),\n execute: async ({ repo, state, limit }, context) => {\n const userId = context?.userId ?? \"current-user\";\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const prs = await github.listPullRequests(owner, repoName, {\n state,\n perPage: limit,\n });\n\n return {\n pullRequests: prs.map((pr: PullRequest) => ({\n number: pr.number,\n title: pr.title,\n state: pr.state,\n isDraft: pr.draft,\n url: pr.html_url,\n author: pr.user.login,\n createdAt: pr.created_at,\n updatedAt: pr.updated_at,\n sourceBranch: pr.head.ref,\n targetBranch: pr.base.ref,\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n labels: pr.labels.map(({ name }) => name),\n })),\n count: prs.length,\n repository: repo,\n message: `Found ${prs.length} ${state} pull request(s) in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
203
203
  "tools/list-repos.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createGitHubClient } from \"../../lib/github-client.ts\";\n\ntype GitHubRepo = {\n name: string;\n full_name: string;\n description: string | null;\n private: boolean;\n html_url: string;\n default_branch: string;\n language: string | null;\n stargazers_count: number;\n forks_count: number;\n open_issues_count: number;\n updated_at: string;\n};\n\nexport default tool({\n id: \"list-repos\",\n description: \"List GitHub repositories for the authenticated user\",\n inputSchema: z.object({\n type: z\n .enum([\"all\", \"owner\", \"public\", \"private\", \"member\"])\n .default(\"all\")\n .describe(\"Type of repositories to list\"),\n sort: z\n .enum([\"created\", \"updated\", \"pushed\", \"full_name\"])\n .default(\"updated\")\n .describe(\"How to sort the repositories\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of repositories to return\"),\n }),\n execute: async ({ type, sort, limit }, context) => {\n // Default to \"current-user\" for development; in production, always pass userId from session\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const github = createGitHubClient(userId);\n const repos = await github.listRepos({ type, sort, perPage: limit });\n\n return {\n repositories: repos.map((repo: GitHubRepo) => ({\n name: repo.name,\n fullName: repo.full_name,\n description: repo.description ?? null,\n isPrivate: repo.private,\n url: repo.html_url,\n defaultBranch: repo.default_branch,\n language: repo.language,\n stars: repo.stargazers_count,\n forks: repo.forks_count,\n openIssues: repo.open_issues_count,\n updatedAt: repo.updated_at,\n })),\n count: repos.length,\n message: `Found ${repos.length} repository(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
@@ -209,7 +209,7 @@ export default {
209
209
  ".env.example": "# Xero OAuth Configuration\n# Get your credentials from https://developer.xero.com/app/manage\nXERO_CLIENT_ID=your-client-id\nXERO_CLIENT_SECRET=your-client-secret\n",
210
210
  "lib/xero-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst XERO_BASE_URL = \"https://api.xero.com/api.xro/2.0\";\n\ninterface XeroResponse<T> {\n Id: string;\n Status: string;\n ProviderName: string;\n DateTimeUTC: string;\n [key: string]: T | string;\n}\n\ninterface XeroInvoice {\n InvoiceID: string;\n InvoiceNumber: string;\n Type: \"ACCREC\" | \"ACCPAY\";\n Status: string;\n LineAmountTypes: string;\n Contact: {\n ContactID: string;\n Name: string;\n };\n LineItems: Array<{\n LineItemID?: string;\n Description: string;\n Quantity: number;\n UnitAmount: number;\n AccountCode?: string;\n TaxType?: string;\n LineAmount: number;\n }>;\n Date: string;\n DueDate: string;\n SubTotal: number;\n TotalTax: number;\n Total: number;\n AmountDue: number;\n AmountPaid: number;\n CurrencyCode: string;\n Reference?: string;\n UpdatedDateUTC: string;\n}\n\ninterface XeroContact {\n ContactID: string;\n ContactNumber?: string;\n Name: string;\n FirstName?: string;\n LastName?: string;\n EmailAddress?: string;\n Addresses?: Array<{\n AddressType: string;\n City?: string;\n Region?: string;\n PostalCode?: string;\n Country?: string;\n AttentionTo?: string;\n }>;\n Phones?: Array<{\n PhoneType: string;\n PhoneNumber?: string;\n PhoneAreaCode?: string;\n PhoneCountryCode?: string;\n }>;\n UpdatedDateUTC: string;\n IsSupplier: boolean;\n IsCustomer: boolean;\n}\n\ninterface XeroTenant {\n tenantId: string;\n tenantType: string;\n tenantName: string;\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Xero. Please connect your account.\");\n return token;\n}\n\nasync function getTenantId(token: string): Promise<string> {\n const response = await fetch(\"https://api.xero.com/connections\", {\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) throw new Error(`Failed to get Xero tenants: ${response.status}`);\n\n const tenants: XeroTenant[] = await response.json();\n const tenantId = tenants[0]?.tenantId;\n\n if (!tenantId) {\n throw new Error(\"No Xero organizations found. Please connect to a Xero organization.\");\n }\n\n return tenantId;\n}\n\nfunction getCollection<T extends Record<string, unknown>, K extends keyof T>(\n response: XeroResponse<T>,\n key: K,\n): T[K] {\n return response[key] as T[K];\n}\n\nasync function xeroFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n const tenantId = await getTenantId(token);\n\n const response = await fetch(`${XERO_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"xero-tenant-id\": tenantId,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(options.headers ?? {}),\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n Message?: string;\n Detail?: string;\n };\n\n const message = error.Message ?? error.Detail ?? response.statusText;\n throw new Error(`Xero API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listInvoices(options?: {\n status?: \"DRAFT\" | \"SUBMITTED\" | \"AUTHORISED\" | \"PAID\" | \"VOIDED\";\n type?: \"ACCREC\" | \"ACCPAY\";\n contactId?: string;\n limit?: number;\n}): Promise<XeroInvoice[]> {\n const params = new URLSearchParams();\n const where: string[] = [];\n\n if (options?.status) where.push(`Status == \"${options.status}\"`);\n if (options?.type) where.push(`Type == \"${options.type}\"`);\n if (options?.contactId) where.push(`Contact.ContactID == Guid(\"${options.contactId}\")`);\n\n if (where.length) params.set(\"where\", where.join(\" AND \"));\n if (options?.limit) params.set(\"page\", \"1\");\n\n const queryString = params.toString();\n const endpoint = `/Invoices${queryString ? `?${queryString}` : \"\"}`;\n\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(endpoint);\n const invoices = (getCollection(response, \"Invoices\") as XeroInvoice[] | undefined) ?? [];\n\n return options?.limit ? invoices.slice(0, options.limit) : invoices;\n}\n\nexport async function getInvoice(invoiceId: string): Promise<XeroInvoice> {\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(\n `/Invoices/${invoiceId}`,\n );\n\n const invoices = getCollection(response, \"Invoices\") as XeroInvoice[] | undefined;\n const invoice = invoices?.[0];\n\n if (!invoice) throw new Error(`Invoice not found: ${invoiceId}`);\n return invoice;\n}\n\nexport async function createInvoice(options: {\n contactId: string;\n type: \"ACCREC\" | \"ACCPAY\";\n date: string;\n dueDate: string;\n lineItems: Array<{\n description: string;\n quantity: number;\n unitAmount: number;\n accountCode?: string;\n taxType?: string;\n }>;\n reference?: string;\n status?: \"DRAFT\" | \"SUBMITTED\" | \"AUTHORISED\";\n}): Promise<XeroInvoice> {\n const invoice = {\n Type: options.type,\n Contact: { ContactID: options.contactId },\n Date: options.date,\n DueDate: options.dueDate,\n LineItems: options.lineItems.map((item) => ({\n Description: item.description,\n Quantity: item.quantity,\n UnitAmount: item.unitAmount,\n AccountCode: item.accountCode,\n TaxType: item.taxType ?? \"NONE\",\n })),\n Reference: options.reference,\n Status: options.status ?? \"DRAFT\",\n LineAmountTypes: \"Exclusive\",\n };\n\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(\"/Invoices\", {\n method: \"POST\",\n body: JSON.stringify({ Invoices: [invoice] }),\n });\n\n const invoices = getCollection(response, \"Invoices\") as XeroInvoice[] | undefined;\n const created = invoices?.[0];\n\n if (!created) throw new Error(\"Failed to create invoice\");\n return created;\n}\n\nexport async function listContacts(options?: {\n isCustomer?: boolean;\n isSupplier?: boolean;\n limit?: number;\n}): Promise<XeroContact[]> {\n const params = new URLSearchParams();\n const where: string[] = [];\n\n if (options?.isCustomer !== undefined) where.push(`IsCustomer == ${options.isCustomer}`);\n if (options?.isSupplier !== undefined) where.push(`IsSupplier == ${options.isSupplier}`);\n\n if (where.length) params.set(\"where\", where.join(\" AND \"));\n\n const queryString = params.toString();\n const endpoint = `/Contacts${queryString ? `?${queryString}` : \"\"}`;\n\n const response = await xeroFetch<XeroResponse<{ Contacts: XeroContact[] }>>(endpoint);\n const contacts = (getCollection(response, \"Contacts\") as XeroContact[] | undefined) ?? [];\n\n return options?.limit ? contacts.slice(0, options.limit) : contacts;\n}\n\nexport async function getContact(contactId: string): Promise<XeroContact> {\n const response = await xeroFetch<XeroResponse<{ Contacts: XeroContact[] }>>(\n `/Contacts/${contactId}`,\n );\n\n const contacts = getCollection(response, \"Contacts\") as XeroContact[] | undefined;\n const contact = contacts?.[0];\n\n if (!contact) throw new Error(`Contact not found: ${contactId}`);\n return contact;\n}\n\nexport async function getCurrentUser(): Promise<{\n userId: string;\n userName: string;\n email: string;\n}> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${XERO_BASE_URL}/Organisation`, {\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) throw new Error(`Failed to get user info: ${response.status}`);\n\n const data: { Organisations: Array<{ Name: string }> } = await response.json();\n\n return {\n userId: \"current-user\",\n userName: data.Organisations[0]?.Name ?? \"Xero User\",\n email: \"user@xero.com\",\n };\n}\n",
211
211
  "app/api/auth/xero/route.ts": "import { createOAuthInitHandler, xeroConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(xeroConfig, { tokenStore: oauthMemoryTokenStore });\n",
212
- "app/api/auth/xero/callback/route.ts": "import { createOAuthCallbackHandler, xeroConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(xeroConfig, { tokenStore: hybridTokenStore });\n",
212
+ "app/api/auth/xero/callback/route.ts": "import { createOAuthCallbackHandler, xeroConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(xeroConfig, { tokenStore: hybridTokenStore });\n",
213
213
  "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listContacts } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description: \"List contacts from Xero. Can filter by customer or supplier type.\",\n inputSchema: z.object({\n isCustomer: z.boolean().optional().describe(\"Filter for customers only\"),\n isSupplier: z.boolean().optional().describe(\"Filter for suppliers only\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of contacts to return\"),\n }),\n async execute({ isCustomer, isSupplier, limit }) {\n const contacts = await listContacts({ isCustomer, isSupplier, limit });\n\n return contacts.map((contact) => ({\n contactId: contact.ContactID,\n contactNumber: contact.ContactNumber,\n name: contact.Name,\n firstName: contact.FirstName,\n lastName: contact.LastName,\n emailAddress: contact.EmailAddress,\n isCustomer: contact.IsCustomer,\n isSupplier: contact.IsSupplier,\n addresses: contact.Addresses?.map((addr) => ({\n addressType: addr.AddressType,\n city: addr.City,\n region: addr.Region,\n postalCode: addr.PostalCode,\n country: addr.Country,\n })),\n phones: contact.Phones?.map((phone) => ({\n phoneType: phone.PhoneType,\n phoneNumber: phone.PhoneNumber,\n })),\n }));\n },\n});\n",
214
214
  "tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getContact } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"get-contact\",\n description: \"Get details of a specific Xero contact by its ID.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The ID of the contact to retrieve\"),\n }),\n async execute({ contactId }) {\n const contact = await getContact(contactId);\n\n return {\n contactId: contact.ContactID,\n contactNumber: contact.ContactNumber,\n name: contact.Name,\n firstName: contact.FirstName,\n lastName: contact.LastName,\n emailAddress: contact.EmailAddress,\n isCustomer: contact.IsCustomer,\n isSupplier: contact.IsSupplier,\n addresses: contact.Addresses?.map((addr) => ({\n addressType: addr.AddressType,\n city: addr.City,\n region: addr.Region,\n postalCode: addr.PostalCode,\n country: addr.Country,\n attentionTo: addr.AttentionTo,\n })),\n phones: contact.Phones?.map((phone) => ({\n phoneType: phone.PhoneType,\n phoneNumber: phone.PhoneNumber,\n phoneAreaCode: phone.PhoneAreaCode,\n phoneCountryCode: phone.PhoneCountryCode,\n })),\n updatedDateUTC: contact.UpdatedDateUTC,\n };\n },\n});\n",
215
215
  "tools/list-invoices.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listInvoices } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"list-invoices\",\n description: \"List invoices from Xero. Can filter by status, type, or contact.\",\n inputSchema: z.object({\n status: z\n .enum([\"DRAFT\", \"SUBMITTED\", \"AUTHORISED\", \"PAID\", \"VOIDED\"])\n .optional()\n .describe(\"Filter by invoice status\"),\n type: z\n .enum([\"ACCREC\", \"ACCPAY\"])\n .optional()\n .describe(\"Filter by invoice type (ACCREC = sales invoice, ACCPAY = bill)\"),\n contactId: z.string().optional().describe(\"Filter by contact ID\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of invoices to return\"),\n }),\n async execute({ status, type, contactId, limit }) {\n const invoices = await listInvoices({ status, type, contactId, limit });\n\n return invoices.map(\n ({\n InvoiceID,\n InvoiceNumber,\n Type,\n Status,\n Contact,\n Date,\n DueDate,\n SubTotal,\n TotalTax,\n Total,\n AmountDue,\n AmountPaid,\n CurrencyCode,\n Reference,\n }) => ({\n invoiceId: InvoiceID,\n invoiceNumber: InvoiceNumber,\n type: Type,\n status: Status,\n contact: Contact.Name,\n date: Date,\n dueDate: DueDate,\n subTotal: SubTotal,\n totalTax: TotalTax,\n total: Total,\n amountDue: AmountDue,\n amountPaid: AmountPaid,\n currencyCode: CurrencyCode,\n reference: Reference,\n }),\n );\n },\n});\n",
@@ -244,7 +244,7 @@ export default {
244
244
  "files": {
245
245
  "lib/sharepoint-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_BASE_URL = \"https://graph.microsoft.com/v1.0\";\n\nexport interface SharePointSite {\n id: string;\n name: string;\n displayName: string;\n description?: string;\n webUrl: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n siteCollection?: {\n hostname: string;\n };\n}\n\nexport interface SharePointDrive {\n id: string;\n name: string;\n description?: string;\n driveType: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n quota?: {\n total: number;\n used: number;\n remaining: number;\n };\n}\n\nexport interface SharePointFile {\n id: string;\n name: string;\n size: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n file?: {\n mimeType: string;\n hashes?: {\n sha1Hash?: string;\n quickXorHash?: string;\n };\n };\n folder?: {\n childCount: number;\n };\n parentReference?: {\n driveId: string;\n id: string;\n path: string;\n };\n createdBy?: {\n user?: {\n displayName: string;\n email?: string;\n };\n };\n lastModifiedBy?: {\n user?: {\n displayName: string;\n email?: string;\n };\n };\n}\n\ninterface GraphResponse<T> {\n value: T[];\n \"@odata.nextLink\"?: string;\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Microsoft. Please connect your account.\");\n return token;\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...(options.headers ?? {}),\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n}\n\nexport async function listSites(options?: {\n search?: string;\n limit?: number;\n}): Promise<SharePointSite[]> {\n const endpoint = options?.search\n ? `/sites?search=${encodeURIComponent(options.search)}`\n : \"/sites?search=*\";\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointSite>>(endpoint);\n return options?.limit ? value.slice(0, options.limit) : value;\n}\n\nexport function getSite(siteId: string): Promise<SharePointSite> {\n return graphFetch<SharePointSite>(`/sites/${siteId}`);\n}\n\nexport function getSiteByPath(hostname: string, sitePath: string): Promise<SharePointSite> {\n return graphFetch<SharePointSite>(`/sites/${hostname}:${sitePath}`);\n}\n\nexport async function listDrives(siteId: string): Promise<SharePointDrive[]> {\n const { value = [] } = await graphFetch<GraphResponse<SharePointDrive>>(`/sites/${siteId}/drives`);\n return value;\n}\n\nexport function getDefaultDrive(siteId: string): Promise<SharePointDrive> {\n return graphFetch<SharePointDrive>(`/sites/${siteId}/drive`);\n}\n\nexport async function listFiles(\n siteId: string,\n driveId: string,\n folderId?: string,\n options?: {\n limit?: number;\n orderBy?: string;\n },\n): Promise<SharePointFile[]> {\n const baseEndpoint = folderId\n ? `/sites/${siteId}/drives/${driveId}/items/${folderId}/children`\n : `/sites/${siteId}/drives/${driveId}/root/children`;\n\n const params = new URLSearchParams();\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n if (options?.limit) params.set(\"$top\", String(options.limit));\n\n const endpoint = params.size ? `${baseEndpoint}?${params.toString()}` : baseEndpoint;\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointFile>>(endpoint);\n return value;\n}\n\nexport function getFile(siteId: string, driveId: string, itemId: string): Promise<SharePointFile> {\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`);\n}\n\nexport function getFileByPath(\n siteId: string,\n driveId: string,\n path: string,\n): Promise<SharePointFile> {\n const encodedPath = encodeURIComponent(path);\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/root:/${encodedPath}`);\n}\n\nexport async function downloadFile(\n siteId: string,\n driveId: string,\n itemId: string,\n): Promise<ArrayBuffer> {\n const token = await requireAccessToken();\n\n await getFile(siteId, driveId, itemId);\n\n const response = await fetch(\n `${GRAPH_BASE_URL}/sites/${siteId}/drives/${driveId}/items/${itemId}/content`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n\n if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);\n\n return response.arrayBuffer();\n}\n\nexport async function downloadFileAsText(\n siteId: string,\n driveId: string,\n itemId: string,\n): Promise<string> {\n const buffer = await downloadFile(siteId, driveId, itemId);\n return new TextDecoder().decode(buffer);\n}\n\nexport async function uploadFile(\n siteId: string,\n driveId: string,\n fileName: string,\n content: string | ArrayBuffer | Blob,\n folderId?: string,\n): Promise<SharePointFile> {\n const token = await requireAccessToken();\n\n const encodedFileName = encodeURIComponent(fileName);\n const endpoint = folderId\n ? `/sites/${siteId}/drives/${driveId}/items/${folderId}:/${encodedFileName}:/content`\n : `/sites/${siteId}/drives/${driveId}/root:/${encodedFileName}:/content`;\n\n let body: ArrayBuffer;\n if (typeof content === \"string\") {\n body = new TextEncoder().encode(content);\n } else if (content instanceof Blob) {\n body = await content.arrayBuffer();\n } else {\n body = content;\n }\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/octet-stream\",\n },\n body,\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to upload file: ${error.error?.message ?? response.statusText}`);\n}\n\nexport function createFolder(\n siteId: string,\n driveId: string,\n folderName: string,\n parentFolderId?: string,\n): Promise<SharePointFile> {\n const endpoint = parentFolderId\n ? `/sites/${siteId}/drives/${driveId}/items/${parentFolderId}/children`\n : `/sites/${siteId}/drives/${driveId}/root/children`;\n\n return graphFetch<SharePointFile>(endpoint, {\n method: \"POST\",\n body: JSON.stringify({\n name: folderName,\n folder: {},\n \"@microsoft.graph.conflictBehavior\": \"rename\",\n }),\n });\n}\n\nexport async function searchFiles(\n siteId: string,\n query: string,\n options?: {\n limit?: number;\n },\n): Promise<SharePointFile[]> {\n const baseEndpoint = `/sites/${siteId}/drive/root/search(q='${encodeURIComponent(query)}')`;\n const endpoint = options?.limit ? `${baseEndpoint}?$top=${options.limit}` : baseEndpoint;\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointFile>>(endpoint);\n return value;\n}\n\nexport async function deleteItem(siteId: string, driveId: string, itemId: string): Promise<void> {\n await graphFetch<void>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`, { method: \"DELETE\" });\n}\n\nexport function moveItem(\n siteId: string,\n driveId: string,\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<SharePointFile> {\n const body: { parentReference: { id: string }; name?: string } = {\n parentReference: { id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function copyItem(\n siteId: string,\n driveId: string,\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<void> {\n const body: { parentReference: { driveId: string; id: string }; name?: string } = {\n parentReference: { driveId, id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n await graphFetch<void>(`/sites/${siteId}/drives/${driveId}/items/${itemId}/copy`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n",
246
246
  "app/api/auth/sharepoint/route.ts": "import { createOAuthInitHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(sharePointConfig, { tokenStore: oauthMemoryTokenStore });\n",
247
- "app/api/auth/sharepoint/callback/route.ts": "import { createOAuthCallbackHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sharePointConfig, { tokenStore: hybridTokenStore });\n",
247
+ "app/api/auth/sharepoint/callback/route.ts": "import { createOAuthCallbackHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sharePointConfig, { tokenStore: hybridTokenStore });\n",
248
248
  "tools/get-site.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getSite, listDrives } from \"../../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"get-site\",\n description:\n \"Get detailed information about a specific SharePoint site including its document libraries (drives).\",\n inputSchema: z.object({\n siteId: z.string().describe(\"The ID of the SharePoint site to retrieve\"),\n includeDrives: z\n .boolean()\n .default(true)\n .describe(\"Whether to include the list of document libraries in the response\"),\n }),\n async execute({ siteId, includeDrives }): Promise<Record<string, unknown>> {\n const site = await getSite(siteId);\n\n const result: Record<string, unknown> = {\n id: site.id,\n name: site.displayName ?? site.name,\n description: site.description,\n url: site.webUrl,\n hostname: site.siteCollection?.hostname,\n created: site.createdDateTime,\n lastModified: site.lastModifiedDateTime,\n };\n\n if (!includeDrives) return result;\n\n const drives = await listDrives(siteId);\n result.documentLibraries = drives.map((drive) => {\n const quota = drive.quota;\n const percentUsed =\n quota && quota.total > 0 ? Math.round((quota.used / quota.total) * 100) : 0;\n\n return {\n id: drive.id,\n name: drive.name,\n description: drive.description,\n type: drive.driveType,\n url: drive.webUrl,\n quota: quota\n ? {\n total: quota.total,\n used: quota.used,\n remaining: quota.remaining,\n percentUsed,\n }\n : undefined,\n };\n });\n\n return result;\n },\n});\n",
249
249
  "tools/list-sites.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listSites } from \"../../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"list-sites\",\n description:\n \"List all SharePoint sites the user has access to. Returns site names, URLs, and IDs.\",\n inputSchema: z.object({\n search: z\n .string()\n .optional()\n .describe(\"Optional search query to filter sites by name or description\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of sites to return\"),\n }),\n async execute({ search, limit }) {\n const sites = await listSites({ search, limit });\n\n return sites.map((site) => ({\n id: site.id,\n name: site.displayName ?? site.name,\n description: site.description,\n url: site.webUrl,\n hostname: site.siteCollection?.hostname,\n created: site.createdDateTime,\n lastModified: site.lastModifiedDateTime,\n }));\n },\n});\n",
250
250
  "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { downloadFileAsText, getFile } from \"../../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description:\n \"Get detailed metadata and optionally download content of a file from SharePoint. Can retrieve text content for text-based files.\",\n inputSchema: z.object({\n siteId: z.string().describe(\"The ID of the SharePoint site\"),\n driveId: z.string().describe(\"The ID of the document library (drive)\"),\n itemId: z.string().describe(\"The ID of the file to retrieve\"),\n includeContent: z\n .boolean()\n .default(false)\n .describe(\n \"Whether to download and include the file content (only works for text-based files)\",\n ),\n contentMaxLength: z\n .number()\n .min(100)\n .max(100000)\n .default(50000)\n .describe(\"Maximum length of content to return if includeContent is true\"),\n }),\n async execute({ siteId, driveId, itemId, includeContent, contentMaxLength }) {\n const file = await getFile(siteId, driveId, itemId);\n\n const result: Record<string, unknown> = {\n id: file.id,\n name: file.name,\n size: file.size,\n sizeFormatted: formatBytes(file.size),\n mimeType: file.file?.mimeType,\n url: file.webUrl,\n created: file.createdDateTime,\n lastModified: file.lastModifiedDateTime,\n createdBy: {\n name: file.createdBy?.user?.displayName,\n email: file.createdBy?.user?.email,\n },\n lastModifiedBy: {\n name: file.lastModifiedBy?.user?.displayName,\n email: file.lastModifiedBy?.user?.email,\n },\n parentReference: {\n driveId: file.parentReference?.driveId,\n id: file.parentReference?.id,\n path: file.parentReference?.path,\n },\n hashes: file.file?.hashes,\n };\n\n if (!includeContent) return result;\n\n const mimeType = file.file?.mimeType;\n if (!mimeType) return result;\n\n const isTextFile = [\n \"text/\",\n \"application/json\",\n \"application/xml\",\n \"application/javascript\",\n \"application/typescript\",\n ].some((type) => mimeType.startsWith(type));\n\n if (!isTextFile) {\n result.contentError = \"File is not a text-based file type\";\n return result;\n }\n\n if (file.size >= contentMaxLength) {\n result.contentError = `File size (${formatBytes(file.size)}) exceeds maximum content length`;\n return result;\n }\n\n try {\n const content = await downloadFileAsText(siteId, driveId, itemId);\n const truncated = content.length > contentMaxLength;\n\n result.content = truncated\n ? `${content.substring(0, contentMaxLength)}\\n\\n[Content truncated...]`\n : content;\n result.contentTruncated = truncated;\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n result.contentError = `Failed to download content: ${message}`;\n }\n\n return result;\n },\n});\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;\n}\n",
@@ -257,7 +257,7 @@ export default {
257
257
  ".env.example": "# Webex OAuth Configuration\n# Get your credentials from https://developer.webex.com/my-apps\nWEBEX_CLIENT_ID=your-client-id\nWEBEX_CLIENT_SECRET=your-client-secret\n",
258
258
  "lib/webex-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst WEBEX_BASE_URL = \"https://webexapis.com/v1\";\n\ninterface WebexMeeting {\n id: string;\n title: string;\n agenda?: string;\n start: string;\n end: string;\n timezone: string;\n hostEmail: string;\n hostDisplayName: string;\n webLink: string;\n sipAddress?: string;\n meetingNumber?: string;\n state: string;\n enabledAutoRecordMeeting?: boolean;\n allowAnyUserToBeCoHost?: boolean;\n}\n\ninterface WebexRoom {\n id: string;\n title: string;\n type: \"direct\" | \"group\";\n isLocked: boolean;\n lastActivity: string;\n creatorId: string;\n created: string;\n}\n\ninterface WebexMessage {\n id: string;\n roomId: string;\n roomType: string;\n text?: string;\n markdown?: string;\n personId: string;\n personEmail: string;\n created: string;\n}\n\ninterface WebexPerson {\n id: string;\n emails: string[];\n displayName: string;\n firstName?: string;\n lastName?: string;\n avatar?: string;\n orgId: string;\n created: string;\n lastActivity?: string;\n status?: string;\n type: string;\n}\n\nasync function webexFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Webex. Please connect your account.\");\n }\n\n const response = await fetch(`${WEBEX_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n message?: string;\n };\n\n throw new Error(\n `Webex API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction setOptionalParam(\n params: URLSearchParams,\n key: string,\n value: string | number | undefined,\n): void {\n if (value === undefined) return;\n params.set(key, String(value));\n}\n\nexport async function getMe(): Promise<WebexPerson> {\n return webexFetch<WebexPerson>(\"/people/me\");\n}\n\n/**\n * List meetings for the authenticated user\n */\nexport async function listMeetings(options?: {\n max?: number;\n from?: string;\n to?: string;\n meetingType?: \"meeting\" | \"webinar\" | \"personalRoomMeeting\";\n state?: \"active\" | \"scheduled\" | \"ended\" | \"missed\" | \"inProgress\";\n}): Promise<WebexMeeting[]> {\n const params = new URLSearchParams();\n\n setOptionalParam(params, \"max\", options?.max);\n setOptionalParam(params, \"from\", options?.from);\n setOptionalParam(params, \"to\", options?.to);\n setOptionalParam(params, \"meetingType\", options?.meetingType);\n setOptionalParam(params, \"state\", options?.state);\n\n const response = await webexFetch<{ items?: WebexMeeting[] }>(\n `/meetings?${params}`,\n );\n\n return response.items ?? [];\n}\n\n/**\n * Get details of a specific meeting\n */\nexport async function getMeeting(meetingId: string): Promise<WebexMeeting> {\n return webexFetch<WebexMeeting>(`/meetings/${meetingId}`);\n}\n\n/**\n * Create a new Webex meeting\n */\nexport async function createMeeting(options: {\n title: string;\n agenda?: string;\n start: string;\n end: string;\n timezone?: string;\n enabledAutoRecordMeeting?: boolean;\n allowAnyUserToBeCoHost?: boolean;\n invitees?: Array<{ email: string; displayName?: string; coHost?: boolean }>;\n}): Promise<WebexMeeting> {\n const body: Record<string, unknown> = {\n title: options.title,\n start: options.start,\n end: options.end,\n timezone: options.timezone ?? \"UTC\",\n };\n\n if (options.agenda) body.agenda = options.agenda;\n if (options.enabledAutoRecordMeeting !== undefined) {\n body.enabledAutoRecordMeeting = options.enabledAutoRecordMeeting;\n }\n if (options.allowAnyUserToBeCoHost !== undefined) {\n body.allowAnyUserToBeCoHost = options.allowAnyUserToBeCoHost;\n }\n if (options.invitees?.length) body.invitees = options.invitees;\n\n return webexFetch<WebexMeeting>(\"/meetings\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\n/**\n * Update an existing meeting\n */\nexport async function updateMeeting(\n meetingId: string,\n updates: {\n title?: string;\n agenda?: string;\n start?: string;\n end?: string;\n timezone?: string;\n enabledAutoRecordMeeting?: boolean;\n },\n): Promise<WebexMeeting> {\n return webexFetch<WebexMeeting>(`/meetings/${meetingId}`, {\n method: \"PATCH\",\n body: JSON.stringify(updates),\n });\n}\n\n/**\n * Delete a meeting\n */\nexport async function deleteMeeting(meetingId: string): Promise<void> {\n await webexFetch<void>(`/meetings/${meetingId}`, { method: \"DELETE\" });\n}\n\n/**\n * List Webex rooms (spaces)\n */\nexport async function listRooms(options?: {\n max?: number;\n type?: \"direct\" | \"group\";\n sortBy?: \"id\" | \"lastactivity\" | \"created\";\n}): Promise<WebexRoom[]> {\n const params = new URLSearchParams();\n\n setOptionalParam(params, \"max\", options?.max);\n setOptionalParam(params, \"type\", options?.type);\n setOptionalParam(params, \"sortBy\", options?.sortBy);\n\n const response = await webexFetch<{ items?: WebexRoom[] }>(`/rooms?${params}`);\n return response.items ?? [];\n}\n\n/**\n * Get details of a specific room\n */\nexport async function getRoom(roomId: string): Promise<WebexRoom> {\n return webexFetch<WebexRoom>(`/rooms/${roomId}`);\n}\n\n/**\n * Create a new room\n */\nexport async function createRoom(options: {\n title: string;\n teamId?: string;\n}): Promise<WebexRoom> {\n return webexFetch<WebexRoom>(\"/rooms\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n}\n\n/**\n * Send a message to a Webex room\n */\nexport async function sendMessage(options: {\n roomId?: string;\n toPersonId?: string;\n toPersonEmail?: string;\n text?: string;\n markdown?: string;\n files?: string[];\n}): Promise<WebexMessage> {\n if (!options.roomId && !options.toPersonId && !options.toPersonEmail) {\n throw new Error(\"Must specify roomId, toPersonId, or toPersonEmail\");\n }\n\n if (!options.text && !options.markdown && !options.files) {\n throw new Error(\"Must specify text, markdown, or files\");\n }\n\n return webexFetch<WebexMessage>(\"/messages\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n}\n\n/**\n * List messages in a room\n */\nexport async function listMessages(options: {\n roomId: string;\n max?: number;\n before?: string;\n beforeMessage?: string;\n}): Promise<WebexMessage[]> {\n const params = new URLSearchParams({ roomId: options.roomId });\n\n setOptionalParam(params, \"max\", options.max);\n setOptionalParam(params, \"before\", options.before);\n setOptionalParam(params, \"beforeMessage\", options.beforeMessage);\n\n const response = await webexFetch<{ items?: WebexMessage[] }>(\n `/messages?${params}`,\n );\n\n return response.items ?? [];\n}\n\n/**\n * Delete a message\n */\nexport async function deleteMessage(messageId: string): Promise<void> {\n await webexFetch<void>(`/messages/${messageId}`, { method: \"DELETE\" });\n}\n",
259
259
  "app/api/auth/webex/route.ts": "import { createOAuthInitHandler, webexConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(webexConfig, { tokenStore: oauthMemoryTokenStore });\n",
260
- "app/api/auth/webex/callback/route.ts": "import { createOAuthCallbackHandler, webexConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(webexConfig, { tokenStore: hybridTokenStore });\n",
260
+ "app/api/auth/webex/callback/route.ts": "import { createOAuthCallbackHandler, webexConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(webexConfig, { tokenStore: hybridTokenStore });\n",
261
261
  "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { sendMessage } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description:\n \"Send a text or markdown message to a Webex room/space or directly to a person.\",\n inputSchema: z\n .object({\n roomId: z\n .string()\n .optional()\n .describe(\"Room ID to send the message to (use this OR toPersonEmail)\"),\n toPersonEmail: z\n .string()\n .email()\n .optional()\n .describe(\"Email address to send a direct message (use this OR roomId)\"),\n text: z\n .string()\n .optional()\n .describe(\"Plain text message content (use this OR markdown)\"),\n markdown: z\n .string()\n .optional()\n .describe(\"Markdown formatted message content (use this OR text)\"),\n })\n .refine((data) => data.roomId || data.toPersonEmail, {\n message: \"Must specify either roomId or toPersonEmail\",\n })\n .refine((data) => data.text || data.markdown, {\n message: \"Must specify either text or markdown\",\n }),\n async execute({ roomId, toPersonEmail, text, markdown }) {\n const message = await sendMessage({ roomId, toPersonEmail, text, markdown });\n\n const destination = roomId ? `room ${roomId}` : toPersonEmail;\n\n return {\n id: message.id,\n roomId: message.roomId,\n text: message.text,\n markdown: message.markdown,\n personEmail: message.personEmail,\n created: message.created,\n message: `Message sent successfully to ${destination}`,\n };\n },\n});\n",
262
262
  "tools/get-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getMeeting } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"get-meeting\",\n description: \"Get detailed information about a specific Webex meeting by its ID.\",\n inputSchema: z.object({\n meetingId: z.string().describe(\"The unique ID of the meeting\"),\n }),\n async execute({ meetingId }) {\n return getMeeting(meetingId);\n },\n});\n",
263
263
  "tools/list-rooms.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listRooms } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"list-rooms\",\n description:\n \"List Webex spaces/rooms. Can filter by type (direct messages or group spaces).\",\n inputSchema: z.object({\n max: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of rooms to return\"),\n type: z\n .enum([\"direct\", \"group\"])\n .optional()\n .describe(\n \"Filter by room type: 'direct' for 1:1 conversations, 'group' for team spaces\",\n ),\n sortBy: z\n .enum([\"id\", \"lastactivity\", \"created\"])\n .default(\"lastactivity\")\n .describe(\"Sort rooms by id, lastactivity, or created date\"),\n }),\n async execute({ max, type, sortBy }) {\n const rooms = await listRooms({ max, type, sortBy });\n\n return rooms.map((room) => ({\n id: room.id,\n title: room.title,\n type: room.type,\n isLocked: room.isLocked,\n lastActivity: room.lastActivity,\n created: room.created,\n }));\n },\n});\n",
@@ -270,7 +270,7 @@ export default {
270
270
  ".env.example": "# Notion Integration\n# Create an integration at https://www.notion.so/my-integrations\n# Make sure to enable \"Public Integration\" for OAuth\n\nNOTION_CLIENT_ID=your_client_id_here\nNOTION_CLIENT_SECRET=your_client_secret_here\n",
271
271
  "lib/notion-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst NOTION_API_VERSION = \"2022-06-28\";\nconst NOTION_BASE_URL = \"https://api.notion.com/v1\";\n\ninterface NotionResponse<T> {\n object: string;\n results?: T[];\n next_cursor?: string | null;\n has_more?: boolean;\n}\n\ninterface NotionPage {\n id: string;\n object: \"page\";\n created_time: string;\n last_edited_time: string;\n parent: { type: string; database_id?: string; page_id?: string };\n properties: Record<string, NotionProperty>;\n url: string;\n}\n\ninterface NotionDatabase {\n id: string;\n object: \"database\";\n title: Array<{ plain_text: string }>;\n properties: Record<string, { type: string }>;\n}\n\ninterface NotionBlock {\n id: string;\n type: string;\n [key: string]: unknown;\n}\n\ninterface NotionProperty {\n type: string;\n title?: Array<{ plain_text: string }>;\n rich_text?: Array<{ plain_text: string }>;\n [key: string]: unknown;\n}\n\nasync function notionFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Notion. Please connect your account.\");\n }\n\n const response = await fetch(`${NOTION_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Notion-Version\": NOTION_API_VERSION,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({} as { message?: string }))) as {\n message?: string;\n };\n throw new Error(\n `Notion API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function searchNotion(\n query: string,\n options?: {\n filter?: { property: \"object\"; value: \"page\" | \"database\" };\n pageSize?: number;\n },\n): Promise<Array<NotionPage | NotionDatabase>> {\n const body: Record<string, unknown> = {\n query,\n ...(options?.filter ? { filter: options.filter } : {}),\n ...(options?.pageSize ? { page_size: options.pageSize } : {}),\n };\n\n const response = await notionFetch<NotionResponse<NotionPage | NotionDatabase>>(\"/search\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.results ?? [];\n}\n\nexport function getPage(pageId: string): Promise<NotionPage> {\n return notionFetch<NotionPage>(`/pages/${pageId}`);\n}\n\nexport async function getPageContent(pageId: string): Promise<NotionBlock[]> {\n const response = await notionFetch<NotionResponse<NotionBlock>>(`/blocks/${pageId}/children`);\n return response.results ?? [];\n}\n\nexport async function queryDatabase(\n databaseId: string,\n options?: {\n filter?: Record<string, unknown>;\n sorts?: Array<{ property: string; direction: \"ascending\" | \"descending\" }>;\n pageSize?: number;\n },\n): Promise<NotionPage[]> {\n const body: Record<string, unknown> = {\n ...(options?.filter ? { filter: options.filter } : {}),\n ...(options?.sorts ? { sorts: options.sorts } : {}),\n ...(options?.pageSize ? { page_size: options.pageSize } : {}),\n };\n\n const response = await notionFetch<NotionResponse<NotionPage>>(\n `/databases/${databaseId}/query`,\n { method: \"POST\", body: JSON.stringify(body) },\n );\n\n return response.results ?? [];\n}\n\nexport function createPage(options: {\n parentId: string;\n parentType: \"database\" | \"page\";\n title: string;\n content?: string;\n properties?: Record<string, unknown>;\n}): Promise<NotionPage> {\n const parent =\n options.parentType === \"database\"\n ? { database_id: options.parentId }\n : { page_id: options.parentId };\n\n const properties: Record<string, unknown> = options.properties ?? {};\n\n if (options.parentType === \"database\") {\n properties.title ??= { title: [{ text: { content: options.title } }] };\n }\n\n const children: Array<Record<string, unknown>> = [];\n\n if (options.parentType === \"page\") {\n children.push({\n object: \"block\",\n type: \"heading_1\",\n heading_1: {\n rich_text: [{ type: \"text\", text: { content: options.title } }],\n },\n });\n }\n\n for (const paragraph of options.content?.split(\"\\n\\n\") ?? []) {\n const trimmed = paragraph.trim();\n if (!trimmed) continue;\n\n children.push({\n object: \"block\",\n type: \"paragraph\",\n paragraph: {\n rich_text: [{ type: \"text\", text: { content: trimmed } }],\n },\n });\n }\n\n return notionFetch<NotionPage>(\"/pages\", {\n method: \"POST\",\n body: JSON.stringify({\n parent,\n properties,\n children: children.length ? children : undefined,\n }),\n });\n}\n\nexport function extractPlainText(blocks: NotionBlock[]): string {\n const texts: string[] = [];\n\n for (const block of blocks) {\n const content = block[block.type] as { rich_text?: Array<{ plain_text: string }> } | undefined;\n const text = content?.rich_text?.map((t) => t.plain_text).join(\"\");\n if (text) texts.push(text);\n }\n\n return texts.join(\"\\n\\n\");\n}\n\nexport function getPageTitle(page: NotionPage): string {\n for (const prop of Object.values(page.properties)) {\n if (prop.type === \"title\" && prop.title) {\n return prop.title.map((t) => t.plain_text).join(\"\");\n }\n }\n\n return \"Untitled\";\n}\n",
272
272
  "app/api/auth/notion/route.ts": "import { createOAuthInitHandler, notionConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(notionConfig, { tokenStore: oauthMemoryTokenStore });\n",
273
- "app/api/auth/notion/callback/route.ts": "import { createOAuthCallbackHandler, notionConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n async getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n async setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n await oauthMemoryTokenStore.setState(state);\n },\n async clearState(state: string) {\n await oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(notionConfig, { tokenStore: hybridTokenStore });\n",
273
+ "app/api/auth/notion/callback/route.ts": "import { createOAuthCallbackHandler, notionConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n async getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n async setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n await oauthMemoryTokenStore.setState(state);\n },\n async clearState(state: string) {\n await oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(notionConfig, { tokenStore: hybridTokenStore });\n",
274
274
  "tools/query-database.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getPageTitle, queryDatabase } from \"../../lib/notion-client.ts\";\n\nexport default tool({\n id: \"query-database\",\n description: \"Query a Notion database to retrieve entries. Supports filtering and sorting.\",\n inputSchema: z.object({\n databaseId: z.string().describe(\"The ID of the Notion database to query\"),\n sortProperty: z.string().optional().describe(\"Property name to sort by\"),\n sortDirection: z\n .enum([\"ascending\", \"descending\"])\n .default(\"descending\")\n .describe(\"Sort direction\"),\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of results\"),\n }),\n async execute({ databaseId, sortProperty, sortDirection, limit }) {\n const results = await queryDatabase(databaseId, {\n sorts: sortProperty ? [{ property: sortProperty, direction: sortDirection }] : undefined,\n pageSize: limit,\n });\n\n return results.map((page) => {\n const properties: Record<string, string> = {};\n\n for (const [key, prop] of Object.entries(page.properties)) {\n if (prop.type !== \"title\" && prop.type !== \"rich_text\") continue;\n\n const text =\n prop.type === \"title\"\n ? prop.title?.map((t) => t.plain_text).join(\"\") ?? \"\"\n : prop.rich_text?.map((t) => t.plain_text).join(\"\") ?? \"\";\n\n properties[key] = text;\n }\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n properties,\n lastEdited: page.last_edited_time,\n };\n });\n },\n});\n",
275
275
  "tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createPage, getPageTitle } from \"../../lib/notion-client.ts\";\n\nexport default tool({\n id: \"create-page\",\n description:\n \"Create a new page in Notion. Can create as a subpage of an existing page or as a new entry in a database.\",\n inputSchema: z.object({\n parentId: z.string().describe(\"The ID of the parent page or database\"),\n parentType: z.enum([\"page\", \"database\"]).describe(\"Whether the parent is a page or database\"),\n title: z.string().describe(\"Title of the new page\"),\n content: z\n .string()\n .optional()\n .describe(\n \"Initial content for the page (plain text, paragraphs separated by double newlines)\",\n ),\n }),\n async execute({ parentId, parentType, title, content }) {\n const page = await createPage({ parentId, parentType, title, content });\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n createdAt: page.created_time,\n };\n },\n});\n",
276
276
  "tools/search-notion.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getPageTitle, searchNotion } from \"../../lib/notion-client.ts\";\n\nexport default tool({\n id: \"search-notion\",\n description:\n \"Search for pages and databases in the connected Notion workspace. Returns matching pages with their titles and IDs.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query to find pages or databases\"),\n type: z\n .enum([\"page\", \"database\", \"all\"])\n .default(\"all\")\n .describe(\"Type of objects to search for\"),\n limit: z\n .number()\n .min(1)\n .max(20)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }),\n async execute({ query, type, limit }) {\n const filter = type === \"all\" ? undefined : { property: \"object\", value: type };\n const results = await searchNotion(query, { filter, pageSize: limit });\n\n return results.map((item) => {\n if (item.object === \"page\") {\n return {\n id: item.id,\n type: \"page\",\n title: getPageTitle(item),\n url: item.url,\n lastEdited: item.last_edited_time,\n };\n }\n\n return {\n id: item.id,\n type: \"database\",\n title: item.title?.map((t) => t.plain_text).join(\"\") ?? \"\",\n url: item.url,\n };\n });\n },\n});\n",
@@ -282,7 +282,7 @@ export default {
282
282
  ".env.example": "# Discord OAuth Configuration\n# Get these from https://discord.com/developers/applications\n\n# Required: Your Discord application's Client ID\nDISCORD_CLIENT_ID=your_client_id_here\n\n# Required: Your Discord application's Client Secret\nDISCORD_CLIENT_SECRET=your_client_secret_here\n\n# Optional: Bot token for advanced bot features\n# Only needed if you want to use bot-specific functionality\nDISCORD_BOT_TOKEN=your_bot_token_here\n",
283
283
  "lib/discord-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst DISCORD_API_VERSION = \"v10\";\nconst DISCORD_BASE_URL = `https://discord.com/api/${DISCORD_API_VERSION}`;\n\ninterface DiscordUser {\n id: string;\n username: string;\n discriminator: string;\n global_name?: string | null;\n avatar?: string | null;\n bot?: boolean;\n system?: boolean;\n mfa_enabled?: boolean;\n banner?: string | null;\n accent_color?: number | null;\n locale?: string;\n verified?: boolean;\n email?: string | null;\n flags?: number;\n premium_type?: number;\n public_flags?: number;\n}\n\ninterface DiscordGuild {\n id: string;\n name: string;\n icon?: string | null;\n owner?: boolean;\n permissions?: string;\n features: string[];\n}\n\ninterface DiscordChannel {\n id: string;\n type: number;\n guild_id?: string;\n position?: number;\n name?: string;\n topic?: string | null;\n nsfw?: boolean;\n last_message_id?: string | null;\n bitrate?: number;\n user_limit?: number;\n rate_limit_per_user?: number;\n recipients?: DiscordUser[];\n icon?: string | null;\n owner_id?: string;\n application_id?: string;\n parent_id?: string | null;\n last_pin_timestamp?: string | null;\n rtc_region?: string | null;\n video_quality_mode?: number;\n message_count?: number;\n member_count?: number;\n flags?: number;\n}\n\ninterface DiscordMessage {\n id: string;\n channel_id: string;\n author: DiscordUser;\n content: string;\n timestamp: string;\n edited_timestamp?: string | null;\n tts: boolean;\n mention_everyone: boolean;\n mentions: DiscordUser[];\n mention_roles: string[];\n attachments: Array<{\n id: string;\n filename: string;\n size: number;\n url: string;\n proxy_url: string;\n height?: number | null;\n width?: number | null;\n content_type?: string;\n }>;\n embeds: unknown[];\n reactions?: Array<{\n count: number;\n me: boolean;\n emoji: {\n id: string | null;\n name: string | null;\n };\n }>;\n pinned: boolean;\n type: number;\n}\n\ninterface DiscordGuildMember {\n user?: DiscordUser;\n nick?: string | null;\n avatar?: string | null;\n roles: string[];\n joined_at: string;\n premium_since?: string | null;\n deaf: boolean;\n mute: boolean;\n flags: number;\n pending?: boolean;\n permissions?: string;\n communication_disabled_until?: string | null;\n}\n\nasync function discordFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Discord. Please connect your account.\");\n }\n\n const response = await fetch(`${DISCORD_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(`Discord API error: ${response.status} ${error.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nfunction buildQuery(\n options: Record<string, string | number | undefined>,\n limits?: Record<string, number>,\n): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(options)) {\n if (value === undefined) continue;\n\n if (typeof value === \"number\") {\n const limit = limits?.[key];\n params.set(key, Math.min(value, limit ?? value).toString());\n continue;\n }\n\n params.set(key, value);\n }\n\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nexport function getCurrentUser(): Promise<DiscordUser> {\n return discordFetch(\"/users/@me\");\n}\n\nexport function listGuilds(): Promise<DiscordGuild[]> {\n return discordFetch(\"/users/@me/guilds\");\n}\n\nexport function getGuild(guildId: string): Promise<DiscordGuild> {\n return discordFetch(`/guilds/${guildId}`);\n}\n\nexport function listChannels(guildId: string): Promise<DiscordChannel[]> {\n return discordFetch(`/guilds/${guildId}/channels`);\n}\n\nexport function getChannel(channelId: string): Promise<DiscordChannel> {\n return discordFetch(`/channels/${channelId}`);\n}\n\nexport function getMessages(\n channelId: string,\n options?: {\n limit?: number;\n before?: string;\n after?: string;\n around?: string;\n },\n): Promise<DiscordMessage[]> {\n const query = buildQuery(\n {\n limit: options?.limit,\n before: options?.before,\n after: options?.after,\n around: options?.around,\n },\n { limit: 100 },\n );\n\n return discordFetch(`/channels/${channelId}/messages${query}`);\n}\n\nexport function sendMessage(\n channelId: string,\n content: string,\n options?: {\n tts?: boolean;\n embeds?: unknown[];\n },\n): Promise<DiscordMessage> {\n const body: Record<string, unknown> = { content };\n\n if (options?.tts !== undefined) body.tts = options.tts;\n if (options?.embeds) body.embeds = options.embeds;\n\n return discordFetch(`/channels/${channelId}/messages`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getGuildMembers(\n guildId: string,\n options?: {\n limit?: number;\n after?: string;\n },\n): Promise<DiscordGuildMember[]> {\n const query = buildQuery({ limit: options?.limit, after: options?.after }, { limit: 1000 });\n return discordFetch(`/guilds/${guildId}/members${query}`);\n}\n\nexport function formatUsername(user: DiscordUser): string {\n if (user.discriminator === \"0\") return user.username;\n return `${user.username}#${user.discriminator}`;\n}\n\nfunction getCdnAssetUrl(\n basePath: string,\n id: string,\n hash: string | null | undefined,\n size: number,\n): string | null {\n if (!hash) return null;\n const extension = hash.startsWith(\"a_\") ? \"gif\" : \"png\";\n return `https://cdn.discordapp.com/${basePath}/${id}/${hash}.${extension}?size=${size}`;\n}\n\nexport function getAvatarUrl(user: DiscordUser, size: number = 128): string | null {\n return getCdnAssetUrl(\"avatars\", user.id, user.avatar, size);\n}\n\nexport function getGuildIconUrl(guild: DiscordGuild, size: number = 128): string | null {\n return getCdnAssetUrl(\"icons\", guild.id, guild.icon, size);\n}\n\nconst CHANNEL_TYPE_NAMES: Record<number, string> = {\n 0: \"Text\",\n 1: \"DM\",\n 2: \"Voice\",\n 3: \"Group DM\",\n 4: \"Category\",\n 5: \"Announcement\",\n 10: \"Announcement Thread\",\n 11: \"Public Thread\",\n 12: \"Private Thread\",\n 13: \"Stage Voice\",\n 14: \"Directory\",\n 15: \"Forum\",\n};\n\nexport function getChannelTypeName(type: number): string {\n return CHANNEL_TYPE_NAMES[type] ?? \"Unknown\";\n}\n",
284
284
  "app/api/auth/discord/route.ts": "import { createOAuthInitHandler, discordConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(discordConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
285
- "app/api/auth/discord/callback/route.ts": "/**\n * Discord OAuth Callback\n *\n * Handles the OAuth callback from Discord and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, discordConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(discordConfig, { tokenStore: hybridTokenStore });\n",
285
+ "app/api/auth/discord/callback/route.ts": "/**\n * Discord OAuth Callback\n *\n * Handles the OAuth callback from Discord and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, discordConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(discordConfig, { tokenStore: hybridTokenStore });\n",
286
286
  "tools/list-guilds.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getGuildIconUrl, listGuilds } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"list-guilds\",\n description:\n \"List all Discord servers (guilds) the authenticated user is a member of. Returns server names, IDs, and basic information.\",\n inputSchema: z.object({\n includeIcons: z\n .boolean()\n .default(false)\n .describe(\"Whether to include icon URLs for servers\"),\n }),\n async execute({ includeIcons }) {\n const guilds = await listGuilds();\n\n return guilds.map((guild) => ({\n id: guild.id,\n name: guild.name,\n owner: guild.owner,\n icon: includeIcons ? getGuildIconUrl(guild) : undefined,\n features: guild.features,\n permissions: guild.permissions,\n }));\n },\n});\n",
287
287
  "tools/get-user.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatUsername, getAvatarUrl, getCurrentUser } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"get-user\",\n description:\n \"Get information about the authenticated Discord user. Returns username, ID, avatar, and account details.\",\n inputSchema: z.object({\n includeAvatar: z.boolean().default(true).describe(\"Whether to include the avatar URL\"),\n }),\n async execute({ includeAvatar }) {\n const user = await getCurrentUser();\n\n return {\n id: user.id,\n username: formatUsername(user),\n globalName: user.global_name,\n avatar: includeAvatar ? getAvatarUrl(user) : undefined,\n bot: user.bot,\n system: user.system,\n mfaEnabled: user.mfa_enabled,\n banner: user.banner,\n accentColor: user.accent_color,\n locale: user.locale,\n verified: user.verified,\n email: user.email,\n premiumType: user.premium_type,\n publicFlags: user.public_flags,\n };\n },\n});\n",
288
288
  "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatUsername, sendMessage } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description: \"Send a message to a Discord channel. Returns the sent message details.\",\n inputSchema: z.object({\n channelId: z.string().describe(\"The ID of the Discord channel to send the message to\"),\n content: z\n .string()\n .min(1)\n .max(2000)\n .describe(\"The message content to send (1-2000 characters)\"),\n tts: z\n .boolean()\n .default(false)\n .describe(\"Whether this message should be sent as text-to-speech\"),\n }),\n async execute({ channelId, content, tts }) {\n const message = await sendMessage(channelId, content, { tts });\n\n return {\n id: message.id,\n content: message.content,\n channelId: message.channel_id,\n timestamp: message.timestamp,\n author: {\n id: message.author.id,\n username: formatUsername(message.author),\n globalName: message.author.global_name,\n },\n tts: message.tts,\n };\n },\n});\n",
@@ -295,7 +295,7 @@ export default {
295
295
  ".env.example": "# Atlassian OAuth credentials\n# Get these from: https://developer.atlassian.com/console/myapps/\nATLASSIAN_CLIENT_ID=your_client_id_here\nATLASSIAN_CLIENT_SECRET=your_client_secret_here\n",
296
296
  "lib/confluence-client.ts": "import { getAccessToken, getCloudId } from \"./token-store.ts\";\n\nconst CONFLUENCE_API_BASE = \"https://api.atlassian.com/ex/confluence\";\n\ninterface ConfluenceResponse<T> {\n results: T[];\n size: number;\n start?: number;\n limit?: number;\n _links?: {\n next?: string;\n base?: string;\n };\n}\n\nexport interface ConfluenceSpace {\n id: string;\n key: string;\n name: string;\n type: string;\n status: string;\n _links: {\n webui: string;\n };\n}\n\nexport interface ConfluencePage {\n id: string;\n type: \"page\" | \"blogpost\";\n status: string;\n title: string;\n spaceId?: string;\n parentId?: string;\n version: {\n number: number;\n message?: string;\n };\n body?: {\n storage?: {\n value: string;\n representation: \"storage\";\n };\n view?: {\n value: string;\n representation: \"view\";\n };\n };\n _links: {\n webui: string;\n tinyui?: string;\n };\n}\n\nexport interface ConfluenceSearchResult {\n content: {\n id: string;\n type: string;\n status: string;\n title: string;\n space?: {\n id: string;\n key: string;\n name: string;\n };\n history?: {\n lastUpdated: {\n when: string;\n };\n };\n _links: {\n webui: string;\n };\n };\n excerpt?: string;\n url: string;\n resultGlobalContainer?: {\n title: string;\n };\n}\n\nasync function confluenceFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const [token, cloudId] = await Promise.all([getAccessToken(), getCloudId()]);\n\n if (!token || !cloudId) {\n throw new Error(\"Not authenticated with Confluence. Please connect your Atlassian account.\");\n }\n\n const url = `${CONFLUENCE_API_BASE}/${cloudId}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(`Confluence API error: ${response.status} ${error.message ?? response.statusText}`);\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction buildEndpoint(path: string, params?: URLSearchParams): string {\n const query = params?.toString();\n return `${path}${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listSpaces(options?: {\n limit?: number;\n type?: \"global\" | \"personal\";\n}): Promise<ConfluenceSpace[]> {\n const params = new URLSearchParams();\n\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.type) params.set(\"type\", options.type);\n\n const response = await confluenceFetch<ConfluenceResponse<ConfluenceSpace>>(\n buildEndpoint(\"/wiki/rest/api/space\", params),\n );\n\n return response.results ?? [];\n}\n\nexport async function searchContent(\n query: string,\n options?: {\n cql?: string;\n limit?: number;\n spaceKey?: string;\n },\n): Promise<ConfluenceSearchResult[]> {\n const params = new URLSearchParams();\n\n let cqlQuery = options?.cql ?? `title ~ \"${query}\" OR text ~ \"${query}\"`;\n if (options?.spaceKey) cqlQuery += ` AND space = \"${options.spaceKey}\"`;\n\n params.set(\"cql\", cqlQuery);\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await confluenceFetch<ConfluenceResponse<ConfluenceSearchResult>>(\n buildEndpoint(\"/wiki/rest/api/search\", params),\n );\n\n return response.results ?? [];\n}\n\nexport function getPage(pageId: string, expand?: string[]): Promise<ConfluencePage> {\n const params = new URLSearchParams();\n if (expand?.length) params.set(\"expand\", expand.join(\",\"));\n\n return confluenceFetch<ConfluencePage>(buildEndpoint(`/wiki/rest/api/content/${pageId}`, params));\n}\n\nexport function getPageContent(pageId: string): Promise<ConfluencePage> {\n return getPage(pageId, [\"body.storage\", \"body.view\", \"version\", \"space\"]);\n}\n\nexport function createPage(options: {\n spaceKey: string;\n title: string;\n content: string;\n parentId?: string;\n type?: \"page\" | \"blogpost\";\n}): Promise<ConfluencePage> {\n const body = {\n type: options.type ?? \"page\",\n title: options.title,\n space: { key: options.spaceKey },\n body: {\n storage: {\n value: options.content,\n representation: \"storage\" as const,\n },\n },\n ...(options.parentId ? { ancestors: [{ id: options.parentId }] } : {}),\n };\n\n return confluenceFetch<ConfluencePage>(\"/wiki/rest/api/content\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updatePage(\n pageId: string,\n options: {\n title?: string;\n content?: string;\n version: number;\n versionMessage?: string;\n },\n): Promise<ConfluencePage> {\n await getPage(pageId, [\"version\"]);\n\n const body: Record<string, unknown> = {\n version: {\n number: options.version,\n message: options.versionMessage,\n },\n type: \"page\",\n };\n\n if (options.title) body.title = options.title;\n\n if (options.content) {\n body.body = {\n storage: {\n value: options.content,\n representation: \"storage\",\n },\n };\n }\n\n return confluenceFetch<ConfluencePage>(`/wiki/rest/api/content/${pageId}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport function extractPlainText(storageHtml: string): string {\n return storageHtml\n .replace(/<[^>]*>/g, \" \")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nexport function formatAsStorage(text: string): string {\n const paragraphs = text.split(\"\\n\\n\").filter((p) => p.trim());\n return paragraphs.map((p) => `<p>${escapeHtml(p.trim())}</p>`).join(\"\\n\");\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n",
297
297
  "app/api/auth/confluence/route.ts": "import { confluenceConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(confluenceConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
298
- "app/api/auth/confluence/callback/route.ts": "import { confluenceConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(confluenceConfig, { tokenStore: hybridTokenStore });\n",
298
+ "app/api/auth/confluence/callback/route.ts": "import { confluenceConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(confluenceConfig, { tokenStore: hybridTokenStore });\n",
299
299
  "tools/update-page.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatAsStorage, getPage, updatePage } from \"../../lib/confluence-client.ts\";\n\nfunction toStorageContent(content?: string): string | undefined {\n if (!content) return undefined;\n\n const trimmed = content.trim();\n if (trimmed.startsWith(\"<\")) return content;\n\n return formatAsStorage(content);\n}\n\nexport default tool({\n id: \"update-page\",\n description:\n \"Update the content or title of an existing Confluence page. Requires the current version number.\",\n inputSchema: z.object({\n pageId: z.string().describe(\"The ID of the page to update\"),\n title: z\n .string()\n .optional()\n .describe(\"New title for the page (leave empty to keep current title)\"),\n content: z\n .string()\n .optional()\n .describe(\"New content for the page (can be plain text or Confluence storage format HTML)\"),\n versionMessage: z\n .string()\n .optional()\n .describe(\"Optional message describing the changes made\"),\n }),\n async execute({ pageId, title, content, versionMessage }) {\n const currentPage = await getPage(pageId, [\"version\"]);\n const storageContent = toStorageContent(content);\n\n const updatedPage = await updatePage(pageId, {\n title,\n content: storageContent,\n version: currentPage.version.number + 1,\n versionMessage,\n });\n\n return {\n id: updatedPage.id,\n title: updatedPage.title,\n type: updatedPage.type,\n url: updatedPage._links.webui,\n version: updatedPage.version.number,\n versionMessage: updatedPage.version.message,\n };\n },\n});\n",
300
300
  "tools/search-content.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { searchContent } from \"../../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"search-content\",\n description:\n \"Search for pages and blog posts in Confluence. Returns matching content with titles, excerpts, and links.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query to find pages or blog posts\"),\n spaceKey: z\n .string()\n .optional()\n .describe(\"Optional space key to limit search to a specific space\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }),\n async execute({ query, spaceKey, limit }) {\n const results = await searchContent(query, { spaceKey, limit });\n\n return results.map((result) => {\n const { content, excerpt, url } = result;\n const space = content.space;\n\n return {\n id: content.id,\n type: content.type,\n title: content.title,\n excerpt,\n url,\n space: space\n ? {\n id: space.id,\n key: space.key,\n name: space.name,\n }\n : undefined,\n lastUpdated: content.history?.lastUpdated.when,\n };\n });\n },\n});\n",
301
301
  "tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createPage, formatAsStorage } from \"../../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"create-page\",\n description:\n \"Create a new page in a Confluence space. Can optionally be created as a child of an existing page.\",\n inputSchema: z.object({\n spaceKey: z\n .string()\n .describe('The key of the space to create the page in (e.g., \"TEAM\", \"DEV\")'),\n title: z.string().describe(\"Title of the new page\"),\n content: z\n .string()\n .describe(\n \"Content for the page (can be plain text or Confluence storage format HTML)\",\n ),\n parentId: z\n .string()\n .optional()\n .describe(\"Optional ID of the parent page to create this as a child page\"),\n type: z\n .enum([\"page\", \"blogpost\"])\n .default(\"page\")\n .describe(\"Type of content to create\"),\n }),\n async execute({ spaceKey, title, content, parentId, type }) {\n const trimmedContent = content.trim();\n const storageContent = trimmedContent.startsWith(\"<\")\n ? trimmedContent\n : formatAsStorage(trimmedContent);\n\n const page = await createPage({\n spaceKey,\n title,\n content: storageContent,\n parentId,\n type,\n });\n\n return {\n id: page.id,\n title: page.title,\n type: page.type,\n url: page._links.webui,\n version: page.version.number,\n spaceId: page.spaceId,\n };\n },\n});\n",
@@ -308,7 +308,7 @@ export default {
308
308
  ".env.example": "# Mailchimp OAuth Configuration\n# Get your credentials from https://admin.mailchimp.com/account/oauth2/\nMAILCHIMP_CLIENT_ID=your-client-id\nMAILCHIMP_CLIENT_SECRET=your-client-secret\n",
309
309
  "lib/mailchimp-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nlet MAILCHIMP_BASE_URL = \"https://us1.api.mailchimp.com/3.0\";\n\ninterface MailchimpCampaign {\n id: string;\n web_id: number;\n type: string;\n create_time: string;\n archive_url: string;\n long_archive_url: string;\n status: string;\n emails_sent: number;\n send_time?: string;\n content_type: string;\n needs_block_refresh: boolean;\n recipients: {\n list_id: string;\n list_name: string;\n segment_text?: string;\n };\n settings: {\n subject_line: string;\n preview_text?: string;\n title: string;\n from_name: string;\n reply_to: string;\n };\n tracking: {\n opens: boolean;\n html_clicks: boolean;\n text_clicks: boolean;\n };\n report_summary?: {\n opens: number;\n unique_opens: number;\n open_rate: number;\n clicks: number;\n subscriber_clicks: number;\n click_rate: number;\n };\n}\n\ninterface MailchimpList {\n id: string;\n web_id: number;\n name: string;\n contact: {\n company: string;\n address1: string;\n city: string;\n state: string;\n zip: string;\n country: string;\n };\n permission_reminder: string;\n campaign_defaults: {\n from_name: string;\n from_email: string;\n subject: string;\n language: string;\n };\n stats: {\n member_count: number;\n total_contacts: number;\n unsubscribe_count: number;\n cleaned_count: number;\n member_count_since_send: number;\n unsubscribe_count_since_send: number;\n cleaned_count_since_send: number;\n campaign_count: number;\n open_rate: number;\n click_rate: number;\n };\n date_created: string;\n list_rating: number;\n subscribe_url_short: string;\n subscribe_url_long: string;\n}\n\ninterface MailchimpMember {\n id: string;\n email_address: string;\n unique_email_id: string;\n contact_id: string;\n full_name: string;\n web_id: number;\n email_type: string;\n status: \"subscribed\" | \"unsubscribed\" | \"cleaned\" | \"pending\" | \"transactional\";\n merge_fields: Record<string, unknown>;\n stats: {\n avg_open_rate: number;\n avg_click_rate: number;\n };\n ip_signup?: string;\n timestamp_signup?: string;\n ip_opt?: string;\n timestamp_opt?: string;\n member_rating: number;\n last_changed: string;\n language: string;\n vip: boolean;\n email_client?: string;\n location?: {\n latitude: number;\n longitude: number;\n gmtoff: number;\n dstoff: number;\n country_code: string;\n timezone: string;\n };\n tags: Array<{ id: number; name: string }>;\n}\n\ninterface MailchimpMetadata {\n dc: string;\n role: string;\n accountname: string;\n user_id: string;\n login: {\n email: string;\n avatar?: string;\n login_id: string;\n login_name: string;\n login_email: string;\n };\n}\n\nfunction buildQueryString(params: Record<string, string | number | undefined>): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n searchParams.set(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function fetchMetadata(token: string): Promise<MailchimpMetadata> {\n const response = await fetch(\"https://login.mailchimp.com/oauth2/metadata\", {\n headers: { Authorization: `OAuth ${token}` },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch Mailchimp metadata: ${response.statusText}`);\n }\n\n return response.json();\n}\n\nasync function initializeBaseUrl(): Promise<void> {\n const token = await getAccessToken();\n if (!token) return;\n\n try {\n const metadata = await fetchMetadata(token);\n MAILCHIMP_BASE_URL = `https://${metadata.dc}.api.mailchimp.com/3.0`;\n } catch (error) {\n // Fallback to us1 if metadata fetch fails\n console.error(\"Failed to fetch Mailchimp metadata:\", error);\n }\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Mailchimp. Please connect your account.\");\n }\n return token;\n}\n\nasync function mailchimpFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n\n if (MAILCHIMP_BASE_URL === \"https://us1.api.mailchimp.com/3.0\") {\n await initializeBaseUrl();\n }\n\n const response = await fetch(`${MAILCHIMP_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { detail?: string };\n throw new Error(`Mailchimp API error: ${response.status} ${error.detail ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function listCampaigns(options?: {\n status?: \"save\" | \"paused\" | \"schedule\" | \"sending\" | \"sent\";\n count?: number;\n offset?: number;\n}): Promise<MailchimpCampaign[]> {\n const query = buildQueryString({\n status: options?.status,\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ campaigns: MailchimpCampaign[] }>(`/campaigns${query}`);\n return response.campaigns;\n}\n\nexport async function getCampaign(campaignId: string): Promise<MailchimpCampaign> {\n return mailchimpFetch<MailchimpCampaign>(`/campaigns/${campaignId}`);\n}\n\nexport async function listLists(options?: { count?: number; offset?: number }): Promise<MailchimpList[]> {\n const query = buildQueryString({\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ lists: MailchimpList[] }>(`/lists${query}`);\n return response.lists;\n}\n\nexport async function getList(listId: string): Promise<MailchimpList> {\n return mailchimpFetch<MailchimpList>(`/lists/${listId}`);\n}\n\nexport async function listMembers(\n listId: string,\n options?: {\n status?: \"subscribed\" | \"unsubscribed\" | \"cleaned\" | \"pending\" | \"transactional\";\n count?: number;\n offset?: number;\n },\n): Promise<MailchimpMember[]> {\n const query = buildQueryString({\n status: options?.status,\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ members: MailchimpMember[] }>(\n `/lists/${listId}/members${query}`,\n );\n return response.members;\n}\n\nexport async function getMetadata(): Promise<MailchimpMetadata> {\n const token = await requireAccessToken();\n return fetchMetadata(token);\n}\n",
310
310
  "app/api/auth/mailchimp/route.ts": "import { createOAuthInitHandler, mailchimpConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(mailchimpConfig, { tokenStore: oauthMemoryTokenStore });\n",
311
- "app/api/auth/mailchimp/callback/route.ts": "import { createOAuthCallbackHandler, mailchimpConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mailchimpConfig, { tokenStore: hybridTokenStore });\n",
311
+ "app/api/auth/mailchimp/callback/route.ts": "import { createOAuthCallbackHandler, mailchimpConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mailchimpConfig, { tokenStore: hybridTokenStore });\n",
312
312
  "tools/get-campaign.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getCampaign } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"get-campaign\",\n description: \"Get details of a specific Mailchimp campaign by its ID.\",\n inputSchema: z.object({\n campaignId: z.string().describe(\"The ID of the campaign to retrieve\"),\n }),\n async execute({ campaignId }) {\n const campaign = await getCampaign(campaignId);\n const reportSummary = campaign.report_summary;\n\n return {\n id: campaign.id,\n webId: campaign.web_id,\n type: campaign.type,\n status: campaign.status,\n title: campaign.settings.title,\n subject: campaign.settings.subject_line,\n previewText: campaign.settings.preview_text,\n fromName: campaign.settings.from_name,\n replyTo: campaign.settings.reply_to,\n listId: campaign.recipients.list_id,\n listName: campaign.recipients.list_name,\n segmentText: campaign.recipients.segment_text,\n emailsSent: campaign.emails_sent,\n sendTime: campaign.send_time,\n createdAt: campaign.create_time,\n archiveUrl: campaign.archive_url,\n longArchiveUrl: campaign.long_archive_url,\n tracking: campaign.tracking,\n reportSummary: reportSummary\n ? {\n opens: reportSummary.opens,\n uniqueOpens: reportSummary.unique_opens,\n openRate: reportSummary.open_rate,\n clicks: reportSummary.clicks,\n subscriberClicks: reportSummary.subscriber_clicks,\n clickRate: reportSummary.click_rate,\n }\n : undefined,\n };\n },\n});\n",
313
313
  "tools/list-lists.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listLists } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"list-lists\",\n description: \"List all audience lists (mailing lists) in Mailchimp with their statistics.\",\n inputSchema: z.object({\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of lists to return\"),\n }),\n async execute({ limit }) {\n const lists = await listLists({ count: limit });\n\n return lists.map((list) => {\n const { contact, campaign_defaults: campaignDefaults, stats } = list;\n\n return {\n id: list.id,\n webId: list.web_id,\n name: list.name,\n dateCreated: list.date_created,\n listRating: list.list_rating,\n subscribeUrl: list.subscribe_url_short,\n contact: {\n company: contact.company,\n city: contact.city,\n state: contact.state,\n country: contact.country,\n },\n campaignDefaults: {\n fromName: campaignDefaults.from_name,\n fromEmail: campaignDefaults.from_email,\n subject: campaignDefaults.subject,\n language: campaignDefaults.language,\n },\n stats: {\n memberCount: stats.member_count,\n totalContacts: stats.total_contacts,\n unsubscribeCount: stats.unsubscribe_count,\n cleanedCount: stats.cleaned_count,\n campaignCount: stats.campaign_count,\n openRate: stats.open_rate,\n clickRate: stats.click_rate,\n },\n };\n });\n },\n});\n",
314
314
  "tools/list-members.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listMembers } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"list-members\",\n description:\n \"List subscribers/members in a Mailchimp audience list. Can filter by subscription status.\",\n inputSchema: z.object({\n listId: z.string().describe(\"The ID of the audience list to get members from\"),\n status: z\n .enum([\"subscribed\", \"unsubscribed\", \"cleaned\", \"pending\", \"transactional\"])\n .optional()\n .describe(\"Filter members by subscription status\"),\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of members to return\"),\n }),\n async execute({ listId, status, limit }) {\n const members = await listMembers(listId, { status, count: limit });\n\n return members.map((member) => {\n const location = member.location\n ? {\n countryCode: member.location.country_code,\n timezone: member.location.timezone,\n latitude: member.location.latitude,\n longitude: member.location.longitude,\n }\n : undefined;\n\n return {\n id: member.id,\n emailAddress: member.email_address,\n uniqueEmailId: member.unique_email_id,\n contactId: member.contact_id,\n fullName: member.full_name,\n status: member.status,\n emailType: member.email_type,\n vip: member.vip,\n language: member.language,\n memberRating: member.member_rating,\n lastChanged: member.last_changed,\n timestampSignup: member.timestamp_signup,\n timestampOpt: member.timestamp_opt,\n ipSignup: member.ip_signup,\n ipOpt: member.ip_opt,\n stats: {\n avgOpenRate: member.stats.avg_open_rate,\n avgClickRate: member.stats.avg_click_rate,\n },\n mergeFields: member.merge_fields,\n tags: member.tags.map(({ id, name }) => ({ id, name })),\n location,\n };\n });\n },\n});\n",
@@ -320,7 +320,7 @@ export default {
320
320
  "files": {
321
321
  "lib/teams-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_API_BASE = \"https://graph.microsoft.com/v1.0\";\n\ninterface GraphResponse<T> {\n \"@odata.context\"?: string;\n \"@odata.nextLink\"?: string;\n value?: T[];\n}\n\nexport interface TeamsChat {\n id: string;\n topic: string | null;\n createdDateTime: string;\n lastUpdatedDateTime: string;\n chatType: \"oneOnOne\" | \"group\" | \"meeting\";\n webUrl?: string;\n members?: ChatMember[];\n}\n\nexport interface ChatMember {\n \"@odata.type\": string;\n id: string;\n displayName?: string;\n userId?: string;\n email?: string;\n}\n\nexport interface ChatMessage {\n id: string;\n messageType: \"message\" | \"chatEvent\" | \"typing\";\n createdDateTime: string;\n lastModifiedDateTime?: string;\n deletedDateTime?: string;\n subject?: string | null;\n summary?: string | null;\n importance: \"normal\" | \"high\" | \"urgent\";\n locale?: string;\n from: {\n user?: {\n id: string;\n displayName?: string;\n userIdentityType?: string;\n };\n };\n body: {\n contentType: \"text\" | \"html\";\n content: string;\n };\n attachments?: Array<{\n id: string;\n contentType: string;\n contentUrl?: string;\n content?: string;\n name?: string;\n }>;\n mentions?: Array<{\n id: number;\n mentionText: string;\n mentioned: {\n user: {\n id: string;\n displayName?: string;\n };\n };\n }>;\n reactions?: Array<{\n reactionType: string;\n createdDateTime: string;\n user: {\n id: string;\n displayName?: string;\n };\n }>;\n}\n\nexport interface Team {\n id: string;\n displayName: string;\n description?: string;\n createdDateTime?: string;\n webUrl?: string;\n isArchived?: boolean;\n visibility?: \"private\" | \"public\";\n}\n\nexport interface Channel {\n id: string;\n displayName: string;\n description?: string;\n email?: string;\n webUrl?: string;\n membershipType?: \"standard\" | \"private\" | \"shared\";\n createdDateTime?: string;\n}\n\nfunction buildEndpoint(path: string, params?: URLSearchParams): string {\n const queryString = params?.toString();\n return queryString ? `${path}?${queryString}` : path;\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Microsoft Teams. Please connect your account.\");\n }\n\n const url = endpoint.startsWith(\"http\") ? endpoint : `${GRAPH_API_BASE}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error?.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function listChats(options?: { limit?: number; expand?: string[] }): Promise<TeamsChat[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n if (options?.expand?.length) params.set(\"$expand\", options.expand.join(\",\"));\n\n const response = await graphFetch<GraphResponse<TeamsChat>>(buildEndpoint(\"/me/chats\", params));\n return response.value ?? [];\n}\n\nexport async function getChatMessages(\n chatId: string,\n options?: { limit?: number; orderBy?: string },\n): Promise<ChatMessage[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n params.set(\"$orderby\", options?.orderBy ?? \"createdDateTime desc\");\n\n const response = await graphFetch<GraphResponse<ChatMessage>>(\n buildEndpoint(`/me/chats/${chatId}/messages`, params),\n );\n return response.value ?? [];\n}\n\nexport function sendChatMessage(\n chatId: string,\n content: string,\n contentType: \"text\" | \"html\" = \"text\",\n): Promise<ChatMessage> {\n return graphFetch<ChatMessage>(`/me/chats/${chatId}/messages`, {\n method: \"POST\",\n body: JSON.stringify({ body: { contentType, content } }),\n });\n}\n\nexport async function listTeams(options?: { limit?: number }): Promise<Team[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n\n const response = await graphFetch<GraphResponse<Team>>(buildEndpoint(\"/me/joinedTeams\", params));\n return response.value ?? [];\n}\n\nexport async function listChannels(teamId: string, options?: { limit?: number }): Promise<Channel[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n\n const response = await graphFetch<GraphResponse<Channel>>(\n buildEndpoint(`/teams/${teamId}/channels`, params),\n );\n return response.value ?? [];\n}\n\nexport function sendChannelMessage(\n teamId: string,\n channelId: string,\n content: string,\n contentType: \"text\" | \"html\" = \"text\",\n subject?: string,\n): Promise<ChatMessage> {\n const body: Record<string, unknown> = { body: { contentType, content } };\n if (subject) body.subject = subject;\n\n return graphFetch<ChatMessage>(`/teams/${teamId}/channels/${channelId}/messages`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function getChannelMessages(\n teamId: string,\n channelId: string,\n options?: { limit?: number; orderBy?: string },\n): Promise<ChatMessage[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n params.set(\"$orderby\", options?.orderBy ?? \"createdDateTime desc\");\n\n const response = await graphFetch<GraphResponse<ChatMessage>>(\n buildEndpoint(`/teams/${teamId}/channels/${channelId}/messages`, params),\n );\n return response.value ?? [];\n}\n\nexport function getCurrentUser(): Promise<{\n id: string;\n displayName: string;\n mail?: string;\n userPrincipalName?: string;\n}> {\n return graphFetch(\"/me\");\n}\n\nexport function getChatDisplayName(chat: TeamsChat): string {\n if (chat.topic) return chat.topic;\n\n const memberNames = chat.members?.flatMap((m) => (m.displayName ? [m.displayName] : [])).join(\", \");\n if (memberNames) return memberNames;\n\n return chat.chatType === \"oneOnOne\" ? \"Direct Chat\" : \"Group Chat\";\n}\n\nexport function getPlainTextContent(message: ChatMessage): string {\n if (message.body.contentType === \"text\") return message.body.content;\n\n return message.body.content\n .replace(/<[^>]*>/g, \"\")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .trim();\n}\n",
322
322
  "app/api/auth/teams/route.ts": "import { createOAuthInitHandler, teamsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(teamsConfig, { tokenStore: oauthMemoryTokenStore });\n",
323
- "app/api/auth/teams/callback/route.ts": "/**\n * Teams OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, teamsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(teamsConfig, { tokenStore: hybridTokenStore });\n",
323
+ "app/api/auth/teams/callback/route.ts": "/**\n * Teams OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, teamsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(teamsConfig, { tokenStore: hybridTokenStore });\n",
324
324
  "tools/list-chats.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getChatDisplayName, listChats } from \"../../lib/teams-client.ts\";\n\nexport default tool({\n id: \"list-chats\",\n description:\n \"List recent Microsoft Teams chats for the authenticated user. Returns chat IDs, names, types, and last updated timestamps.\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of chats to return (1-50)\"),\n expandMembers: z\n .boolean()\n .default(false)\n .describe(\"Include chat member information\"),\n }),\n async execute({ limit, expandMembers }) {\n const chats = await listChats({\n limit,\n expand: expandMembers ? [\"members\"] : undefined,\n });\n\n return chats.map((chat) => {\n const members = expandMembers\n ? chat.members?.map(({ id, displayName, email }) => ({\n id,\n displayName,\n email,\n }))\n : undefined;\n\n return {\n id: chat.id,\n name: getChatDisplayName(chat),\n type: chat.chatType,\n topic: chat.topic,\n lastUpdated: chat.lastUpdatedDateTime,\n created: chat.createdDateTime,\n webUrl: chat.webUrl,\n memberCount: chat.members?.length,\n members,\n };\n });\n },\n});\n",
325
325
  "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { sendChannelMessage, sendChatMessage } from \"../../lib/teams-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description:\n \"Send a message to a Microsoft Teams chat or channel. For chats, use the chatId. For channels, use both teamId and channelId.\",\n inputSchema: z\n .object({\n chatId: z\n .string()\n .optional()\n .describe(\"The ID of the chat to send the message to (use this for direct/group chats)\"),\n teamId: z\n .string()\n .optional()\n .describe(\"The ID of the team (use with channelId for channel messages)\"),\n channelId: z\n .string()\n .optional()\n .describe(\"The ID of the channel (use with teamId for channel messages)\"),\n content: z.string().min(1).describe(\"The message content to send\"),\n contentType: z.enum([\"text\", \"html\"]).default(\"text\").describe(\"Content format: text or html\"),\n subject: z.string().optional().describe(\"Subject line (only for channel messages)\"),\n })\n .refine(\n (data) =>\n (data.chatId && !data.teamId && !data.channelId) ||\n (!data.chatId && data.teamId && data.channelId),\n { message: \"Either provide chatId OR both teamId and channelId\" },\n ),\n async execute({ chatId, teamId, channelId, content, contentType, subject }) {\n if (chatId) {\n const message = await sendChatMessage(chatId, content, contentType);\n return {\n success: true,\n messageId: message.id,\n type: \"chat\",\n chatId,\n createdAt: message.createdDateTime,\n content: message.body.content,\n };\n }\n\n if (!teamId || !channelId) {\n throw new Error(\"Invalid parameters: provide either chatId or both teamId and channelId\");\n }\n\n const message = await sendChannelMessage(teamId, channelId, content, contentType, subject);\n return {\n success: true,\n messageId: message.id,\n type: \"channel\",\n teamId,\n channelId,\n subject,\n createdAt: message.createdDateTime,\n content: message.body.content,\n };\n },\n});\n",
326
326
  "tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listChannels } from \"../../lib/teams-client.ts\";\n\nexport default tool({\n id: \"list-channels\",\n description:\n \"List all channels in a specific Microsoft Team. Use list-teams first to get team IDs. Returns channel IDs, names, descriptions, and types.\",\n inputSchema: z.object({\n teamId: z.string().describe(\"The ID of the team to list channels from\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(25)\n .describe(\"Maximum number of channels to return (1-50)\"),\n }),\n async execute({ teamId, limit }) {\n const channels = await listChannels(teamId, { limit });\n\n return channels.map((channel) => ({\n id: channel.id,\n name: channel.displayName,\n description: channel.description,\n email: channel.email,\n webUrl: channel.webUrl,\n membershipType: channel.membershipType,\n createdAt: channel.createdDateTime,\n }));\n },\n});\n",
@@ -333,7 +333,7 @@ export default {
333
333
  ".env.example": "# Shopify OAuth Configuration\n# Get your credentials from https://partners.shopify.com\nSHOPIFY_CLIENT_ID=your-client-id\nSHOPIFY_CLIENT_SECRET=your-client-secret\nSHOPIFY_SHOP_DOMAIN=mystore.myshopify.com\n",
334
334
  "lib/shopify-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst SHOPIFY_SHOP_DOMAIN = process.env.SHOPIFY_SHOP_DOMAIN ?? \"shop.myshopify.com\";\nconst SHOPIFY_API_VERSION = \"2024-01\";\nconst SHOPIFY_BASE_URL = `https://${SHOPIFY_SHOP_DOMAIN}/admin/api/${SHOPIFY_API_VERSION}`;\n\ninterface ShopifyProduct {\n id: number;\n title: string;\n body_html: string;\n vendor: string;\n product_type: string;\n created_at: string;\n updated_at: string;\n published_at: string | null;\n status: string;\n tags: string;\n variants: Array<{\n id: number;\n title: string;\n price: string;\n sku: string;\n inventory_quantity: number;\n }>;\n images: Array<{\n id: number;\n src: string;\n alt: string | null;\n }>;\n}\n\ninterface ShopifyOrder {\n id: number;\n order_number: number;\n email: string;\n created_at: string;\n updated_at: string;\n total_price: string;\n subtotal_price: string;\n total_tax: string;\n currency: string;\n financial_status: string;\n fulfillment_status: string | null;\n customer: {\n id: number;\n email: string;\n first_name: string;\n last_name: string;\n } | null;\n line_items: Array<{\n id: number;\n title: string;\n quantity: number;\n price: string;\n sku: string;\n variant_title: string;\n }>;\n shipping_address: {\n address1: string;\n city: string;\n province: string;\n country: string;\n zip: string;\n } | null;\n}\n\ninterface ShopifyCustomer {\n id: number;\n email: string;\n first_name: string;\n last_name: string;\n phone: string | null;\n created_at: string;\n updated_at: string;\n orders_count: number;\n total_spent: string;\n tags: string;\n state: string;\n verified_email: boolean;\n addresses: Array<{\n id: number;\n address1: string;\n city: string;\n province: string;\n country: string;\n zip: string;\n default: boolean;\n }>;\n}\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function shopifyFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Shopify. Please connect your account.\");\n }\n\n const response = await fetch(`${SHOPIFY_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n \"X-Shopify-Access-Token\": token,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n let errors: string | undefined;\n try {\n const body = (await response.json()) as { errors?: string };\n errors = body.errors;\n } catch {\n // ignore JSON parse errors\n }\n\n throw new Error(`Shopify API error: ${response.status} ${errors ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function listProducts(options?: {\n limit?: number;\n status?: \"active\" | \"archived\" | \"draft\";\n productType?: string;\n}): Promise<ShopifyProduct[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.status) params.set(\"status\", options.status);\n if (options?.productType) params.set(\"product_type\", options.productType);\n\n const { products } = await shopifyFetch<{ products: ShopifyProduct[] }>(\n `/products.json${buildQuery(params)}`,\n );\n return products;\n}\n\nexport async function getProduct(productId: number | string): Promise<ShopifyProduct> {\n const { product } = await shopifyFetch<{ product: ShopifyProduct }>(`/products/${productId}.json`);\n return product;\n}\n\nexport async function listOrders(options?: {\n limit?: number;\n status?: \"open\" | \"closed\" | \"cancelled\" | \"any\";\n financialStatus?: \"pending\" | \"authorized\" | \"paid\" | \"refunded\" | \"voided\";\n fulfillmentStatus?: \"shipped\" | \"partial\" | \"unshipped\" | \"any\" | \"unfulfilled\";\n}): Promise<ShopifyOrder[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.status) params.set(\"status\", options.status);\n if (options?.financialStatus) params.set(\"financial_status\", options.financialStatus);\n if (options?.fulfillmentStatus) params.set(\"fulfillment_status\", options.fulfillmentStatus);\n\n const { orders } = await shopifyFetch<{ orders: ShopifyOrder[] }>(\n `/orders.json${buildQuery(params)}`,\n );\n return orders;\n}\n\nexport async function getOrder(orderId: number | string): Promise<ShopifyOrder> {\n const { order } = await shopifyFetch<{ order: ShopifyOrder }>(`/orders/${orderId}.json`);\n return order;\n}\n\nexport async function listCustomers(options?: {\n limit?: number;\n query?: string;\n}): Promise<ShopifyCustomer[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.query) params.set(\"query\", options.query);\n\n const { customers } = await shopifyFetch<{ customers: ShopifyCustomer[] }>(\n `/customers.json${buildQuery(params)}`,\n );\n return customers;\n}\n\nexport async function getCustomer(customerId: number | string): Promise<ShopifyCustomer> {\n const { customer } = await shopifyFetch<{ customer: ShopifyCustomer }>(\n `/customers/${customerId}.json`,\n );\n return customer;\n}\n\nexport async function getShopInfo(): Promise<{\n id: number;\n name: string;\n email: string;\n domain: string;\n currency: string;\n timezone: string;\n}> {\n const { shop } = await shopifyFetch<{\n shop: {\n id: number;\n name: string;\n email: string;\n domain: string;\n currency: string;\n timezone: string;\n };\n }>(\"/shop.json\");\n\n return shop;\n}\n",
335
335
  "app/api/auth/shopify/route.ts": "import { createOAuthInitHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(shopifyConfig, { tokenStore: oauthMemoryTokenStore });\n",
336
- "app/api/auth/shopify/callback/route.ts": "import { createOAuthCallbackHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(shopifyConfig, { tokenStore: hybridTokenStore });\n",
336
+ "app/api/auth/shopify/callback/route.ts": "import { createOAuthCallbackHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(shopifyConfig, { tokenStore: hybridTokenStore });\n",
337
337
  "tools/get-order.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getOrder } from \"../../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"get-order\",\n description: \"Get details of a specific Shopify order by its ID.\",\n inputSchema: z.object({\n orderId: z.union([z.number(), z.string()]).describe(\"The ID of the order to retrieve\"),\n }),\n async execute({ orderId }) {\n const order = await getOrder(orderId);\n\n return {\n id: order.id,\n orderNumber: order.order_number,\n email: order.email,\n createdAt: order.created_at,\n updatedAt: order.updated_at,\n totalPrice: order.total_price,\n subtotalPrice: order.subtotal_price,\n totalTax: order.total_tax,\n currency: order.currency,\n financialStatus: order.financial_status,\n fulfillmentStatus: order.fulfillment_status,\n customer: order.customer\n ? {\n id: order.customer.id,\n email: order.customer.email,\n firstName: order.customer.first_name,\n lastName: order.customer.last_name,\n }\n : null,\n lineItems: order.line_items.map((item) => ({\n id: item.id,\n title: item.title,\n quantity: item.quantity,\n price: item.price,\n sku: item.sku,\n variantTitle: item.variant_title,\n })),\n shippingAddress: order.shipping_address\n ? {\n address1: order.shipping_address.address1,\n city: order.shipping_address.city,\n province: order.shipping_address.province,\n country: order.shipping_address.country,\n zip: order.shipping_address.zip,\n }\n : null,\n };\n },\n});\n",
338
338
  "tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listCustomers } from \"../../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"list-customers\",\n description: \"List customers from your Shopify store. Can search by query string.\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(250)\n .default(20)\n .describe(\"Maximum number of customers to return\"),\n query: z\n .string()\n .optional()\n .describe(\"Search query to filter customers (e.g., email, name)\"),\n }),\n async execute({ limit, query }) {\n const customers = await listCustomers({ limit, query });\n\n return customers.map((customer) => ({\n id: customer.id,\n email: customer.email,\n firstName: customer.first_name,\n lastName: customer.last_name,\n phone: customer.phone,\n createdAt: customer.created_at,\n updatedAt: customer.updated_at,\n ordersCount: customer.orders_count,\n totalSpent: customer.total_spent,\n tags: customer.tags,\n state: customer.state,\n verifiedEmail: customer.verified_email,\n addresses: customer.addresses.map((address) => ({\n id: address.id,\n address1: address.address1,\n city: address.city,\n province: address.province,\n country: address.country,\n zip: address.zip,\n default: address.default,\n })),\n }));\n },\n});\n",
339
339
  "tools/get-product.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getProduct } from \"../../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"get-product\",\n description: \"Get details of a specific Shopify product by its ID.\",\n inputSchema: z.object({\n productId: z.union([z.number(), z.string()]).describe(\"The ID of the product to retrieve\"),\n }),\n async execute({ productId }) {\n const product = await getProduct(productId);\n\n return {\n id: product.id,\n title: product.title,\n bodyHtml: product.body_html,\n vendor: product.vendor,\n productType: product.product_type,\n status: product.status,\n tags: product.tags,\n createdAt: product.created_at,\n updatedAt: product.updated_at,\n publishedAt: product.published_at,\n variants: product.variants.map((variant) => ({\n id: variant.id,\n title: variant.title,\n price: variant.price,\n sku: variant.sku,\n inventoryQuantity: variant.inventory_quantity,\n })),\n images: product.images.map((image) => ({\n id: image.id,\n src: image.src,\n alt: image.alt,\n })),\n };\n },\n});\n",
@@ -346,7 +346,7 @@ export default {
346
346
  ".env.example": "# HubSpot OAuth Configuration\n# Get these from https://app.hubspot.com/developer\n\nHUBSPOT_CLIENT_ID=your_client_id_here\nHUBSPOT_CLIENT_SECRET=your_client_secret_here\n",
347
347
  "lib/hubspot-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst HUBSPOT_BASE_URL = \"https://api.hubapi.com\";\n\ninterface HubSpotPagination {\n after?: string;\n next?: {\n after: string;\n link: string;\n };\n}\n\ninterface HubSpotResponse<T> {\n results: T[];\n paging?: HubSpotPagination;\n}\n\ninterface HubSpotContact {\n id: string;\n properties: {\n email?: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n createdate?: string;\n lastmodifieddate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\ninterface HubSpotCompany {\n id: string;\n properties: {\n name?: string;\n domain?: string;\n city?: string;\n state?: string;\n country?: string;\n industry?: string;\n phone?: string;\n createdate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\ninterface HubSpotDeal {\n id: string;\n properties: {\n dealname?: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n createdate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\nfunction buildQueryString(options: {\n limit?: number;\n after?: string;\n properties?: string[];\n defaultProperties: string[];\n}): string {\n const params = new URLSearchParams();\n\n if (options.limit) params.set(\"limit\", options.limit.toString());\n if (options.after) params.set(\"after\", options.after);\n\n const properties =\n options.properties?.length ? options.properties : options.defaultProperties;\n\n for (const prop of properties) params.append(\"properties\", prop);\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : \"\";\n}\n\nasync function hubspotFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with HubSpot. Please connect your account.\");\n }\n\n const response = await fetch(`${HUBSPOT_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(\n `HubSpot API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\n// ============================================================================\n// CONTACTS\n// ============================================================================\n\nexport function listContacts(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotContact>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotContact>>(`/crm/v3/objects/contacts${query}`);\n}\n\nexport function getContact(contactId: string, properties?: string[]): Promise<HubSpotContact> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\", \"website\"],\n });\n\n return hubspotFetch<HubSpotContact>(`/crm/v3/objects/contacts/${contactId}${query}`);\n}\n\nexport function createContact(properties: {\n email: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotContact> {\n return hubspotFetch<HubSpotContact>(\"/crm/v3/objects/contacts\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function updateContact(\n contactId: string,\n properties: {\n email?: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n [key: string]: string | undefined;\n },\n): Promise<HubSpotContact> {\n return hubspotFetch<HubSpotContact>(`/crm/v3/objects/contacts/${contactId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function searchContacts(options: {\n query?: string;\n filterGroups?: Array<{\n filters: Array<{\n propertyName: string;\n operator: string;\n value: string;\n }>;\n }>;\n properties?: string[];\n limit?: number;\n after?: string;\n}): Promise<HubSpotResponse<HubSpotContact>> {\n const body: Record<string, unknown> = {\n properties: options.properties?.length\n ? options.properties\n : [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\"],\n };\n\n if (options.filterGroups) body.filterGroups = options.filterGroups;\n if (options.limit) body.limit = options.limit;\n if (options.after) body.after = options.after;\n\n return hubspotFetch<HubSpotResponse<HubSpotContact>>(\"/crm/v3/objects/contacts/search\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\n// ============================================================================\n// COMPANIES\n// ============================================================================\n\nexport function listCompanies(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotCompany>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"name\", \"domain\", \"city\", \"state\", \"industry\", \"phone\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotCompany>>(`/crm/v3/objects/companies${query}`);\n}\n\nexport function getCompany(companyId: string, properties?: string[]): Promise<HubSpotCompany> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"name\", \"domain\", \"city\", \"state\", \"country\", \"industry\", \"phone\"],\n });\n\n return hubspotFetch<HubSpotCompany>(`/crm/v3/objects/companies/${companyId}${query}`);\n}\n\nexport function createCompany(properties: {\n name: string;\n domain?: string;\n city?: string;\n state?: string;\n country?: string;\n industry?: string;\n phone?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotCompany> {\n return hubspotFetch<HubSpotCompany>(\"/crm/v3/objects/companies\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\n// ============================================================================\n// DEALS\n// ============================================================================\n\nexport function listDeals(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotDeal>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"dealname\", \"amount\", \"dealstage\", \"pipeline\", \"closedate\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotDeal>>(`/crm/v3/objects/deals${query}`);\n}\n\nexport function getDeal(dealId: string, properties?: string[]): Promise<HubSpotDeal> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"dealname\", \"amount\", \"dealstage\", \"pipeline\", \"closedate\"],\n });\n\n return hubspotFetch<HubSpotDeal>(`/crm/v3/objects/deals/${dealId}${query}`);\n}\n\nexport function createDeal(properties: {\n dealname: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotDeal> {\n return hubspotFetch<HubSpotDeal>(\"/crm/v3/objects/deals\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function updateDeal(\n dealId: string,\n properties: {\n dealname?: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n [key: string]: string | undefined;\n },\n): Promise<HubSpotDeal> {\n return hubspotFetch<HubSpotDeal>(`/crm/v3/objects/deals/${dealId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ properties }),\n });\n}\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\nexport function formatContactName(contact: HubSpotContact): string {\n const parts = [contact.properties.firstname, contact.properties.lastname].filter(\n (p): p is string => Boolean(p),\n );\n\n if (parts.length) return parts.join(\" \");\n return contact.properties.email ?? \"Unnamed Contact\";\n}\n\nexport function formatCompanyName(company: HubSpotCompany): string {\n return company.properties.name ?? company.properties.domain ?? \"Unnamed Company\";\n}\n\nexport function formatDealName(deal: HubSpotDeal): string {\n return deal.properties.dealname ?? \"Unnamed Deal\";\n}\n\nexport type { HubSpotCompany, HubSpotContact, HubSpotDeal, HubSpotResponse };\n",
348
348
  "app/api/auth/hubspot/route.ts": "import { createOAuthInitHandler, hubspotConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(hubspotConfig, { tokenStore: oauthMemoryTokenStore });\n",
349
- "app/api/auth/hubspot/callback/route.ts": "import { createOAuthCallbackHandler, hubspotConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(hubspotConfig, { tokenStore: hybridTokenStore });\n",
349
+ "app/api/auth/hubspot/callback/route.ts": "import { createOAuthCallbackHandler, hubspotConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(hubspotConfig, { tokenStore: hybridTokenStore });\n",
350
350
  "tools/create-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createContact, formatContactName } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"create-contact\",\n description: \"Create a new contact in HubSpot CRM. Email is required, other fields are optional.\",\n inputSchema: z.object({\n email: z.string().email().describe(\"Contact email address (required)\"),\n firstname: z.string().optional().describe(\"First name\"),\n lastname: z.string().optional().describe(\"Last name\"),\n phone: z.string().optional().describe(\"Phone number\"),\n company: z.string().optional().describe(\"Company name\"),\n jobtitle: z.string().optional().describe(\"Job title\"),\n website: z.string().optional().describe(\"Website URL\"),\n }),\n async execute({ email, firstname, lastname, phone, company, jobtitle, website }) {\n const properties: Record<string, string> = { email };\n\n if (firstname) properties.firstname = firstname;\n if (lastname) properties.lastname = lastname;\n if (phone) properties.phone = phone;\n if (company) properties.company = company;\n if (jobtitle) properties.jobtitle = jobtitle;\n if (website) properties.website = website;\n\n const contact = await createContact(properties);\n const name = formatContactName(contact);\n\n return {\n id: contact.id,\n name,\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n website: contact.properties.website,\n createdAt: contact.createdAt,\n message: `Successfully created contact: ${name}`,\n };\n },\n});\n",
351
351
  "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatContactName, listContacts } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List contacts from your HubSpot CRM. Returns contact information including name, email, phone, company, and job title.\",\n inputSchema: z.object({\n limit: z.number().min(1).max(100).default(10).describe(\"Maximum number of contacts to return\"),\n properties: z\n .array(z.string())\n .optional()\n .describe(\"Additional properties to retrieve (e.g., website, city, state)\"),\n }),\n async execute({ limit, properties }) {\n const response = await listContacts({ limit, properties });\n\n return {\n contacts: response.results.map((contact) => {\n if (!properties) {\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n additionalProperties: undefined,\n };\n }\n\n const additionalProperties = Object.fromEntries(\n properties\n .filter((prop) => contact.properties[prop] !== undefined)\n .map((prop) => [prop, contact.properties[prop]]),\n );\n\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n additionalProperties,\n };\n }),\n hasMore: Boolean(response.paging?.next),\n nextAfter: response.paging?.next?.after,\n };\n },\n});\n",
352
352
  "tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatContactName, getContact } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"get-contact\",\n description:\n \"Get detailed information about a specific contact in HubSpot CRM by their contact ID.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The HubSpot contact ID\"),\n properties: z\n .array(z.string())\n .optional()\n .describe(\n \"Additional properties to retrieve (e.g., website, city, state, notes)\",\n ),\n }),\n async execute({ contactId, properties }) {\n const contact = await getContact(contactId, properties);\n\n const additionalProperties = properties\n ? Object.fromEntries(\n properties\n .filter((prop) => contact.properties[prop] !== undefined)\n .map((prop) => [prop, contact.properties[prop]]),\n )\n : undefined;\n\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n website: contact.properties.website,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n archived: contact.archived,\n additionalProperties,\n allProperties: contact.properties,\n };\n },\n});\n",
@@ -384,7 +384,7 @@ export default {
384
384
  ".env.example": "# OneDrive Integration Environment Variables\n\n# Microsoft Azure App Client ID\n# Get this from https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\nMICROSOFT_CLIENT_ID=your_client_id_here\n\n# Microsoft Azure App Client Secret\n# Get this from https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\nMICROSOFT_CLIENT_SECRET=your_client_secret_here\n\n# Setup Instructions:\n# 1. Go to https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\n# 2. Create a new app registration or select an existing one\n# 3. Note the Application (client) ID\n# 4. Create a new client secret under \"Certificates & secrets\"\n# 5. Add the OAuth2 redirect URI under \"Authentication\": http://localhost:3000/api/auth/onedrive/callback\n# 6. Grant the following Microsoft Graph API permissions under \"API permissions\":\n# - Files.Read\n# - Files.ReadWrite\n# - Files.Read.All\n# - Files.ReadWrite.All\n# - offline_access\n# 7. Grant admin consent if required by your organization\n",
385
385
  "lib/onedrive-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_API_URL = \"https://graph.microsoft.com/v1.0\";\n\nexport interface DriveItem {\n id: string;\n name: string;\n size?: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n parentReference?: {\n driveId: string;\n id: string;\n path: string;\n };\n file?: {\n mimeType: string;\n hashes?: {\n quickXorHash?: string;\n sha1Hash?: string;\n sha256Hash?: string;\n };\n };\n folder?: {\n childCount: number;\n };\n \"@microsoft.graph.downloadUrl\"?: string;\n}\n\nexport interface FileMetadata {\n id: string;\n name: string;\n size: number;\n mimeType: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n downloadUrl?: string;\n}\n\nexport interface FolderMetadata {\n id: string;\n name: string;\n childCount: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n}\n\nexport interface ListFilesResult {\n value: DriveItem[];\n \"@odata.nextLink\"?: string;\n}\n\nexport interface SearchResult {\n value: DriveItem[];\n \"@odata.nextLink\"?: string;\n}\n\nasync function getTokenOrThrow(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with OneDrive. Please connect your account.\");\n }\n return token;\n}\n\nasync function onedriveFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getTokenOrThrow();\n const url = endpoint.startsWith(\"http\") ? endpoint : `${GRAPH_API_URL}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `OneDrive API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport function listFiles(\n folderId: string = \"root\",\n options?: {\n orderBy?: string;\n top?: number;\n select?: string[];\n },\n): Promise<ListFilesResult> {\n const params = new URLSearchParams();\n\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n if (options?.top) params.set(\"$top\", options.top.toString());\n if (options?.select?.length) params.set(\"$select\", options.select.join(\",\"));\n\n const queryString = params.toString();\n const endpoint = `/me/drive/items/${folderId}/children${queryString ? `?${queryString}` : \"\"}`;\n\n return onedriveFetch<ListFilesResult>(endpoint);\n}\n\nexport function getFile(itemId: string): Promise<DriveItem> {\n return onedriveFetch<DriveItem>(`/me/drive/items/${itemId}`);\n}\n\nexport async function downloadFile(itemId: string): Promise<{\n content: string;\n metadata: FileMetadata;\n}> {\n const item = await getFile(itemId);\n\n if (!item.file) throw new Error(\"Item is not a file\");\n\n const downloadUrl = item[\"@microsoft.graph.downloadUrl\"];\n if (!downloadUrl) throw new Error(\"Download URL not available\");\n\n const response = await fetch(downloadUrl);\n if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);\n\n const content = await response.text();\n\n return {\n content,\n metadata: {\n id: item.id,\n name: item.name,\n size: item.size ?? 0,\n mimeType: item.file.mimeType,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n webUrl: item.webUrl,\n downloadUrl,\n },\n };\n}\n\nexport async function uploadFile(\n fileName: string,\n content: string,\n parentFolderId: string = \"root\",\n): Promise<DriveItem> {\n const token = await getTokenOrThrow();\n const endpoint = `${GRAPH_API_URL}/me/drive/items/${parentFolderId}:/${fileName}:/content`;\n\n const response = await fetch(endpoint, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/octet-stream\",\n },\n body: content,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to upload file: ${error.error?.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport function createFolder(\n folderName: string,\n parentFolderId: string = \"root\",\n): Promise<DriveItem> {\n return onedriveFetch<DriveItem>(`/me/drive/items/${parentFolderId}/children`, {\n method: \"POST\",\n body: JSON.stringify({\n name: folderName,\n folder: {},\n \"@microsoft.graph.conflictBehavior\": \"rename\",\n }),\n });\n}\n\nexport function searchFiles(\n query: string,\n options?: {\n top?: number;\n },\n): Promise<SearchResult> {\n const params = new URLSearchParams({ q: query });\n if (options?.top) params.set(\"$top\", options.top.toString());\n\n return onedriveFetch<SearchResult>(\n `/me/drive/root/search(q='${encodeURIComponent(query)}')?${params.toString()}`,\n );\n}\n\nexport async function deleteFile(itemId: string): Promise<void> {\n const token = await getTokenOrThrow();\n\n const response = await fetch(`${GRAPH_API_URL}/me/drive/items/${itemId}`, {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to delete item: ${error.error?.message ?? response.statusText}`);\n }\n}\n\nexport function moveFile(\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<DriveItem> {\n const body: Record<string, unknown> = {\n parentReference: { id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n return onedriveFetch<DriveItem>(`/me/drive/items/${itemId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport function formatFileSize(bytes: number): string {\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n}\n\nexport function isFile(item: DriveItem): boolean {\n return item.file !== undefined;\n}\n\nexport function isFolder(item: DriveItem): boolean {\n return item.folder !== undefined;\n}\n",
386
386
  "app/api/auth/onedrive/route.ts": "import { createOAuthInitHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(oneDriveConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
387
- "app/api/auth/onedrive/callback/route.ts": "/**\n * OneDrive OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): Promise<void> {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string): Promise<void> {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(oneDriveConfig, { tokenStore: hybridTokenStore });\n",
387
+ "app/api/auth/onedrive/callback/route.ts": "/**\n * OneDrive OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): Promise<void> {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string): Promise<void> {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(oneDriveConfig, { tokenStore: hybridTokenStore });\n",
388
388
  "tools/download-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { downloadFile, formatFileSize } from \"../../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"download-file\",\n description: \"Download file content from OneDrive. Returns the file content and metadata.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the file to download\"),\n preview: z\n .boolean()\n .default(false)\n .describe(\"If true, return only first 1000 characters as preview\"),\n }),\n async execute({ itemId, preview }) {\n const { content, metadata } = await downloadFile(itemId);\n\n const shouldTruncate = preview && content.length > 1000;\n\n return {\n content: preview ? content.substring(0, 1000) : content,\n isTruncated: shouldTruncate,\n metadata: {\n id: metadata.id,\n name: metadata.name,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n mimeType: metadata.mimeType,\n createdDateTime: metadata.createdDateTime,\n lastModifiedDateTime: metadata.lastModifiedDateTime,\n webUrl: metadata.webUrl,\n },\n message: shouldTruncate\n ? `Retrieved preview (first 1000 characters) of ${metadata.name}`\n : `Retrieved full content of ${metadata.name}`,\n };\n },\n});\n",
389
389
  "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatFileSize, isFile, isFolder, searchFiles } from \"../../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in OneDrive by name or content. Returns matching items with their paths and metadata.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query to find files or folders\"),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n }),\n async execute({ query, maxResults }) {\n const result = await searchFiles(query, { top: maxResults });\n\n const matches = result.value.map((item) => {\n const baseInfo = {\n id: item.id,\n name: item.name,\n webUrl: item.webUrl,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n parentPath: item.parentReference?.path,\n };\n\n if (isFile(item)) {\n const size = item.size ?? 0;\n\n return {\n ...baseInfo,\n type: \"file\" as const,\n size,\n sizeFormatted: formatFileSize(size),\n mimeType: item.file?.mimeType,\n };\n }\n\n if (isFolder(item)) {\n return {\n ...baseInfo,\n type: \"folder\" as const,\n childCount: item.folder?.childCount ?? 0,\n };\n }\n\n return { ...baseInfo, type: \"unknown\" as const };\n });\n\n return {\n matches,\n count: matches.length,\n hasMore: Boolean(result[\"@odata.nextLink\"]),\n query,\n };\n },\n});\n",
390
390
  "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatFileSize, isFile, isFolder, listFiles } from \"../../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List files and folders in a OneDrive folder. Returns file/folder names, types, sizes, and modification dates.\",\n inputSchema: z.object({\n folderId: z\n .string()\n .default(\"root\")\n .describe('Folder ID or \"root\" for the root folder'),\n orderBy: z\n .string()\n .optional()\n .describe('Order by field (e.g., \"name\", \"lastModifiedDateTime desc\")'),\n limit: z\n .number()\n .min(1)\n .max(200)\n .default(100)\n .describe(\"Maximum number of items to return\"),\n }),\n async execute({ folderId, orderBy, limit }) {\n const result = await listFiles(folderId, { orderBy, top: limit });\n\n const items = result.value.map((item) => {\n const baseInfo = {\n id: item.id,\n name: item.name,\n webUrl: item.webUrl,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n };\n\n if (isFile(item)) {\n const size = item.size ?? 0;\n\n return {\n ...baseInfo,\n type: \"file\" as const,\n size,\n sizeFormatted: formatFileSize(size),\n mimeType: item.file?.mimeType,\n };\n }\n\n if (isFolder(item)) {\n return {\n ...baseInfo,\n type: \"folder\" as const,\n childCount: item.folder?.childCount ?? 0,\n };\n }\n\n return { ...baseInfo, type: \"unknown\" as const };\n });\n\n return {\n items,\n count: items.length,\n hasMore: Boolean(result[\"@odata.nextLink\"]),\n };\n },\n});\n",
@@ -406,7 +406,7 @@ export default {
406
406
  "files": {
407
407
  "lib/airtable-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst AIRTABLE_BASE_URL = \"https://api.airtable.com/v0\";\nconst AIRTABLE_META_BASE_URL = \"https://api.airtable.com/v0/meta\";\n\ninterface AirtableResponse<T> {\n records?: T[];\n offset?: string;\n}\n\ninterface AirtableBase {\n id: string;\n name: string;\n permissionLevel: string;\n}\n\ninterface AirtableBaseSchema {\n tables: Array<{\n id: string;\n name: string;\n primaryFieldId: string;\n fields: Array<{\n id: string;\n name: string;\n type: string;\n options?: Record<string, unknown>;\n }>;\n views: Array<{\n id: string;\n name: string;\n type: string;\n }>;\n }>;\n}\n\nexport interface AirtableRecord {\n id: string;\n createdTime: string;\n fields: Record<string, unknown>;\n}\n\nfunction getTokenOrThrow(): string {\n const token = getAccessToken();\n if (token) return token;\n throw new Error(\"Not authenticated with Airtable. Please connect your account.\");\n}\n\nasync function apiFetch<T>(\n baseUrl: string,\n endpoint: string,\n options: RequestInit,\n errorPrefix: string,\n): Promise<T> {\n const token = getTokenOrThrow();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `${errorPrefix}: ${response.status} ${error?.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction airtableFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiFetch<T>(AIRTABLE_BASE_URL, endpoint, options, \"Airtable API error\");\n}\n\nfunction metaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiFetch<T>(AIRTABLE_META_BASE_URL, endpoint, options, \"Airtable Meta API error\");\n}\n\nexport async function listBases(): Promise<AirtableBase[]> {\n const response = await metaFetch<{ bases: AirtableBase[] }>(\"/bases\");\n return response.bases ?? [];\n}\n\nexport function getBase(baseId: string): Promise<AirtableBaseSchema> {\n return metaFetch<AirtableBaseSchema>(`/bases/${baseId}/tables`);\n}\n\nexport async function listRecords(\n baseId: string,\n tableIdOrName: string,\n options?: {\n fields?: string[];\n filterByFormula?: string;\n maxRecords?: number;\n pageSize?: number;\n sort?: Array<{ field: string; direction: \"asc\" | \"desc\" }>;\n view?: string;\n offset?: string;\n },\n): Promise<{ records: AirtableRecord[]; offset?: string }> {\n const params = new URLSearchParams();\n\n options?.fields?.forEach((field) => params.append(\"fields[]\", field));\n if (options?.filterByFormula) params.append(\"filterByFormula\", options.filterByFormula);\n if (options?.maxRecords) params.append(\"maxRecords\", String(options.maxRecords));\n if (options?.pageSize) params.append(\"pageSize\", String(options.pageSize));\n options?.sort?.forEach((s, i) => {\n params.append(`sort[${i}][field]`, s.field);\n params.append(`sort[${i}][direction]`, s.direction);\n });\n if (options?.view) params.append(\"view\", options.view);\n if (options?.offset) params.append(\"offset\", options.offset);\n\n const queryString = params.toString();\n const endpoint = `/${baseId}/${encodeURIComponent(tableIdOrName)}${\n queryString ? `?${queryString}` : \"\"\n }`;\n\n const response = await airtableFetch<AirtableResponse<AirtableRecord>>(endpoint);\n\n return { records: response.records ?? [], offset: response.offset };\n}\n\nexport function getRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n );\n}\n\nexport function createRecord(\n baseId: string,\n tableIdOrName: string,\n fields: Record<string, unknown>,\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(`/${baseId}/${encodeURIComponent(tableIdOrName)}`, {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n });\n}\n\nexport async function createRecords(\n baseId: string,\n tableIdOrName: string,\n records: Array<{ fields: Record<string, unknown> }>,\n): Promise<AirtableRecord[]> {\n const response = await airtableFetch<{ records: AirtableRecord[] }>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}`,\n {\n method: \"POST\",\n body: JSON.stringify({ records }),\n },\n );\n\n return response.records;\n}\n\nexport function updateRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n fields: Record<string, unknown>,\n options?: { destructive?: boolean },\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n {\n method: options?.destructive ? \"PUT\" : \"PATCH\",\n body: JSON.stringify({ fields }),\n },\n );\n}\n\nexport function deleteRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n): Promise<{ id: string; deleted: boolean }> {\n return airtableFetch<{ id: string; deleted: boolean }>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n { method: \"DELETE\" },\n );\n}\n\nexport function formatFieldValue(value: unknown): string {\n if (value == null) return \"\";\n if (Array.isArray(value)) return value.map((v) => formatFieldValue(v)).join(\", \");\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n",
408
408
  "app/api/auth/airtable/route.ts": "import { airtableConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(airtableConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
409
- "app/api/auth/airtable/callback/route.ts": "import { airtableConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(airtableConfig, { tokenStore: hybridTokenStore });\n",
409
+ "app/api/auth/airtable/callback/route.ts": "import { airtableConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(airtableConfig, { tokenStore: hybridTokenStore });\n",
410
410
  "tools/create-record.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createRecord } from \"../../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"create-record\",\n description:\n \"Create a new record in an Airtable table. Provide field names and values as an object. Returns the created record with its ID.\",\n inputSchema: z.object({\n baseId: z.string().describe('The ID of the Airtable base (starts with \"app\")'),\n tableIdOrName: z.string().describe(\"The ID or name of the table\"),\n fields: z\n .record(z.unknown())\n .describe(\n 'Object with field names as keys and their values. Field names must match exactly. Example: { \"Name\": \"John Doe\", \"Email\": \"john@example.com\", \"Status\": \"Active\" }',\n ),\n }),\n async execute({ baseId, tableIdOrName, fields }) {\n const record = await createRecord(baseId, tableIdOrName, fields);\n\n return { id: record.id, createdTime: record.createdTime, fields: record.fields };\n },\n});\n",
411
411
  "tools/list-bases.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listBases } from \"../../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"list-bases\",\n description:\n \"List all accessible Airtable bases in the connected account. Returns base IDs, names, and permission levels.\",\n inputSchema: z.object({}),\n async execute() {\n return listBases();\n },\n});\n",
412
412
  "tools/get-base.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getBase } from \"../../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"get-base\",\n description:\n \"Get the schema and structure of an Airtable base, including all tables, fields, and views. Useful for understanding the data model before querying or creating records.\",\n inputSchema: z.object({\n baseId: z.string().describe('The ID of the Airtable base (starts with \"app\")'),\n }),\n async execute({ baseId }) {\n const { tables } = await getBase(baseId);\n\n return {\n tables: tables.map((table) => ({\n id: table.id,\n name: table.name,\n primaryFieldId: table.primaryFieldId,\n fields: table.fields.map((field) => ({\n id: field.id,\n name: field.name,\n type: field.type,\n options: field.options,\n })),\n views: table.views.map((view) => ({\n id: view.id,\n name: view.name,\n type: view.type,\n })),\n })),\n };\n },\n});\n",
@@ -419,7 +419,7 @@ export default {
419
419
  ".env.example": "# GitLab OAuth Configuration\n# Create a new application at: https://gitlab.com/-/profile/applications\n# Set the redirect URI to: http://localhost:3000/api/auth/gitlab/callback\n# (Update the URL for production)\n\nGITLAB_CLIENT_ID=your_gitlab_application_id\nGITLAB_CLIENT_SECRET=your_gitlab_application_secret\n",
420
420
  "lib/gitlab-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GITLAB_BASE_URL = \"https://gitlab.com/api/v4\";\n\nexport interface GitLabProject {\n id: number;\n name: string;\n name_with_namespace: string;\n description: string | null;\n web_url: string;\n path_with_namespace: string;\n default_branch: string;\n visibility: \"private\" | \"internal\" | \"public\";\n created_at: string;\n last_activity_at: string;\n}\n\nexport interface GitLabIssue {\n id: number;\n iid: number;\n project_id: number;\n title: string;\n description: string | null;\n state: \"opened\" | \"closed\";\n created_at: string;\n updated_at: string;\n closed_at: string | null;\n labels: string[];\n milestone: {\n id: number;\n title: string;\n } | null;\n assignees: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n author: {\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n };\n web_url: string;\n time_stats: {\n time_estimate: number;\n total_time_spent: number;\n };\n}\n\nexport interface GitLabMergeRequest {\n id: number;\n iid: number;\n project_id: number;\n title: string;\n description: string | null;\n state: \"opened\" | \"closed\" | \"merged\";\n created_at: string;\n updated_at: string;\n merged_at: string | null;\n closed_at: string | null;\n target_branch: string;\n source_branch: string;\n author: {\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n };\n assignees: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n reviewers: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n labels: string[];\n draft: boolean;\n web_url: string;\n changes_count: string;\n diff_refs: {\n base_sha: string;\n head_sha: string;\n start_sha: string;\n };\n}\n\nexport interface GitLabUser {\n id: number;\n username: string;\n name: string;\n email: string;\n avatar_url: string;\n web_url: string;\n}\n\nfunction encodeProjectId(projectId: number | string): number | string {\n return typeof projectId === \"string\" ? encodeURIComponent(projectId) : projectId;\n}\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function gitlabFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with GitLab. Please connect your account.\");\n\n const response = await fetch(`${GITLAB_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n message?: string;\n error?: string;\n };\n\n const message = error.message ?? error.error ?? response.statusText;\n throw new Error(`GitLab API error: ${response.status} ${message}`);\n }\n\n return (await response.json()) as T;\n}\n\nexport function getCurrentUser(): Promise<GitLabUser> {\n return gitlabFetch<GitLabUser>(\"/user\");\n}\n\nexport function listProjects(options?: {\n membership?: boolean;\n search?: string;\n orderBy?: \"id\" | \"name\" | \"created_at\" | \"updated_at\" | \"last_activity_at\";\n sort?: \"asc\" | \"desc\";\n perPage?: number;\n}): Promise<GitLabProject[]> {\n const params = new URLSearchParams();\n\n if (options?.membership !== false) params.set(\"membership\", \"true\");\n if (options?.search) params.set(\"search\", options.search);\n if (options?.orderBy) params.set(\"order_by\", options.orderBy);\n if (options?.sort) params.set(\"sort\", options.sort);\n if (options?.perPage) params.set(\"per_page\", options.perPage.toString());\n\n return gitlabFetch<GitLabProject[]>(`/projects${buildQuery(params)}`);\n}\n\nexport function getProject(projectId: number | string): Promise<GitLabProject> {\n return gitlabFetch<GitLabProject>(`/projects/${encodeProjectId(projectId)}`);\n}\n\nexport function searchIssues(options: {\n scope?: \"created_by_me\" | \"assigned_to_me\" | \"all\";\n state?: \"opened\" | \"closed\" | \"all\";\n labels?: string[];\n search?: string;\n projectId?: number | string;\n perPage?: number;\n}): Promise<GitLabIssue[]> {\n const params = new URLSearchParams();\n\n if (options.scope) params.set(\"scope\", options.scope);\n if (options.state) params.set(\"state\", options.state);\n if (options.labels?.length) params.set(\"labels\", options.labels.join(\",\"));\n if (options.search) params.set(\"search\", options.search);\n if (options.perPage) params.set(\"per_page\", options.perPage.toString());\n\n const base = options.projectId\n ? `/projects/${encodeProjectId(options.projectId)}/issues`\n : \"/issues\";\n\n return gitlabFetch<GitLabIssue[]>(`${base}${buildQuery(params)}`);\n}\n\nexport function getIssue(projectId: number | string, issueIid: number): Promise<GitLabIssue> {\n return gitlabFetch<GitLabIssue>(`/projects/${encodeProjectId(projectId)}/issues/${issueIid}`);\n}\n\nexport function createIssue(\n projectId: number | string,\n options: {\n title: string;\n description?: string;\n labels?: string[];\n assigneeIds?: number[];\n milestoneId?: number;\n dueDate?: string;\n },\n): Promise<GitLabIssue> {\n const body: Record<string, unknown> = { title: options.title };\n\n if (options.description) body.description = options.description;\n if (options.labels?.length) body.labels = options.labels.join(\",\");\n if (options.assigneeIds?.length) body.assignee_ids = options.assigneeIds;\n if (options.milestoneId) body.milestone_id = options.milestoneId;\n if (options.dueDate) body.due_date = options.dueDate;\n\n return gitlabFetch<GitLabIssue>(`/projects/${encodeProjectId(projectId)}/issues`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function updateIssue(\n projectId: number | string,\n issueIid: number,\n options: {\n title?: string;\n description?: string;\n state?: \"opened\" | \"closed\";\n labels?: string[];\n assigneeIds?: number[];\n },\n): Promise<GitLabIssue> {\n const body: Record<string, unknown> = {};\n\n if (options.title) body.title = options.title;\n if (options.description !== undefined) body.description = options.description;\n if (options.state) body.state_event = options.state === \"closed\" ? \"close\" : \"reopen\";\n if (options.labels) body.labels = options.labels.join(\",\");\n if (options.assigneeIds) body.assignee_ids = options.assigneeIds;\n\n return gitlabFetch<GitLabIssue>(`/projects/${encodeProjectId(projectId)}/issues/${issueIid}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport function listMergeRequests(options?: {\n scope?: \"created_by_me\" | \"assigned_to_me\" | \"all\";\n state?: \"opened\" | \"closed\" | \"merged\" | \"all\";\n labels?: string[];\n projectId?: number | string;\n perPage?: number;\n}): Promise<GitLabMergeRequest[]> {\n const params = new URLSearchParams();\n\n if (options?.scope) params.set(\"scope\", options.scope);\n if (options?.state) params.set(\"state\", options.state);\n if (options?.labels?.length) params.set(\"labels\", options.labels.join(\",\"));\n if (options?.perPage) params.set(\"per_page\", options.perPage.toString());\n\n const base = options?.projectId\n ? `/projects/${encodeProjectId(options.projectId)}/merge_requests`\n : \"/merge_requests\";\n\n return gitlabFetch<GitLabMergeRequest[]>(`${base}${buildQuery(params)}`);\n}\n\nexport function getMergeRequest(\n projectId: number | string,\n mrIid: number,\n): Promise<GitLabMergeRequest> {\n return gitlabFetch<GitLabMergeRequest>(\n `/projects/${encodeProjectId(projectId)}/merge_requests/${mrIid}`,\n );\n}\n\nexport function formatIssueForDisplay(issue: GitLabIssue): string {\n const assignees = issue.assignees.map((a) => `@${a.username}`).join(\", \");\n const labels = issue.labels.length ? `[${issue.labels.join(\", \")}]` : \"\";\n\n return `#${issue.iid}: ${issue.title} ${labels}\nState: ${issue.state}\nAssignees: ${assignees || \"None\"}\nCreated: ${new Date(issue.created_at).toLocaleDateString()}\nURL: ${issue.web_url}`;\n}\n\nexport function formatMergeRequestForDisplay(mr: GitLabMergeRequest): string {\n const assignees = mr.assignees.map((a) => `@${a.username}`).join(\", \");\n const reviewers = mr.reviewers.map((r) => `@${r.username}`).join(\", \");\n const labels = mr.labels.length ? `[${mr.labels.join(\", \")}]` : \"\";\n\n return `!${mr.iid}: ${mr.title} ${labels}\nState: ${mr.state}${mr.draft ? \" (Draft)\" : \"\"}\nSource: ${mr.source_branch} → Target: ${mr.target_branch}\nAuthor: @${mr.author.username}\nAssignees: ${assignees || \"None\"}\nReviewers: ${reviewers || \"None\"}\nCreated: ${new Date(mr.created_at).toLocaleDateString()}\nURL: ${mr.web_url}`;\n}\n",
421
421
  "app/api/auth/gitlab/route.ts": "import { createOAuthInitHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(gitlabConfig, { tokenStore: oauthMemoryTokenStore });\n",
422
- "app/api/auth/gitlab/callback/route.ts": "import { createOAuthCallbackHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gitlabConfig, { tokenStore: hybridTokenStore });\n",
422
+ "app/api/auth/gitlab/callback/route.ts": "import { createOAuthCallbackHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gitlabConfig, { tokenStore: hybridTokenStore });\n",
423
423
  "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatIssueForDisplay, searchIssues } from \"../../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"search-issues\",\n description:\n \"Search for issues in GitLab projects. Can search across all accessible projects or within a specific project. Returns issue titles, states, assignees, and labels.\",\n inputSchema: z.object({\n scope: z\n .enum([\"created_by_me\", \"assigned_to_me\", \"all\"])\n .default(\"all\")\n .describe(\"Scope of issues to search\"),\n state: z\n .enum([\"opened\", \"closed\", \"all\"])\n .default(\"opened\")\n .describe(\"State of issues to search for\"),\n search: z.string().optional().describe(\"Search query to filter issues by title or description\"),\n labels: z.array(z.string()).optional().describe('Filter by labels (e.g., [\"bug\", \"urgent\"])'),\n projectId: z\n .union([z.number(), z.string()])\n .optional()\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n limit: z.number().min(1).max(100).default(20).describe(\"Maximum number of results to return\"),\n }),\n async execute({ scope, state, search, labels, projectId, limit }) {\n const issues = await searchIssues({\n scope,\n state,\n search,\n labels,\n projectId,\n perPage: limit,\n });\n\n if (issues.length === 0) {\n return {\n message: \"No issues found matching the criteria.\",\n count: 0,\n issues: [],\n };\n }\n\n return {\n count: issues.length,\n issues: issues.map((issue) => {\n const description = issue.description ?? \"\";\n const truncatedDescription =\n description.length > 200 ? `${description.substring(0, 200)}...` : description;\n\n return {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n state: issue.state,\n labels: issue.labels,\n assignees: issue.assignees.map(({ username, name }) => ({ username, name })),\n author: {\n username: issue.author.username,\n name: issue.author.name,\n },\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n webUrl: issue.web_url,\n description: truncatedDescription,\n };\n }),\n summary: issues.map(formatIssueForDisplay).join(\"\\n\\n\"),\n };\n },\n});\n",
424
424
  "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createIssue } from \"../../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"create-issue\",\n description:\n \"Create a new issue in a GitLab project. Can set title, description, labels, assignees, milestone, and due date.\",\n inputSchema: z.object({\n projectId: z\n .union([z.number(), z.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n title: z.string().min(1).describe(\"Issue title\"),\n description: z.string().optional().describe(\"Issue description in Markdown format\"),\n labels: z.array(z.string()).optional().describe('Labels to apply (e.g., [\"bug\", \"urgent\"])'),\n assigneeIds: z.array(z.number()).optional().describe(\"User IDs to assign the issue to\"),\n milestoneId: z.number().optional().describe(\"Milestone ID to associate with the issue\"),\n dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n }),\n async execute({ projectId, title, description, labels, assigneeIds, milestoneId, dueDate }) {\n const issue = await createIssue(projectId, {\n title,\n description,\n labels,\n assigneeIds,\n milestoneId,\n dueDate,\n });\n\n return {\n success: true,\n message: `Issue created successfully: #${issue.iid}`,\n issue: {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n state: issue.state,\n labels: issue.labels,\n assignees: issue.assignees.map(({ username, name }) => ({ username, name })),\n webUrl: issue.web_url,\n createdAt: issue.created_at,\n },\n };\n },\n});\n",
425
425
  "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listProjects } from \"../../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"list-projects\",\n description:\n \"List GitLab projects accessible to the authenticated user. Can search, filter by membership, and sort results.\",\n inputSchema: z.object({\n search: z.string().optional().describe(\"Search query to filter projects by name or path\"),\n membership: z.boolean().default(true).describe(\"Only show projects where user is a member\"),\n orderBy: z\n .enum([\"id\", \"name\", \"created_at\", \"updated_at\", \"last_activity_at\"])\n .default(\"last_activity_at\")\n .describe(\"Field to order results by\"),\n sort: z.enum([\"asc\", \"desc\"]).default(\"desc\").describe(\"Sort direction\"),\n limit: z.number().min(1).max(100).default(20).describe(\"Maximum number of results to return\"),\n }),\n async execute({ search, membership, orderBy, sort, limit }) {\n const projects = await listProjects({\n search,\n membership,\n orderBy,\n sort,\n perPage: limit,\n });\n\n const mappedProjects = projects.map((project) => ({\n id: project.id,\n name: project.name,\n nameWithNamespace: project.name_with_namespace,\n path: project.path_with_namespace,\n description: project.description ?? \"No description\",\n visibility: project.visibility,\n defaultBranch: project.default_branch,\n webUrl: project.web_url,\n createdAt: project.created_at,\n lastActivityAt: project.last_activity_at,\n }));\n\n if (!mappedProjects.length) {\n return {\n message: \"No projects found matching the criteria.\",\n count: 0,\n projects: [],\n };\n }\n\n return {\n count: mappedProjects.length,\n projects: mappedProjects,\n };\n },\n});\n",
@@ -432,7 +432,7 @@ export default {
432
432
  ".env.example": "# Monday.com OAuth Configuration\n# Get your credentials from https://monday.com/developers/apps\nMONDAY_CLIENT_ID=your-client-id\nMONDAY_CLIENT_SECRET=your-client-secret\n",
433
433
  "lib/monday-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst MONDAY_API_URL = \"https://api.monday.com/v2\";\n\ninterface MondayResponse<T> {\n data: T;\n account_id?: number;\n errors?: Array<{ message: string; locations?: unknown[] }>;\n}\n\ninterface MondayBoard {\n id: string;\n name: string;\n description?: string;\n board_kind: string;\n state: string;\n workspace_id?: string;\n created_at?: string;\n updated_at?: string;\n}\n\ninterface MondayItem {\n id: string;\n name: string;\n state?: string;\n board?: {\n id: string;\n name: string;\n };\n group?: {\n id: string;\n title: string;\n };\n column_values?: Array<{\n id: string;\n text?: string;\n title?: string;\n type?: string;\n value?: string;\n }>;\n created_at?: string;\n updated_at?: string;\n}\n\ninterface MondayUser {\n id: string;\n name: string;\n email: string;\n account: {\n id: string;\n name: string;\n };\n}\n\nasync function mondayFetch<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Monday.com. Please connect your account.\");\n }\n\n const response = await fetch(MONDAY_API_URL, {\n method: \"POST\",\n headers: {\n Authorization: token,\n \"Content-Type\": \"application/json\",\n \"API-Version\": \"2024-10\",\n },\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`Monday.com API error: ${response.status} ${response.statusText}`);\n }\n\n const result: MondayResponse<T> = await response.json();\n const errorMessage = result.errors?.[0]?.message;\n if (errorMessage) {\n throw new Error(`Monday.com GraphQL error: ${errorMessage}`);\n }\n\n return result.data;\n}\n\nexport async function getMe(): Promise<MondayUser> {\n const query = `\n query {\n me {\n id\n name\n email\n account {\n id\n name\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ me: MondayUser }>(query);\n return data.me;\n}\n\nexport async function listBoards(options?: {\n limit?: number;\n page?: number;\n workspaceIds?: string[];\n}): Promise<MondayBoard[]> {\n const limit = options?.limit ?? 50;\n const page = options?.page ?? 1;\n\n const workspaceIds = options?.workspaceIds?.length\n ? options.workspaceIds.map((id) => parseInt(id, 10))\n : null;\n\n const workspaceFilter = workspaceIds ? `, workspace_ids: [${workspaceIds.join(\",\")}]` : \"\";\n\n const query = `\n query {\n boards(limit: ${limit}, page: ${page}${workspaceFilter}) {\n id\n name\n description\n board_kind\n state\n workspace_id\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: MondayBoard[] }>(query);\n return data.boards;\n}\n\nexport async function getBoard(boardId: string): Promise<MondayBoard> {\n const query = `\n query {\n boards(ids: [${boardId}]) {\n id\n name\n description\n board_kind\n state\n workspace_id\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: MondayBoard[] }>(query);\n const board = data.boards?.[0];\n if (!board) {\n throw new Error(`Board with ID ${boardId} not found`);\n }\n\n return board;\n}\n\nexport async function listItems(options: {\n boardId: string;\n limit?: number;\n page?: number;\n}): Promise<MondayItem[]> {\n const limit = options.limit ?? 50;\n const page = options.page ?? 1;\n\n const query = `\n query {\n boards(ids: [${options.boardId}]) {\n items_page(limit: ${limit}, query_params: {page: ${page}}) {\n items {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n column_values {\n id\n text\n title\n type\n value\n }\n created_at\n updated_at\n }\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: Array<{ items_page: { items: MondayItem[] } }> }>(query);\n return data.boards?.[0]?.items_page.items ?? [];\n}\n\nexport async function getItem(itemId: string): Promise<MondayItem> {\n const query = `\n query {\n items(ids: [${itemId}]) {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n column_values {\n id\n text\n title\n type\n value\n }\n created_at\n updated_at\n }\n }\n `;\n\n const data = await mondayFetch<{ items: MondayItem[] }>(query);\n const item = data.items?.[0];\n if (!item) {\n throw new Error(`Item with ID ${itemId} not found`);\n }\n\n return item;\n}\n\nexport async function createItem(options: {\n boardId: string;\n groupId?: string;\n itemName: string;\n columnValues?: Record<string, unknown>;\n}): Promise<MondayItem> {\n const groupId = options.groupId ? `group_id: \"${options.groupId}\",` : \"\";\n const columnValues = options.columnValues\n ? `column_values: ${JSON.stringify(JSON.stringify(options.columnValues))},`\n : \"\";\n\n const query = `\n mutation {\n create_item(\n board_id: ${options.boardId},\n ${groupId}\n item_name: \"${options.itemName}\",\n ${columnValues}\n ) {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n created_at\n }\n }\n `;\n\n const data = await mondayFetch<{ create_item: MondayItem }>(query);\n return data.create_item;\n}\n\nexport async function updateItem(\n itemId: string,\n updates: {\n columnValues?: Record<string, unknown>;\n name?: string;\n },\n): Promise<MondayItem> {\n if (updates.name) {\n const nameQuery = `\n mutation {\n change_simple_column_value(\n item_id: ${itemId},\n column_id: \"name\",\n value: \"${updates.name}\"\n ) {\n id\n name\n }\n }\n `;\n await mondayFetch<{ change_simple_column_value: MondayItem }>(nameQuery);\n }\n\n if (!updates.columnValues) {\n return getItem(itemId);\n }\n\n const columnValuesStr = JSON.stringify(JSON.stringify(updates.columnValues));\n const query = `\n mutation {\n change_multiple_column_values(\n item_id: ${itemId},\n column_values: ${columnValuesStr}\n ) {\n id\n name\n state\n column_values {\n id\n text\n title\n type\n value\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ change_multiple_column_values: MondayItem }>(query);\n return data.change_multiple_column_values;\n}\n",
434
434
  "app/api/auth/monday/route.ts": "import { createOAuthInitHandler, mondayConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(mondayConfig, { tokenStore: oauthMemoryTokenStore });\n",
435
- "app/api/auth/monday/callback/route.ts": "import { createOAuthCallbackHandler, mondayConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mondayConfig, { tokenStore: hybridTokenStore });\n",
435
+ "app/api/auth/monday/callback/route.ts": "import { createOAuthCallbackHandler, mondayConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mondayConfig, { tokenStore: hybridTokenStore });\n",
436
436
  "tools/list-items.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listItems } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"list-items\",\n description: \"List items from a Monday.com board. Items are the rows in a board.\",\n inputSchema: z.object({\n boardId: z.string().describe(\"The ID of the board to list items from\"),\n limit: z.number().min(1).max(100).default(50).describe(\"Maximum number of items to return\"),\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ boardId, limit, page }) {\n const items = await listItems({ boardId, limit, page });\n\n return items.map((item) => ({\n id: item.id,\n name: item.name,\n state: item.state,\n board: item.board,\n group: item.group,\n columnValues: item.column_values?.map((col) => ({\n id: col.id,\n title: col.title,\n text: col.text,\n type: col.type,\n })),\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n }));\n },\n});\n",
437
437
  "tools/update-item.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateItem } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"update-item\",\n description: \"Update an existing Monday.com item. Can update the name and/or column values.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the item to update\"),\n name: z.string().optional().describe(\"New name/title for the item\"),\n columnValues: z\n .record(z.unknown())\n .optional()\n .describe(\n \"Column values to update as a key-value object. Keys are column IDs, values depend on column type.\",\n ),\n }),\n async execute({ itemId, name, columnValues }) {\n const item = await updateItem(itemId, { name, columnValues });\n\n return {\n success: true,\n item: {\n id: item.id,\n name: item.name,\n state: item.state,\n columnValues: item.column_values?.map((column) => ({\n id: column.id,\n title: column.title,\n text: column.text,\n type: column.type,\n })),\n },\n };\n },\n});\n",
438
438
  "tools/list-boards.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listBoards } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"list-boards\",\n description: \"List all boards in Monday.com. Can optionally filter by workspace IDs.\",\n inputSchema: z.object({\n workspaceIds: z\n .array(z.string())\n .optional()\n .describe(\"Optional list of workspace IDs to filter boards\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of boards to return\"),\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ workspaceIds, limit, page }) {\n const boards = await listBoards({ workspaceIds, limit, page });\n\n return boards.map((board) => ({\n id: board.id,\n name: board.name,\n description: board.description,\n boardKind: board.board_kind,\n state: board.state,\n workspaceId: board.workspace_id,\n }));\n },\n});\n",
@@ -445,7 +445,7 @@ export default {
445
445
  ".env.example": "# Bitbucket OAuth Configuration\n# Create a new OAuth consumer at: https://bitbucket.org/account/settings/app-passwords/\n# Or create an OAuth consumer at: https://bitbucket.org/{workspace}/workspace/settings/oauth-consumers/new\n# Set the callback URL to: http://localhost:3000/api/auth/bitbucket/callback\n# (Update the URL for production)\n# Required permissions: repository, pullrequest, issue, account\n\nBITBUCKET_CLIENT_ID=your_bitbucket_client_id\nBITBUCKET_CLIENT_SECRET=your_bitbucket_client_secret\n",
446
446
  "lib/bitbucket-client.ts": "import { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n\n return undefined;\n}\n\nconst BITBUCKET_API_BASE = \"https://api.bitbucket.org/2.0\";\n\nexport interface BitbucketUser {\n uuid: string;\n username: string;\n display_name: string;\n account_id: string;\n links: {\n avatar: { href: string };\n html: { href: string };\n };\n}\n\nexport interface Repository {\n uuid: string;\n name: string;\n full_name: string;\n description: string | null;\n is_private: boolean;\n mainbranch: { name: string } | null;\n language: string;\n size: number;\n updated_on: string;\n created_on: string;\n links: {\n html: { href: string };\n clone: Array<{ href: string; name: string }>;\n };\n owner: {\n username: string;\n display_name: string;\n };\n}\n\nexport interface PullRequest {\n id: number;\n title: string;\n description: string;\n state: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\";\n author: {\n username: string;\n display_name: string;\n };\n created_on: string;\n updated_on: string;\n source: {\n branch: { name: string };\n repository: { full_name: string };\n };\n destination: {\n branch: { name: string };\n repository: { full_name: string };\n };\n links: {\n html: { href: string };\n diff: { href: string };\n };\n comment_count: number;\n task_count: number;\n}\n\nexport interface Issue {\n id: number;\n title: string;\n content: {\n raw: string;\n } | null;\n state: \"new\" | \"open\" | \"resolved\" | \"on hold\" | \"invalid\" | \"duplicate\" | \"wontfix\" | \"closed\";\n kind: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n created_on: string;\n updated_on: string;\n reporter: {\n username: string;\n display_name: string;\n };\n assignee: {\n username: string;\n display_name: string;\n } | null;\n links: {\n html: { href: string };\n };\n}\n\nexport const bitbucketOAuthProvider = {\n name: \"bitbucket\",\n authorizationUrl: \"https://bitbucket.org/site/oauth2/authorize\",\n tokenUrl: \"https://bitbucket.org/site/oauth2/access_token\",\n clientId: getEnv(\"BITBUCKET_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"BITBUCKET_CLIENT_SECRET\") ?? \"\",\n scopes: [\"repository\", \"pullrequest\", \"issue\", \"account\"],\n callbackPath: \"/api/auth/bitbucket/callback\",\n};\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nexport function createBitbucketClient(userId: string): {\n getCurrentUser(): Promise<BitbucketUser>;\n listRepositories(options?: { role?: \"owner\" | \"contributor\" | \"member\"; perPage?: number }): Promise<Repository[]>;\n getRepository(workspace: string, repoSlug: string): Promise<Repository>;\n listPullRequests(\n workspace: string,\n repoSlug: string,\n options?: { state?: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\"; perPage?: number },\n ): Promise<PullRequest[]>;\n getPullRequest(workspace: string, repoSlug: string, pullRequestId: number): Promise<PullRequest>;\n createPullRequest(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n sourceBranch: string;\n destinationBranch: string;\n closeSourceBranch?: boolean;\n },\n ): Promise<PullRequest>;\n listIssues(\n workspace: string,\n repoSlug: string,\n options?: {\n state?:\n | \"new\"\n | \"open\"\n | \"resolved\"\n | \"on hold\"\n | \"invalid\"\n | \"duplicate\"\n | \"wontfix\"\n | \"closed\";\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n perPage?: number;\n },\n ): Promise<Issue[]>;\n createIssue(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n },\n ): Promise<Issue>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(bitbucketOAuthProvider, userId, \"bitbucket\");\n if (!token) throw new Error(\"Bitbucket not connected. Please connect your Bitbucket account first.\");\n return token;\n }\n\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${BITBUCKET_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Bitbucket API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n return {\n getCurrentUser(): Promise<BitbucketUser> {\n return apiRequest(\"/user\");\n },\n\n async listRepositories(\n options: {\n role?: \"owner\" | \"contributor\" | \"member\";\n perPage?: number;\n } = {},\n ): Promise<Repository[]> {\n const params = new URLSearchParams();\n if (options.role) params.set(\"role\", options.role);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: Repository[] }>(`/repositories${buildQuery(params)}`);\n return values;\n },\n\n getRepository(workspace: string, repoSlug: string): Promise<Repository> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}`);\n },\n\n async listPullRequests(\n workspace: string,\n repoSlug: string,\n options: {\n state?: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\";\n perPage?: number;\n } = {},\n ): Promise<PullRequest[]> {\n const params = new URLSearchParams();\n if (options.state) params.set(\"state\", options.state);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: PullRequest[] }>(\n `/repositories/${workspace}/${repoSlug}/pullrequests${buildQuery(params)}`,\n );\n return values;\n },\n\n getPullRequest(workspace: string, repoSlug: string, pullRequestId: number): Promise<PullRequest> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/pullrequests/${pullRequestId}`);\n },\n\n createPullRequest(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n sourceBranch: string;\n destinationBranch: string;\n closeSourceBranch?: boolean;\n },\n ): Promise<PullRequest> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/pullrequests`, {\n method: \"POST\",\n body: JSON.stringify({\n title: options.title,\n description: options.description,\n source: { branch: { name: options.sourceBranch } },\n destination: { branch: { name: options.destinationBranch } },\n close_source_branch: options.closeSourceBranch,\n }),\n });\n },\n\n async listIssues(\n workspace: string,\n repoSlug: string,\n options: {\n state?:\n | \"new\"\n | \"open\"\n | \"resolved\"\n | \"on hold\"\n | \"invalid\"\n | \"duplicate\"\n | \"wontfix\"\n | \"closed\";\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n perPage?: number;\n } = {},\n ): Promise<Issue[]> {\n const params = new URLSearchParams();\n if (options.state) params.set(\"q\", `state=\"${options.state}\"`);\n if (options.kind) params.set(\"kind\", options.kind);\n if (options.priority) params.set(\"priority\", options.priority);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: Issue[] }>(\n `/repositories/${workspace}/${repoSlug}/issues${buildQuery(params)}`,\n );\n return values;\n },\n\n createIssue(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n },\n ): Promise<Issue> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/issues`, {\n method: \"POST\",\n body: JSON.stringify({\n title: options.title,\n content: options.description ? { raw: options.description } : undefined,\n kind: options.kind ?? \"bug\",\n priority: options.priority ?? \"major\",\n }),\n });\n },\n };\n}\n\nexport type BitbucketClient = ReturnType<typeof createBitbucketClient>;\n",
447
447
  "app/api/auth/bitbucket/route.ts": "import { bitbucketConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(bitbucketConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
448
- "app/api/auth/bitbucket/callback/route.ts": "/**\n * Bitbucket OAuth Callback\n *\n * Handles the OAuth callback from Atlassian and stores the tokens.\n */\n\nimport { bitbucketConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n\n getState(state: string): unknown {\n return oauthMemoryTokenStore.getState(state);\n },\n\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): unknown {\n return oauthMemoryTokenStore.setState(state);\n },\n\n clearState(state: string): unknown {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(bitbucketConfig, { tokenStore: hybridTokenStore });\n",
448
+ "app/api/auth/bitbucket/callback/route.ts": "/**\n * Bitbucket OAuth Callback\n *\n * Handles the OAuth callback from Atlassian and stores the tokens.\n */\n\nimport { bitbucketConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string): Promise<unknown> {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n\n async clearTokens(serviceId: string): Promise<void> {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n\n getState(state: string): unknown {\n return oauthMemoryTokenStore.getState(state);\n },\n\n setState(state: { state: string; codeVerifier?: string; createdAt: number }): unknown {\n return oauthMemoryTokenStore.setState(state);\n },\n\n clearState(state: string): unknown {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(bitbucketConfig, { tokenStore: hybridTokenStore });\n",
449
449
  "tools/list-repositories.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.ts\";\n\ntype BitbucketRepo = {\n name: string;\n full_name: string;\n description: string | null;\n is_private: boolean;\n mainbranch: { name: string } | null;\n language: string;\n updated_on: string;\n created_on: string;\n links: { html: { href: string } };\n owner: { username: string; display_name: string };\n};\n\nexport default tool({\n id: \"list-repositories\",\n description: \"List Bitbucket repositories for the authenticated user\",\n inputSchema: z.object({\n role: z\n .enum([\"owner\", \"contributor\", \"member\"])\n .optional()\n .describe(\"Filter repositories by role\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of repositories to return\"),\n }),\n execute: async ({ role, limit }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const repos = await bitbucket.listRepositories({ role, perPage: limit });\n\n return {\n repositories: repos.map((repo: BitbucketRepo) => ({\n name: repo.name,\n fullName: repo.full_name,\n description: repo.description ?? null,\n isPrivate: repo.is_private,\n mainBranch: repo.mainbranch?.name ?? null,\n language: repo.language,\n url: repo.links.html.href,\n owner: {\n username: repo.owner.username,\n displayName: repo.owner.display_name,\n },\n updatedOn: repo.updated_on,\n createdOn: repo.created_on,\n })),\n count: repos.length,\n message: `Found ${repos.length} repository(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n",
450
450
  "tools/list-pull-requests.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.ts\";\n\ntype PullRequest = {\n id: number;\n title: string;\n state: string;\n author: {\n username: string;\n display_name: string;\n };\n created_on: string;\n updated_on: string;\n source: {\n branch: { name: string };\n };\n destination: {\n branch: { name: string };\n };\n links: {\n html: { href: string };\n };\n comment_count: number;\n task_count: number;\n};\n\nexport default tool({\n id: \"list-pull-requests\",\n description: \"List pull requests for a Bitbucket repository\",\n inputSchema: z.object({\n workspace: z.string().describe(\"Workspace name or UUID\"),\n repoSlug: z.string().describe(\"Repository slug (e.g., 'my-repo')\"),\n state: z\n .enum([\"OPEN\", \"MERGED\", \"DECLINED\", \"SUPERSEDED\"])\n .default(\"OPEN\")\n .describe(\"State of pull requests to list\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of pull requests to return\"),\n }),\n execute: async ({ workspace, repoSlug, state, limit }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const prs = await bitbucket.listPullRequests(workspace, repoSlug, {\n state,\n perPage: limit,\n });\n\n const repository = `${workspace}/${repoSlug}`;\n\n return {\n pullRequests: prs.map((pr: PullRequest) => ({\n id: pr.id,\n title: pr.title,\n state: pr.state,\n author: {\n username: pr.author.username,\n displayName: pr.author.display_name,\n },\n url: pr.links.html.href,\n sourceBranch: pr.source.branch.name,\n destinationBranch: pr.destination.branch.name,\n commentCount: pr.comment_count,\n taskCount: pr.task_count,\n createdOn: pr.created_on,\n updatedOn: pr.updated_on,\n })),\n count: prs.length,\n repository,\n message: `Found ${prs.length} ${state} pull request(s) in ${repository}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n",
451
451
  "tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.ts\";\n\ntype BitbucketIssue = {\n id: number;\n title: string;\n state: string;\n kind: string;\n priority: string;\n created_on: string;\n updated_on: string;\n reporter: {\n username: string;\n display_name: string;\n };\n assignee: {\n username: string;\n display_name: string;\n } | null;\n links: {\n html: { href: string };\n };\n content: {\n raw: string;\n } | null;\n};\n\nexport default tool({\n id: \"list-issues\",\n description: \"List issues for a Bitbucket repository\",\n inputSchema: z.object({\n workspace: z.string().describe(\"Workspace name or UUID\"),\n repoSlug: z.string().describe(\"Repository slug (e.g., 'my-repo')\"),\n state: z\n .enum([\n \"new\",\n \"open\",\n \"resolved\",\n \"on hold\",\n \"invalid\",\n \"duplicate\",\n \"wontfix\",\n \"closed\",\n ])\n .optional()\n .describe(\"Filter by issue state\"),\n kind: z\n .enum([\"bug\", \"enhancement\", \"proposal\", \"task\"])\n .optional()\n .describe(\"Filter by issue kind\"),\n priority: z\n .enum([\"trivial\", \"minor\", \"major\", \"critical\", \"blocker\"])\n .optional()\n .describe(\"Filter by priority level\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of issues to return\"),\n }),\n execute: async (\n { workspace, repoSlug, state, kind, priority, limit },\n context,\n ) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const issues = await bitbucket.listIssues(workspace, repoSlug, {\n state,\n kind,\n priority,\n perPage: limit,\n });\n\n const repository = `${workspace}/${repoSlug}`;\n\n return {\n issues: issues.map((issue: BitbucketIssue) => ({\n id: issue.id,\n title: issue.title,\n state: issue.state,\n kind: issue.kind,\n priority: issue.priority,\n description: issue.content?.raw ?? null,\n reporter: {\n username: issue.reporter.username,\n displayName: issue.reporter.display_name,\n },\n assignee: issue.assignee\n ? {\n username: issue.assignee.username,\n displayName: issue.assignee.display_name,\n }\n : null,\n url: issue.links.html.href,\n createdOn: issue.created_on,\n updatedOn: issue.updated_on,\n })),\n count: issues.length,\n repository,\n filters: {\n state: state ?? \"all\",\n kind: kind ?? \"all\",\n priority: priority ?? \"all\",\n },\n message: `Found ${issues.length} issue(s) in ${repository}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n",
@@ -457,7 +457,7 @@ export default {
457
457
  ".env.example": "# Zoom OAuth Configuration\n# Get your credentials from https://marketplace.zoom.us/develop/create\nZOOM_CLIENT_ID=your-client-id\nZOOM_CLIENT_SECRET=your-client-secret\n",
458
458
  "lib/zoom-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst ZOOM_BASE_URL = \"https://api.zoom.us/v2\";\n\ninterface ZoomMeeting {\n id: number;\n uuid: string;\n topic: string;\n type: number;\n start_time: string;\n duration: number;\n timezone: string;\n agenda: string;\n created_at: string;\n join_url: string;\n password?: string;\n host_id: string;\n host_email: string;\n status: string;\n settings?: {\n host_video: boolean;\n participant_video: boolean;\n join_before_host: boolean;\n mute_upon_entry: boolean;\n watermark: boolean;\n audio: string;\n auto_recording: string;\n };\n}\n\ninterface ZoomUser {\n id: string;\n first_name: string;\n last_name: string;\n email: string;\n type: number;\n pmi: number;\n timezone: string;\n verified: number;\n created_at: string;\n last_login_time: string;\n pic_url: string;\n}\n\ninterface ZoomMeetingList {\n meetings: ZoomMeeting[];\n page_count: number;\n page_number: number;\n page_size: number;\n total_records: number;\n}\n\ninterface MeetingSettingsInput {\n hostVideo?: boolean;\n participantVideo?: boolean;\n joinBeforeHost?: boolean;\n muteUponEntry?: boolean;\n watermark?: boolean;\n audio?: \"both\" | \"telephony\" | \"voip\";\n autoRecording?: \"local\" | \"cloud\" | \"none\";\n}\n\nfunction toZoomSettings(\n settings?: MeetingSettingsInput,\n): Record<string, unknown> | undefined {\n if (!settings) return undefined;\n\n return {\n host_video: settings.hostVideo,\n participant_video: settings.participantVideo,\n join_before_host: settings.joinBeforeHost,\n mute_upon_entry: settings.muteUponEntry,\n watermark: settings.watermark,\n audio: settings.audio,\n auto_recording: settings.autoRecording,\n };\n}\n\nasync function zoomFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Zoom. Please connect your account.\");\n }\n\n const response = await fetch(`${ZOOM_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(`Zoom API error: ${response.status} ${error.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function getUser(): Promise<ZoomUser> {\n return zoomFetch<ZoomUser>(\"/users/me\");\n}\n\nexport async function listMeetings(\n options: {\n userId?: string;\n type?: \"scheduled\" | \"live\" | \"upcoming\" | \"upcoming_meetings\" | \"previous_meetings\";\n pageSize?: number;\n pageNumber?: number;\n } = {},\n): Promise<ZoomMeeting[]> {\n const userId = options.userId ?? \"me\";\n const params = new URLSearchParams({\n type: options.type ?? \"scheduled\",\n page_size: String(options.pageSize ?? 30),\n page_number: String(options.pageNumber ?? 1),\n });\n\n const response = await zoomFetch<ZoomMeetingList>(`/users/${userId}/meetings?${params}`);\n return response.meetings;\n}\n\nexport async function getMeeting(meetingId: string | number): Promise<ZoomMeeting> {\n return zoomFetch<ZoomMeeting>(`/meetings/${meetingId}`);\n}\n\nexport async function createMeeting(options: {\n userId?: string;\n topic: string;\n type?: 1 | 2 | 3 | 8; // 1=Instant, 2=Scheduled, 3=Recurring with no fixed time, 8=Recurring with fixed time\n startTime?: string; // ISO 8601 format\n duration?: number; // In minutes\n timezone?: string;\n password?: string;\n agenda?: string;\n settings?: MeetingSettingsInput;\n}): Promise<ZoomMeeting> {\n const userId = options.userId ?? \"me\";\n const body: Record<string, unknown> = {\n topic: options.topic,\n type: options.type ?? 2,\n };\n\n if (options.startTime) body.start_time = options.startTime;\n if (options.duration) body.duration = options.duration;\n if (options.timezone) body.timezone = options.timezone;\n if (options.password) body.password = options.password;\n if (options.agenda) body.agenda = options.agenda;\n\n const settings = toZoomSettings(options.settings);\n if (settings) body.settings = settings;\n\n return zoomFetch<ZoomMeeting>(`/users/${userId}/meetings`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updateMeeting(\n meetingId: string | number,\n updates: {\n topic?: string;\n type?: 1 | 2 | 3 | 8;\n startTime?: string;\n duration?: number;\n timezone?: string;\n password?: string;\n agenda?: string;\n settings?: MeetingSettingsInput;\n },\n): Promise<void> {\n const body: Record<string, unknown> = {};\n\n if (updates.topic !== undefined) body.topic = updates.topic;\n if (updates.type !== undefined) body.type = updates.type;\n if (updates.startTime !== undefined) body.start_time = updates.startTime;\n if (updates.duration !== undefined) body.duration = updates.duration;\n if (updates.timezone !== undefined) body.timezone = updates.timezone;\n if (updates.password !== undefined) body.password = updates.password;\n if (updates.agenda !== undefined) body.agenda = updates.agenda;\n\n const settings = toZoomSettings(updates.settings);\n if (settings) body.settings = settings;\n\n await zoomFetch<void>(`/meetings/${meetingId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function deleteMeeting(\n meetingId: string | number,\n options?: {\n occurrenceId?: string;\n scheduleForReminder?: boolean;\n },\n): Promise<void> {\n const params = new URLSearchParams();\n if (options?.occurrenceId) params.set(\"occurrence_id\", options.occurrenceId);\n if (options?.scheduleForReminder !== undefined) {\n params.set(\"schedule_for_reminder\", String(options.scheduleForReminder));\n }\n\n const queryString = params.toString();\n await zoomFetch<void>(`/meetings/${meetingId}${queryString ? `?${queryString}` : \"\"}`, {\n method: \"DELETE\",\n });\n}\n",
459
459
  "app/api/auth/zoom/route.ts": "import { createOAuthInitHandler, zoomConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(zoomConfig, { tokenStore: oauthMemoryTokenStore });\n",
460
- "app/api/auth/zoom/callback/route.ts": "import { createOAuthCallbackHandler, zoomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(zoomConfig, { tokenStore: hybridTokenStore });\n",
460
+ "app/api/auth/zoom/callback/route.ts": "import { createOAuthCallbackHandler, zoomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(zoomConfig, { tokenStore: hybridTokenStore });\n",
461
461
  "tools/update-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"update-meeting\",\n description: \"Update an existing Zoom meeting with new settings.\",\n inputSchema: z.object({\n meetingId: z.union([z.string(), z.number()]).describe(\"The meeting ID to update\"),\n topic: z.string().optional().describe(\"The new topic/title of the meeting\"),\n type: z\n .enum([\"1\", \"2\", \"3\", \"8\"])\n .transform((val) => parseInt(val, 10) as 1 | 2 | 3 | 8)\n .optional()\n .describe(\n \"Meeting type: 1=Instant, 2=Scheduled, 3=Recurring with no fixed time, 8=Recurring with fixed time\",\n ),\n startTime: z.string().optional().describe(\"New start time in ISO 8601 format\"),\n duration: z.number().min(1).optional().describe(\"New meeting duration in minutes\"),\n timezone: z.string().optional().describe(\"New timezone\"),\n password: z.string().optional().describe(\"New meeting password\"),\n agenda: z.string().optional().describe(\"New meeting agenda or description\"),\n hostVideo: z.boolean().optional().describe(\"Start video when host joins\"),\n participantVideo: z.boolean().optional().describe(\"Start video when participants join\"),\n joinBeforeHost: z.boolean().optional().describe(\"Allow participants to join before host\"),\n muteUponEntry: z.boolean().optional().describe(\"Mute participants upon entry\"),\n autoRecording: z.enum([\"local\", \"cloud\", \"none\"]).optional().describe(\"Automatic recording setting\"),\n }),\n async execute({\n meetingId,\n topic,\n type,\n startTime,\n duration,\n timezone,\n password,\n agenda,\n hostVideo,\n participantVideo,\n joinBeforeHost,\n muteUponEntry,\n autoRecording,\n }): Promise<{ success: true; message: string }> {\n const settings = { hostVideo, participantVideo, joinBeforeHost, muteUponEntry, autoRecording };\n const hasSettings = Object.values(settings).some((value) => value !== undefined);\n\n await updateMeeting(meetingId, {\n topic,\n type,\n startTime,\n duration,\n timezone,\n password,\n agenda,\n settings: hasSettings ? settings : undefined,\n });\n\n return { success: true, message: `Meeting ${meetingId} updated successfully` };\n },\n});\n",
462
462
  "tools/get-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"get-meeting\",\n description: \"Get detailed information about a specific Zoom meeting by its ID.\",\n inputSchema: z.object({\n meetingId: z.union([z.string(), z.number()]).describe(\"The meeting ID or UUID\"),\n }),\n async execute({ meetingId }) {\n const meeting = await getMeeting(meetingId);\n const settings = meeting.settings;\n\n return {\n id: meeting.id,\n uuid: meeting.uuid,\n topic: meeting.topic,\n type: meeting.type,\n startTime: meeting.start_time,\n duration: meeting.duration,\n timezone: meeting.timezone,\n agenda: meeting.agenda,\n joinUrl: meeting.join_url,\n password: meeting.password,\n hostId: meeting.host_id,\n hostEmail: meeting.host_email,\n status: meeting.status,\n createdAt: meeting.created_at,\n settings: settings && {\n hostVideo: settings.host_video,\n participantVideo: settings.participant_video,\n joinBeforeHost: settings.join_before_host,\n muteUponEntry: settings.mute_upon_entry,\n watermark: settings.watermark,\n audio: settings.audio,\n autoRecording: settings.auto_recording,\n },\n };\n },\n});\n",
463
463
  "tools/list-meetings.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listMeetings } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"list-meetings\",\n description:\n \"List Zoom meetings for the current user. Can filter by meeting type (scheduled, live, upcoming, etc.).\",\n inputSchema: z.object({\n type: z\n .enum([\"scheduled\", \"live\", \"upcoming\", \"upcoming_meetings\", \"previous_meetings\"])\n .default(\"scheduled\")\n .describe(\"Type of meetings to list\"),\n pageSize: z\n .number()\n .min(1)\n .max(300)\n .default(30)\n .describe(\"Number of meetings to return per page\"),\n pageNumber: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ type, pageSize, pageNumber }) {\n const meetings = await listMeetings({ type, pageSize, pageNumber });\n\n return meetings.map((meeting) => ({\n id: meeting.id,\n uuid: meeting.uuid,\n topic: meeting.topic,\n type: meeting.type,\n startTime: meeting.start_time,\n duration: meeting.duration,\n timezone: meeting.timezone,\n agenda: meeting.agenda,\n joinUrl: meeting.join_url,\n password: meeting.password,\n status: meeting.status,\n }));\n },\n});\n",
@@ -470,7 +470,7 @@ export default {
470
470
  ".env.example": "# Twitter / X Integration\n# Create an app at https://developer.twitter.com/en/portal/dashboard\n# Enable OAuth 2.0 and add callback URL: http://localhost:3000/api/auth/twitter/callback\n\nTWITTER_CLIENT_ID=your_client_id_here\nTWITTER_CLIENT_SECRET=your_client_secret_here\n",
471
471
  "lib/twitter-client.ts": "/**\n * Twitter API Client\n *\n * Provides a type-safe interface to Twitter API v2 operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst TWITTER_API_BASE = \"https://api.twitter.com/2\";\n\nexport interface TwitterUser {\n id: string;\n name: string;\n username: string;\n created_at?: string;\n description?: string;\n location?: string;\n profile_image_url?: string;\n public_metrics?: {\n followers_count: number;\n following_count: number;\n tweet_count: number;\n listed_count: number;\n };\n verified?: boolean;\n}\n\nexport interface Tweet {\n id: string;\n text: string;\n author_id?: string;\n created_at?: string;\n public_metrics?: {\n retweet_count: number;\n reply_count: number;\n like_count: number;\n quote_count: number;\n };\n referenced_tweets?: Array<{\n type: string;\n id: string;\n }>;\n entities?: {\n hashtags?: Array<{ tag: string }>;\n mentions?: Array<{ username: string; id: string }>;\n urls?: Array<{ url: string; expanded_url: string }>;\n };\n}\n\nexport interface SearchResult {\n data: Tweet[];\n meta: {\n newest_id?: string;\n oldest_id?: string;\n result_count: number;\n next_token?: string;\n };\n}\n\nexport const twitterOAuthProvider = {\n name: \"twitter\",\n authorizationUrl: \"https://twitter.com/i/oauth2/authorize\",\n tokenUrl: \"https://api.twitter.com/2/oauth2/token\",\n clientId: getEnv(\"TWITTER_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"TWITTER_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"tweet.read\",\n \"tweet.write\",\n \"users.read\",\n \"follows.read\",\n \"offline.access\",\n ],\n callbackPath: \"/api/auth/twitter/callback\",\n usePKCE: true,\n};\n\nexport function createTwitterClient(userId: string): {\n getMe: () => Promise<TwitterUser>;\n getUserById: (userId: string) => Promise<TwitterUser>;\n getTweets: (\n userId: string,\n options?: { maxResults?: number; excludeReplies?: boolean },\n ) => Promise<Tweet[]>;\n getTweet: (tweetId: string) => Promise<Tweet>;\n postTweet: (text: string) => Promise<{ id: string; text: string }>;\n searchTweets: (\n query: string,\n options?: { maxResults?: number; sortOrder?: \"recency\" | \"relevancy\" },\n ) => Promise<SearchResult>;\n getTimeline: (options?: { maxResults?: number }) => Promise<Tweet[]>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(twitterOAuthProvider, userId, \"twitter\");\n if (token) return token;\n\n throw new Error(\n \"Twitter not connected. Please connect your Twitter account first.\",\n );\n }\n\n async function twitterFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${TWITTER_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response\n .json()\n .catch(() => ({ detail: response.statusText }));\n\n throw new Error(\n `Twitter API error: ${error.detail || error.title || response.statusText}`,\n );\n }\n\n function createTweetFieldsParams(\n options: { maxResults?: number },\n tweetFields: string,\n ): URLSearchParams {\n return new URLSearchParams({\n max_results: String(options.maxResults ?? 10),\n \"tweet.fields\": tweetFields,\n });\n }\n\n return {\n async getMe(): Promise<TwitterUser> {\n const result = await twitterFetch<{ data: TwitterUser }>(\n \"/users/me?user.fields=created_at,description,location,profile_image_url,public_metrics,verified\",\n );\n return result.data;\n },\n\n async getUserById(userId: string): Promise<TwitterUser> {\n const result = await twitterFetch<{ data: TwitterUser }>(\n `/users/${userId}?user.fields=created_at,description,location,profile_image_url,public_metrics,verified`,\n );\n return result.data;\n },\n\n async getTweets(\n userId: string,\n options: { maxResults?: number; excludeReplies?: boolean } = {},\n ): Promise<Tweet[]> {\n const params = createTweetFieldsParams(\n options,\n \"created_at,public_metrics,referenced_tweets,entities\",\n );\n\n if (options.excludeReplies) params.set(\"exclude\", \"replies\");\n\n const result = await twitterFetch<{ data: Tweet[] }>(\n `/users/${userId}/tweets?${params.toString()}`,\n );\n return result.data ?? [];\n },\n\n async getTweet(tweetId: string): Promise<Tweet> {\n const result = await twitterFetch<{ data: Tweet }>(\n `/tweets/${tweetId}?tweet.fields=created_at,public_metrics,referenced_tweets,entities,author_id`,\n );\n return result.data;\n },\n\n async postTweet(text: string): Promise<{ id: string; text: string }> {\n const result = await twitterFetch<{ data: { id: string; text: string } }>(\n \"/tweets\",\n {\n method: \"POST\",\n body: JSON.stringify({ text }),\n },\n );\n return result.data;\n },\n\n searchTweets(\n query: string,\n options: { maxResults?: number; sortOrder?: \"recency\" | \"relevancy\" } = {},\n ): Promise<SearchResult> {\n const params = new URLSearchParams({\n query,\n max_results: String(options.maxResults ?? 10),\n \"tweet.fields\":\n \"created_at,public_metrics,referenced_tweets,entities,author_id\",\n sort_order: options.sortOrder ?? \"recency\",\n });\n\n return twitterFetch<SearchResult>(\n `/tweets/search/recent?${params.toString()}`,\n );\n },\n\n async getTimeline(options: { maxResults?: number } = {}): Promise<Tweet[]> {\n const me = await twitterFetch<{ data: TwitterUser }>(\"/users/me\");\n const params = createTweetFieldsParams(\n options,\n \"created_at,public_metrics,referenced_tweets,entities,author_id\",\n );\n\n const result = await twitterFetch<{ data: Tweet[] }>(\n `/users/${me.data.id}/timelines/reverse_chronological?${params.toString()}`,\n );\n return result.data ?? [];\n },\n };\n}\n\nexport type TwitterClient = ReturnType<typeof createTwitterClient>;\n",
472
472
  "app/api/auth/twitter/route.ts": "import { createOAuthInitHandler, twitterConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(twitterConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
473
- "app/api/auth/twitter/callback/route.ts": "import { createOAuthCallbackHandler, twitterConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(twitterConfig, { tokenStore: hybridTokenStore });\n",
473
+ "app/api/auth/twitter/callback/route.ts": "import { createOAuthCallbackHandler, twitterConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(twitterConfig, { tokenStore: hybridTokenStore });\n",
474
474
  "tools/post-tweet.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"post-tweet\",\n description: \"Post a new tweet to Twitter/X. Maximum length is 280 characters.\",\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(280)\n .describe(\"Tweet text content (max 280 characters)\"),\n }),\n execute: async ({ text }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const result = await twitter.postTweet(text);\n\n return {\n success: true,\n tweetId: result.id,\n text: result.text,\n message: \"Tweet posted successfully!\",\n url: `https://twitter.com/i/web/status/${result.id}`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n",
475
475
  "tools/search-tweets.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"search-tweets\",\n description:\n \"Search recent tweets on Twitter/X. Returns up to 10 recent tweets matching the query.\",\n inputSchema: z.object({\n query: z\n .string()\n .min(1)\n .describe(\n \"Search query (supports Twitter search operators like 'from:', 'to:', '#hashtag', etc.)\",\n ),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of tweets to return (default: 10)\"),\n sortOrder: z\n .enum([\"recency\", \"relevancy\"])\n .optional()\n .describe(\"Sort order: 'recency' (default) or 'relevancy'\"),\n }),\n execute: async ({ query, maxResults, sortOrder }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const result = await twitter.searchTweets(query, {\n maxResults: maxResults ?? 10,\n sortOrder: sortOrder ?? \"recency\",\n });\n\n const tweets = result.data ?? [];\n const count = result.meta.result_count;\n\n if (tweets.length === 0) {\n return {\n success: true,\n tweets: [],\n count: 0,\n message: `No tweets found matching query: \"${query}\"`,\n };\n }\n\n return {\n success: true,\n tweets: tweets.map((tweet) => ({\n id: tweet.id,\n text: tweet.text,\n author_id: tweet.author_id,\n created_at: tweet.created_at,\n metrics: tweet.public_metrics,\n hashtags: tweet.entities?.hashtags?.map((h) => h.tag),\n mentions: tweet.entities?.mentions?.map((m) => m.username),\n })),\n count,\n message: `Found ${count} tweets matching query: \"${query}\"`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n",
476
476
  "tools/get-timeline.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"get-timeline\",\n description: \"Get the authenticated user's home timeline (tweets from followed accounts)\",\n inputSchema: z.object({\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of tweets to return (default: 10)\"),\n }),\n execute: async ({ maxResults }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const tweets = await twitter.getTimeline({ maxResults: maxResults ?? 10 });\n\n if (!tweets?.length) {\n return {\n success: true,\n tweets: [],\n count: 0,\n message: \"No tweets found in your timeline.\",\n };\n }\n\n return {\n success: true,\n tweets: tweets.map((tweet) => ({\n id: tweet.id,\n text: tweet.text,\n author_id: tweet.author_id,\n created_at: tweet.created_at,\n metrics: tweet.public_metrics,\n hashtags: tweet.entities?.hashtags?.map((h) => h.tag),\n mentions: tweet.entities?.mentions?.map((m) => m.username),\n })),\n count: tweets.length,\n message: `Retrieved ${tweets.length} tweets from your timeline.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n"
@@ -482,7 +482,7 @@ export default {
482
482
  "lib/figma-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst FIGMA_BASE_URL = \"https://api.figma.com/v1\";\n\nexport interface FigmaFile {\n document: FigmaNode;\n components: Record<string, FigmaComponent>;\n componentSets: Record<string, FigmaComponentSet>;\n schemaVersion: number;\n styles: Record<string, FigmaStyle>;\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n role: string;\n editorType: string;\n linkAccess: string;\n}\n\nexport interface FigmaNode {\n id: string;\n name: string;\n type: string;\n children?: FigmaNode[];\n visible?: boolean;\n locked?: boolean;\n absoluteBoundingBox?: {\n x: number;\n y: number;\n width: number;\n height: number;\n };\n fills?: Array<{\n type: string;\n color?: {\n r: number;\n g: number;\n b: number;\n a: number;\n };\n }>;\n strokes?: unknown[];\n strokeWeight?: number;\n effects?: unknown[];\n cornerRadius?: number;\n rectangleCornerRadii?: number[];\n characters?: string;\n style?: {\n fontFamily?: string;\n fontSize?: number;\n fontWeight?: number;\n lineHeightPx?: number;\n };\n}\n\nexport interface FigmaComponent {\n key: string;\n name: string;\n description: string;\n componentSetId?: string;\n documentationLinks: unknown[];\n}\n\nexport interface FigmaComponentSet {\n key: string;\n name: string;\n description: string;\n documentationLinks: unknown[];\n}\n\nexport interface FigmaStyle {\n key: string;\n name: string;\n description: string;\n styleType: \"FILL\" | \"TEXT\" | \"EFFECT\" | \"GRID\";\n}\n\nexport interface FigmaComment {\n id: string;\n file_key: string;\n parent_id?: string;\n user: {\n id: string;\n handle: string;\n img_url: string;\n };\n created_at: string;\n resolved_at?: string;\n message: string;\n client_meta: {\n x?: number;\n y?: number;\n node_id?: string[];\n node_offset?: { x: number; y: number };\n };\n order_id: string;\n}\n\nexport interface FigmaProject {\n id: string;\n name: string;\n}\n\nexport interface FigmaTeamProject {\n id: string;\n name: string;\n}\n\nexport interface FigmaUser {\n id: string;\n handle: string;\n img_url: string;\n email?: string;\n}\n\nasync function figmaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Figma. Please connect your account.\");\n }\n\n const response = await fetch(`${FIGMA_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) {\n return response.json() as Promise<T>;\n }\n\n const error = (await response.json().catch(() => ({}))) as { message?: string; err?: string };\n throw new Error(\n `Figma API error: ${response.status} ${error.message ?? error.err ?? response.statusText}`,\n );\n}\n\nexport function getMe(): Promise<FigmaUser> {\n return figmaFetch<FigmaUser>(\"/me\");\n}\n\nexport function getFile(\n fileKey: string,\n options?: {\n version?: string;\n ids?: string[];\n depth?: number;\n geometry?: \"paths\" | \"bounds\";\n plugin_data?: string;\n branch_data?: boolean;\n },\n): Promise<FigmaFile> {\n const params = new URLSearchParams();\n\n if (options?.version) params.set(\"version\", options.version);\n if (options?.ids?.length) params.set(\"ids\", options.ids.join(\",\"));\n if (options?.depth) params.set(\"depth\", String(options.depth));\n if (options?.geometry) params.set(\"geometry\", options.geometry);\n if (options?.plugin_data) params.set(\"plugin_data\", options.plugin_data);\n if (options?.branch_data) params.set(\"branch_data\", \"true\");\n\n const query = params.toString();\n const url = query ? `/files/${fileKey}?${query}` : `/files/${fileKey}`;\n\n return figmaFetch<FigmaFile>(url);\n}\n\nexport function getFileNodes(\n fileKey: string,\n nodeIds: string[],\n): Promise<{\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n nodes: Record<string, { document: FigmaNode; components: Record<string, FigmaComponent> }>;\n}> {\n const params = new URLSearchParams({ ids: nodeIds.join(\",\") });\n return figmaFetch(`/files/${fileKey}/nodes?${params.toString()}`);\n}\n\nexport function getFileImages(\n fileKey: string,\n nodeIds: string[],\n options?: {\n format?: \"jpg\" | \"png\" | \"svg\" | \"pdf\";\n scale?: number;\n svg_include_id?: boolean;\n svg_simplify_stroke?: boolean;\n use_absolute_bounds?: boolean;\n version?: string;\n },\n): Promise<{\n err?: string;\n images: Record<string, string | null>;\n status?: number;\n}> {\n const params = new URLSearchParams({\n ids: nodeIds.join(\",\"),\n format: options?.format ?? \"png\",\n });\n\n if (options?.scale) params.set(\"scale\", String(options.scale));\n if (options?.svg_include_id) params.set(\"svg_include_id\", \"true\");\n if (options?.svg_simplify_stroke) params.set(\"svg_simplify_stroke\", \"true\");\n if (options?.use_absolute_bounds) params.set(\"use_absolute_bounds\", \"true\");\n if (options?.version) params.set(\"version\", options.version);\n\n return figmaFetch(`/images/${fileKey}?${params.toString()}`);\n}\n\nexport function getComments(fileKey: string): Promise<{ comments: FigmaComment[] }> {\n return figmaFetch<{ comments: FigmaComment[] }>(`/files/${fileKey}/comments`);\n}\n\nexport function postComment(\n fileKey: string,\n message: string,\n options?: {\n client_meta?: { x?: number; y?: number; node_id?: string[] };\n parent_id?: string;\n },\n): Promise<FigmaComment> {\n return figmaFetch<FigmaComment>(`/files/${fileKey}/comments`, {\n method: \"POST\",\n body: JSON.stringify({\n message,\n client_meta: options?.client_meta ?? {},\n ...(options?.parent_id ? { parent_id: options.parent_id } : {}),\n }),\n });\n}\n\nexport function getTeamProjects(teamId: string): Promise<{ projects: FigmaTeamProject[] }> {\n return figmaFetch<{ projects: FigmaTeamProject[] }>(`/teams/${teamId}/projects`);\n}\n\nexport function getProjectFiles(projectId: string): Promise<{\n files: Array<{\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n }>;\n}> {\n return figmaFetch(`/projects/${projectId}/files`);\n}\n\nexport function getUserFiles(): Promise<{\n files: Array<{\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n }>;\n}> {\n throw new Error(\n \"Getting user files requires team ID. Use getTeamProjects and getProjectFiles instead.\",\n );\n}\n\nexport function extractComponents(file: FigmaFile): Array<{\n key: string;\n name: string;\n description: string;\n type: \"component\" | \"component_set\";\n}> {\n const components = Object.entries(file.components).map(([key, component]) => ({\n key,\n name: component.name,\n description: component.description,\n type: \"component\" as const,\n }));\n\n const componentSets = Object.entries(file.componentSets).map(([key, componentSet]) => ({\n key,\n name: componentSet.name,\n description: componentSet.description,\n type: \"component_set\" as const,\n }));\n\n return [...components, ...componentSets];\n}\n\nexport function extractStyles(file: FigmaFile): Array<{\n key: string;\n name: string;\n description: string;\n type: string;\n}> {\n return Object.entries(file.styles).map(([key, style]) => ({\n key,\n name: style.name,\n description: style.description,\n type: style.styleType,\n }));\n}\n\nexport function findNodesByType(node: FigmaNode, type: string): FigmaNode[] {\n const results: FigmaNode[] = [];\n\n if (node.type === type) {\n results.push(node);\n }\n\n for (const child of node.children ?? []) {\n results.push(...findNodesByType(child, type));\n }\n\n return results;\n}\n\nexport function getFileSummary(file: FigmaFile): {\n name: string;\n lastModified: string;\n componentCount: number;\n componentSetCount: number;\n styleCount: number;\n pageCount: number;\n} {\n return {\n name: file.name,\n lastModified: file.lastModified,\n componentCount: Object.keys(file.components).length,\n componentSetCount: Object.keys(file.componentSets).length,\n styleCount: Object.keys(file.styles).length,\n pageCount: file.document.children?.length ?? 0,\n };\n}\n",
483
483
  "lib/types.ts": "export type NodeType =\n | \"DOCUMENT\"\n | \"CANVAS\"\n | \"FRAME\"\n | \"GROUP\"\n | \"VECTOR\"\n | \"BOOLEAN_OPERATION\"\n | \"STAR\"\n | \"LINE\"\n | \"ELLIPSE\"\n | \"REGULAR_POLYGON\"\n | \"RECTANGLE\"\n | \"TEXT\"\n | \"SLICE\"\n | \"COMPONENT\"\n | \"COMPONENT_SET\"\n | \"INSTANCE\";\n\nexport type BlendMode =\n | \"NORMAL\"\n | \"DARKEN\"\n | \"MULTIPLY\"\n | \"LINEAR_BURN\"\n | \"COLOR_BURN\"\n | \"LIGHTEN\"\n | \"SCREEN\"\n | \"LINEAR_DODGE\"\n | \"COLOR_DODGE\"\n | \"OVERLAY\"\n | \"SOFT_LIGHT\"\n | \"HARD_LIGHT\"\n | \"DIFFERENCE\"\n | \"EXCLUSION\"\n | \"HUE\"\n | \"SATURATION\"\n | \"COLOR\"\n | \"LUMINOSITY\";\n\nexport type EasingType = \"EASE_IN\" | \"EASE_OUT\" | \"EASE_IN_AND_OUT\" | \"LINEAR\";\n\nexport interface Vector2D {\n x: number;\n y: number;\n}\n\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface Transform {\n /** 2D transformation matrix [[a, b, tx], [c, d, ty]] */\n matrix: [[number, number, number], [number, number, number]];\n}\n\nexport type PaintType =\n | \"SOLID\"\n | \"GRADIENT_LINEAR\"\n | \"GRADIENT_RADIAL\"\n | \"GRADIENT_ANGULAR\"\n | \"GRADIENT_DIAMOND\"\n | \"IMAGE\"\n | \"EMOJI\";\n\nexport interface Color {\n r: number;\n g: number;\n b: number;\n a: number;\n}\n\nexport interface ColorStop {\n position: number;\n color: Color;\n}\n\nexport interface Paint {\n type: PaintType;\n visible?: boolean;\n opacity?: number;\n color?: Color;\n blendMode?: BlendMode;\n gradientHandlePositions?: Vector2D[];\n gradientStops?: ColorStop[];\n scaleMode?: \"FILL\" | \"FIT\" | \"TILE\" | \"STRETCH\";\n imageTransform?: Transform;\n scalingFactor?: number;\n imageRef?: string;\n gifRef?: string;\n}\n\nexport type EffectType = \"INNER_SHADOW\" | \"DROP_SHADOW\" | \"LAYER_BLUR\" | \"BACKGROUND_BLUR\";\n\nexport interface Effect {\n type: EffectType;\n visible?: boolean;\n radius?: number;\n color?: Color;\n blendMode?: BlendMode;\n offset?: Vector2D;\n spread?: number;\n}\n\nexport type LayoutConstraintVertical = \"TOP\" | \"BOTTOM\" | \"CENTER\" | \"TOP_BOTTOM\" | \"SCALE\";\nexport type LayoutConstraintHorizontal = \"LEFT\" | \"RIGHT\" | \"CENTER\" | \"LEFT_RIGHT\" | \"SCALE\";\n\nexport interface LayoutConstraint {\n vertical: LayoutConstraintVertical;\n horizontal: LayoutConstraintHorizontal;\n}\n\nexport type LayoutAlign = \"MIN\" | \"CENTER\" | \"MAX\" | \"STRETCH\" | \"INHERIT\";\nexport type LayoutMode = \"NONE\" | \"HORIZONTAL\" | \"VERTICAL\";\n\nexport interface LayoutGrid {\n pattern: \"COLUMNS\" | \"ROWS\" | \"GRID\";\n sectionSize?: number;\n visible?: boolean;\n color?: Color;\n alignment?: \"MIN\" | \"MAX\" | \"CENTER\" | \"STRETCH\";\n gutterSize?: number;\n offset?: number;\n count?: number;\n}\n\nexport type TextAlignHorizontal = \"LEFT\" | \"CENTER\" | \"RIGHT\" | \"JUSTIFIED\";\nexport type TextAlignVertical = \"TOP\" | \"CENTER\" | \"BOTTOM\";\nexport type TextCase = \"ORIGINAL\" | \"UPPER\" | \"LOWER\" | \"TITLE\";\nexport type TextDecoration = \"NONE\" | \"STRIKETHROUGH\" | \"UNDERLINE\";\n\nexport interface TypeStyle {\n fontFamily: string;\n fontPostScriptName?: string;\n paragraphSpacing?: number;\n paragraphIndent?: number;\n italic?: boolean;\n fontWeight: number;\n fontSize: number;\n textAlignHorizontal?: TextAlignHorizontal;\n textAlignVertical?: TextAlignVertical;\n letterSpacing?: number;\n fills?: Paint[];\n lineHeightPx?: number;\n lineHeightPercent?: number;\n lineHeightPercentFontSize?: number;\n lineHeightUnit?: \"PIXELS\" | \"FONT_SIZE_%\" | \"INTRINSIC_%\";\n}\n\nexport interface Component {\n key: string;\n name: string;\n description: string;\n componentSetId?: string;\n documentationLinks: string[];\n remote?: boolean;\n}\n\nexport interface ComponentSet {\n key: string;\n name: string;\n description: string;\n documentationLinks: string[];\n remote?: boolean;\n}\n\nexport type StyleType = \"FILL\" | \"TEXT\" | \"EFFECT\" | \"GRID\";\n\nexport interface Style {\n key: string;\n name: string;\n description: string;\n styleType: StyleType;\n remote?: boolean;\n}\n\nexport type ExportFormat = \"JPG\" | \"PNG\" | \"SVG\" | \"PDF\";\n\nexport interface ExportSettings {\n suffix: string;\n format: ExportFormat;\n constraint?: {\n type: \"SCALE\" | \"WIDTH\" | \"HEIGHT\";\n value: number;\n };\n}\n\nexport interface Comment {\n id: string;\n file_key: string;\n parent_id?: string;\n user: User;\n created_at: string;\n resolved_at?: string;\n message: string;\n client_meta: CommentClientMeta;\n order_id: string;\n}\n\nexport interface CommentClientMeta {\n x?: number;\n y?: number;\n node_id?: string[];\n node_offset?: Vector2D;\n}\n\nexport interface User {\n id: string;\n handle: string;\n img_url: string;\n email?: string;\n}\n\nexport interface FileResponse {\n document: Node;\n components: Record<string, Component>;\n componentSets: Record<string, ComponentSet>;\n schemaVersion: number;\n styles: Record<string, Style>;\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n role: \"owner\" | \"editor\" | \"viewer\";\n editorType: \"figma\" | \"figjam\";\n linkAccess: \"view\" | \"edit\" | \"org_view\" | \"org_edit\";\n}\n\nexport interface NodeBase {\n id: string;\n name: string;\n visible?: boolean;\n type: NodeType;\n pluginData?: unknown;\n sharedPluginData?: unknown;\n locked?: boolean;\n}\n\nexport interface NodeWithChildren extends NodeBase {\n children: Node[];\n}\n\nexport interface DocumentNode extends NodeWithChildren {\n type: \"DOCUMENT\";\n}\n\nexport interface CanvasNode extends NodeWithChildren {\n type: \"CANVAS\";\n backgroundColor: Color;\n prototypeStartNodeID?: string;\n prototypeDevice?: {\n type: string;\n rotation: \"NONE\" | \"CCW_90\";\n };\n exportSettings?: ExportSettings[];\n}\n\nexport interface FrameNode extends NodeWithChildren {\n type: \"FRAME\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n clipsContent?: boolean;\n background: Paint[];\n backgroundColor?: Color;\n fills?: Paint[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeDashes?: number[];\n cornerRadius?: number;\n rectangleCornerRadii?: [number, number, number, number];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n layoutMode?: LayoutMode;\n primaryAxisSizingMode?: \"FIXED\" | \"AUTO\";\n counterAxisSizingMode?: \"FIXED\" | \"AUTO\";\n primaryAxisAlignItems?: LayoutAlign;\n counterAxisAlignItems?: LayoutAlign;\n paddingLeft?: number;\n paddingRight?: number;\n paddingTop?: number;\n paddingBottom?: number;\n itemSpacing?: number;\n layoutGrids?: LayoutGrid[];\n effects?: Effect[];\n isMask?: boolean;\n isMaskOutline?: boolean;\n transitionNodeID?: string;\n transitionDuration?: number;\n transitionEasing?: EasingType;\n opacity?: number;\n}\n\nexport interface GroupNode extends NodeWithChildren {\n type: \"GROUP\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n clipsContent?: boolean;\n blendMode?: BlendMode;\n effects?: Effect[];\n opacity?: number;\n}\n\nexport type VectorNodeType =\n | \"VECTOR\"\n | \"BOOLEAN_OPERATION\"\n | \"STAR\"\n | \"LINE\"\n | \"ELLIPSE\"\n | \"REGULAR_POLYGON\"\n | \"RECTANGLE\";\n\nexport interface VectorNode extends NodeBase {\n type: VectorNodeType;\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n fills?: Paint[];\n fillGeometry?: unknown[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeCap?: \"NONE\" | \"ROUND\" | \"SQUARE\" | \"LINE_ARROW\" | \"TRIANGLE_ARROW\";\n strokeJoin?: \"MITER\" | \"BEVEL\" | \"ROUND\";\n strokeDashes?: number[];\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeGeometry?: unknown[];\n cornerRadius?: number;\n rectangleCornerRadii?: [number, number, number, number];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n effects?: Effect[];\n isMask?: boolean;\n opacity?: number;\n}\n\nexport interface TextNode extends NodeBase {\n type: \"TEXT\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n fills?: Paint[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeDashes?: number[];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n effects?: Effect[];\n characters: string;\n style: TypeStyle;\n characterStyleOverrides?: number[];\n styleOverrideTable?: Record<number, TypeStyle>;\n opacity?: number;\n}\n\nexport interface ComponentNode extends FrameNode {\n type: \"COMPONENT\";\n}\n\nexport interface ComponentSetNode extends FrameNode {\n type: \"COMPONENT_SET\";\n}\n\nexport interface InstanceNode extends FrameNode {\n type: \"INSTANCE\";\n componentId: string;\n overrides?: unknown[];\n}\n\nexport type Node =\n | DocumentNode\n | CanvasNode\n | FrameNode\n | GroupNode\n | VectorNode\n | TextNode\n | ComponentNode\n | ComponentSetNode\n | InstanceNode;\n\nexport interface Project {\n id: string;\n name: string;\n}\n\nexport interface FileReference {\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n}\n\nexport interface ProjectFilesResponse {\n files: FileReference[];\n}\n\nexport interface TeamProjectsResponse {\n projects: Project[];\n}\n\nexport interface Version {\n id: string;\n created_at: string;\n label?: string;\n description?: string;\n user: User;\n thumbnail_url?: string;\n}\n\nexport interface VersionsResponse {\n versions: Version[];\n pagination?: {\n next_page?: number;\n };\n}\n",
484
484
  "app/api/auth/figma/route.ts": "import { createOAuthInitHandler, figmaConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(figmaConfig, { tokenStore: oauthMemoryTokenStore });\n",
485
- "app/api/auth/figma/callback/route.ts": "import { createOAuthCallbackHandler, figmaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(figmaConfig, { tokenStore: hybridTokenStore });\n",
485
+ "app/api/auth/figma/callback/route.ts": "import { createOAuthCallbackHandler, figmaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(figmaConfig, { tokenStore: hybridTokenStore });\n",
486
486
  "tools/post-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { postComment } from \"../../lib/figma-client.ts\";\n\nexport default tool({\n id: \"post-comment\",\n description:\n \"Post a comment on a Figma file. Can be a new comment or a reply to an existing comment thread.\",\n inputSchema: z.object({\n fileKey: z.string().describe(\"The file key (from the Figma URL)\"),\n message: z.string().min(1).describe(\"The comment message to post\"),\n parentId: z\n .string()\n .optional()\n .describe(\"ID of parent comment to reply to (for threaded replies)\"),\n nodeId: z.string().optional().describe(\"ID of the Figma node to attach the comment to\"),\n x: z.number().optional().describe(\"X coordinate for comment placement (0-1, relative to canvas)\"),\n y: z.number().optional().describe(\"Y coordinate for comment placement (0-1, relative to canvas)\"),\n }),\n async execute({ fileKey, message, parentId, nodeId, x, y }) {\n const clientMeta: { x?: number; y?: number; node_id?: string[] } = {};\n\n if (x !== undefined) clientMeta.x = x;\n if (y !== undefined) clientMeta.y = y;\n if (nodeId) clientMeta.node_id = [nodeId];\n\n const comment = await postComment(fileKey, message, {\n client_meta: Object.keys(clientMeta).length ? clientMeta : undefined,\n parent_id: parentId,\n });\n\n return {\n success: true,\n comment: {\n id: comment.id,\n message: comment.message,\n author: {\n handle: comment.user.handle,\n avatar: comment.user.img_url,\n },\n createdAt: comment.created_at,\n isReply: Boolean(comment.parent_id),\n fileUrl: `https://www.figma.com/file/${fileKey}`,\n },\n };\n },\n});\n",
487
487
  "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getProjectFiles, getTeamProjects } from \"../../lib/figma-client.ts\";\n\nexport default tool({\n id: \"list-projects\",\n description:\n \"List all projects in a Figma team. Optionally include file counts and recent files for each project.\",\n inputSchema: z.object({\n teamId: z.string().describe(\"The team ID to list projects from\"),\n includeFiles: z.boolean().default(false).describe(\"Include recent files for each project\"),\n filesPerProject: z\n .number()\n .min(1)\n .max(10)\n .default(5)\n .describe(\"Number of recent files to include per project (if includeFiles is true)\"),\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of projects to return\"),\n }),\n async execute({ teamId, includeFiles, filesPerProject, limit }) {\n const { projects: allProjects } = await getTeamProjects(teamId);\n const projects = allProjects.slice(0, limit);\n\n if (!includeFiles) {\n return { projects: projects.map(({ id, name }) => ({ id, name })) };\n }\n\n const projectsWithFiles = await Promise.all(\n projects.map(async ({ id, name }) => {\n try {\n const { files } = await getProjectFiles(id);\n const recentFiles = files.slice(0, filesPerProject).map((file) => ({\n key: file.key,\n name: file.name,\n thumbnailUrl: file.thumbnail_url,\n lastModified: file.last_modified,\n url: `https://www.figma.com/file/${file.key}`,\n }));\n\n return { id, name, fileCount: files.length, recentFiles };\n } catch (error) {\n return {\n id,\n name,\n fileCount: 0,\n recentFiles: [],\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n }),\n );\n\n return { projects: projectsWithFiles, totalProjects: projects.length };\n },\n});\n",
488
488
  "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport {\n extractComponents,\n extractStyles,\n getFile,\n getFileSummary,\n} from \"../../lib/figma-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description:\n \"Get detailed information about a Figma file including components, styles, and structure. Returns file metadata, component list, and style information.\",\n inputSchema: z.object({\n fileKey: z.string().describe(\"The file key (from the Figma URL)\"),\n includeComponents: z\n .boolean()\n .default(true)\n .describe(\"Include component information\"),\n includeStyles: z.boolean().default(true).describe(\"Include style information\"),\n depth: z\n .number()\n .min(1)\n .max(10)\n .optional()\n .describe(\"Depth of nodes to traverse (default: all)\"),\n }),\n async execute({ fileKey, includeComponents, includeStyles, depth }) {\n const file = await getFile(fileKey, { depth });\n\n return {\n summary: getFileSummary(file),\n url: `https://www.figma.com/file/${fileKey}`,\n thumbnailUrl: file.thumbnailUrl,\n pages: file.document.children?.map((page) => ({\n id: page.id,\n name: page.name,\n type: page.type,\n })) ?? [],\n ...(includeComponents ? { components: extractComponents(file) } : {}),\n ...(includeStyles ? { styles: extractStyles(file) } : {}),\n };\n },\n});\n",
@@ -495,7 +495,7 @@ export default {
495
495
  ".env.example": "# Pipedrive OAuth Configuration\n# Get your credentials from https://developers.pipedrive.com/docs/marketplace\nPIPEDRIVE_CLIENT_ID=your-client-id\nPIPEDRIVE_CLIENT_SECRET=your-client-secret\n",
496
496
  "lib/pipedrive-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst PIPEDRIVE_BASE_URL = \"https://api.pipedrive.com/v1\";\n\ninterface PipedriveResponse<T> {\n success: boolean;\n data: T;\n additional_data?: {\n pagination?: {\n start: number;\n limit: number;\n more_items_in_collection: boolean;\n next_start?: number;\n };\n };\n}\n\ninterface PipedriveDeal {\n id: number;\n title: string;\n value: number;\n currency: string;\n status: string;\n stage_id: number;\n person_id: number | null;\n person_name: string | null;\n org_id: number | null;\n org_name: string | null;\n owner_name: string;\n expected_close_date: string | null;\n add_time: string;\n update_time: string;\n won_time: string | null;\n lost_time: string | null;\n close_time: string | null;\n}\n\ninterface PipedrivePerson {\n id: number;\n name: string;\n first_name: string;\n last_name: string;\n email: Array<{ value: string; primary: boolean }>;\n phone: Array<{ value: string; primary: boolean }>;\n org_id: number | null;\n org_name: string | null;\n owner_id: number;\n owner_name: string;\n add_time: string;\n update_time: string;\n}\n\ninterface PipedriveStage {\n id: number;\n name: string;\n order_nr: number;\n pipeline_id: number;\n pipeline_name: string;\n}\n\nasync function pipedriveFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Pipedrive. Please connect your account.\");\n }\n\n const url = new URL(`${PIPEDRIVE_BASE_URL}${endpoint}`);\n // SECURITY: Pipedrive's v1 API requires the token as a query parameter.\n // Tokens in query params may be recorded in browser history, server/proxy\n // access logs, and leaked via the Referer header. The Referrer-Policy\n // header (set by Veryfront's security middleware) mitigates the Referer leak.\n // This is an API design limitation — there is no Authorization header alternative.\n url.searchParams.set(\"api_token\", token);\n\n const response = await fetch(url.toString(), {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({} as { error?: string }));\n throw new Error(`Pipedrive API error: ${response.status} ${error.error ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nfunction buildEndpoint(path: string, params: URLSearchParams): string {\n const queryString = params.toString();\n return queryString ? `${path}?${queryString}` : path;\n}\n\nexport async function listDeals(options?: {\n status?: \"open\" | \"won\" | \"lost\" | \"all\";\n ownerId?: number;\n stageId?: number;\n limit?: number;\n}): Promise<PipedriveDeal[]> {\n const params = new URLSearchParams();\n\n if (options?.status) params.set(\"status\", options.status);\n if (options?.ownerId) params.set(\"user_id\", options.ownerId.toString());\n if (options?.stageId) params.set(\"stage_id\", options.stageId.toString());\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal[]>>(\n buildEndpoint(\"/deals\", params),\n );\n\n return response.data || [];\n}\n\nexport async function getDeal(dealId: number): Promise<PipedriveDeal> {\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(`/deals/${dealId}`);\n return response.data;\n}\n\nexport async function createDeal(options: {\n title: string;\n value?: number;\n currency?: string;\n personId?: number;\n orgId?: number;\n stageId?: number;\n expectedCloseDate?: string;\n}): Promise<PipedriveDeal> {\n const body: Record<string, unknown> = { title: options.title };\n\n if (options.value !== undefined) body.value = options.value;\n if (options.currency) body.currency = options.currency;\n if (options.personId) body.person_id = options.personId;\n if (options.orgId) body.org_id = options.orgId;\n if (options.stageId) body.stage_id = options.stageId;\n if (options.expectedCloseDate) body.expected_close_date = options.expectedCloseDate;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(\"/deals\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function updateDeal(\n dealId: number,\n updates: {\n title?: string;\n value?: number;\n status?: string;\n stageId?: number;\n personId?: number;\n orgId?: number;\n expectedCloseDate?: string;\n },\n): Promise<PipedriveDeal> {\n const body: Record<string, unknown> = {};\n\n if (updates.title !== undefined) body.title = updates.title;\n if (updates.value !== undefined) body.value = updates.value;\n if (updates.status !== undefined) body.status = updates.status;\n if (updates.stageId !== undefined) body.stage_id = updates.stageId;\n if (updates.personId !== undefined) body.person_id = updates.personId;\n if (updates.orgId !== undefined) body.org_id = updates.orgId;\n if (updates.expectedCloseDate !== undefined) body.expected_close_date = updates.expectedCloseDate;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(`/deals/${dealId}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function listPersons(options?: {\n searchTerm?: string;\n limit?: number;\n}): Promise<PipedrivePerson[]> {\n const params = new URLSearchParams();\n\n if (options?.searchTerm) params.set(\"term\", options.searchTerm);\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson[]>>(\n buildEndpoint(\"/persons\", params),\n );\n\n return response.data || [];\n}\n\nexport async function getPerson(personId: number): Promise<PipedrivePerson> {\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson>>(`/persons/${personId}`);\n return response.data;\n}\n\nexport async function createPerson(options: {\n name: string;\n email?: string;\n phone?: string;\n orgId?: number;\n}): Promise<PipedrivePerson> {\n const body: Record<string, unknown> = { name: options.name };\n\n if (options.email) body.email = [{ value: options.email, primary: true }];\n if (options.phone) body.phone = [{ value: options.phone, primary: true }];\n if (options.orgId) body.org_id = options.orgId;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson>>(\"/persons\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function listStages(): Promise<PipedriveStage[]> {\n const response = await pipedriveFetch<PipedriveResponse<PipedriveStage[]>>(\"/stages\");\n return response.data || [];\n}\n\nexport async function getCurrentUser(): Promise<{ id: number; name: string; email: string }> {\n const response = await pipedriveFetch<PipedriveResponse<{ id: number; name: string; email: string }>>(\n \"/users/me\",\n );\n return response.data;\n}\n",
497
497
  "app/api/auth/pipedrive/route.ts": "import { createOAuthInitHandler, pipedriveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(pipedriveConfig, { tokenStore: oauthMemoryTokenStore });\n",
498
- "app/api/auth/pipedrive/callback/route.ts": "import { createOAuthCallbackHandler, pipedriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(pipedriveConfig, { tokenStore: hybridTokenStore });\n",
498
+ "app/api/auth/pipedrive/callback/route.ts": "import { createOAuthCallbackHandler, pipedriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(pipedriveConfig, { tokenStore: hybridTokenStore });\n",
499
499
  "tools/get-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getDeal } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"get-deal\",\n description: \"Get detailed information about a specific deal in Pipedrive by its ID.\",\n inputSchema: z.object({\n dealId: z.number().describe(\"The ID of the deal to retrieve\"),\n }),\n async execute({ dealId }) {\n const deal = await getDeal(dealId);\n\n return {\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n status: deal.status,\n stageId: deal.stage_id,\n personId: deal.person_id,\n personName: deal.person_name,\n orgId: deal.org_id,\n orgName: deal.org_name,\n ownerName: deal.owner_name,\n expectedCloseDate: deal.expected_close_date,\n addTime: deal.add_time,\n updateTime: deal.update_time,\n wonTime: deal.won_time,\n lostTime: deal.lost_time,\n closeTime: deal.close_time,\n };\n },\n});\n",
500
500
  "tools/list-deals.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listDeals } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"list-deals\",\n description:\n \"List deals from Pipedrive. Can filter by status, owner, or stage to get specific deals in the sales pipeline.\",\n inputSchema: z.object({\n status: z\n .enum([\"open\", \"won\", \"lost\", \"all\"])\n .default(\"open\")\n .describe(\"Filter deals by status\"),\n ownerId: z.number().optional().describe(\"Filter deals by owner user ID\"),\n stageId: z.number().optional().describe(\"Filter deals by stage ID\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of deals to return\"),\n }),\n async execute({ status, ownerId, stageId, limit }) {\n const deals = await listDeals({ status, ownerId, stageId, limit });\n\n return deals.map((deal) => ({\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n status: deal.status,\n stageId: deal.stage_id,\n personName: deal.person_name,\n orgName: deal.org_name,\n ownerName: deal.owner_name,\n expectedCloseDate: deal.expected_close_date,\n addTime: deal.add_time,\n updateTime: deal.update_time,\n }));\n },\n});\n",
501
501
  "tools/create-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDeal } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"create-deal\",\n description: \"Create a new deal in the Pipedrive sales pipeline.\",\n inputSchema: z.object({\n title: z.string().describe(\"The title/name of the deal\"),\n value: z.number().optional().describe(\"The monetary value of the deal\"),\n currency: z\n .string()\n .default(\"USD\")\n .describe(\"Currency code (e.g., USD, EUR, GBP)\"),\n personId: z\n .number()\n .optional()\n .describe(\"ID of the person/contact associated with the deal\"),\n orgId: z\n .number()\n .optional()\n .describe(\"ID of the organization associated with the deal\"),\n stageId: z.number().optional().describe(\"ID of the pipeline stage for the deal\"),\n expectedCloseDate: z\n .string()\n .optional()\n .describe(\"Expected close date in YYYY-MM-DD format\"),\n }),\n async execute(input) {\n const deal = await createDeal(input);\n\n return {\n success: true,\n deal: {\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n stageId: deal.stage_id,\n personName: deal.person_name,\n orgName: deal.org_name,\n expectedCloseDate: deal.expected_close_date,\n },\n };\n },\n});\n",
@@ -508,7 +508,7 @@ export default {
508
508
  ".env.example": "# Asana OAuth Configuration\n# Get your credentials from https://app.asana.com/0/developer-console\nASANA_CLIENT_ID=your-client-id\nASANA_CLIENT_SECRET=your-client-secret\n",
509
509
  "lib/asana-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst ASANA_BASE_URL = \"https://app.asana.com/api/1.0\";\n\ninterface AsanaResponse<T> {\n data: T;\n next_page?: { offset: string } | null;\n}\n\ninterface AsanaTask {\n gid: string;\n name: string;\n notes: string;\n completed: boolean;\n due_on: string | null;\n assignee: { gid: string; name: string } | null;\n projects: Array<{ gid: string; name: string }>;\n created_at: string;\n modified_at: string;\n}\n\ninterface AsanaProject {\n gid: string;\n name: string;\n notes: string;\n workspace: { gid: string; name: string };\n created_at: string;\n modified_at: string;\n}\n\ninterface AsanaWorkspace {\n gid: string;\n name: string;\n}\n\nasync function asanaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Asana. Please connect your account.\");\n }\n\n const response = await fetch(`${ASANA_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) {\n return response.json();\n }\n\n let error: unknown = {};\n try {\n error = await response.json();\n } catch {\n // ignore JSON parse errors\n }\n\n const message =\n (error as { errors?: Array<{ message?: string }> })?.errors?.[0]?.message ?? response.statusText;\n\n throw new Error(`Asana API error: ${response.status} ${message}`);\n}\n\nexport async function listWorkspaces(): Promise<AsanaWorkspace[]> {\n const { data } = await asanaFetch<AsanaResponse<AsanaWorkspace[]>>(\"/workspaces\");\n return data;\n}\n\nexport async function listProjects(workspaceGid: string): Promise<AsanaProject[]> {\n const { data } = await asanaFetch<AsanaResponse<AsanaProject[]>>(\n `/workspaces/${workspaceGid}/projects?opt_fields=name,notes,created_at,modified_at`,\n );\n return data;\n}\n\nexport async function listTasks(options: {\n projectGid?: string;\n assigneeGid?: string;\n workspaceGid?: string;\n completedSince?: string;\n}): Promise<AsanaTask[]> {\n const params = new URLSearchParams({\n opt_fields: \"name,notes,completed,due_on,assignee.name,projects.name,created_at,modified_at\",\n });\n\n if (options.completedSince) {\n params.set(\"completed_since\", options.completedSince);\n }\n\n let endpoint = \"/tasks\";\n if (options.projectGid) {\n endpoint = `/projects/${options.projectGid}/tasks`;\n } else if (options.assigneeGid && options.workspaceGid) {\n params.set(\"assignee\", options.assigneeGid);\n params.set(\"workspace\", options.workspaceGid);\n }\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask[]>>(`${endpoint}?${params}`);\n return data;\n}\n\nexport async function getTask(taskGid: string): Promise<AsanaTask> {\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(\n `/tasks/${taskGid}?opt_fields=name,notes,completed,due_on,assignee.name,projects.name,created_at,modified_at`,\n );\n return data;\n}\n\nexport async function createTask(options: {\n projectGid: string;\n name: string;\n notes?: string;\n dueOn?: string;\n assigneeGid?: string;\n}): Promise<AsanaTask> {\n const body: Record<string, unknown> = {\n name: options.name,\n projects: [options.projectGid],\n };\n\n if (options.notes) body.notes = options.notes;\n if (options.dueOn) body.due_on = options.dueOn;\n if (options.assigneeGid) body.assignee = options.assigneeGid;\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(\"/tasks\", {\n method: \"POST\",\n body: JSON.stringify({ data: body }),\n });\n\n return data;\n}\n\nexport async function updateTask(\n taskGid: string,\n updates: {\n name?: string;\n notes?: string;\n completed?: boolean;\n dueOn?: string;\n assigneeGid?: string;\n },\n): Promise<AsanaTask> {\n const body: Record<string, unknown> = {};\n\n if (updates.name !== undefined) body.name = updates.name;\n if (updates.notes !== undefined) body.notes = updates.notes;\n if (updates.completed !== undefined) body.completed = updates.completed;\n if (updates.dueOn !== undefined) body.due_on = updates.dueOn;\n if (updates.assigneeGid !== undefined) body.assignee = updates.assigneeGid;\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(`/tasks/${taskGid}`, {\n method: \"PUT\",\n body: JSON.stringify({ data: body }),\n });\n\n return data;\n}\n\nexport async function getMe(): Promise<{ gid: string; name: string; email: string }> {\n const { data } = await asanaFetch<AsanaResponse<{ gid: string; name: string; email: string }>>(\n \"/users/me\",\n );\n return data;\n}\n",
510
510
  "app/api/auth/asana/route.ts": "import { asanaConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(asanaConfig, { tokenStore: oauthMemoryTokenStore });\n",
511
- "app/api/auth/asana/callback/route.ts": "import { asanaConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(asanaConfig, { tokenStore: hybridTokenStore });\n",
511
+ "app/api/auth/asana/callback/route.ts": "import { asanaConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(asanaConfig, { tokenStore: hybridTokenStore });\n",
512
512
  "tools/get-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTask } from \"../../lib/asana-client.ts\";\n\nexport default tool({\n id: \"get-task\",\n description: \"Get details of a specific Asana task by its GID.\",\n inputSchema: z.object({\n taskGid: z.string().describe(\"The GID of the task to retrieve\"),\n }),\n async execute({ taskGid }) {\n const task = await getTask(taskGid);\n\n return {\n gid: task.gid,\n name: task.name,\n notes: task.notes,\n completed: task.completed,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n projects: task.projects.map(({ gid, name }) => ({ gid, name })),\n createdAt: task.created_at,\n modifiedAt: task.modified_at,\n };\n },\n});\n",
513
513
  "tools/update-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateTask } from \"../../lib/asana-client.ts\";\n\nexport default tool({\n id: \"update-task\",\n description: \"Update an existing Asana task.\",\n inputSchema: z.object({\n taskGid: z.string().describe(\"The GID of the task to update\"),\n name: z.string().optional().describe(\"New name/title for the task\"),\n notes: z.string().optional().describe(\"New description or notes\"),\n completed: z.boolean().optional().describe(\"Mark the task as completed or not\"),\n dueOn: z.string().optional().describe(\"New due date in YYYY-MM-DD format\"),\n assigneeGid: z.string().optional().describe(\"GID of the user to reassign the task to\"),\n }),\n async execute({ taskGid, ...updates }) {\n const task = await updateTask(taskGid, updates);\n\n return {\n success: true,\n task: {\n gid: task.gid,\n name: task.name,\n completed: task.completed,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n },\n };\n },\n});\n",
514
514
  "tools/create-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTask } from \"../../lib/asana-client.ts\";\n\nexport default tool({\n id: \"create-task\",\n description: \"Create a new task in an Asana project.\",\n inputSchema: z.object({\n projectGid: z.string().describe(\"The GID of the project to create the task in\"),\n name: z.string().describe(\"The name/title of the task\"),\n notes: z.string().optional().describe(\"Description or notes for the task\"),\n dueOn: z.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n assigneeGid: z.string().optional().describe(\"GID of the user to assign the task to\"),\n }),\n async execute({ projectGid, name, notes, dueOn, assigneeGid }) {\n const task = await createTask({\n projectGid,\n name,\n notes,\n dueOn,\n assigneeGid,\n });\n\n return {\n success: true,\n task: {\n gid: task.gid,\n name: task.name,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n },\n };\n },\n});\n",
@@ -521,7 +521,7 @@ export default {
521
521
  ".env.example": "# Trello OAuth Configuration\n# Get your credentials from https://trello.com/app-key\nTRELLO_CLIENT_ID=your-api-key\nTRELLO_CLIENT_SECRET=your-oauth-secret\n",
522
522
  "lib/trello-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst TRELLO_BASE_URL = \"https://api.trello.com/1\";\n\ninterface TrelloBoard {\n id: string;\n name: string;\n desc: string;\n closed: boolean;\n url: string;\n prefs: {\n background: string;\n backgroundColor: string;\n };\n dateLastActivity: string;\n}\n\ninterface TrelloList {\n id: string;\n name: string;\n closed: boolean;\n idBoard: string;\n pos: number;\n}\n\ninterface TrelloCard {\n id: string;\n name: string;\n desc: string;\n closed: boolean;\n idBoard: string;\n idList: string;\n idMembers: string[];\n labels: Array<{\n id: string;\n name: string;\n color: string;\n }>;\n due: string | null;\n dueComplete: boolean;\n url: string;\n dateLastActivity: string;\n}\n\ninterface TrelloMember {\n id: string;\n fullName: string;\n username: string;\n avatarUrl: string;\n}\n\nasync function trelloFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Trello. Please connect your account.\");\n }\n\n const clientId = process.env.TRELLO_CLIENT_ID;\n if (!clientId) {\n throw new Error(\"TRELLO_CLIENT_ID environment variable is not set.\");\n }\n\n const url = new URL(`${TRELLO_BASE_URL}${endpoint}`);\n // SECURITY: Trello's REST API requires key and token as query parameters.\n // Tokens in query params may be recorded in browser history, server/proxy\n // access logs, and leaked via the Referer header. The Referrer-Policy\n // header (set by Veryfront's security middleware) mitigates the Referer leak.\n // This is an API design limitation — there is no Authorization header alternative.\n url.searchParams.set(\"key\", clientId);\n url.searchParams.set(\"token\", token);\n\n const response = await fetch(url.toString(), {\n ...options,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n const message = errorText || response.statusText;\n throw new Error(`Trello API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listBoards(): Promise<TrelloBoard[]> {\n return trelloFetch<TrelloBoard[]>(\n \"/members/me/boards?fields=name,desc,closed,url,prefs,dateLastActivity\",\n );\n}\n\nexport async function getBoard(boardId: string): Promise<TrelloBoard> {\n return trelloFetch<TrelloBoard>(\n `/boards/${boardId}?fields=name,desc,closed,url,prefs,dateLastActivity`,\n );\n}\n\nexport async function listLists(boardId: string): Promise<TrelloList[]> {\n return trelloFetch<TrelloList[]>(\n `/boards/${boardId}/lists?fields=name,closed,idBoard,pos`,\n );\n}\n\nexport async function listCards(options: {\n boardId?: string;\n listId?: string;\n limit?: number;\n}): Promise<TrelloCard[]> {\n const { boardId, listId, limit = 50 } = options;\n\n const fields =\n \"name,desc,closed,idBoard,idList,idMembers,labels,due,dueComplete,url,dateLastActivity\";\n\n if (listId) {\n return trelloFetch<TrelloCard[]>(\n `/lists/${listId}/cards?fields=${fields}&limit=${limit}`,\n );\n }\n\n if (boardId) {\n return trelloFetch<TrelloCard[]>(\n `/boards/${boardId}/cards?fields=${fields}&limit=${limit}`,\n );\n }\n\n throw new Error(\"Either boardId or listId must be provided\");\n}\n\nexport async function getCard(cardId: string): Promise<TrelloCard> {\n return trelloFetch<TrelloCard>(\n \"/cards/\" +\n `${cardId}?fields=name,desc,closed,idBoard,idList,idMembers,labels,due,dueComplete,url,dateLastActivity`,\n );\n}\n\nexport async function createCard(options: {\n listId: string;\n name: string;\n desc?: string;\n due?: string;\n pos?: string | number;\n idMembers?: string[];\n idLabels?: string[];\n}): Promise<TrelloCard> {\n const params = new URLSearchParams({\n idList: options.listId,\n name: options.name,\n });\n\n if (options.desc) params.set(\"desc\", options.desc);\n if (options.due) params.set(\"due\", options.due);\n if (options.pos !== undefined) params.set(\"pos\", String(options.pos));\n if (options.idMembers) params.set(\"idMembers\", options.idMembers.join(\",\"));\n if (options.idLabels) params.set(\"idLabels\", options.idLabels.join(\",\"));\n\n return trelloFetch<TrelloCard>(`/cards?${params}`, { method: \"POST\" });\n}\n\nexport async function updateCard(\n cardId: string,\n updates: {\n name?: string;\n desc?: string;\n closed?: boolean;\n idList?: string;\n due?: string | null;\n dueComplete?: boolean;\n idMembers?: string[];\n idLabels?: string[];\n pos?: string | number;\n },\n): Promise<TrelloCard> {\n const params = new URLSearchParams();\n\n if (updates.name !== undefined) params.set(\"name\", updates.name);\n if (updates.desc !== undefined) params.set(\"desc\", updates.desc);\n if (updates.closed !== undefined) params.set(\"closed\", String(updates.closed));\n if (updates.idList !== undefined) params.set(\"idList\", updates.idList);\n if (updates.due !== undefined) params.set(\"due\", updates.due ?? \"\");\n if (updates.dueComplete !== undefined) params.set(\"dueComplete\", String(updates.dueComplete));\n if (updates.idMembers !== undefined) params.set(\"idMembers\", updates.idMembers.join(\",\"));\n if (updates.idLabels !== undefined) params.set(\"idLabels\", updates.idLabels.join(\",\"));\n if (updates.pos !== undefined) params.set(\"pos\", String(updates.pos));\n\n return trelloFetch<TrelloCard>(`/cards/${cardId}?${params}`, { method: \"PUT\" });\n}\n\nexport async function getMe(): Promise<TrelloMember> {\n return trelloFetch<TrelloMember>(\"/members/me?fields=fullName,username,avatarUrl\");\n}\n",
523
523
  "app/api/auth/trello/route.ts": "import { createOAuthInitHandler, trelloConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(trelloConfig, { tokenStore: oauthMemoryTokenStore });\n",
524
- "app/api/auth/trello/callback/route.ts": "import { createOAuthCallbackHandler, trelloConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(trelloConfig, { tokenStore: hybridTokenStore });\n",
524
+ "app/api/auth/trello/callback/route.ts": "import { createOAuthCallbackHandler, trelloConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(trelloConfig, { tokenStore: hybridTokenStore });\n",
525
525
  "tools/list-cards.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listCards } from \"../../lib/trello-client.ts\";\n\nexport default tool({\n id: \"list-cards\",\n description:\n \"List cards from Trello. Can filter by board or list. Provide either boardId or listId.\",\n inputSchema: z.object({\n boardId: z.string().optional().describe(\"Board ID to list cards from\"),\n listId: z.string().optional().describe(\"List ID to list cards from\"),\n includeArchived: z\n .boolean()\n .default(false)\n .describe(\"Include archived/closed cards\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of cards to return\"),\n }),\n async execute({ boardId, listId, includeArchived, limit }) {\n if (!boardId && !listId) {\n return { cards: [], message: \"Please specify either a boardId or listId\" };\n }\n\n const cards = await listCards({ boardId, listId, limit });\n const visibleCards = includeArchived\n ? cards\n : cards.filter((card) => !card.closed);\n\n return visibleCards.map(\n ({\n id,\n name,\n desc,\n url,\n closed,\n idList,\n idBoard,\n due,\n dueComplete,\n labels,\n idMembers,\n dateLastActivity,\n }) => ({\n id,\n name,\n desc,\n url,\n closed,\n idList,\n idBoard,\n due,\n dueComplete,\n labels: labels.map(({ id, name, color }) => ({ id, name, color })),\n memberIds: idMembers,\n lastActivity: dateLastActivity,\n }),\n );\n },\n});\n",
526
526
  "tools/list-boards.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listBoards } from \"../../lib/trello-client.ts\";\n\nexport default tool({\n id: \"list-boards\",\n description: \"List all Trello boards accessible to the current user.\",\n inputSchema: z.object({\n includeArchived: z\n .boolean()\n .default(false)\n .describe(\"Include archived/closed boards\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of boards to return\"),\n }),\n async execute({ includeArchived, limit }) {\n const boards = await listBoards();\n\n const visibleBoards = includeArchived\n ? boards\n : boards.filter((board) => !board.closed);\n\n return visibleBoards.slice(0, limit).map((board) => ({\n id: board.id,\n name: board.name,\n desc: board.desc,\n url: board.url,\n closed: board.closed,\n backgroundColor: board.prefs?.backgroundColor,\n lastActivity: board.dateLastActivity,\n }));\n },\n});\n",
527
527
  "tools/get-card.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getCard } from \"../../lib/trello-client.ts\";\n\nexport default tool({\n id: \"get-card\",\n description: \"Get details of a specific Trello card by its ID.\",\n inputSchema: z.object({\n cardId: z.string().describe(\"The ID of the card to retrieve\"),\n }),\n async execute({ cardId }) {\n const card = await getCard(cardId);\n\n return {\n id: card.id,\n name: card.name,\n desc: card.desc,\n url: card.url,\n closed: card.closed,\n idList: card.idList,\n idBoard: card.idBoard,\n due: card.due,\n dueComplete: card.dueComplete,\n labels: card.labels.map(({ id, name, color }) => ({ id, name, color })),\n memberIds: card.idMembers,\n lastActivity: card.dateLastActivity,\n };\n },\n});\n",
@@ -534,7 +534,7 @@ export default {
534
534
  ".env.example": "# Microsoft Outlook Integration\n# Get these from: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\n\n# Your Microsoft Azure App Client ID (Application ID)\nMICROSOFT_CLIENT_ID=your_client_id_here\n\n# Your Microsoft Azure App Client Secret\nMICROSOFT_CLIENT_SECRET=your_client_secret_here\n",
535
535
  "lib/outlook-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_BASE_URL = \"https://graph.microsoft.com/v1.0\";\n\ninterface GraphResponse<T> {\n value?: T[];\n \"@odata.nextLink\"?: string;\n}\n\nexport interface OutlookMessage {\n id: string;\n subject: string;\n bodyPreview: string;\n body: {\n contentType: \"text\" | \"html\";\n content: string;\n };\n from: {\n emailAddress: {\n name: string;\n address: string;\n };\n };\n toRecipients: Array<{\n emailAddress: {\n name: string;\n address: string;\n };\n }>;\n ccRecipients?: Array<{\n emailAddress: {\n name: string;\n address: string;\n };\n }>;\n receivedDateTime: string;\n sentDateTime: string;\n isRead: boolean;\n hasAttachments: boolean;\n importance: \"low\" | \"normal\" | \"high\";\n conversationId: string;\n webLink: string;\n}\n\nexport interface OutlookFolder {\n id: string;\n displayName: string;\n parentFolderId: string;\n childFolderCount: number;\n unreadItemCount: number;\n totalItemCount: number;\n}\n\nexport interface SendEmailOptions {\n to: string[];\n subject: string;\n body: string;\n cc?: string[];\n bcc?: string[];\n importance?: \"low\" | \"normal\" | \"high\";\n bodyType?: \"text\" | \"html\";\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Microsoft. Please connect your account.\");\n }\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function listEmails(options?: {\n folderId?: string;\n top?: number;\n skip?: number;\n filter?: string;\n orderBy?: string;\n}): Promise<OutlookMessage[]> {\n const params = new URLSearchParams();\n\n if (options?.top != null) params.set(\"$top\", options.top.toString());\n if (options?.skip != null) params.set(\"$skip\", options.skip.toString());\n if (options?.filter) params.set(\"$filter\", options.filter);\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n\n const folderPath = options?.folderId\n ? `/mailFolders/${options.folderId}/messages`\n : \"/messages\";\n\n const queryString = params.toString();\n const endpoint = queryString ? `${folderPath}?${queryString}` : folderPath;\n\n const response = await graphFetch<GraphResponse<OutlookMessage>>(endpoint);\n return response.value ?? [];\n}\n\nexport function getEmail(messageId: string): Promise<OutlookMessage> {\n return graphFetch<OutlookMessage>(`/messages/${messageId}`);\n}\n\nexport async function sendEmail(options: SendEmailOptions): Promise<void> {\n const message = {\n subject: options.subject,\n body: {\n contentType: options.bodyType ?? \"text\",\n content: options.body,\n },\n toRecipients: options.to.map((email) => ({\n emailAddress: { address: email },\n })),\n ccRecipients: options.cc?.map((email) => ({\n emailAddress: { address: email },\n })),\n bccRecipients: options.bcc?.map((email) => ({\n emailAddress: { address: email },\n })),\n importance: options.importance ?? \"normal\",\n };\n\n await graphFetch(\"/sendMail\", {\n method: \"POST\",\n body: JSON.stringify({ message }),\n });\n}\n\nexport async function searchEmails(options: {\n query: string;\n top?: number;\n skip?: number;\n}): Promise<OutlookMessage[]> {\n const params = new URLSearchParams({ $search: `\"${options.query}\"` });\n\n if (options.top != null) params.set(\"$top\", options.top.toString());\n if (options.skip != null) params.set(\"$skip\", options.skip.toString());\n\n const response = await graphFetch<GraphResponse<OutlookMessage>>(`/messages?${params.toString()}`);\n return response.value ?? [];\n}\n\nexport async function listFolders(): Promise<OutlookFolder[]> {\n const response = await graphFetch<GraphResponse<OutlookFolder>>(\"/mailFolders\");\n return response.value ?? [];\n}\n\nasync function setReadState(messageId: string, isRead: boolean): Promise<void> {\n await graphFetch(`/messages/${messageId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ isRead }),\n });\n}\n\nexport async function markAsRead(messageId: string): Promise<void> {\n await setReadState(messageId, true);\n}\n\nexport async function markAsUnread(messageId: string): Promise<void> {\n await setReadState(messageId, false);\n}\n\nexport async function deleteEmail(messageId: string): Promise<void> {\n await graphFetch(`/messages/${messageId}`, { method: \"DELETE\" });\n}\n\nexport async function moveEmail(messageId: string, destinationFolderId: string): Promise<void> {\n await graphFetch(`/messages/${messageId}/move`, {\n method: \"POST\",\n body: JSON.stringify({ destinationId: destinationFolderId }),\n });\n}\n\nexport function formatEmail(message: OutlookMessage): string {\n const from = message.from.emailAddress.name || message.from.emailAddress.address;\n const to = message.toRecipients.map((r) => r.emailAddress.address).join(\", \");\n const date = new Date(message.receivedDateTime).toLocaleString();\n const read = message.isRead ? \"Yes\" : \"No\";\n\n return `From: ${from}\nTo: ${to}\nSubject: ${message.subject}\nDate: ${date}\nRead: ${read}\n\n${message.bodyPreview}`;\n}\n",
536
536
  "app/api/auth/outlook/route.ts": "import { createOAuthInitHandler, outlookConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(outlookConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
537
- "app/api/auth/outlook/callback/route.ts": "import { createOAuthCallbackHandler, outlookConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(outlookConfig, { tokenStore: hybridTokenStore });\n",
537
+ "app/api/auth/outlook/callback/route.ts": "import { createOAuthCallbackHandler, outlookConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(outlookConfig, { tokenStore: hybridTokenStore });\n",
538
538
  "tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getEmail } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"get-email\",\n description:\n \"Get detailed information about a specific email, including full body content, recipients, and metadata.\",\n inputSchema: z.object({\n messageId: z.string().describe(\"The ID of the email message to retrieve\"),\n includeBody: z\n .boolean()\n .default(true)\n .describe(\"Include full email body content\"),\n }),\n async execute({ messageId, includeBody }) {\n const message = await getEmail(messageId);\n\n const body = includeBody\n ? {\n contentType: message.body.contentType,\n content: message.body.content,\n }\n : undefined;\n\n return {\n id: message.id,\n subject: message.subject,\n from: {\n name: message.from.emailAddress.name,\n email: message.from.emailAddress.address,\n },\n to: message.toRecipients.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\n cc: message.ccRecipients?.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\n body,\n bodyPreview: message.bodyPreview,\n receivedAt: message.receivedDateTime,\n sentAt: message.sentDateTime,\n isRead: message.isRead,\n hasAttachments: message.hasAttachments,\n importance: message.importance,\n conversationId: message.conversationId,\n webLink: message.webLink,\n };\n },\n});\n",
539
539
  "tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { sendEmail } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"send-email\",\n description:\n \"Send a new email message. Supports multiple recipients, CC, BCC, and importance levels.\",\n inputSchema: z.object({\n to: z.array(z.string().email()).min(1).describe(\"Email addresses of recipients\"),\n subject: z.string().min(1).describe(\"Email subject line\"),\n body: z.string().min(1).describe(\"Email body content\"),\n cc: z.array(z.string().email()).optional().describe(\"Email addresses to CC\"),\n bcc: z.array(z.string().email()).optional().describe(\"Email addresses to BCC\"),\n importance: z\n .enum([\"low\", \"normal\", \"high\"])\n .default(\"normal\")\n .describe(\"Email importance level\"),\n bodyType: z\n .enum([\"text\", \"html\"])\n .default(\"text\")\n .describe(\"Body content type (text or html)\"),\n }),\n async execute({ to, subject, body, cc, bcc, importance, bodyType }) {\n await sendEmail({ to, subject, body, cc, bcc, importance, bodyType });\n\n return {\n success: true,\n message: `Email sent successfully to ${to.join(\", \")}`,\n recipients: { to, cc, bcc },\n };\n },\n});\n",
540
540
  "tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { searchEmails } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"search-emails\",\n description:\n \"Search emails by query string. Searches across subject, body, sender, and recipients. Supports advanced search syntax.\",\n inputSchema: z.object({\n query: z\n .string()\n .min(1)\n .describe(\"Search query (searches subject, body, from, to fields)\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }),\n async execute({ query, limit }) {\n const messages = await searchEmails({ query, top: limit });\n\n return {\n totalResults: messages.length,\n emails: messages.map((msg) => ({\n id: msg.id,\n subject: msg.subject,\n from: {\n name: msg.from.emailAddress.name,\n email: msg.from.emailAddress.address,\n },\n to: msg.toRecipients.map((r) => ({\n name: r.emailAddress.name,\n email: r.emailAddress.address,\n })),\n preview: msg.bodyPreview,\n receivedAt: msg.receivedDateTime,\n isRead: msg.isRead,\n hasAttachments: msg.hasAttachments,\n importance: msg.importance,\n webLink: msg.webLink,\n })),\n };\n },\n});\n",
@@ -558,7 +558,7 @@ export default {
558
558
  "files": {
559
559
  "lib/salesforce-client.ts": "import { getAccessToken, getInstanceUrl } from \"./token-store.ts\";\n\nconst API_VERSION = \"v59.0\";\n\ninterface SalesforceQueryResponse<T> {\n totalSize: number;\n done: boolean;\n records: T[];\n nextRecordsUrl?: string;\n}\n\ninterface SalesforceAccount {\n Id: string;\n Name: string;\n Type?: string;\n Industry?: string;\n Website?: string;\n Phone?: string;\n BillingStreet?: string;\n BillingCity?: string;\n BillingState?: string;\n BillingPostalCode?: string;\n BillingCountry?: string;\n NumberOfEmployees?: number;\n AnnualRevenue?: number;\n Description?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceContact {\n Id: string;\n FirstName?: string;\n LastName: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Department?: string;\n AccountId?: string;\n MailingStreet?: string;\n MailingCity?: string;\n MailingState?: string;\n MailingPostalCode?: string;\n MailingCountry?: string;\n Description?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceOpportunity {\n Id: string;\n Name: string;\n AccountId?: string;\n Amount?: number;\n StageName: string;\n Probability?: number;\n CloseDate: string;\n Type?: string;\n LeadSource?: string;\n Description?: string;\n NextStep?: string;\n IsClosed: boolean;\n IsWon: boolean;\n ForecastCategory?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceLead {\n Id: string;\n FirstName?: string;\n LastName: string;\n Company: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Status: string;\n LeadSource?: string;\n Industry?: string;\n Street?: string;\n City?: string;\n State?: string;\n PostalCode?: string;\n Country?: string;\n Website?: string;\n Description?: string;\n Rating?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\nasync function salesforceFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Salesforce. Please connect your account.\");\n }\n\n const instanceUrl = getInstanceUrl();\n if (!instanceUrl) {\n throw new Error(\"Salesforce instance URL not found. Please reconnect your account.\");\n }\n\n const url = endpoint.startsWith(\"http\")\n ? endpoint\n : `${instanceUrl}/services/data/${API_VERSION}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const message = error?.[0]?.message ?? error?.message ?? response.statusText;\n throw new Error(`Salesforce API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport function query<T = any>(soql: string): Promise<SalesforceQueryResponse<T>> {\n return salesforceFetch<SalesforceQueryResponse<T>>(`/query?q=${encodeURIComponent(soql)}`);\n}\n\nfunction buildListSoql(params: {\n object: string;\n fields: string[];\n where?: string;\n limit: number;\n offset: number;\n}): string {\n const { object, fields, where, limit, offset } = params;\n\n let soql = `SELECT ${fields.join(\", \")} FROM ${object}`;\n if (where) soql += ` WHERE ${where}`;\n soql += ` ORDER BY LastModifiedDate DESC LIMIT ${limit} OFFSET ${offset}`;\n\n return soql;\n}\n\nasync function getSingleRecord<T>(params: {\n object: string;\n id: string;\n fields: string[];\n notFoundMessage: string;\n}): Promise<T> {\n const { object, id, fields, notFoundMessage } = params;\n const soql = `SELECT ${fields.join(\", \")} FROM ${object} WHERE Id = '${id}'`;\n const result = await query<T>(soql);\n\n if (result.totalSize === 0) throw new Error(notFoundMessage);\n return result.records[0];\n}\n\n// ============================================================================\n// ACCOUNTS\n// ============================================================================\n\nexport function listAccounts(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n}): Promise<SalesforceQueryResponse<SalesforceAccount>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"Name\",\n \"Type\",\n \"Industry\",\n \"Website\",\n \"Phone\",\n \"BillingCity\",\n \"BillingState\",\n \"BillingCountry\",\n \"NumberOfEmployees\",\n \"AnnualRevenue\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return query<SalesforceAccount>(buildListSoql({ object: \"Account\", fields, limit, offset }));\n}\n\nexport function getAccount(accountId: string, fields?: string[]): Promise<SalesforceAccount> {\n const selectedFields = fields ?? [\n \"Id\",\n \"Name\",\n \"Type\",\n \"Industry\",\n \"Website\",\n \"Phone\",\n \"BillingStreet\",\n \"BillingCity\",\n \"BillingState\",\n \"BillingPostalCode\",\n \"BillingCountry\",\n \"NumberOfEmployees\",\n \"AnnualRevenue\",\n \"Description\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceAccount>({\n object: \"Account\",\n id: accountId,\n fields: selectedFields,\n notFoundMessage: `Account with ID ${accountId} not found`,\n });\n}\n\nexport function createAccount(data: {\n Name: string;\n Type?: string;\n Industry?: string;\n Website?: string;\n Phone?: string;\n BillingStreet?: string;\n BillingCity?: string;\n BillingState?: string;\n BillingPostalCode?: string;\n BillingCountry?: string;\n NumberOfEmployees?: number;\n AnnualRevenue?: number;\n Description?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Account\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// CONTACTS\n// ============================================================================\n\nexport function listContacts(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n accountId?: string;\n}): Promise<SalesforceQueryResponse<SalesforceContact>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Email\",\n \"Phone\",\n \"Title\",\n \"Department\",\n \"AccountId\",\n \"MailingCity\",\n \"MailingState\",\n \"MailingCountry\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined;\n\n return query<SalesforceContact>(buildListSoql({ object: \"Contact\", fields, where, limit, offset }));\n}\n\nexport function getContact(contactId: string, fields?: string[]): Promise<SalesforceContact> {\n const selectedFields = fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Email\",\n \"Phone\",\n \"MobilePhone\",\n \"Title\",\n \"Department\",\n \"AccountId\",\n \"MailingStreet\",\n \"MailingCity\",\n \"MailingState\",\n \"MailingPostalCode\",\n \"MailingCountry\",\n \"Description\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceContact>({\n object: \"Contact\",\n id: contactId,\n fields: selectedFields,\n notFoundMessage: `Contact with ID ${contactId} not found`,\n });\n}\n\nexport function createContact(data: {\n LastName: string;\n FirstName?: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Department?: string;\n AccountId?: string;\n MailingStreet?: string;\n MailingCity?: string;\n MailingState?: string;\n MailingPostalCode?: string;\n MailingCountry?: string;\n Description?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Contact\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// OPPORTUNITIES\n// ============================================================================\n\nexport function listOpportunities(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n accountId?: string;\n}): Promise<SalesforceQueryResponse<SalesforceOpportunity>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"Name\",\n \"AccountId\",\n \"Amount\",\n \"StageName\",\n \"Probability\",\n \"CloseDate\",\n \"Type\",\n \"LeadSource\",\n \"IsClosed\",\n \"IsWon\",\n \"ForecastCategory\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined;\n\n return query<SalesforceOpportunity>(\n buildListSoql({ object: \"Opportunity\", fields, where, limit, offset }),\n );\n}\n\nexport function getOpportunity(opportunityId: string, fields?: string[]): Promise<SalesforceOpportunity> {\n const selectedFields = fields ?? [\n \"Id\",\n \"Name\",\n \"AccountId\",\n \"Amount\",\n \"StageName\",\n \"Probability\",\n \"CloseDate\",\n \"Type\",\n \"LeadSource\",\n \"Description\",\n \"NextStep\",\n \"IsClosed\",\n \"IsWon\",\n \"ForecastCategory\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceOpportunity>({\n object: \"Opportunity\",\n id: opportunityId,\n fields: selectedFields,\n notFoundMessage: `Opportunity with ID ${opportunityId} not found`,\n });\n}\n\nexport function createOpportunity(data: {\n Name: string;\n StageName: string;\n CloseDate: string;\n AccountId?: string;\n Amount?: number;\n Probability?: number;\n Type?: string;\n LeadSource?: string;\n Description?: string;\n NextStep?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Opportunity\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// LEADS\n// ============================================================================\n\nexport function listLeads(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n status?: string;\n}): Promise<SalesforceQueryResponse<SalesforceLead>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Company\",\n \"Email\",\n \"Phone\",\n \"Title\",\n \"Status\",\n \"LeadSource\",\n \"Industry\",\n \"City\",\n \"State\",\n \"Country\",\n \"Rating\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.status ? `Status = '${options.status}'` : undefined;\n\n return query<SalesforceLead>(buildListSoql({ object: \"Lead\", fields, where, limit, offset }));\n}\n\nexport function createLead(data: {\n LastName: string;\n Company: string;\n FirstName?: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Status?: string;\n LeadSource?: string;\n Industry?: string;\n Street?: string;\n City?: string;\n State?: string;\n PostalCode?: string;\n Country?: string;\n Website?: string;\n Description?: string;\n Rating?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Lead\", {\n method: \"POST\",\n body: JSON.stringify({ ...data, Status: data.Status ?? \"Open - Not Contacted\" }),\n });\n}\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\nfunction formatPersonName(firstName?: string, lastName?: string, email?: string, fallback = \"Unnamed\"): string {\n const parts = [firstName, lastName].filter(Boolean);\n if (parts.length) return parts.join(\" \");\n return email ?? fallback;\n}\n\nexport function formatContactName(contact: SalesforceContact): string {\n return formatPersonName(contact.FirstName, contact.LastName, contact.Email, \"Unnamed Contact\");\n}\n\nexport function formatLeadName(lead: SalesforceLead): string {\n return formatPersonName(lead.FirstName, lead.LastName, lead.Email, \"Unnamed Lead\");\n}\n\nexport function formatAddress(\n street?: string,\n city?: string,\n state?: string,\n postalCode?: string,\n country?: string,\n): string {\n return [street, city, state, postalCode, country].filter(Boolean).join(\", \");\n}\n\nexport type {\n SalesforceAccount,\n SalesforceContact,\n SalesforceLead,\n SalesforceOpportunity,\n SalesforceQueryResponse,\n};\n",
560
560
  "app/api/auth/salesforce/route.ts": "import { createOAuthInitHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(salesforceConfig, {\n tokenStore: oauthMemoryTokenStore,\n});\n",
561
- "app/api/auth/salesforce/callback/route.ts": "import { createOAuthCallbackHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(salesforceConfig, { tokenStore: hybridTokenStore });\n",
561
+ "app/api/auth/salesforce/callback/route.ts": "import { createOAuthCallbackHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(salesforceConfig, { tokenStore: hybridTokenStore });\n",
562
562
  "tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatAddress, getAccount } from \"../../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"get-account\",\n description:\n \"Get detailed information about a specific account in Salesforce CRM by their account ID.\",\n inputSchema: z.object({\n accountId: z\n .string()\n .describe(\"The Salesforce account ID (e.g., 001XXXXXXXXXXXXXXX)\"),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n \"Additional fields to retrieve (e.g., Description, Owner.Name, ParentId)\",\n ),\n }),\n async execute({ accountId, fields }) {\n const account = await getAccount(accountId, fields);\n\n const billingAddress =\n formatAddress(\n account.BillingStreet,\n account.BillingCity,\n account.BillingState,\n account.BillingPostalCode,\n account.BillingCountry,\n ) || undefined;\n\n const additionalFields = fields?.length\n ? Object.fromEntries(\n fields\n .filter((field) => account[field] !== undefined)\n .map((field) => [field, account[field]]),\n )\n : undefined;\n\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingAddress,\n billingStreet: account.BillingStreet,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingPostalCode: account.BillingPostalCode,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n description: account.Description,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields,\n };\n },\n});\n",
563
563
  "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatContactName, listContacts } from \"../../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List contacts from your Salesforce CRM. Returns contact information including name, email, phone, title, and account association.\",\n inputSchema: z.object({\n limit: z.number().min(1).max(100).default(10).describe(\"Maximum number of contacts to return\"),\n offset: z.number().min(0).default(0).describe(\"Number of records to skip for pagination\"),\n accountId: z.string().optional().describe(\"Filter contacts by Account ID\"),\n fields: z\n .array(z.string())\n .optional()\n .describe(\"Additional fields to retrieve (e.g., Account.Name, Owner.Name, LeadSource)\"),\n }),\n async execute({ limit, offset, accountId, fields }) {\n const response = await listContacts({ limit, offset, accountId, fields });\n\n return {\n contacts: response.records.map((contact) => {\n const additionalFields = fields\n ? Object.fromEntries(\n fields.flatMap((field) => {\n const value = contact[field];\n return value === undefined ? [] : [[field, value]];\n }),\n )\n : undefined;\n\n return {\n id: contact.Id,\n name: formatContactName(contact),\n firstName: contact.FirstName,\n lastName: contact.LastName,\n email: contact.Email,\n phone: contact.Phone,\n mobilePhone: contact.MobilePhone,\n title: contact.Title,\n department: contact.Department,\n accountId: contact.AccountId,\n mailingCity: contact.MailingCity,\n mailingState: contact.MailingState,\n mailingCountry: contact.MailingCountry,\n createdDate: contact.CreatedDate,\n lastModifiedDate: contact.LastModifiedDate,\n additionalFields,\n };\n }),\n totalSize: response.totalSize,\n hasMore: !response.done,\n };\n },\n});\n",
564
564
  "tools/list-accounts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listAccounts } from \"../../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"list-accounts\",\n description:\n \"List accounts from your Salesforce CRM. Returns account information including name, type, industry, website, and billing details.\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of accounts to return\"),\n offset: z\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of records to skip for pagination\"),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n \"Additional fields to retrieve (e.g., Description, Owner.Name, ParentId)\",\n ),\n }),\n async execute({ limit, offset, fields }) {\n const response = await listAccounts({ limit, offset, fields });\n\n return {\n accounts: response.records.map((account) => {\n if (!fields?.length) {\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields: undefined,\n };\n }\n\n const additionalFields = Object.fromEntries(\n fields\n .filter((field) => account[field] !== undefined)\n .map((field) => [field, account[field]]),\n );\n\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields,\n };\n }),\n totalSize: response.totalSize,\n hasMore: !response.done,\n };\n },\n});\n",
@@ -584,7 +584,7 @@ export default {
584
584
  "files": {
585
585
  "lib/slack-client.ts": "/**\n * Slack API Client\n *\n * Provides a type-safe interface to Slack API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore - Deno global\n return Deno.env.get(key);\n }\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) {\n // @ts-ignore - process global\n return process.env[key];\n }\n\n return undefined;\n}\n\nconst SLACK_API_BASE = \"https://slack.com/api\";\n\nexport interface SlackChannel {\n id: string;\n name: string;\n is_channel: boolean;\n is_private: boolean;\n is_member: boolean;\n topic?: { value: string };\n purpose?: { value: string };\n}\n\nexport interface SlackMessage {\n type: string;\n user?: string;\n text: string;\n ts: string;\n thread_ts?: string;\n reply_count?: number;\n reactions?: Array<{ name: string; count: number }>;\n}\n\nexport interface SlackUser {\n id: string;\n name: string;\n real_name: string;\n profile: {\n display_name: string;\n email?: string;\n image_48?: string;\n };\n}\n\n/**\n * Slack OAuth provider configuration\n */\nexport const slackOAuthProvider = {\n name: \"slack\",\n authorizationUrl: \"https://slack.com/oauth/v2/authorize\",\n tokenUrl: \"https://slack.com/api/oauth.v2.access\",\n clientId: getEnv(\"SLACK_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"SLACK_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"channels:history\",\n \"channels:read\",\n \"chat:write\",\n \"users:read\",\n \"im:history\",\n \"im:read\",\n ],\n callbackPath: \"/api/auth/slack/callback\",\n};\n\nexport interface SlackClient {\n listChannels(options?: {\n limit?: number;\n excludeArchived?: boolean;\n }): Promise<SlackChannel[]>;\n getMessages(\n channelId: string,\n options?: { limit?: number; oldest?: string },\n ): Promise<SlackMessage[]>;\n sendMessage(\n channelId: string,\n text: string,\n options?: { threadTs?: string; unfurlLinks?: boolean },\n ): Promise<{ ts: string; channel: string }>;\n getUser(userId: string): Promise<SlackUser>;\n getThread(channelId: string, threadTs: string): Promise<SlackMessage[]>;\n searchMessages(\n query: string,\n options?: { count?: number },\n ): Promise<SlackMessage[]>;\n}\n\n/**\n * Create a Slack client for a specific user\n */\nexport function createSlackClient(userId: string): SlackClient {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(slackOAuthProvider, userId, \"slack\");\n if (!token) {\n throw new Error(\n \"Slack not connected. Please connect your Slack account first.\",\n );\n }\n return token;\n }\n\n async function apiRequest<T>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${SLACK_API_BASE}/${method}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json; charset=utf-8\",\n },\n body: JSON.stringify(params),\n });\n\n const data = await response.json();\n\n if (!data.ok) {\n throw new Error(`Slack API error: ${data.error}`);\n }\n\n return data as T;\n }\n\n return {\n /**\n * List channels the user is a member of\n */\n async listChannels(options = {}): Promise<SlackChannel[]> {\n const result = await apiRequest<{ channels: SlackChannel[] }>(\n \"conversations.list\",\n {\n limit: options.limit ?? 100,\n exclude_archived: options.excludeArchived ?? true,\n types: \"public_channel,private_channel\",\n },\n );\n return result.channels;\n },\n\n /**\n * Get messages from a channel\n */\n async getMessages(\n channelId: string,\n options = {},\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{ messages: SlackMessage[] }>(\n \"conversations.history\",\n {\n channel: channelId,\n limit: options.limit ?? 20,\n oldest: options.oldest,\n },\n );\n return result.messages;\n },\n\n /**\n * Send a message to a channel\n */\n async sendMessage(\n channelId: string,\n text: string,\n options = {},\n ): Promise<{ ts: string; channel: string }> {\n return apiRequest<{ ts: string; channel: string }>(\"chat.postMessage\", {\n channel: channelId,\n text,\n thread_ts: options.threadTs,\n unfurl_links: options.unfurlLinks ?? true,\n });\n },\n\n /**\n * Get user info\n */\n async getUser(userId: string): Promise<SlackUser> {\n const result = await apiRequest<{ user: SlackUser }>(\"users.info\", {\n user: userId,\n });\n return result.user;\n },\n\n /**\n * Get thread replies\n */\n async getThread(\n channelId: string,\n threadTs: string,\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{ messages: SlackMessage[] }>(\n \"conversations.replies\",\n {\n channel: channelId,\n ts: threadTs,\n },\n );\n return result.messages;\n },\n\n /**\n * Search messages\n */\n async searchMessages(\n query: string,\n options = {},\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{\n messages: { matches: SlackMessage[] };\n }>(\"search.messages\", {\n query,\n count: options.count ?? 20,\n });\n return result.messages.matches;\n },\n };\n}\n",
586
586
  "app/api/auth/slack/route.ts": "import { createOAuthInitHandler, slackConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(slackConfig, { tokenStore: oauthMemoryTokenStore });\n",
587
- "app/api/auth/slack/callback/route.ts": "import { createOAuthCallbackHandler, slackConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(slackConfig, { tokenStore: hybridTokenStore });\n",
587
+ "app/api/auth/slack/callback/route.ts": "import { createOAuthCallbackHandler, slackConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(slackConfig, { tokenStore: hybridTokenStore });\n",
588
588
  "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSlackClient } from \"../../lib/slack-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description: \"Send a message to a Slack channel\",\n inputSchema: z.object({\n channel: z\n .string()\n .describe(\"Channel ID or name (e.g., 'C1234567890' or '#general')\"),\n text: z.string().min(1).describe(\"Message text to send\"),\n threadTs: z\n .string()\n .optional()\n .describe(\"Thread timestamp to reply to (for threaded messages)\"),\n }),\n execute: async ({ channel, text, threadTs }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const slack = createSlackClient(userId);\n const result = await slack.sendMessage(channel, text, { threadTs });\n\n return {\n success: true,\n messageTs: result.ts,\n channel: result.channel,\n message: threadTs\n ? `Reply sent to thread in ${channel}.`\n : `Message sent to ${channel}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n throw error;\n }\n },\n});\n",
589
589
  "tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSlackClient } from \"../../lib/slack-client.ts\";\n\ntype SlackChannel = {\n id: string;\n name: string;\n is_private: boolean;\n is_member: boolean;\n topic?: { value: string };\n purpose?: { value: string };\n};\n\nexport default tool({\n id: \"list-channels\",\n description: \"List Slack channels the user is a member of\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of channels to return\"),\n excludeArchived: z\n .boolean()\n .default(true)\n .describe(\"Exclude archived channels\"),\n }),\n execute: async ({ limit, excludeArchived }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const slack = createSlackClient(userId);\n const channels = await slack.listChannels({ limit, excludeArchived });\n const count = channels.length;\n\n return {\n channels: channels.map((ch: SlackChannel) => ({\n id: ch.id,\n name: ch.name,\n isPrivate: ch.is_private,\n isMember: ch.is_member,\n topic: ch.topic?.value ?? null,\n purpose: ch.purpose?.value ?? null,\n })),\n count,\n message: `Found ${count} channel(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n\n throw error;\n }\n },\n});\n",
590
590
  "tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createSlackClient } from \"../../lib/slack-client.ts\";\n\ntype SlackMessage = {\n text?: string;\n user?: string;\n ts: string;\n thread_ts?: string;\n reply_count?: number;\n reactions?: Array<{ name: string; count: number }>;\n};\n\nexport default tool({\n id: \"get-messages\",\n description: \"Get recent messages from a Slack channel\",\n inputSchema: z.object({\n channel: z.string().describe(\"Channel ID (e.g., 'C1234567890')\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of messages to return\"),\n }),\n execute: async ({ channel, limit }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const slack = createSlackClient(userId);\n const messages = await slack.getMessages(channel, { limit });\n\n return {\n messages: messages.map((msg: SlackMessage) => ({\n text: msg.text ?? \"\",\n user: msg.user ?? \"unknown\",\n timestamp: msg.ts,\n threadTs: msg.thread_ts,\n replyCount: msg.reply_count ?? 0,\n reactions: msg.reactions?.map((r) => `${r.name} (${r.count})`) ?? [],\n })),\n count: messages.length,\n channel,\n message: `Retrieved ${messages.length} message(s) from channel.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n throw error;\n }\n },\n});\n"
@@ -595,7 +595,7 @@ export default {
595
595
  ".env.example": "# ClickUp OAuth Configuration\n# Get your credentials from https://app.clickup.com/settings/apps\nCLICKUP_CLIENT_ID=your-client-id\nCLICKUP_CLIENT_SECRET=your-client-secret\n",
596
596
  "lib/clickup-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst CLICKUP_BASE_URL = \"https://api.clickup.com/api/v2\";\n\ninterface ClickUpTask {\n id: string;\n name: string;\n description: string;\n status: {\n status: string;\n color: string;\n type: string;\n };\n date_created: string;\n date_updated: string;\n date_closed: string | null;\n creator: {\n id: number;\n username: string;\n email: string;\n };\n assignees: Array<{\n id: number;\n username: string;\n email: string;\n }>;\n tags: Array<{\n name: string;\n tag_fg: string;\n tag_bg: string;\n }>;\n due_date: string | null;\n start_date: string | null;\n priority: {\n id: string;\n priority: string;\n color: string;\n } | null;\n list: {\n id: string;\n name: string;\n };\n folder: {\n id: string;\n name: string;\n };\n space: {\n id: string;\n name: string;\n };\n}\n\ninterface ClickUpList {\n id: string;\n name: string;\n orderindex: number;\n content: string;\n status: {\n status: string;\n color: string;\n };\n priority: {\n priority: string;\n color: string;\n } | null;\n assignee: {\n id: number;\n username: string;\n email: string;\n } | null;\n task_count: number;\n due_date: string | null;\n start_date: string | null;\n folder: {\n id: string;\n name: string;\n hidden: boolean;\n access: boolean;\n };\n space: {\n id: string;\n name: string;\n access: boolean;\n };\n archived: boolean;\n}\n\ninterface ClickUpFolder {\n id: string;\n name: string;\n orderindex: number;\n override_statuses: boolean;\n hidden: boolean;\n space: {\n id: string;\n name: string;\n };\n task_count: string;\n lists: ClickUpList[];\n}\n\ninterface ClickUpSpace {\n id: string;\n name: string;\n private: boolean;\n statuses: Array<{\n status: string;\n type: string;\n orderindex: number;\n color: string;\n }>;\n multiple_assignees: boolean;\n features: {\n due_dates: {\n enabled: boolean;\n start_date: boolean;\n remap_due_dates: boolean;\n remap_closed_due_date: boolean;\n };\n time_tracking: {\n enabled: boolean;\n };\n tags: {\n enabled: boolean;\n };\n time_estimates: {\n enabled: boolean;\n };\n checklists: {\n enabled: boolean;\n };\n custom_fields: {\n enabled: boolean;\n };\n remap_dependencies: {\n enabled: boolean;\n };\n dependency_warning: {\n enabled: boolean;\n };\n portfolios: {\n enabled: boolean;\n };\n };\n}\n\nfunction buildCustomTaskParams(options?: { customTaskIds?: boolean; teamId?: string }): URLSearchParams {\n const params = new URLSearchParams();\n\n if (!options?.customTaskIds) return params;\n\n params.set(\"custom_task_ids\", \"true\");\n if (options.teamId) params.set(\"team_id\", options.teamId);\n\n return params;\n}\n\nfunction withQuery(endpoint: string, params: URLSearchParams): string {\n const queryString = params.toString();\n return queryString ? `${endpoint}?${queryString}` : endpoint;\n}\n\nasync function clickupFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with ClickUp. Please connect your account.\");\n\n const response = await fetch(`${CLICKUP_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: token,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({} as { err?: string; error?: string }));\n const message = error.err ?? error.error ?? response.statusText;\n\n throw new Error(`ClickUp API error: ${response.status} ${message}`);\n}\n\nexport async function listSpaces(teamId: string): Promise<ClickUpSpace[]> {\n const response = await clickupFetch<{ spaces: ClickUpSpace[] }>(`/team/${teamId}/space`);\n return response.spaces;\n}\n\nexport async function listFolders(spaceId: string): Promise<ClickUpFolder[]> {\n const response = await clickupFetch<{ folders: ClickUpFolder[] }>(`/space/${spaceId}/folder`);\n return response.folders;\n}\n\nexport async function listLists(folderId: string): Promise<ClickUpList[]> {\n const response = await clickupFetch<{ lists: ClickUpList[] }>(`/folder/${folderId}/list`);\n return response.lists;\n}\n\nexport async function listFolderlessLists(spaceId: string): Promise<ClickUpList[]> {\n const response = await clickupFetch<{ lists: ClickUpList[] }>(`/space/${spaceId}/list`);\n return response.lists;\n}\n\nexport async function listTasks(options: {\n listId?: string;\n spaceId?: string;\n folderId?: string;\n assignees?: number[];\n statuses?: string[];\n includeClosed?: boolean;\n orderBy?: string;\n subtasks?: boolean;\n}): Promise<ClickUpTask[]> {\n const params = new URLSearchParams();\n\n options.assignees?.forEach((assignee) => params.append(\"assignees[]\", assignee.toString()));\n options.statuses?.forEach((status) => params.append(\"statuses[]\", status));\n\n if (options.includeClosed !== undefined) params.set(\"include_closed\", options.includeClosed.toString());\n if (options.orderBy) params.set(\"order_by\", options.orderBy);\n if (options.subtasks !== undefined) params.set(\"subtasks\", options.subtasks.toString());\n\n let endpoint = \"/task\";\n if (options.listId) endpoint = `/list/${options.listId}/task`;\n else if (options.folderId) endpoint = `/folder/${options.folderId}/task`;\n else if (options.spaceId) endpoint = `/space/${options.spaceId}/task`;\n\n const response = await clickupFetch<{ tasks: ClickUpTask[] }>(withQuery(endpoint, params));\n return response.tasks;\n}\n\nexport async function getTask(\n taskId: string,\n options?: {\n customTaskIds?: boolean;\n teamId?: string;\n includeSubtasks?: boolean;\n },\n): Promise<ClickUpTask> {\n const params = buildCustomTaskParams(options);\n if (options?.includeSubtasks) params.set(\"include_subtasks\", \"true\");\n\n return clickupFetch<ClickUpTask>(withQuery(`/task/${taskId}`, params));\n}\n\nexport async function createTask(options: {\n listId: string;\n name: string;\n description?: string;\n assignees?: number[];\n tags?: string[];\n status?: string;\n priority?: number;\n dueDate?: number;\n dueDateTime?: boolean;\n timeEstimate?: number;\n startDate?: number;\n startDateTime?: boolean;\n notifyAll?: boolean;\n parent?: string;\n linksTo?: string;\n checkRequired?: boolean;\n customTaskIds?: boolean;\n teamId?: string;\n}): Promise<ClickUpTask> {\n const body: Record<string, unknown> = { name: options.name };\n\n if (options.description) body.description = options.description;\n if (options.assignees) body.assignees = options.assignees;\n if (options.tags) body.tags = options.tags;\n if (options.status) body.status = options.status;\n if (options.priority !== undefined) body.priority = options.priority;\n\n if (options.dueDate) {\n body.due_date = options.dueDate;\n if (options.dueDateTime !== undefined) body.due_date_time = options.dueDateTime;\n }\n\n if (options.timeEstimate) body.time_estimate = options.timeEstimate;\n\n if (options.startDate) {\n body.start_date = options.startDate;\n if (options.startDateTime !== undefined) body.start_date_time = options.startDateTime;\n }\n\n if (options.notifyAll !== undefined) body.notify_all = options.notifyAll;\n if (options.parent) body.parent = options.parent;\n if (options.linksTo) body.links_to = options.linksTo;\n\n const url = withQuery(`/list/${options.listId}/task`, buildCustomTaskParams(options));\n\n return clickupFetch<ClickUpTask>(url, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updateTask(\n taskId: string,\n updates: {\n name?: string;\n description?: string;\n status?: string;\n priority?: number | null;\n dueDate?: number | null;\n dueDateTime?: boolean;\n timeEstimate?: number | null;\n startDate?: number | null;\n startDateTime?: boolean;\n assignees?: {\n add?: number[];\n rem?: number[];\n };\n archived?: boolean;\n },\n options?: {\n customTaskIds?: boolean;\n teamId?: string;\n },\n): Promise<ClickUpTask> {\n const body: Record<string, unknown> = {};\n\n if (updates.name !== undefined) body.name = updates.name;\n if (updates.description !== undefined) body.description = updates.description;\n if (updates.status !== undefined) body.status = updates.status;\n if (updates.priority !== undefined) body.priority = updates.priority;\n\n if (updates.dueDate !== undefined) {\n body.due_date = updates.dueDate;\n if (updates.dueDateTime !== undefined) body.due_date_time = updates.dueDateTime;\n }\n\n if (updates.timeEstimate !== undefined) body.time_estimate = updates.timeEstimate;\n\n if (updates.startDate !== undefined) {\n body.start_date = updates.startDate;\n if (updates.startDateTime !== undefined) body.start_date_time = updates.startDateTime;\n }\n\n if (updates.assignees) body.assignees = updates.assignees;\n if (updates.archived !== undefined) body.archived = updates.archived;\n\n const url = withQuery(`/task/${taskId}`, buildCustomTaskParams(options));\n\n return clickupFetch<ClickUpTask>(url, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function getAuthorizedUser(): Promise<{\n user: {\n id: number;\n username: string;\n email: string;\n color: string;\n profilePicture: string;\n };\n}> {\n return clickupFetch<{\n user: {\n id: number;\n username: string;\n email: string;\n color: string;\n profilePicture: string;\n };\n }>(\"/user\");\n}\n\nexport async function getTeams(): Promise<\n Array<{\n id: string;\n name: string;\n color: string;\n avatar: string | null;\n members: Array<{\n user: {\n id: number;\n username: string;\n email: string;\n };\n }>;\n }>\n> {\n const response = await clickupFetch<{\n teams: Array<{\n id: string;\n name: string;\n color: string;\n avatar: string | null;\n members: Array<{\n user: {\n id: number;\n username: string;\n email: string;\n };\n }>;\n }>;\n }>(\"/team\");\n\n return response.teams;\n}\n",
597
597
  "app/api/auth/clickup/route.ts": "import { clickupConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(clickupConfig, { tokenStore: oauthMemoryTokenStore });\n",
598
- "app/api/auth/clickup/callback/route.ts": "import { clickupConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(clickupConfig, { tokenStore: hybridTokenStore });\n",
598
+ "app/api/auth/clickup/callback/route.ts": "import { clickupConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(clickupConfig, { tokenStore: hybridTokenStore });\n",
599
599
  "tools/get-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTask } from \"../../lib/clickup-client.ts\";\n\nfunction toIsoDate(value?: string | null): string | null {\n if (!value) return null;\n return new Date(Number.parseInt(value, 10)).toISOString();\n}\n\nexport default tool({\n id: \"get-task\",\n description: \"Get detailed information about a specific ClickUp task by ID.\",\n inputSchema: z.object({\n taskId: z.string().describe(\"The ID of the task to retrieve\"),\n includeSubtasks: z.boolean().default(false).describe(\"Include subtasks in the response\"),\n customTaskIds: z.boolean().default(false).describe(\"Use custom task IDs instead of internal IDs\"),\n teamId: z.string().optional().describe(\"Team ID (required when using custom task IDs)\"),\n }),\n async execute({ taskId, includeSubtasks, customTaskIds, teamId }) {\n const task = await getTask(taskId, { includeSubtasks, customTaskIds, teamId });\n\n const { creator, list, folder, space } = task;\n\n return {\n id: task.id,\n name: task.name,\n description: task.description,\n status: task.status.status,\n priority: task.priority?.priority ?? \"none\",\n dueDate: toIsoDate(task.due_date),\n startDate: toIsoDate(task.start_date),\n dateCreated: task.date_created,\n dateUpdated: task.date_updated,\n dateClosed: task.date_closed,\n creator: {\n id: creator.id,\n username: creator.username,\n email: creator.email,\n },\n assignees: task.assignees.map(({ id, username, email }) => ({ id, username, email })),\n tags: task.tags.map(({ name }) => name),\n list: {\n id: list.id,\n name: list.name,\n },\n folder: {\n id: folder.id,\n name: folder.name,\n },\n space: {\n id: space.id,\n name: space.name,\n },\n };\n },\n});\n",
600
600
  "tools/update-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateTask } from \"../../lib/clickup-client.ts\";\n\nexport default tool({\n id: \"update-task\",\n description: \"Update an existing ClickUp task.\",\n inputSchema: z.object({\n taskId: z.string().describe(\"The ID of the task to update\"),\n name: z.string().optional().describe(\"New name/title for the task\"),\n description: z.string().optional().describe(\"New description for the task\"),\n status: z.string().optional().describe(\"New status name for the task\"),\n priority: z\n .number()\n .min(1)\n .max(4)\n .optional()\n .describe(\n \"Priority level: 1 (urgent), 2 (high), 3 (normal), 4 (low). Use null to remove priority.\",\n ),\n dueDate: z\n .number()\n .optional()\n .describe(\"Due date in Unix timestamp (milliseconds). Use null to remove due date.\"),\n startDate: z\n .number()\n .optional()\n .describe(\"Start date in Unix timestamp (milliseconds). Use null to remove start date.\"),\n timeEstimate: z\n .number()\n .optional()\n .describe(\"Time estimate in milliseconds. Use null to remove time estimate.\"),\n addAssignees: z.array(z.number()).optional().describe(\"Array of user IDs to add as assignees\"),\n removeAssignees: z\n .array(z.number())\n .optional()\n .describe(\"Array of user IDs to remove from assignees\"),\n archived: z.boolean().optional().describe(\"Archive or unarchive the task\"),\n customTaskIds: z.boolean().default(false).describe(\"Use custom task IDs instead of internal IDs\"),\n teamId: z.string().optional().describe(\"Team ID (required when using custom task IDs)\"),\n }),\n async execute({\n taskId,\n name,\n description,\n status,\n priority,\n dueDate,\n startDate,\n timeEstimate,\n addAssignees,\n removeAssignees,\n archived,\n customTaskIds,\n teamId,\n }) {\n const updates: Record<string, unknown> = {};\n\n if (name !== undefined) updates.name = name;\n if (description !== undefined) updates.description = description;\n if (status !== undefined) updates.status = status;\n if (priority !== undefined) updates.priority = priority;\n if (dueDate !== undefined) updates.dueDate = dueDate;\n if (startDate !== undefined) updates.startDate = startDate;\n if (timeEstimate !== undefined) updates.timeEstimate = timeEstimate;\n if (archived !== undefined) updates.archived = archived;\n\n if (addAssignees || removeAssignees) {\n updates.assignees = {\n ...(addAssignees ? { add: addAssignees } : {}),\n ...(removeAssignees ? { rem: removeAssignees } : {}),\n };\n }\n\n const task = await updateTask(\n taskId,\n updates,\n customTaskIds ? { customTaskIds, teamId } : undefined,\n );\n\n return {\n success: true,\n task: {\n id: task.id,\n name: task.name,\n status: task.status.status,\n dueDate: task.due_date ? new Date(Number(task.due_date)).toISOString() : null,\n priority: task.priority?.priority ?? \"none\",\n assignees: task.assignees.map((a) => a.username),\n url: `https://app.clickup.com/t/${task.id}`,\n },\n };\n },\n});\n",
601
601
  "tools/create-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTask } from \"../../lib/clickup-client.ts\";\n\nexport default tool({\n id: \"create-task\",\n description: \"Create a new task in a ClickUp list.\",\n inputSchema: z.object({\n listId: z.string().describe(\"The ID of the list to create the task in\"),\n name: z.string().describe(\"The name/title of the task\"),\n description: z.string().optional().describe(\"Description or details for the task\"),\n assignees: z.array(z.number()).optional().describe(\"Array of user IDs to assign the task to\"),\n tags: z.array(z.string()).optional().describe(\"Array of tag names to add to the task\"),\n status: z.string().optional().describe(\"Status name for the task\"),\n priority: z\n .number()\n .min(1)\n .max(4)\n .optional()\n .describe(\"Priority level: 1 (urgent), 2 (high), 3 (normal), 4 (low)\"),\n dueDate: z.number().optional().describe(\"Due date in Unix timestamp (milliseconds)\"),\n startDate: z.number().optional().describe(\"Start date in Unix timestamp (milliseconds)\"),\n timeEstimate: z.number().optional().describe(\"Time estimate in milliseconds\"),\n notifyAll: z.boolean().default(false).describe(\"Notify all assignees when task is created\"),\n }),\n async execute(input) {\n const task = await createTask(input);\n const dueDate = task.due_date ? new Date(Number(task.due_date)).toISOString() : null;\n\n return {\n success: true,\n task: {\n id: task.id,\n name: task.name,\n status: task.status.status,\n dueDate,\n priority: task.priority?.priority ?? \"none\",\n assignees: task.assignees.map((assignee) => assignee.username),\n list: task.list.name,\n url: `https://app.clickup.com/t/${task.id}`,\n },\n };\n },\n});\n",
@@ -609,7 +609,7 @@ export default {
609
609
  "lib/oauth.ts": "import { type OAuthToken, tokenStore } from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction buildTokenRequest(\n provider: OAuthProvider,\n body: Record<string, string>,\n): RequestInit {\n return {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n ...body,\n }),\n };\n}\n\nasync function fetchToken(\n provider: OAuthProvider,\n body: Record<string, string>,\n errorPrefix: string,\n): Promise<any> {\n const response = await fetch(\n provider.tokenUrl,\n buildTokenRequest(provider, body),\n );\n\n if (response.ok) return response.json();\n\n const error = await response.text();\n throw new Error(`${errorPrefix}: ${response.status} - ${error}`);\n}\n\nfunction toOAuthToken(data: any, fallbackRefreshToken?: string): OAuthToken {\n const expiresIn = data.expires_in;\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? fallbackRefreshToken,\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await fetchToken(\n provider,\n {\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n },\n \"Token exchange failed\",\n );\n\n return toOAuthToken(data);\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await fetchToken(\n provider,\n {\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n },\n \"Token refresh failed\",\n );\n\n return toOAuthToken(data, refreshToken);\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired) return token.accessToken;\n if (!token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
610
610
  "lib/drive-client.ts": "import { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface DriveFile {\n id: string;\n name: string;\n mimeType: string;\n kind: string;\n createdTime: string;\n modifiedTime: string;\n size?: string;\n webViewLink?: string;\n webContentLink?: string;\n iconLink?: string;\n thumbnailLink?: string;\n parents?: string[];\n starred?: boolean;\n trashed?: boolean;\n shared?: boolean;\n owners?: Array<{\n displayName: string;\n emailAddress: string;\n photoLink?: string;\n }>;\n lastModifyingUser?: {\n displayName: string;\n emailAddress: string;\n photoLink?: string;\n };\n capabilities?: {\n canEdit?: boolean;\n canComment?: boolean;\n canShare?: boolean;\n canDelete?: boolean;\n canDownload?: boolean;\n };\n}\n\nexport interface DriveFileList {\n files: DriveFile[];\n nextPageToken?: string;\n incompleteSearch?: boolean;\n}\n\nexport interface CreateFolderOptions {\n name: string;\n parentId?: string;\n description?: string;\n}\n\nexport interface UploadFileOptions {\n name: string;\n content: string;\n mimeType: string;\n parentId?: string;\n description?: string;\n}\n\nexport interface ListFilesOptions {\n folderId?: string;\n pageSize?: number;\n pageToken?: string;\n orderBy?: string;\n query?: string;\n}\n\nexport interface SearchFilesOptions {\n query: string;\n pageSize?: number;\n pageToken?: string;\n orderBy?: string;\n}\n\nexport const driveOAuthProvider = {\n name: \"drive\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/drive.readonly\",\n \"https://www.googleapis.com/auth/drive.file\",\n ],\n callbackPath: \"/api/auth/drive/callback\",\n};\n\nexport function createDriveClient(userId: string): {\n listFiles(options?: ListFilesOptions): Promise<DriveFileList>;\n getFile(fileId: string): Promise<DriveFile>;\n searchFiles(options: SearchFilesOptions): Promise<DriveFileList>;\n createFolder(options: CreateFolderOptions): Promise<DriveFile>;\n uploadFile(options: UploadFileOptions): Promise<DriveFile>;\n downloadFile(fileId: string): Promise<string>;\n deleteFile(fileId: string): Promise<void>;\n copyFile(fileId: string, name: string, parentId?: string): Promise<DriveFile>;\n updateFile(\n fileId: string,\n updates: { name?: string; description?: string; starred?: boolean },\n ): Promise<DriveFile>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(driveOAuthProvider, userId, \"drive\");\n if (!token) {\n throw new Error(\"Google Drive not connected. Please connect your Google account first.\");\n }\n return token;\n }\n\n async function driveApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${DRIVE_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive API error: ${response.status} - ${error}`);\n }\n\n if (response.status === 204) return undefined as T;\n return response.json();\n }\n\n function buildMetadata(options: {\n name: string;\n mimeType: string;\n parentId?: string;\n description?: string;\n }): Record<string, unknown> {\n const metadata: Record<string, unknown> = {\n name: options.name,\n mimeType: options.mimeType,\n };\n\n if (options.parentId) metadata.parents = [options.parentId];\n if (options.description) metadata.description = options.description;\n\n return metadata;\n }\n\n const fileFields =\n \"id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink,iconLink,thumbnailLink,parents,starred,trashed,shared,owners,lastModifyingUser,capabilities\";\n\n return {\n async listFiles(options: ListFilesOptions = {}): Promise<DriveFileList> {\n const params = new URLSearchParams({\n fields: `nextPageToken,incompleteSearch,files(${fileFields})`,\n pageSize: String(options.pageSize ?? 100),\n orderBy: options.orderBy ?? \"modifiedTime desc\",\n });\n\n let query = \"trashed=false\";\n if (options.folderId) query += ` and '${options.folderId}' in parents`;\n if (options.query) query += ` and ${options.query}`;\n\n params.append(\"q\", query);\n if (options.pageToken) params.append(\"pageToken\", options.pageToken);\n\n return driveApiRequest<DriveFileList>(`/files?${params.toString()}`);\n },\n\n async getFile(fileId: string): Promise<DriveFile> {\n const params = new URLSearchParams({ fields: fileFields });\n return driveApiRequest<DriveFile>(`/files/${fileId}?${params.toString()}`);\n },\n\n async searchFiles(options: SearchFilesOptions): Promise<DriveFileList> {\n const params = new URLSearchParams({\n fields:\n \"nextPageToken,incompleteSearch,files(id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink,iconLink,thumbnailLink,parents,starred,trashed)\",\n pageSize: String(options.pageSize ?? 100),\n q: `${options.query} and trashed=false`,\n orderBy: options.orderBy ?? \"modifiedTime desc\",\n });\n\n if (options.pageToken) params.append(\"pageToken\", options.pageToken);\n\n return driveApiRequest<DriveFileList>(`/files?${params.toString()}`);\n },\n\n async createFolder(options: CreateFolderOptions): Promise<DriveFile> {\n const metadata = buildMetadata({\n name: options.name,\n mimeType: \"application/vnd.google-apps.folder\",\n parentId: options.parentId,\n description: options.description,\n });\n\n return driveApiRequest<DriveFile>(\"/files\", {\n method: \"POST\",\n body: JSON.stringify(metadata),\n });\n },\n\n async uploadFile(options: UploadFileOptions): Promise<DriveFile> {\n const accessToken = await getAccessToken();\n\n const boundary = \"-------314159265358979323846\";\n const delimiter = `\\r\\n--${boundary}\\r\\n`;\n const closeDelimiter = `\\r\\n--${boundary}--`;\n\n const metadata = buildMetadata({\n name: options.name,\n mimeType: options.mimeType,\n parentId: options.parentId,\n description: options.description,\n });\n\n const multipartRequestBody =\n delimiter +\n \"Content-Type: application/json\\r\\n\\r\\n\" +\n JSON.stringify(metadata) +\n delimiter +\n `Content-Type: ${options.mimeType}\\r\\n\\r\\n` +\n options.content +\n closeDelimiter;\n\n const response = await fetch(\n \"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink\",\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body: multipartRequestBody,\n },\n );\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive upload error: ${response.status} - ${error}`);\n }\n\n return response.json();\n },\n\n async downloadFile(fileId: string): Promise<string> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${DRIVE_API_BASE}/files/${fileId}?alt=media`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive download error: ${response.status} - ${error}`);\n }\n\n return response.text();\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await driveApiRequest(`/files/${fileId}`, { method: \"DELETE\" });\n },\n\n async copyFile(fileId: string, name: string, parentId?: string): Promise<DriveFile> {\n const metadata: Record<string, unknown> = { name };\n if (parentId) metadata.parents = [parentId];\n\n return driveApiRequest<DriveFile>(`/files/${fileId}/copy`, {\n method: \"POST\",\n body: JSON.stringify(metadata),\n });\n },\n\n async updateFile(\n fileId: string,\n updates: { name?: string; description?: string; starred?: boolean },\n ): Promise<DriveFile> {\n return driveApiRequest<DriveFile>(`/files/${fileId}`, {\n method: \"PATCH\",\n body: JSON.stringify(updates),\n });\n },\n };\n}\n\nexport type DriveClient = ReturnType<typeof createDriveClient>;\n",
611
611
  "app/api/auth/drive/route.ts": "import { createOAuthInitHandler, driveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(driveConfig, { tokenStore: oauthMemoryTokenStore });\n",
612
- "app/api/auth/drive/callback/route.ts": "/**\n * Google Drive OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, driveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(driveConfig, { tokenStore: hybridTokenStore });\n",
612
+ "app/api/auth/drive/callback/route.ts": "/**\n * Google Drive OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, driveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n return tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n clearTokens(serviceId: string) {\n return tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(driveConfig, { tokenStore: hybridTokenStore });\n",
613
613
  "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in Google Drive using queries. Supports searching by name, content, type, and more. Use Drive query syntax (e.g., \\\"name contains 'report'\\\", \\\"mimeType='application/pdf'\\\").\",\n inputSchema: z.object({\n query: z\n .string()\n .describe(\n \"Search query using Drive query syntax. Examples: \\\"name contains 'report'\\\", \\\"mimeType='application/pdf'\\\", \\\"fullText contains 'budget'\\\"\",\n ),\n pageSize: z\n .number()\n .min(1)\n .max(1000)\n .default(100)\n .describe(\"Maximum number of files to return\"),\n pageToken: z\n .string()\n .optional()\n .describe(\"Token for pagination to get next page of results\"),\n orderBy: z\n .enum([\n \"createdTime\",\n \"folder\",\n \"modifiedByMeTime\",\n \"modifiedTime\",\n \"name\",\n \"quotaBytesUsed\",\n \"recency\",\n \"sharedWithMeTime\",\n \"starred\",\n \"viewedByMeTime\",\n ])\n .optional()\n .describe(\"Field to sort results by\"),\n }),\n async execute({ query, pageSize, pageToken, orderBy }) {\n const client = createDriveClient(DEFAULT_USER_ID);\n\n const result = await client.searchFiles({\n query,\n pageSize,\n pageToken,\n orderBy: orderBy ? `${orderBy} desc` : undefined,\n });\n\n const files = result.files.map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n starred: file.starred,\n shared: file.shared,\n parents: file.parents,\n }));\n\n const nextPageToken = result.nextPageToken;\n\n return {\n files,\n nextPageToken,\n hasMore: Boolean(nextPageToken),\n incompleteSearch: result.incompleteSearch,\n };\n },\n});\n",
614
614
  "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"get-file\",\n description:\n \"Get detailed metadata about a specific file or folder in Google Drive. Returns detailed information including sharing settings, owners, and capabilities.\",\n inputSchema: z.object({\n fileId: z.string().describe(\"The ID of the file or folder to retrieve\"),\n }),\n async execute({ fileId }) {\n const client = createDriveClient(DEFAULT_USER_ID);\n const file = await client.getFile(fileId);\n\n const lastModifyingUser = file.lastModifyingUser\n ? {\n name: file.lastModifyingUser.displayName,\n email: file.lastModifyingUser.emailAddress,\n photoLink: file.lastModifyingUser.photoLink,\n }\n : undefined;\n\n return {\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n webContentLink: file.webContentLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n parents: file.parents,\n starred: file.starred,\n trashed: file.trashed,\n shared: file.shared,\n owners: file.owners?.map((owner) => ({\n name: owner.displayName,\n email: owner.emailAddress,\n photoLink: owner.photoLink,\n })),\n lastModifyingUser,\n capabilities: file.capabilities,\n };\n },\n});\n",
615
615
  "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List files and folders in Google Drive. Can list from a specific folder or root. Returns file names, IDs, types, and metadata.\",\n inputSchema: z.object({\n folderId: z\n .string()\n .optional()\n .describe(\n \"ID of the folder to list files from. If not provided, lists from root.\",\n ),\n pageSize: z\n .number()\n .min(1)\n .max(1000)\n .default(100)\n .describe(\"Maximum number of files to return\"),\n pageToken: z\n .string()\n .optional()\n .describe(\"Token for pagination to get next page of results\"),\n orderBy: z\n .enum([\n \"createdTime\",\n \"folder\",\n \"modifiedByMeTime\",\n \"modifiedTime\",\n \"name\",\n \"quotaBytesUsed\",\n \"recency\",\n \"sharedWithMeTime\",\n \"starred\",\n \"viewedByMeTime\",\n ])\n .optional()\n .describe(\"Field to sort results by\"),\n }),\n async execute({ folderId, pageSize, pageToken, orderBy }) {\n const client = createDriveClient(DEFAULT_USER_ID);\n\n const result = await client.listFiles({\n folderId,\n pageSize,\n pageToken,\n orderBy: orderBy ? `${orderBy} desc` : undefined,\n });\n\n const nextPageToken = result.nextPageToken;\n\n return {\n files: result.files.map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n starred: file.starred,\n shared: file.shared,\n })),\n nextPageToken,\n hasMore: Boolean(nextPageToken),\n };\n },\n});\n",
@@ -635,7 +635,7 @@ export default {
635
635
  ".env.example": "# Box OAuth Configuration\n# Get your credentials from https://app.box.com/developers/console\nBOX_CLIENT_ID=your-client-id\nBOX_CLIENT_SECRET=your-client-secret\n",
636
636
  "lib/box-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst BOX_BASE_URL = \"https://api.box.com/2.0\";\nconst BOX_UPLOAD_URL = \"https://upload.box.com/api/2.0/files/content\";\n\ninterface BoxItemCollection<T> {\n total_count: number;\n entries: T[];\n offset: number;\n limit: number;\n}\n\ninterface BoxFile {\n id: string;\n type: \"file\";\n name: string;\n size: number;\n created_at: string;\n modified_at: string;\n description: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n created_by: {\n id: string;\n name: string;\n };\n modified_by: {\n id: string;\n name: string;\n };\n shared_link?: {\n url: string;\n };\n}\n\ninterface BoxFolder {\n id: string;\n type: \"folder\";\n name: string;\n created_at: string;\n modified_at: string;\n description: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n created_by: {\n id: string;\n name: string;\n };\n modified_by: {\n id: string;\n name: string;\n };\n item_collection?: {\n total_count: number;\n };\n}\n\ntype BoxItem = BoxFile | BoxFolder;\n\ninterface BoxSearchResult {\n type: \"file\" | \"folder\";\n id: string;\n name: string;\n size?: number;\n created_at: string;\n modified_at: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Box. Please connect your account.\");\n }\n return token;\n}\n\nasync function parseErrorMessage(\n response: Response,\n fallback: string,\n): Promise<string> {\n const error = await response.json().catch(() => ({} as { message?: string }));\n return `${fallback}: ${response.status} ${error.message ?? response.statusText}`;\n}\n\nasync function boxFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${BOX_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box API error\"));\n }\n\n return (await response.json()) as T;\n}\n\nfunction toUploadBlob(fileContent: string | Buffer | Blob): Blob {\n if (typeof fileContent === \"string\") {\n return new Blob([fileContent], { type: \"text/plain\" });\n }\n\n if (fileContent instanceof Buffer) {\n return new Blob([fileContent]);\n }\n\n return fileContent;\n}\n\n/**\n * List files and folders in a Box folder\n */\nexport async function listFiles(options: {\n folderId?: string;\n limit?: number;\n offset?: number;\n}): Promise<BoxItem[]> {\n const { folderId = \"0\", limit = 100, offset = 0 } = options;\n\n const params = new URLSearchParams({\n limit: limit.toString(),\n offset: offset.toString(),\n fields:\n \"id,type,name,size,created_at,modified_at,description,path_collection,created_by,modified_by\",\n });\n\n const response = await boxFetch<BoxItemCollection<BoxItem>>(\n `/folders/${folderId}/items?${params}`,\n );\n\n return response.entries;\n}\n\n/**\n * Get details of a specific file or folder\n */\nexport async function getFile(\n itemId: string,\n itemType: \"file\" | \"folder\" = \"file\",\n): Promise<BoxItem> {\n const endpoint = itemType === \"file\" ? `/files/${itemId}` : `/folders/${itemId}`;\n\n const params = new URLSearchParams({\n fields:\n \"id,type,name,size,created_at,modified_at,description,path_collection,created_by,modified_by,shared_link\",\n });\n\n return boxFetch<BoxItem>(`${endpoint}?${params}`);\n}\n\n/**\n * Upload a file to Box\n */\nexport async function uploadFile(options: {\n parentFolderId: string;\n fileName: string;\n fileContent: string | Buffer | Blob;\n}): Promise<BoxFile> {\n const { parentFolderId, fileName, fileContent } = options;\n\n const token = await requireAccessToken();\n\n const formData = new FormData();\n formData.append(\n \"attributes\",\n JSON.stringify({\n name: fileName,\n parent: { id: parentFolderId },\n }),\n );\n formData.append(\"file\", toUploadBlob(fileContent), fileName);\n\n const response = await fetch(BOX_UPLOAD_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n },\n body: formData,\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box upload error\"));\n }\n\n const result = (await response.json()) as { entries: BoxFile[] };\n return result.entries[0];\n}\n\n/**\n * Download a file from Box\n */\nexport async function downloadFile(fileId: string): Promise<{\n content: ArrayBuffer;\n fileName: string;\n mimeType: string;\n}> {\n const token = await requireAccessToken();\n const fileInfo = (await getFile(fileId, \"file\")) as BoxFile;\n\n const response = await fetch(`${BOX_BASE_URL}/files/${fileId}/content`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box download error\"));\n }\n\n const content = await response.arrayBuffer();\n const mimeType = response.headers.get(\"content-type\") ?? \"application/octet-stream\";\n\n return { content, fileName: fileInfo.name, mimeType };\n}\n\n/**\n * Create a new folder in Box\n */\nexport async function createFolder(options: {\n parentFolderId: string;\n name: string;\n}): Promise<BoxFolder> {\n const { parentFolderId, name } = options;\n\n return boxFetch<BoxFolder>(\"/folders\", {\n method: \"POST\",\n body: JSON.stringify({\n name,\n parent: { id: parentFolderId },\n }),\n });\n}\n\n/**\n * Search for files and folders in Box\n */\nexport async function searchFiles(options: {\n query: string;\n limit?: number;\n offset?: number;\n contentTypes?: string[];\n}): Promise<BoxSearchResult[]> {\n const { query, limit = 100, offset = 0, contentTypes } = options;\n\n const params = new URLSearchParams({\n query,\n limit: limit.toString(),\n offset: offset.toString(),\n fields: \"id,type,name,size,created_at,modified_at,path_collection\",\n });\n\n if (contentTypes?.length) {\n params.set(\"content_types\", contentTypes.join(\",\"));\n }\n\n const response = await boxFetch<BoxItemCollection<BoxSearchResult>>(\n `/search?${params}`,\n );\n\n return response.entries;\n}\n\n/**\n * Get current user info\n */\nexport async function getMe(): Promise<{ id: string; name: string; login: string }> {\n return boxFetch<{ id: string; name: string; login: string }>(\"/users/me\");\n}\n",
637
637
  "app/api/auth/box/route.ts": "import { boxConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(boxConfig, { tokenStore: oauthMemoryTokenStore });\n",
638
- "app/api/auth/box/callback/route.ts": "import { boxConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(boxConfig, { tokenStore: hybridTokenStore });\n",
638
+ "app/api/auth/box/callback/route.ts": "import { boxConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(boxConfig, { tokenStore: hybridTokenStore });\n",
639
639
  "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { searchFiles } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in Box by name or content. Returns matching items with their details.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query string to find files and folders\"),\n limit: z.number().min(1).max(100).default(50).describe(\"Maximum number of results to return\"),\n offset: z.number().min(0).default(0).describe(\"Number of results to skip for pagination\"),\n contentTypes: z\n .array(z.string())\n .optional()\n .describe(\"Filter by content types (e.g., ['name', 'description', 'file_content'])\"),\n }),\n async execute({ query, limit, offset, contentTypes }) {\n const results = await searchFiles({ query, limit, offset, contentTypes });\n\n return results.map((item) => {\n const path = item.path_collection?.entries.map((e) => e.name).join(\"/\") ?? \"/\";\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: item.size,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n path,\n };\n });\n },\n});\n",
640
640
  "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getFile } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description: \"Get detailed information about a specific file or folder in Box.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the file or folder\"),\n itemType: z\n .enum([\"file\", \"folder\"])\n .default(\"file\")\n .describe(\"Whether the item is a file or folder\"),\n }),\n async execute({ itemId, itemType }) {\n const item = await getFile(itemId, itemType);\n const isFile = item.type === \"file\";\n const path = item.path_collection?.entries.map((e) => e.name).join(\"/\") ?? \"/\";\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: isFile ? item.size : undefined,\n description: item.description,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n createdBy: item.created_by?.name,\n modifiedBy: item.modified_by?.name,\n path,\n sharedLink: isFile ? item.shared_link?.url : undefined,\n };\n },\n});\n",
641
641
  "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listFiles } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List files and folders from a Box folder. Use folder ID '0' for the root folder.\",\n inputSchema: z.object({\n folderId: z\n .string()\n .default(\"0\")\n .describe(\"Folder ID to list files from (use '0' for root folder)\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of items to return\"),\n offset: z\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of items to skip for pagination\"),\n }),\n async execute({ folderId, limit, offset }) {\n const items = await listFiles({ folderId, limit, offset });\n\n return items.map((item) => {\n const pathEntries = item.path_collection?.entries;\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: item.type === \"file\" ? item.size : undefined,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n createdBy: item.created_by?.name,\n modifiedBy: item.modified_by?.name,\n path: pathEntries ? pathEntries.map((e) => e.name).join(\"/\") : \"/\",\n };\n });\n },\n});\n",
@@ -648,7 +648,7 @@ export default {
648
648
  ".env.example": "# =============================================================================\n# Google Calendar Integration Setup\n# =============================================================================\n#\n# STEP 1: Create a Google Cloud Project\n# Visit: https://console.cloud.google.com/projectcreate\n#\n# STEP 2: Enable the Google Calendar API\n# Visit: https://console.cloud.google.com/apis/library/calendar-json.googleapis.com\n# Click \"Enable\" to activate the Calendar API for your project\n#\n# STEP 3: Configure OAuth Consent Screen\n# Visit: https://console.cloud.google.com/apis/credentials/consent\n# - Choose \"External\" user type (or \"Internal\" for Workspace)\n# - Fill in app name, support email\n# - Add scopes: calendar.readonly, calendar.events\n# - Add your email as a test user (required for development)\n#\n# STEP 4: Create OAuth Credentials\n# Visit: https://console.cloud.google.com/apis/credentials\n# - Click \"Create Credentials\" > \"OAuth client ID\"\n# - Application type: \"Web application\"\n# - Add Authorized redirect URI: http://localhost:3000/api/auth/calendar/callback\n# - Copy the Client ID and Client Secret below\n#\n# =============================================================================\n\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n",
649
649
  "lib/calendar-client.ts": "/**\n * Google Calendar API Client\n *\n * Provides a type-safe interface to Google Calendar API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore - Deno global\n return Deno.env.get(key);\n }\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) {\n // @ts-ignore - process global\n return process.env[key];\n }\n\n return undefined;\n}\n\nconst CALENDAR_API_BASE = \"https://www.googleapis.com/calendar/v3\";\n\nexport interface CalendarEvent {\n id: string;\n summary: string;\n description?: string;\n location?: string;\n start: {\n dateTime?: string;\n date?: string;\n timeZone?: string;\n };\n end: {\n dateTime?: string;\n date?: string;\n timeZone?: string;\n };\n attendees?: Array<{\n email: string;\n responseStatus: \"needsAction\" | \"declined\" | \"tentative\" | \"accepted\";\n displayName?: string;\n }>;\n htmlLink: string;\n status: \"confirmed\" | \"tentative\" | \"cancelled\";\n organizer?: { email: string; displayName?: string };\n}\n\nexport interface CreateEventOptions {\n summary: string;\n description?: string;\n location?: string;\n start: Date | string;\n end: Date | string;\n attendees?: string[];\n timeZone?: string;\n}\n\nexport interface FreeBusySlot {\n start: string;\n end: string;\n}\n\n/**\n * Google Calendar OAuth provider configuration\n */\nexport const calendarOAuthProvider = {\n name: \"calendar\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/calendar.readonly\",\n \"https://www.googleapis.com/auth/calendar.events\",\n ],\n callbackPath: \"/api/auth/calendar/callback\",\n};\n\ntype ListEventsOptions = {\n maxResults?: number;\n timeMin?: Date | string;\n timeMax?: Date | string;\n calendarId?: string;\n};\n\ntype FreeBusyOptions = {\n timeMin: Date | string;\n timeMax: Date | string;\n calendarId?: string;\n};\n\ntype FindFreeSlotsOptions = FreeBusyOptions & {\n durationMinutes: number;\n};\n\ntype CalendarClientShape = {\n listEvents(options?: ListEventsOptions): Promise<CalendarEvent[]>;\n getTodayEvents(): Promise<CalendarEvent[]>;\n createEvent(options: CreateEventOptions, calendarId?: string): Promise<CalendarEvent>;\n getFreeBusy(options: FreeBusyOptions): Promise<FreeBusySlot[]>;\n findFreeSlots(options: FindFreeSlotsOptions): Promise<Array<{ start: Date; end: Date }>>;\n deleteEvent(eventId: string, calendarId?: string): Promise<void>;\n};\n\n/**\n * Create a Calendar client for a specific user\n */\nexport function createCalendarClient(userId: string): CalendarClientShape {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(calendarOAuthProvider, userId, \"calendar\");\n if (!token) {\n throw new Error(\"Calendar not connected. Please connect your Google Calendar first.\");\n }\n return token;\n }\n\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${CALENDAR_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Calendar API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n async function listEvents(options: ListEventsOptions = {}): Promise<CalendarEvent[]> {\n const params = new URLSearchParams();\n\n const timeMin = options.timeMin ? new Date(options.timeMin) : new Date();\n params.set(\"timeMin\", timeMin.toISOString());\n\n if (options.timeMax) {\n params.set(\"timeMax\", new Date(options.timeMax).toISOString());\n }\n\n params.set(\"maxResults\", String(options.maxResults ?? 10));\n params.set(\"singleEvents\", \"true\");\n params.set(\"orderBy\", \"startTime\");\n\n const calendarId = encodeURIComponent(options.calendarId ?? \"primary\");\n const result = await apiRequest<{ items: CalendarEvent[] }>(\n `/calendars/${calendarId}/events?${params.toString()}`,\n );\n\n return result.items ?? [];\n }\n\n function getTodayEvents(): Promise<CalendarEvent[]> {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n\n const tomorrow = new Date(today);\n tomorrow.setDate(tomorrow.getDate() + 1);\n\n return listEvents({ timeMin: today, timeMax: tomorrow, maxResults: 50 });\n }\n\n function createEvent(options: CreateEventOptions, calendarId = \"primary\"): Promise<CalendarEvent> {\n const startDate = typeof options.start === \"string\" ? options.start : options.start.toISOString();\n const endDate = typeof options.end === \"string\" ? options.end : options.end.toISOString();\n const timeZone = options.timeZone ?? \"UTC\";\n\n const event = {\n summary: options.summary,\n description: options.description,\n location: options.location,\n start: { dateTime: startDate, timeZone },\n end: { dateTime: endDate, timeZone },\n attendees: options.attendees?.map((email) => ({ email })),\n };\n\n return apiRequest<CalendarEvent>(`/calendars/${encodeURIComponent(calendarId)}/events`, {\n method: \"POST\",\n body: JSON.stringify(event),\n });\n }\n\n async function getFreeBusy(options: FreeBusyOptions): Promise<FreeBusySlot[]> {\n const calendarId = options.calendarId ?? \"primary\";\n\n const result = await apiRequest<{\n calendars: Record<string, { busy: FreeBusySlot[] }>;\n }>(\"/freeBusy\", {\n method: \"POST\",\n body: JSON.stringify({\n timeMin: new Date(options.timeMin).toISOString(),\n timeMax: new Date(options.timeMax).toISOString(),\n items: [{ id: calendarId }],\n }),\n });\n\n return result.calendars[calendarId]?.busy ?? [];\n }\n\n async function findFreeSlots(\n options: FindFreeSlotsOptions,\n ): Promise<Array<{ start: Date; end: Date }>> {\n const busySlots = await getFreeBusy(options);\n\n const freeSlots: Array<{ start: Date; end: Date }> = [];\n const rangeStart = new Date(options.timeMin);\n const rangeEnd = new Date(options.timeMax);\n const durationMs = options.durationMinutes * 60 * 1000;\n\n let currentStart = rangeStart;\n\n const sortedBusy = busySlots\n .map((s) => ({ start: new Date(s.start), end: new Date(s.end) }))\n .sort((a, b) => a.start.getTime() - b.start.getTime());\n\n for (const busy of sortedBusy) {\n if (busy.start.getTime() - currentStart.getTime() >= durationMs) {\n freeSlots.push({ start: new Date(currentStart), end: new Date(busy.start) });\n }\n\n if (busy.end > currentStart) {\n currentStart = busy.end;\n }\n }\n\n if (rangeEnd.getTime() - currentStart.getTime() >= durationMs) {\n freeSlots.push({ start: new Date(currentStart), end: rangeEnd });\n }\n\n return freeSlots;\n }\n\n async function deleteEvent(eventId: string, calendarId = \"primary\"): Promise<void> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(\n `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`,\n {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${accessToken}` },\n },\n );\n\n if (!response.ok && response.status !== 204) {\n throw new Error(`Failed to delete event: ${response.status}`);\n }\n }\n\n return {\n listEvents,\n getTodayEvents,\n createEvent,\n getFreeBusy,\n findFreeSlots,\n deleteEvent,\n };\n}\n\nexport type CalendarClient = ReturnType<typeof createCalendarClient>;\n",
650
650
  "app/api/auth/calendar/route.ts": "import { calendarConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(calendarConfig, { tokenStore: oauthMemoryTokenStore });\n",
651
- "app/api/auth/calendar/callback/route.ts": "/**\n * Calendar OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { calendarConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(calendarConfig, { tokenStore: hybridTokenStore });\n",
651
+ "app/api/auth/calendar/callback/route.ts": "/**\n * Calendar OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { calendarConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(calendarConfig, { tokenStore: hybridTokenStore });\n",
652
652
  "tools/find-free-time.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createCalendarClient } from \"../../lib/calendar-client.ts\";\n\ntype FreeSlot = { start: Date; end: Date };\n\nexport default tool({\n id: \"find-free-time\",\n description: \"Find available time slots in the calendar for scheduling\",\n inputSchema: z.object({\n durationMinutes: z\n .number()\n .min(15)\n .max(480)\n .default(60)\n .describe(\"Duration needed in minutes\"),\n daysToSearch: z\n .number()\n .min(1)\n .max(14)\n .default(7)\n .describe(\"Number of days to search ahead\"),\n workingHoursOnly: z\n .boolean()\n .default(true)\n .describe(\"Only show slots during working hours (9 AM - 6 PM)\"),\n }),\n execute: async (\n { durationMinutes, daysToSearch, workingHoursOnly },\n context,\n ): Promise<unknown> => {\n // Default to \"current-user\" for development; in production, always pass userId from session\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const calendar = createCalendarClient(userId);\n\n const now = new Date();\n const searchEnd = new Date();\n searchEnd.setDate(searchEnd.getDate() + daysToSearch);\n\n const freeSlots = (await calendar.findFreeSlots({\n timeMin: now,\n timeMax: searchEnd,\n durationMinutes,\n })) as FreeSlot[];\n\n const slots = workingHoursOnly\n ? freeSlots.filter(({ start, end }) => {\n const startHour = start.getHours();\n const endHour = end.getHours();\n return startHour >= 9 && endHour <= 18;\n })\n : freeSlots;\n\n const formattedSlots = slots.slice(0, 10).map(({ start, end }) => {\n const duration = Math.round((end.getTime() - start.getTime()) / 60000);\n\n return {\n start: start.toISOString(),\n end: end.toISOString(),\n durationMinutes: duration,\n date: start.toLocaleDateString(\"en-US\", {\n weekday: \"long\",\n month: \"short\",\n day: \"numeric\",\n }),\n timeRange: `${start.toLocaleTimeString(\"en-US\", {\n hour: \"numeric\",\n minute: \"2-digit\",\n })} - ${end.toLocaleTimeString(\"en-US\", {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}`,\n };\n });\n\n const count = formattedSlots.length;\n\n return {\n freeSlots: formattedSlots,\n count,\n searchCriteria: {\n durationMinutes,\n daysToSearch,\n workingHoursOnly,\n },\n message:\n count > 0\n ? `Found ${count} available slot(s) of ${durationMinutes} minutes or more.`\n : `No free slots of ${durationMinutes} minutes found in the next ${daysToSearch} days.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n",
653
653
  "tools/list-events.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createCalendarClient } from \"../../lib/calendar-client.ts\";\n\ntype CalendarEvent = {\n id: string;\n summary: string;\n description?: string;\n location?: string;\n start: { dateTime?: string; date?: string };\n end: { dateTime?: string; date?: string };\n status: string;\n htmlLink: string;\n attendees?: Array<{ email: string; displayName?: string; responseStatus?: string }>;\n};\n\nexport default tool({\n id: \"list-events\",\n description: \"List upcoming calendar events. By default shows events from now onwards.\",\n inputSchema: z.object({\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of events to return\"),\n daysAhead: z.number().min(1).max(30).default(7).describe(\"Number of days to look ahead\"),\n todayOnly: z.boolean().default(false).describe(\"Only show events for today\"),\n }),\n execute: async ({ maxResults, daysAhead, todayOnly }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const calendar = createCalendarClient(userId);\n\n const events = todayOnly\n ? ((await calendar.getTodayEvents()) as CalendarEvent[])\n : ((await calendar.listEvents({\n maxResults,\n timeMin: new Date(),\n timeMax: new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000),\n })) as CalendarEvent[]);\n\n return {\n events: events.map((event) => ({\n id: event.id,\n title: event.summary,\n description: event.description ?? null,\n location: event.location ?? null,\n start: event.start.dateTime || event.start.date,\n end: event.end.dateTime || event.end.date,\n isAllDay: !event.start.dateTime,\n status: event.status,\n url: event.htmlLink,\n attendees:\n event.attendees?.map((a) => ({\n email: a.email,\n name: a.displayName,\n status: a.responseStatus,\n })) ?? [],\n })),\n count: events.length,\n message: todayOnly\n ? `Found ${events.length} event(s) for today.`\n : `Found ${events.length} event(s) in the next ${daysAhead} days.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n",
654
654
  "tools/create-event.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createCalendarClient } from \"../../lib/calendar-client.ts\";\n\nexport default tool({\n id: \"create-event\",\n description: \"Create a new event in Google Calendar\",\n inputSchema: z.object({\n title: z.string().min(1).describe(\"Event title\"),\n startTime: z\n .string()\n .describe(\"Start time in ISO 8601 format (e.g., '2024-01-15T09:00:00')\"),\n endTime: z\n .string()\n .describe(\"End time in ISO 8601 format (e.g., '2024-01-15T10:00:00')\"),\n description: z.string().optional().describe(\"Event description\"),\n location: z.string().optional().describe(\"Event location\"),\n attendees: z\n .array(z.string().email())\n .optional()\n .describe(\"Email addresses of attendees to invite\"),\n timeZone: z\n .string()\n .default(\"UTC\")\n .describe(\"Time zone for the event (e.g., 'America/New_York')\"),\n }),\n execute: async (\n { title, startTime, endTime, description, location, attendees, timeZone },\n context,\n ) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const calendar = createCalendarClient(userId);\n const event = await calendar.createEvent({\n summary: title,\n start: startTime,\n end: endTime,\n description,\n location,\n attendees,\n timeZone,\n });\n\n return {\n success: true,\n event: {\n id: event.id,\n title: event.summary,\n start: event.start.dateTime ?? event.start.date,\n end: event.end.dateTime ?? event.end.date,\n url: event.htmlLink,\n location: event.location,\n attendees: event.attendees?.map((a: { email: string }) => a.email) ?? [],\n },\n message: `Event \"${title}\" created successfully.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n"
@@ -659,7 +659,7 @@ export default {
659
659
  ".env.example": "# Linear Integration\n# Create an OAuth application at https://linear.app/settings/api\n# Set the callback URL to: http://localhost:3000/api/auth/linear/callback (or your production URL)\n\nLINEAR_CLIENT_ID=your_client_id_here\nLINEAR_CLIENT_SECRET=your_client_secret_here\n",
660
660
  "lib/linear-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst LINEAR_API_URL = \"https://api.linear.app/graphql\";\n\nexport interface LinearIssue {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n priority: number;\n priorityLabel: string;\n state: {\n id: string;\n name: string;\n type: string;\n };\n assignee?: {\n id: string;\n name: string;\n email: string;\n };\n team: {\n id: string;\n name: string;\n key: string;\n };\n project?: {\n id: string;\n name: string;\n };\n labels: {\n nodes: Array<{\n id: string;\n name: string;\n color: string;\n }>;\n };\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearProject {\n id: string;\n name: string;\n description?: string;\n state: string;\n progress: number;\n url: string;\n lead?: {\n id: string;\n name: string;\n };\n teams: {\n nodes: Array<{\n id: string;\n name: string;\n key: string;\n }>;\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LinearTeam {\n id: string;\n name: string;\n key: string;\n}\n\nexport interface LinearWorkflowState {\n id: string;\n name: string;\n type: string;\n}\n\ninterface GraphQLResponse<T> {\n data?: T;\n errors?: Array<{\n message: string;\n path?: string[];\n }>;\n}\n\nasync function linearFetch<T>(query: string, variables?: Record<string, unknown>): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Linear. Please connect your account.\");\n }\n\n const response = await fetch(LINEAR_API_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`Linear API error: ${response.status} ${response.statusText}`);\n }\n\n const json: GraphQLResponse<T> = await response.json();\n\n const errorMessage = json.errors?.[0]?.message;\n if (errorMessage) {\n throw new Error(`Linear GraphQL error: ${errorMessage}`);\n }\n\n if (!json.data) {\n throw new Error(\"Linear API returned no data\");\n }\n\n return json.data;\n}\n\nexport async function searchIssues(\n query: string,\n options?: {\n limit?: number;\n includeArchived?: boolean;\n },\n): Promise<LinearIssue[]> {\n const gqlQuery = `\n query SearchIssues($query: String!, $first: Int, $includeArchived: Boolean) {\n issueSearch(query: $query, first: $first, includeArchived: $includeArchived) {\n nodes {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const data = await linearFetch<{ issueSearch: { nodes: LinearIssue[] } }>(gqlQuery, {\n query,\n first: options?.limit ?? 10,\n includeArchived: options?.includeArchived ?? false,\n });\n\n return data.issueSearch.nodes;\n}\n\nexport async function getIssue(issueId: string): Promise<LinearIssue> {\n const query = `\n query GetIssue($id: String!) {\n issue(id: $id) {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n const data = await linearFetch<{ issue: LinearIssue }>(query, { id: issueId });\n return data.issue;\n}\n\nexport async function createIssue(options: {\n teamId: string;\n title: string;\n description?: string;\n priority?: number;\n stateId?: string;\n assigneeId?: string;\n projectId?: string;\n labelIds?: string[];\n}): Promise<LinearIssue> {\n const mutation = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const input: Record<string, unknown> = {\n teamId: options.teamId,\n title: options.title,\n };\n\n if (options.description) input.description = options.description;\n if (options.priority !== undefined) input.priority = options.priority;\n if (options.stateId) input.stateId = options.stateId;\n if (options.assigneeId) input.assigneeId = options.assigneeId;\n if (options.projectId) input.projectId = options.projectId;\n if (options.labelIds?.length) input.labelIds = options.labelIds;\n\n const data = await linearFetch<{ issueCreate: { success: boolean; issue: LinearIssue } }>(mutation, {\n input,\n });\n\n if (!data.issueCreate.success) {\n throw new Error(\"Failed to create issue\");\n }\n\n return data.issueCreate.issue;\n}\n\nexport async function updateIssue(\n issueId: string,\n options: {\n title?: string;\n description?: string;\n priority?: number;\n stateId?: string;\n assigneeId?: string;\n projectId?: string;\n labelIds?: string[];\n },\n): Promise<LinearIssue> {\n const mutation = `\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const input: Record<string, unknown> = {};\n\n if (options.title) input.title = options.title;\n if (options.description !== undefined) input.description = options.description;\n if (options.priority !== undefined) input.priority = options.priority;\n if (options.stateId) input.stateId = options.stateId;\n if (options.assigneeId) input.assigneeId = options.assigneeId;\n if (options.projectId) input.projectId = options.projectId;\n if (options.labelIds) input.labelIds = options.labelIds;\n\n const data = await linearFetch<{ issueUpdate: { success: boolean; issue: LinearIssue } }>(mutation, {\n id: issueId,\n input,\n });\n\n if (!data.issueUpdate.success) {\n throw new Error(\"Failed to update issue\");\n }\n\n return data.issueUpdate.issue;\n}\n\nexport async function listProjects(options?: {\n limit?: number;\n includeArchived?: boolean;\n}): Promise<LinearProject[]> {\n const query = `\n query ListProjects($first: Int, $includeArchived: Boolean) {\n projects(first: $first, includeArchived: $includeArchived) {\n nodes {\n id\n name\n description\n state\n progress\n url\n lead {\n id\n name\n }\n teams {\n nodes {\n id\n name\n key\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n `;\n\n const data = await linearFetch<{ projects: { nodes: LinearProject[] } }>(query, {\n first: options?.limit ?? 20,\n includeArchived: options?.includeArchived ?? false,\n });\n\n return data.projects.nodes;\n}\n\nexport async function getTeams(): Promise<LinearTeam[]> {\n const query = `\n query GetTeams {\n teams {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const data = await linearFetch<{ teams: { nodes: LinearTeam[] } }>(query);\n return data.teams.nodes;\n}\n\nexport async function getWorkflowStates(teamId: string): Promise<LinearWorkflowState[]> {\n const query = `\n query GetWorkflowStates($teamId: String!) {\n team(id: $teamId) {\n states {\n nodes {\n id\n name\n type\n }\n }\n }\n }\n `;\n\n const data = await linearFetch<{ team: { states: { nodes: LinearWorkflowState[] } } }>(query, {\n teamId,\n });\n\n return data.team.states.nodes;\n}\n",
661
661
  "app/api/auth/linear/route.ts": "import { createOAuthInitHandler, linearConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(linearConfig, { tokenStore: oauthMemoryTokenStore });\n",
662
- "app/api/auth/linear/callback/route.ts": "/**\n * Linear OAuth Callback\n *\n * Handles the OAuth callback from Linear and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, linearConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(linearConfig, { tokenStore: hybridTokenStore });\n",
662
+ "app/api/auth/linear/callback/route.ts": "/**\n * Linear OAuth Callback\n *\n * Handles the OAuth callback from Linear and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, linearConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(linearConfig, { tokenStore: hybridTokenStore });\n",
663
663
  "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { searchIssues } from \"../../lib/linear-client.ts\";\n\nexport default tool({\n id: \"search-issues\",\n description:\n \"Search for Linear issues by title or description. Returns matching issues with their details including status, assignee, and team.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query to find issues (searches in title and description)\"),\n limit: z.number().min(1).max(50).default(10).describe(\"Maximum number of results to return\"),\n includeArchived: z\n .boolean()\n .default(false)\n .describe(\"Whether to include archived issues in results\"),\n }),\n async execute({ query, limit, includeArchived }) {\n const issues = await searchIssues(query, { limit, includeArchived });\n\n return issues.map((issue) => {\n const assignee = issue.assignee\n ? { name: issue.assignee.name, email: issue.assignee.email }\n : null;\n\n const project = issue.project ? { name: issue.project.name } : null;\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n status: issue.state.name,\n statusType: issue.state.type,\n assignee,\n team: { name: issue.team.name, key: issue.team.key },\n project,\n labels: issue.labels.nodes.map((label) => ({\n name: label.name,\n color: label.color,\n })),\n url: issue.url,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n };\n });\n },\n});\n",
664
664
  "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createIssue } from \"../../lib/linear-client.ts\";\n\nexport default tool({\n id: \"create-issue\",\n description:\n \"Create a new Linear issue in a specified team. You can optionally set priority, assign to someone, add to a project, and attach labels.\",\n inputSchema: z.object({\n teamId: z\n .string()\n .describe(\n \"The ID of the team to create the issue in. Use list-projects tool first if you need to find team IDs.\",\n ),\n title: z.string().describe(\"Title of the issue\"),\n description: z\n .string()\n .optional()\n .describe(\"Detailed description of the issue (supports markdown)\"),\n priority: z\n .number()\n .min(0)\n .max(4)\n .optional()\n .describe(\"Priority level: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low\"),\n stateId: z\n .string()\n .optional()\n .describe('Workflow state ID (e.g., \"Todo\", \"In Progress\", \"Done\")'),\n assigneeId: z.string().optional().describe(\"User ID to assign the issue to\"),\n projectId: z.string().optional().describe(\"Project ID to add the issue to\"),\n labelIds: z\n .array(z.string())\n .optional()\n .describe(\"Array of label IDs to attach to the issue\"),\n }),\n async execute(\n { teamId, title, description, priority, stateId, assigneeId, projectId, labelIds },\n ) {\n const issue = await createIssue({\n teamId,\n title,\n description,\n priority,\n stateId,\n assigneeId,\n projectId,\n labelIds,\n });\n\n const assignee = issue.assignee\n ? { name: issue.assignee.name, email: issue.assignee.email }\n : null;\n\n const project = issue.project ? { name: issue.project.name } : null;\n\n const labels = issue.labels.nodes.map((label) => ({\n name: label.name,\n color: label.color,\n }));\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n status: issue.state.name,\n assignee,\n team: {\n name: issue.team.name,\n key: issue.team.key,\n },\n project,\n labels,\n url: issue.url,\n createdAt: issue.createdAt,\n };\n },\n});\n",
665
665
  "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listProjects } from \"../../lib/linear-client.ts\";\n\nexport default tool({\n id: \"list-projects\",\n description:\n \"List all projects in the Linear workspace. Returns project details including name, state, progress, and associated teams.\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of projects to return\"),\n includeArchived: z\n .boolean()\n .default(false)\n .describe(\"Whether to include archived projects in results\"),\n }),\n async execute({ limit, includeArchived }) {\n const projects = await listProjects({ limit, includeArchived });\n\n return projects.map((project) => ({\n id: project.id,\n name: project.name,\n description: project.description,\n state: project.state,\n progress: Math.round(project.progress * 100),\n url: project.url,\n lead: project.lead\n ? { id: project.lead.id, name: project.lead.name }\n : null,\n teams: project.teams.nodes.map((team) => ({\n id: team.id,\n name: team.name,\n key: team.key,\n })),\n createdAt: project.createdAt,\n updatedAt: project.updatedAt,\n }));\n },\n});\n",
@@ -693,7 +693,7 @@ export default {
693
693
  ".env.example": "# QuickBooks OAuth Configuration\n# Get your credentials from https://developer.intuit.com/app/developer/myapps\nQUICKBOOKS_CLIENT_ID=your-client-id\nQUICKBOOKS_CLIENT_SECRET=your-client-secret\n",
694
694
  "lib/quickbooks-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst QUICKBOOKS_BASE_URL = \"https://quickbooks.api.intuit.com/v3\";\n\nfunction getRealmId(): string {\n const realmId = process.env.QUICKBOOKS_REALM_ID;\n if (!realmId) {\n throw new Error(\"QUICKBOOKS_REALM_ID environment variable is required\");\n }\n return realmId;\n}\n\ninterface QuickBooksResponse<T> {\n QueryResponse?: {\n [key: string]: T[] | number | undefined;\n maxResults?: number;\n startPosition?: number;\n };\n Invoice?: T;\n Customer?: T;\n time?: string;\n}\n\ninterface QuickBooksInvoice {\n Id: string;\n DocNumber: string;\n TxnDate: string;\n DueDate?: string;\n TotalAmt: number;\n Balance: number;\n CustomerRef: {\n value: string;\n name: string;\n };\n Line: Array<{\n Id: string;\n LineNum: number;\n Description?: string;\n Amount: number;\n DetailType: string;\n SalesItemLineDetail?: {\n ItemRef: {\n value: string;\n name: string;\n };\n Qty?: number;\n UnitPrice?: number;\n };\n }>;\n EmailStatus?: string;\n BillEmail?: {\n Address: string;\n };\n TxnStatus?: string;\n MetaData?: {\n CreateTime: string;\n LastUpdatedTime: string;\n };\n}\n\ninterface QuickBooksCustomer {\n Id: string;\n DisplayName: string;\n CompanyName?: string;\n GivenName?: string;\n FamilyName?: string;\n PrimaryEmailAddr?: {\n Address: string;\n };\n PrimaryPhone?: {\n FreeFormNumber: string;\n };\n BillAddr?: {\n Line1?: string;\n City?: string;\n CountrySubDivisionCode?: string;\n PostalCode?: string;\n };\n Balance: number;\n Active: boolean;\n MetaData?: {\n CreateTime: string;\n LastUpdatedTime: string;\n };\n}\n\nasync function quickbooksFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with QuickBooks. Please connect your account.\");\n }\n\n const url = `${QUICKBOOKS_BASE_URL}/company/${getRealmId()}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const message = error.Fault?.Error?.[0]?.Message ?? response.statusText;\n throw new Error(`QuickBooks API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listInvoices(options?: {\n customerId?: string;\n maxResults?: number;\n}): Promise<QuickBooksInvoice[]> {\n const maxResults = options?.maxResults ?? 100;\n\n let query = `SELECT * FROM Invoice MAXRESULTS ${maxResults}`;\n if (options?.customerId) {\n query = `SELECT * FROM Invoice WHERE CustomerRef = '${options.customerId}' MAXRESULTS ${maxResults}`;\n }\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\n `/query?query=${encodeURIComponent(query)}`,\n );\n\n return response.QueryResponse?.Invoice ?? [];\n}\n\nexport async function getInvoice(invoiceId: string): Promise<QuickBooksInvoice> {\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\n `/invoice/${invoiceId}`,\n );\n\n const invoice = response.Invoice;\n if (!invoice) {\n throw new Error(`Invoice ${invoiceId} not found`);\n }\n\n return invoice;\n}\n\nexport async function createInvoice(options: {\n customerId: string;\n lineItems: Array<{\n description?: string;\n amount: number;\n itemId?: string;\n quantity?: number;\n unitPrice?: number;\n }>;\n txnDate?: string;\n dueDate?: string;\n customerMemo?: string;\n}): Promise<QuickBooksInvoice> {\n const lines = options.lineItems.map((item, index) => {\n const line: Record<string, unknown> = {\n LineNum: index + 1,\n Amount: item.amount,\n DetailType: \"SalesItemLineDetail\",\n };\n\n if (item.description) {\n line.Description = item.description;\n }\n\n if (item.itemId) {\n line.SalesItemLineDetail = {\n ItemRef: { value: item.itemId },\n Qty: item.quantity ?? 1,\n UnitPrice: item.unitPrice ?? item.amount,\n };\n }\n\n return line;\n });\n\n const invoiceData: Record<string, unknown> = {\n CustomerRef: { value: options.customerId },\n Line: lines,\n };\n\n if (options.txnDate) invoiceData.TxnDate = options.txnDate;\n if (options.dueDate) invoiceData.DueDate = options.dueDate;\n if (options.customerMemo) invoiceData.CustomerMemo = { value: options.customerMemo };\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\"/invoice\", {\n method: \"POST\",\n body: JSON.stringify(invoiceData),\n });\n\n const invoice = response.Invoice;\n if (!invoice) {\n throw new Error(\"Failed to create invoice\");\n }\n\n return invoice;\n}\n\nexport async function listCustomers(options?: {\n maxResults?: number;\n active?: boolean;\n}): Promise<QuickBooksCustomer[]> {\n const maxResults = options?.maxResults ?? 100;\n\n let query = `SELECT * FROM Customer MAXRESULTS ${maxResults}`;\n if (options?.active !== undefined) {\n query = `SELECT * FROM Customer WHERE Active = ${options.active} MAXRESULTS ${maxResults}`;\n }\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksCustomer>>(\n `/query?query=${encodeURIComponent(query)}`,\n );\n\n return response.QueryResponse?.Customer ?? [];\n}\n\nexport async function getCustomer(customerId: string): Promise<QuickBooksCustomer> {\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksCustomer>>(\n `/customer/${customerId}`,\n );\n\n const customer = response.Customer;\n if (!customer) {\n throw new Error(`Customer ${customerId} not found`);\n }\n\n return customer;\n}\n",
695
695
  "app/api/auth/quickbooks/route.ts": "import { createOAuthInitHandler, quickbooksConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(quickbooksConfig, { tokenStore: oauthMemoryTokenStore });\n",
696
- "app/api/auth/quickbooks/callback/route.ts": "import { createOAuthCallbackHandler, quickbooksConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(quickbooksConfig, { tokenStore: hybridTokenStore });\n",
696
+ "app/api/auth/quickbooks/callback/route.ts": "import { createOAuthCallbackHandler, quickbooksConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(quickbooksConfig, { tokenStore: hybridTokenStore });\n",
697
697
  "tools/list-invoices.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listInvoices } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"list-invoices\",\n description: \"List invoices from QuickBooks. Can optionally filter by customer ID.\",\n inputSchema: z.object({\n customerId: z.string().optional().describe(\"Customer ID to filter invoices by\"),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of invoices to return\"),\n }),\n async execute({ customerId, maxResults }) {\n const invoices = await listInvoices({ customerId, maxResults });\n\n return invoices.map((invoice) => {\n const customerRef = invoice.CustomerRef;\n\n return {\n id: invoice.Id,\n docNumber: invoice.DocNumber,\n txnDate: invoice.TxnDate,\n dueDate: invoice.DueDate,\n totalAmount: invoice.TotalAmt,\n balance: invoice.Balance,\n customer: {\n id: customerRef.value,\n name: customerRef.name,\n },\n status: invoice.TxnStatus,\n emailStatus: invoice.EmailStatus,\n lineItems: invoice.Line.map((line) => {\n const detail = line.SalesItemLineDetail;\n\n return {\n description: line.Description,\n amount: line.Amount,\n quantity: detail?.Qty,\n unitPrice: detail?.UnitPrice,\n };\n }),\n };\n });\n },\n});\n",
698
698
  "tools/create-invoice.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createInvoice } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"create-invoice\",\n description: \"Create a new invoice in QuickBooks.\",\n inputSchema: z.object({\n customerId: z.string().describe(\"The ID of the customer to invoice\"),\n lineItems: z\n .array(\n z.object({\n description: z.string().optional().describe(\"Description of the line item\"),\n amount: z.number().describe(\"Total amount for this line item\"),\n itemId: z.string().optional().describe(\"QuickBooks item/service ID\"),\n quantity: z.number().optional().describe(\"Quantity of items\"),\n unitPrice: z.number().optional().describe(\"Price per unit\"),\n }),\n )\n .describe(\"Line items for the invoice\"),\n txnDate: z.string().optional().describe(\"Transaction date in YYYY-MM-DD format\"),\n dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n customerMemo: z.string().optional().describe(\"Memo/note for the customer\"),\n }),\n async execute({ customerId, lineItems, txnDate, dueDate, customerMemo }) {\n const invoice = await createInvoice({\n customerId,\n lineItems,\n txnDate,\n dueDate,\n customerMemo,\n });\n\n return {\n success: true,\n invoice: {\n id: invoice.Id,\n docNumber: invoice.DocNumber,\n txnDate: invoice.TxnDate,\n dueDate: invoice.DueDate,\n totalAmount: invoice.TotalAmt,\n balance: invoice.Balance,\n customer: {\n id: invoice.CustomerRef.value,\n name: invoice.CustomerRef.name,\n },\n },\n };\n },\n});\n",
699
699
  "tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listCustomers } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"list-customers\",\n description:\n \"List customers from QuickBooks. Can optionally filter by active status.\",\n inputSchema: z.object({\n active: z\n .boolean()\n .optional()\n .describe(\n \"Filter by active status (true for active, false for inactive)\",\n ),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of customers to return\"),\n }),\n async execute({ active, maxResults }) {\n const customers = await listCustomers({ active, maxResults });\n\n return customers.map((customer) => {\n const billAddr = customer.BillAddr;\n\n const address = billAddr\n ? {\n line1: billAddr.Line1,\n city: billAddr.City,\n state: billAddr.CountrySubDivisionCode,\n postalCode: billAddr.PostalCode,\n }\n : undefined;\n\n return {\n id: customer.Id,\n displayName: customer.DisplayName,\n companyName: customer.CompanyName,\n givenName: customer.GivenName,\n familyName: customer.FamilyName,\n email: customer.PrimaryEmailAddr?.Address,\n phone: customer.PrimaryPhone?.FreeFormNumber,\n address,\n balance: customer.Balance,\n active: customer.Active,\n };\n });\n },\n});\n",
@@ -706,7 +706,7 @@ export default {
706
706
  ".env.example": "# Intercom OAuth Configuration\n# Get your credentials from https://developers.intercom.com/building-apps/\nINTERCOM_CLIENT_ID=your-client-id\nINTERCOM_CLIENT_SECRET=your-client-secret\n",
707
707
  "lib/intercom-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst INTERCOM_BASE_URL = \"https://api.intercom.io\";\n\ninterface IntercomResponse<T> {\n type: string;\n data?: T;\n pages?: {\n next?: string | null;\n page: number;\n per_page: number;\n total_pages: number;\n };\n}\n\ninterface IntercomContact {\n type: \"contact\";\n id: string;\n external_id?: string;\n email?: string;\n phone?: string;\n name?: string;\n avatar?: string;\n role?: string;\n created_at: number;\n updated_at: number;\n signed_up_at?: number;\n last_seen_at?: number;\n owner_id?: number;\n custom_attributes?: Record<string, unknown>;\n tags?: Array<{ id: string; name: string }>;\n}\n\ninterface IntercomConversation {\n type: \"conversation\";\n id: string;\n created_at: number;\n updated_at: number;\n source: {\n type: string;\n id: string;\n delivered_as: string;\n subject?: string;\n body?: string;\n author: {\n type: string;\n id: string;\n name?: string;\n email?: string;\n };\n };\n contacts?: Array<{\n type: string;\n id: string;\n }>;\n teammates?: Array<{\n type: string;\n id: string;\n }>;\n title?: string;\n state: \"open\" | \"closed\" | \"snoozed\";\n read: boolean;\n waiting_since?: number;\n snoozed_until?: number;\n priority?: \"priority\" | \"not_priority\";\n conversation_parts?: {\n type: string;\n conversation_parts: Array<{\n type: string;\n id: string;\n part_type: string;\n body: string;\n created_at: number;\n updated_at: number;\n author: {\n type: string;\n id: string;\n name?: string;\n email?: string;\n };\n }>;\n };\n}\n\ninterface IntercomMessageRequest {\n message_type: \"inapp\" | \"email\" | \"comment\";\n body: string;\n from: {\n type: \"admin\" | \"user\" | \"contact\";\n id: string;\n };\n to?: {\n type: \"user\" | \"contact\";\n id?: string;\n email?: string;\n };\n}\n\nfunction hasMorePages(pages?: IntercomResponse<unknown>[\"pages\"]): boolean {\n return pages ? pages.page < pages.total_pages : false;\n}\n\nasync function intercomFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Intercom. Please connect your account.\");\n\n const response = await fetch(`${INTERCOM_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n \"Intercom-Version\": \"2.11\",\n ...options.headers,\n },\n });\n\n if (response.ok) return (await response.json()) as T;\n\n const error = (await response.json().catch(() => ({}))) as {\n errors?: Array<{ message?: string }>;\n message?: string;\n };\n\n const message = error.errors?.[0]?.message ?? error.message ?? response.statusText;\n throw new Error(`Intercom API error: ${response.status} ${message}`);\n}\n\nexport async function listContacts(\n options: { page?: number; perPage?: number } = {},\n): Promise<{ contacts: IntercomContact[]; hasMore: boolean }> {\n const params = new URLSearchParams({ per_page: String(options.perPage ?? 50) });\n if (options.page) params.set(\"page\", String(options.page));\n\n const response = await intercomFetch<IntercomResponse<IntercomContact[]>>(`/contacts?${params}`);\n\n return { contacts: response.data ?? [], hasMore: hasMorePages(response.pages) };\n}\n\nexport async function getContact(contactId: string): Promise<IntercomContact> {\n return intercomFetch<IntercomContact>(`/contacts/${contactId}`);\n}\n\nexport async function searchContacts(query: { email?: string; name?: string }): Promise<IntercomContact[]> {\n const value: Array<Record<string, unknown>> = [];\n\n if (query.email) value.push({ field: \"email\", operator: \"=\", value: query.email });\n if (query.name) value.push({ field: \"name\", operator: \"~\", value: query.name });\n\n const response = await intercomFetch<IntercomResponse<IntercomContact[]>>(\"/contacts/search\", {\n method: \"POST\",\n body: JSON.stringify({ query: { operator: \"AND\", value } }),\n });\n\n return response.data ?? [];\n}\n\nexport async function listConversations(\n options: { page?: number; perPage?: number; open?: boolean } = {},\n): Promise<{ conversations: IntercomConversation[]; hasMore: boolean }> {\n const params = new URLSearchParams({\n per_page: String(options.perPage ?? 50),\n display_as: \"plaintext\",\n });\n\n if (options.page) params.set(\"page\", String(options.page));\n if (options.open !== undefined) params.set(\"state\", options.open ? \"open\" : \"closed\");\n\n const response = await intercomFetch<IntercomResponse<IntercomConversation[]>>(\n `/conversations?${params}`,\n );\n\n return { conversations: response.data ?? [], hasMore: hasMorePages(response.pages) };\n}\n\nexport async function getConversation(conversationId: string): Promise<IntercomConversation> {\n return intercomFetch<IntercomConversation>(`/conversations/${conversationId}`);\n}\n\nexport async function sendMessage(options: {\n conversationId?: string;\n body: string;\n messageType?: \"comment\" | \"note\";\n adminId?: string;\n}): Promise<IntercomConversation> {\n if (!options.conversationId) throw new Error(\"conversationId is required to send a message\");\n\n const payload: Record<string, unknown> = {\n message_type: options.messageType ?? \"comment\",\n type: \"admin\",\n body: options.body,\n ...(options.adminId ? { admin_id: options.adminId } : {}),\n };\n\n return intercomFetch<IntercomConversation>(`/conversations/${options.conversationId}/reply`, {\n method: \"POST\",\n body: JSON.stringify(payload),\n });\n}\n\nexport async function createMessage(options: {\n contactId?: string;\n email?: string;\n body: string;\n messageType: \"inapp\" | \"email\";\n fromId: string;\n}): Promise<{ type: string; id: string }> {\n const messageBody: IntercomMessageRequest = {\n message_type: options.messageType,\n body: options.body,\n from: { type: \"admin\", id: options.fromId },\n };\n\n if (options.contactId) {\n messageBody.to = { type: \"contact\", id: options.contactId };\n } else if (options.email) {\n messageBody.to = { type: \"contact\", email: options.email };\n } else {\n throw new Error(\"Either contactId or email is required\");\n }\n\n return intercomFetch<{ type: string; id: string }>(\"/messages\", {\n method: \"POST\",\n body: JSON.stringify(messageBody),\n });\n}\n\nexport async function getMe(): Promise<{ type: string; id: string; name: string; email: string }> {\n return intercomFetch<{ type: string; id: string; name: string; email: string }>(\"/me\");\n}\n",
708
708
  "app/api/auth/intercom/route.ts": "import { createOAuthInitHandler, intercomConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nexport const GET = createOAuthInitHandler(intercomConfig, { tokenStore: oauthMemoryTokenStore });\n",
709
- "app/api/auth/intercom/callback/route.ts": "import { createOAuthCallbackHandler, intercomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(intercomConfig, { tokenStore: hybridTokenStore });\n",
709
+ "app/api/auth/intercom/callback/route.ts": "import { createOAuthCallbackHandler, intercomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT)\nconst USER_ID = \"current-user\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string) {\n return tokenStore.getToken(USER_ID, serviceId);\n },\n async setTokens(\n serviceId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(USER_ID, serviceId, tokens);\n },\n async clearTokens(serviceId: string) {\n await tokenStore.revokeToken(USER_ID, serviceId);\n },\n getState(state: string) {\n return oauthMemoryTokenStore.getState(state);\n },\n setState(state: { state: string; codeVerifier?: string; createdAt: number }) {\n return oauthMemoryTokenStore.setState(state);\n },\n clearState(state: string) {\n return oauthMemoryTokenStore.clearState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(intercomConfig, { tokenStore: hybridTokenStore });\n",
710
710
  "tools/list-conversations.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listConversations } from \"../../lib/intercom-client.ts\";\n\nfunction toIsoSeconds(seconds?: number | null): string | null {\n if (seconds == null) return null;\n return new Date(seconds * 1000).toISOString();\n}\n\nexport default tool({\n id: \"list-conversations\",\n description: \"List conversations from Intercom. Can filter by open/closed status.\",\n inputSchema: z.object({\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n perPage: z\n .number()\n .min(1)\n .max(150)\n .default(50)\n .describe(\"Number of conversations per page (max 150)\"),\n open: z\n .boolean()\n .optional()\n .describe(\"Filter by open (true) or closed (false) conversations\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of conversations to return\"),\n }),\n async execute({ page, perPage, open, limit }) {\n const { conversations, hasMore } = await listConversations({ page, perPage, open });\n\n return {\n conversations: conversations.slice(0, limit).map((conv) => ({\n id: conv.id,\n title: conv.title,\n state: conv.state,\n read: conv.read,\n priority: conv.priority,\n createdAt: toIsoSeconds(conv.created_at) as string,\n updatedAt: toIsoSeconds(conv.updated_at) as string,\n waitingSince: toIsoSeconds(conv.waiting_since),\n snoozedUntil: toIsoSeconds(conv.snoozed_until),\n source: {\n type: conv.source.type,\n subject: conv.source.subject,\n body: conv.source.body,\n author: {\n id: conv.source.author.id,\n name: conv.source.author.name,\n email: conv.source.author.email,\n },\n },\n contactIds: conv.contacts?.map((c) => c.id),\n teammateIds: conv.teammates?.map((t) => t.id),\n })),\n hasMore,\n page,\n };\n },\n});\n",
711
711
  "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listContacts } from \"../../lib/intercom-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List contacts from Intercom workspace. Returns contact information including email, name, and metadata.\",\n inputSchema: z.object({\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n perPage: z\n .number()\n .min(1)\n .max(150)\n .default(50)\n .describe(\"Number of contacts per page (max 150)\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of contacts to return\"),\n }),\n async execute({ page, perPage, limit }) {\n const { contacts, hasMore } = await listContacts({ page, perPage });\n\n return {\n contacts: contacts.slice(0, limit).map((contact) => {\n const createdAt = new Date(contact.created_at * 1000).toISOString();\n const updatedAt = new Date(contact.updated_at * 1000).toISOString();\n const lastSeenAt = contact.last_seen_at\n ? new Date(contact.last_seen_at * 1000).toISOString()\n : null;\n\n return {\n id: contact.id,\n email: contact.email,\n name: contact.name,\n phone: contact.phone,\n role: contact.role,\n createdAt,\n updatedAt,\n lastSeenAt,\n ownerId: contact.owner_id,\n tags: contact.tags?.map((tag) => tag.name),\n };\n }),\n hasMore,\n page,\n };\n },\n});\n",
712
712
  "tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getContact } from \"../../lib/intercom-client.ts\";\n\nfunction toIsoOrNull(timestamp?: number | null): string | null {\n if (!timestamp) return null;\n return new Date(timestamp * 1000).toISOString();\n}\n\nfunction toIso(timestamp: number): string {\n return new Date(timestamp * 1000).toISOString();\n}\n\nexport default tool({\n id: \"get-contact\",\n description: \"Get details of a specific contact from Intercom by their ID.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The ID of the contact to retrieve\"),\n }),\n async execute({ contactId }) {\n const contact = await getContact(contactId);\n\n return {\n id: contact.id,\n email: contact.email,\n name: contact.name,\n phone: contact.phone,\n role: contact.role,\n externalId: contact.external_id,\n avatar: contact.avatar,\n createdAt: toIso(contact.created_at),\n updatedAt: toIso(contact.updated_at),\n signedUpAt: toIsoOrNull(contact.signed_up_at),\n lastSeenAt: toIsoOrNull(contact.last_seen_at),\n ownerId: contact.owner_id,\n customAttributes: contact.custom_attributes,\n tags: contact.tags?.map((tag) => ({ id: tag.id, name: tag.name })),\n };\n },\n});\n",