feat(wealthbox): added wealthbox crm (#669)

* feat: wealthbox

* feat: added tools

* feat: tested and finished tools

* feat: tested and finished tools

* feat: added refresh token

* fix: added docs

* bun lint

* feat: removed files #669

* fix: greptile comments

* fix: stringified messages #669

* add visibilty to params

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Adam Gough
2025-07-14 13:07:55 -07:00
committed by GitHub
parent 348b524d86
commit d65bdaf546
23 changed files with 2949 additions and 0 deletions

View File

@@ -48,6 +48,7 @@
"twilio_sms",
"typeform",
"vision",
"wealthbox",
"whatsapp",
"x",
"youtube"

View File

@@ -0,0 +1,190 @@
---
title: Wealthbox
description: Interact with Wealthbox
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="wealthbox"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
version='1.0'
viewBox='50 -50 200 200'
>
<g fill='#106ED4' stroke='none' transform='translate(0, 200) scale(0.15, -0.15)'>
<path d='M764 1542 c-110 -64 -230 -134 -266 -156 -42 -24 -71 -49 -78 -65 -7 -19 -10 -126 -8 -334 3 -291 4 -307 23 -326 11 -11 103 -67 205 -126 102 -59 219 -127 261 -151 42 -24 85 -44 96 -44 23 0 527 288 561 320 22 22 22 23 22 340 0 288 -2 320 -17 338 -32 37 -537 322 -569 321 -18 0 -107 -46 -230 -117z m445 -144 c108 -62 206 -123 219 -135 22 -22 22 -26 22 -261 0 -214 -2 -242 -17 -260 -23 -26 -414 -252 -437 -252 -9 0 -70 31 -134 69 -64 37 -161 94 -215 125 l-97 57 2 261 3 261 210 123 c116 67 219 123 229 123 10 1 107 -50 215 -111z' />
<path d='M700 1246 l-55 -32 -3 -211 -2 -211 37 -23 c21 -12 52 -30 69 -40 l30 -18 103 59 c56 33 109 60 117 60 8 0 62 -27 119 -60 l104 -60 63 37 c35 21 66 42 70 48 4 5 8 101 8 212 l0 202 -62 35 -63 35 -3 -197 c-1 -108 -6 -200 -11 -205 -5 -5 -54 17 -114 52 -58 34 -108 61 -111 61 -2 0 -51 -27 -107 -60 -56 -32 -106 -57 -111 -54 -4 3 -8 95 -8 205 0 109 -3 199 -7 199 -5 -1 -33 -16 -63 -34z' />
</g>
</svg>`}
/>
## Usage Instructions
Integrate Wealthbox functionality to manage notes, contacts, and tasks. Read content from existing notes, contacts, and tasks and write to them using OAuth authentication. Supports text content manipulation for note creation and editing.
## Tools
### `wealthbox_read_note`
Read content from a Wealthbox note
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `noteId` | string | No | The ID of the note to read \(optional\) |
#### Output
| Parameter | Type |
| --------- | ---- |
| `note` | string |
| `metadata` | string |
| `noteId` | string |
| `itemType` | string |
### `wealthbox_write_note`
Create or update a Wealthbox note
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `content` | string | Yes | The main body of the note |
| `contactId` | string | No | ID of contact to link to this note |
#### Output
| Parameter | Type |
| --------- | ---- |
| `note` | string |
| `metadata` | string |
| `itemType` | string |
### `wealthbox_read_contact`
Read content from a Wealthbox contact
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `contactId` | string | Yes | The ID of the contact to read |
#### Output
| Parameter | Type |
| --------- | ---- |
| `contact` | string |
| `metadata` | string |
| `contactId` | string |
| `itemType` | string |
### `wealthbox_write_contact`
Create a new Wealthbox contact
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `firstName` | string | Yes | The first name of the contact |
| `lastName` | string | Yes | The last name of the contact |
| `emailAddress` | string | No | The email address of the contact |
| `backgroundInformation` | string | No | Background information about the contact |
#### Output
| Parameter | Type |
| --------- | ---- |
| `contact` | string |
| `metadata` | string |
| `itemType` | string |
### `wealthbox_read_task`
Read content from a Wealthbox task
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `taskId` | string | No | The ID of the task to read \(optional\) |
#### Output
| Parameter | Type |
| --------- | ---- |
| `task` | string |
| `metadata` | string |
| `taskId` | string |
| `itemType` | string |
### `wealthbox_write_task`
Create or update a Wealthbox task
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `title` | string | Yes | The name/title of the task |
| `dueDate` | string | Yes | The due date and time of the task |
| `complete` | boolean | No | Whether the task is complete |
| `category` | number | No | The category ID the task belongs to |
| `contactId` | string | No | ID of contact to link to this task |
#### Output
| Parameter | Type |
| --------- | ---- |
| `task` | string |
| `metadata` | string |
| `taskId` | string |
| `itemType` | string |
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `operation` | string | Yes | Operation |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `note` | any | note output from the block |
| `notes` | any | notes output from the block |
| `contact` | any | contact output from the block |
| `contacts` | any | contacts output from the block |
| `task` | any | task output from the block |
| `tasks` | any | tasks output from the block |
| `metadata` | json | metadata output from the block |
| `success` | any | success output from the block |
## Notes
- Category: `tools`
- Type: `wealthbox`

View File

@@ -0,0 +1,153 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('WealthboxItemAPI')
/**
* Get a single item (note, contact, task) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get parameters from query
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const itemId = searchParams.get('itemId')
const type = searchParams.get('type') || 'contact'
if (!credentialId || !itemId) {
logger.warn(`[${requestId}] Missing required parameters`, { credentialId, itemId })
return NextResponse.json({ error: 'Credential ID and Item ID are required' }, { status: 400 })
}
// Validate item type - only handle contacts now
if (type !== 'contact') {
logger.warn(`[${requestId}] Invalid item type: ${type}`)
return NextResponse.json(
{ error: 'Invalid item type. Only contact is supported.' },
{ status: 400 }
)
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Determine the endpoint based on item type - only contacts
const endpoints = {
contact: 'contacts',
}
const endpoint = endpoints[type as keyof typeof endpoints]
logger.info(`[${requestId}] Fetching ${type} ${itemId} from Wealthbox`)
// Make request to Wealthbox API
const response = await fetch(`https://api.crmworkspace.com/v1/${endpoint}/${itemId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
{
error: errorText,
endpoint,
itemId,
}
)
if (response.status === 404) {
return NextResponse.json({ error: 'Item not found' }, { status: 404 })
}
return NextResponse.json(
{ error: `Failed to fetch ${type} from Wealthbox` },
{ status: response.status }
)
}
const data = await response.json()
logger.info(`[${requestId}] Wealthbox API response structure`, {
type,
dataKeys: Object.keys(data || {}),
hasContacts: !!data.contacts,
totalCount: data.meta?.total_count,
})
// Transform the response to match our expected format
let items: any[] = []
if (type === 'contact') {
// Handle single contact response - API returns contact data directly when fetching by ID
if (data?.id) {
// Single contact response
const item = {
id: data.id?.toString() || '',
name: `${data.first_name || ''} ${data.last_name || ''}`.trim() || `Contact ${data.id}`,
type: 'contact',
content: data.background_info || '',
createdAt: data.created_at,
updatedAt: data.updated_at,
}
items = [item]
} else {
logger.warn(`[${requestId}] Unexpected contact response format`, { data })
items = []
}
}
logger.info(
`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox (total: ${data.meta?.total_count || 'unknown'})`
)
return NextResponse.json({ item: items[0] }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Wealthbox item`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,168 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('WealthboxItemsAPI')
/**
* Get items (notes, contacts, tasks) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get parameters from query
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const type = searchParams.get('type') || 'contact'
const query = searchParams.get('query') || ''
if (!credentialId) {
logger.warn(`[${requestId}] Missing credential ID`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Validate item type - only handle contacts now
if (type !== 'contact') {
logger.warn(`[${requestId}] Invalid item type: ${type}`)
return NextResponse.json(
{ error: 'Invalid item type. Only contact is supported.' },
{ status: 400 }
)
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Use correct endpoints based on documentation - only for contacts
const endpoints = {
contact: 'contacts',
}
const endpoint = endpoints[type as keyof typeof endpoints]
// Build URL - using correct API base URL
const url = new URL(`https://api.crmworkspace.com/v1/${endpoint}`)
logger.info(`[${requestId}] Fetching ${type}s from Wealthbox`, {
endpoint,
url: url.toString(),
hasQuery: !!query.trim(),
})
// Make request to Wealthbox API
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
{
error: errorText,
endpoint,
url: url.toString(),
}
)
return NextResponse.json(
{ error: `Failed to fetch ${type}s from Wealthbox` },
{ status: response.status }
)
}
const data = await response.json()
logger.info(`[${requestId}] Wealthbox API response structure`, {
type,
status: response.status,
dataKeys: Object.keys(data || {}),
hasContacts: !!data.contacts,
dataStructure: typeof data === 'object' ? Object.keys(data) : 'not an object',
})
// Transform the response based on type and correct response format
let items: any[] = []
if (type === 'contact') {
const contacts = data.contacts || []
if (!Array.isArray(contacts)) {
logger.warn(`[${requestId}] Contacts is not an array`, {
contacts,
dataType: typeof contacts,
})
return NextResponse.json({ items: [] }, { status: 200 })
}
items = contacts.map((item: any) => ({
id: item.id?.toString() || '',
name: `${item.first_name || ''} ${item.last_name || ''}`.trim() || `Contact ${item.id}`,
type: 'contact',
content: item.background_information || '',
createdAt: item.created_at,
updatedAt: item.updated_at,
}))
}
// Apply client-side filtering if query is provided
if (query.trim()) {
const searchTerm = query.trim().toLowerCase()
items = items.filter(
(item) =>
item.name.toLowerCase().includes(searchTerm) ||
item.content.toLowerCase().includes(searchTerm)
)
}
logger.info(`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox`, {
totalItems: items.length,
hasSearchQuery: !!query.trim(),
})
return NextResponse.json({ items }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Wealthbox items`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,132 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('WealthboxItemAPI')
/**
* Get a single item (note, contact, task) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get parameters from query
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const itemId = searchParams.get('itemId')
const type = searchParams.get('type') || 'note'
if (!credentialId || !itemId) {
logger.warn(`[${requestId}] Missing required parameters`, { credentialId, itemId })
return NextResponse.json({ error: 'Credential ID and Item ID are required' }, { status: 400 })
}
// Validate item type
if (!['note', 'contact', 'task'].includes(type)) {
logger.warn(`[${requestId}] Invalid item type: ${type}`)
return NextResponse.json({ error: 'Invalid item type' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Determine the endpoint based on item type
const endpoints = {
note: 'notes',
contact: 'contacts',
task: 'tasks',
}
const endpoint = endpoints[type as keyof typeof endpoints]
logger.info(`[${requestId}] Fetching ${type} ${itemId} from Wealthbox`)
// Make request to Wealthbox API
const response = await fetch(`https://api.crmworkspace.com/v1/${endpoint}/${itemId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
{
error: errorText,
endpoint,
itemId,
}
)
if (response.status === 404) {
return NextResponse.json({ error: 'Item not found' }, { status: 404 })
}
return NextResponse.json(
{ error: `Failed to fetch ${type} from Wealthbox` },
{ status: response.status }
)
}
const data = await response.json()
// Transform the response to match our expected format
const item = {
id: data.id?.toString() || itemId,
name:
data.content || data.name || `${data.first_name} ${data.last_name}` || `${type} ${data.id}`,
type,
content: data.content || '',
createdAt: data.created_at,
updatedAt: data.updated_at,
}
logger.info(`[${requestId}] Successfully fetched ${type} ${itemId} from Wealthbox`)
return NextResponse.json({ item }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Wealthbox item`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,166 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('WealthboxItemsAPI')
// Interface for transformed Wealthbox items
interface WealthboxItem {
id: string
name: string
type: string
content: string
createdAt: string
updatedAt: string
}
/**
* Get items (notes, contacts, tasks) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const type = searchParams.get('type') || 'contact'
const query = searchParams.get('query') || ''
if (!credentialId) {
logger.warn(`[${requestId}] Missing credential ID`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
if (type !== 'contact') {
logger.warn(`[${requestId}] Invalid item type: ${type}`)
return NextResponse.json(
{ error: 'Invalid item type. Only contact is supported.' },
{ status: 400 }
)
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
const endpoints = {
contact: 'contacts',
}
const endpoint = endpoints[type as keyof typeof endpoints]
const url = new URL(`https://api.crmworkspace.com/v1/${endpoint}`)
logger.info(`[${requestId}] Fetching ${type}s from Wealthbox`, {
endpoint,
url: url.toString(),
hasQuery: !!query.trim(),
})
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
{
error: errorText,
endpoint,
url: url.toString(),
}
)
return NextResponse.json(
{ error: `Failed to fetch ${type}s from Wealthbox` },
{ status: response.status }
)
}
const data = await response.json()
logger.info(`[${requestId}] Wealthbox API raw response`, {
type,
status: response.status,
dataKeys: Object.keys(data || {}),
hasContacts: !!data.contacts,
dataStructure: typeof data === 'object' ? Object.keys(data) : 'not an object',
})
let items: WealthboxItem[] = []
if (type === 'contact') {
const contacts = data.contacts || []
if (!Array.isArray(contacts)) {
logger.warn(`[${requestId}] Contacts is not an array`, {
contacts,
dataType: typeof contacts,
})
return NextResponse.json({ items: [] }, { status: 200 })
}
items = contacts.map((item: any) => ({
id: item.id?.toString() || '',
name: `${item.first_name || ''} ${item.last_name || ''}`.trim() || `Contact ${item.id}`,
type: 'contact',
content: item.background_information || '',
createdAt: item.created_at,
updatedAt: item.updated_at,
}))
}
if (query.trim()) {
const searchTerm = query.trim().toLowerCase()
items = items.filter(
(item) =>
item.name.toLowerCase().includes(searchTerm) ||
item.content.toLowerCase().includes(searchTerm)
)
}
logger.info(`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox`, {
totalItems: items.length,
hasSearchQuery: !!query.trim(),
})
return NextResponse.json({ items }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Wealthbox items`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,472 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
import { WealthboxIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { createLogger } from '@/lib/logs/console-logger'
import {
type Credential,
getProviderIdFromServiceId,
getServiceIdFromScopes,
type OAuthProvider,
} from '@/lib/oauth'
import { OAuthRequiredModal } from '../../credential-selector/components/oauth-required-modal'
const logger = createLogger('WealthboxFileSelector')
export interface WealthboxItemInfo {
id: string
name: string
type: 'contact'
content?: string
createdAt?: string
updatedAt?: string
}
interface WealthboxFileSelectorProps {
value: string
onChange: (value: string, itemInfo?: WealthboxItemInfo) => void
provider: OAuthProvider
requiredScopes?: string[]
label?: string
disabled?: boolean
serviceId?: string
showPreview?: boolean
onFileInfoChange?: (itemInfo: WealthboxItemInfo | null) => void
itemType?: 'contact'
}
export function WealthboxFileSelector({
value,
onChange,
provider,
requiredScopes = [],
label = 'Select item',
disabled = false,
serviceId,
showPreview = true,
onFileInfoChange,
itemType = 'contact',
}: WealthboxFileSelectorProps) {
const [open, setOpen] = useState(false)
const [credentials, setCredentials] = useState<Credential[]>([])
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('')
const [selectedItemId, setSelectedItemId] = useState(value)
const [selectedItem, setSelectedItem] = useState<WealthboxItemInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [isLoadingSelectedItem, setIsLoadingSelectedItem] = useState(false)
const [isLoadingItems, setIsLoadingItems] = useState(false)
const [availableItems, setAvailableItems] = useState<WealthboxItemInfo[]>([])
const [searchQuery, setSearchQuery] = useState<string>('')
const [showOAuthModal, setShowOAuthModal] = useState(false)
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
const initialFetchRef = useRef(false)
// Determine the appropriate service ID based on provider and scopes
const getServiceId = (): string => {
if (serviceId) return serviceId
return getServiceIdFromScopes(provider, requiredScopes)
}
// Determine the appropriate provider ID based on service and scopes
const getProviderId = (): string => {
const effectiveServiceId = getServiceId()
return getProviderIdFromServiceId(effectiveServiceId)
}
// Fetch available credentials for this provider
const fetchCredentials = useCallback(async () => {
setIsLoading(true)
setCredentialsLoaded(false)
try {
const providerId = getProviderId()
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
if (response.ok) {
const data = await response.json()
setCredentials(data.credentials)
// Auto-select logic for credentials
if (data.credentials.length > 0) {
if (
selectedCredentialId &&
data.credentials.some((cred: Credential) => cred.id === selectedCredentialId)
) {
// Keep the current selection
} else {
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
if (defaultCred) {
setSelectedCredentialId(defaultCred.id)
} else if (data.credentials.length === 1) {
setSelectedCredentialId(data.credentials[0].id)
}
}
}
}
} catch (error) {
logger.error('Error fetching credentials:', { error })
} finally {
setIsLoading(false)
setCredentialsLoaded(true)
}
}, [provider, getProviderId, selectedCredentialId])
// Debounced search function
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null)
// Fetch available items for the selected credential
const fetchAvailableItems = useCallback(async () => {
if (!selectedCredentialId) return
setIsLoadingItems(true)
try {
const queryParams = new URLSearchParams({
credentialId: selectedCredentialId,
type: itemType,
})
if (searchQuery.trim()) {
queryParams.append('query', searchQuery.trim())
}
const response = await fetch(`/api/auth/oauth/wealthbox/items?${queryParams.toString()}`)
if (response.ok) {
const data = await response.json()
setAvailableItems(data.items || [])
} else {
logger.error('Error fetching available items:', {
error: await response.text(),
})
setAvailableItems([])
}
} catch (error) {
logger.error('Error fetching available items:', { error })
setAvailableItems([])
} finally {
setIsLoadingItems(false)
}
}, [selectedCredentialId, searchQuery, itemType])
// Fetch a single item by ID
const fetchItemById = useCallback(
async (itemId: string) => {
if (!selectedCredentialId || !itemId) return null
setIsLoadingSelectedItem(true)
try {
const queryParams = new URLSearchParams({
credentialId: selectedCredentialId,
itemId: itemId,
type: itemType,
})
const response = await fetch(`/api/auth/oauth/wealthbox/item?${queryParams.toString()}`)
if (response.ok) {
const data = await response.json()
if (data.item) {
setSelectedItem(data.item)
onFileInfoChange?.(data.item)
return data.item
}
} else {
const errorText = await response.text()
logger.error('Error fetching item by ID:', { error: errorText })
if (response.status === 404 || response.status === 403) {
logger.info('Item not accessible, clearing selection')
setSelectedItemId('')
onChange('')
onFileInfoChange?.(null)
}
}
return null
} catch (error) {
logger.error('Error fetching item by ID:', { error })
return null
} finally {
setIsLoadingSelectedItem(false)
}
},
[selectedCredentialId, itemType, onFileInfoChange, onChange]
)
// Fetch credentials on initial mount
useEffect(() => {
if (!initialFetchRef.current) {
fetchCredentials()
initialFetchRef.current = true
}
}, [fetchCredentials])
// Fetch available items only when dropdown is opened
useEffect(() => {
if (selectedCredentialId && open) {
fetchAvailableItems()
}
}, [selectedCredentialId, open, fetchAvailableItems])
// Fetch the selected item metadata only once when needed
useEffect(() => {
if (
value &&
value !== selectedItemId &&
selectedCredentialId &&
credentialsLoaded &&
!selectedItem &&
!isLoadingSelectedItem
) {
fetchItemById(value)
}
}, [
value,
selectedItemId,
selectedCredentialId,
credentialsLoaded,
selectedItem,
isLoadingSelectedItem,
fetchItemById,
])
// Handle search input changes with debouncing
const handleSearchChange = useCallback(
(newQuery: string) => {
setSearchQuery(newQuery)
// Clear existing timeout
if (searchTimeout) {
clearTimeout(searchTimeout)
}
// Set new timeout for search
const timeout = setTimeout(() => {
if (selectedCredentialId) {
fetchAvailableItems()
}
}, 300) // 300ms debounce
setSearchTimeout(timeout)
},
[selectedCredentialId, fetchAvailableItems, searchTimeout]
)
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
}
}, [searchTimeout])
// Handle selecting an item
const handleItemSelect = (item: WealthboxItemInfo) => {
setSelectedItemId(item.id)
setSelectedItem(item)
onChange(item.id, item)
onFileInfoChange?.(item)
setOpen(false)
setSearchQuery('')
}
// Handle adding a new credential
const handleAddCredential = () => {
setShowOAuthModal(true)
setOpen(false)
setSearchQuery('')
}
// Clear selection
const handleClearSelection = () => {
setSelectedItemId('')
setSelectedItem(null)
onChange('', undefined)
onFileInfoChange?.(null)
}
const getItemTypeLabel = () => {
switch (itemType) {
case 'contact':
return 'Contacts'
default:
return 'Contacts'
}
}
return (
<>
<div className='space-y-2'>
<Popover
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen)
if (!isOpen) {
setSearchQuery('')
if (searchTimeout) {
clearTimeout(searchTimeout)
setSearchTimeout(null)
}
}
}}
>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
disabled={disabled}
>
{selectedItem ? (
<div className='flex items-center gap-2 overflow-hidden'>
<WealthboxIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedItem.name}</span>
</div>
) : selectedItemId && isLoadingSelectedItem && selectedCredentialId ? (
<div className='flex items-center gap-2'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='text-muted-foreground'>Loading...</span>
</div>
) : (
<div className='flex items-center gap-2'>
<WealthboxIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-[300px] p-0' align='start'>
<Command shouldFilter={false}>
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
<input
placeholder={`Search ${itemType}s...`}
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
className='flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50'
/>
</div>
<CommandList>
<CommandEmpty>
{isLoadingItems ? `Loading ${itemType}s...` : `No ${itemType}s found.`}
</CommandEmpty>
{credentials.length > 1 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
Switch Account
</div>
{credentials.map((cred) => (
<CommandItem
key={cred.id}
value={`account-${cred.id}`}
onSelect={() => setSelectedCredentialId(cred.id)}
>
<div className='flex items-center gap-2'>
<WealthboxIcon className='h-4 w-4' />
<span className='font-normal'>{cred.name}</span>
</div>
{cred.id === selectedCredentialId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)}
{availableItems.length > 0 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
{getItemTypeLabel()}
</div>
{availableItems.map((item) => (
<CommandItem
key={item.id}
value={`item-${item.id}-${item.name}`}
onSelect={() => handleItemSelect(item)}
>
<div className='flex items-center gap-2 overflow-hidden'>
<WealthboxIcon className='h-4 w-4' />
<div className='min-w-0 flex-1'>
<span className='truncate font-normal'>{item.name}</span>
{item.updatedAt && (
<div className='text-muted-foreground text-xs'>
Updated {new Date(item.updatedAt).toLocaleDateString()}
</div>
)}
</div>
</div>
{item.id === selectedItemId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)}
{credentials.length === 0 && (
<CommandGroup>
<CommandItem onSelect={handleAddCredential}>
<div className='flex items-center gap-2 text-primary'>
<WealthboxIcon className='h-4 w-4' />
<span>Connect Wealthbox account</span>
</div>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{showPreview && selectedItem && (
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
<div className='absolute top-2 right-2'>
<Button
variant='ghost'
size='icon'
className='h-5 w-5 hover:bg-muted'
onClick={handleClearSelection}
>
<X className='h-3 w-3' />
</Button>
</div>
<div className='flex items-center gap-3 pr-4'>
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-muted/20'>
<WealthboxIcon className='h-4 w-4' />
</div>
<div className='min-w-0 flex-1 overflow-hidden'>
<div className='flex items-center gap-2'>
<h4 className='truncate font-medium text-xs'>{selectedItem.name}</h4>
{selectedItem.updatedAt && (
<span className='whitespace-nowrap text-muted-foreground text-xs'>
{new Date(selectedItem.updatedAt).toLocaleDateString()}
</span>
)}
</div>
<div className='text-muted-foreground text-xs capitalize'>{selectedItem.type}</div>
</div>
</div>
</div>
)}
</div>
{showOAuthModal && (
<OAuthRequiredModal
isOpen={showOAuthModal}
onClose={() => setShowOAuthModal(false)}
toolName='Wealthbox'
provider={provider}
requiredScopes={requiredScopes}
serviceId={getServiceId()}
/>
)}
</>
)
}

View File

@@ -22,6 +22,8 @@ import type { MicrosoftFileInfo } from './components/microsoft-file-selector'
import { MicrosoftFileSelector } from './components/microsoft-file-selector'
import type { TeamsMessageInfo } from './components/teams-message-selector'
import { TeamsMessageSelector } from './components/teams-message-selector'
import type { WealthboxItemInfo } from './components/wealthbox-file-selector'
import { WealthboxFileSelector } from './components/wealthbox-file-selector'
interface FileSelectorInputProps {
blockId: string
@@ -54,6 +56,8 @@ export function FileSelectorInput({
const [messageInfo, setMessageInfo] = useState<TeamsMessageInfo | null>(null)
const [selectedCalendarId, setSelectedCalendarId] = useState<string>('')
const [calendarInfo, setCalendarInfo] = useState<GoogleCalendarInfo | null>(null)
const [selectedWealthboxItemId, setSelectedWealthboxItemId] = useState<string>('')
const [wealthboxItemInfo, setWealthboxItemInfo] = useState<WealthboxItemInfo | null>(null)
// Get provider-specific values
const provider = subBlock.provider || 'google-drive'
@@ -63,6 +67,7 @@ export function FileSelectorInput({
const isMicrosoftTeams = provider === 'microsoft-teams'
const isMicrosoftExcel = provider === 'microsoft-excel'
const isGoogleCalendar = subBlock.provider === 'google-calendar'
const isWealthbox = provider === 'wealthbox'
// For Confluence and Jira, we need the domain and credentials
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
// For Discord, we need the bot token and server ID
@@ -85,6 +90,8 @@ export function FileSelectorInput({
setSelectedMessageId(value)
} else if (isGoogleCalendar) {
setSelectedCalendarId(value)
} else if (isWealthbox) {
setSelectedWealthboxItemId(value)
} else {
setSelectedFileId(value)
}
@@ -100,6 +107,8 @@ export function FileSelectorInput({
setSelectedMessageId(value)
} else if (isGoogleCalendar) {
setSelectedCalendarId(value)
} else if (isWealthbox) {
setSelectedWealthboxItemId(value)
} else {
setSelectedFileId(value)
}
@@ -113,6 +122,7 @@ export function FileSelectorInput({
isDiscord,
isMicrosoftTeams,
isGoogleCalendar,
isWealthbox,
isPreview,
previewValue,
])
@@ -151,6 +161,13 @@ export function FileSelectorInput({
setStoreValue(calendarId)
}
// Handle Wealthbox item selection
const handleWealthboxItemChange = (itemId: string, info?: WealthboxItemInfo) => {
setSelectedWealthboxItemId(itemId)
setWealthboxItemInfo(info || null)
setStoreValue(itemId)
}
// For Google Drive
const clientId = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
const apiKey = env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
@@ -369,6 +386,47 @@ export function FileSelectorInput({
)
}
// Render Wealthbox selector
if (isWealthbox) {
// Get credential using the same pattern as other tools
const credential = (getValue(blockId, 'credential') as string) || ''
// Only handle contacts now - both notes and tasks use short-input
if (subBlock.id === 'contactId') {
const itemType = 'contact'
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<WealthboxFileSelector
value={selectedWealthboxItemId}
onChange={handleWealthboxItemChange}
provider='wealthbox'
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || `Select ${itemType}`}
disabled={disabled || !credential}
showPreview={true}
onFileInfoChange={setWealthboxItemInfo}
itemType={itemType}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select Wealthbox credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// If it's noteId or taskId, we should not render the file selector since they now use short-input
return null
}
// Default to Google Drive picker
return (
<GoogleDrivePicker

View File

@@ -0,0 +1,221 @@
import { WealthboxIcon } from '@/components/icons'
import type { WealthboxReadResponse, WealthboxWriteResponse } from '@/tools/wealthbox/types'
import type { BlockConfig } from '../types'
type WealthboxResponse = WealthboxReadResponse | WealthboxWriteResponse
export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
type: 'wealthbox',
name: 'Wealthbox',
description: 'Interact with Wealthbox',
longDescription:
'Integrate Wealthbox functionality to manage notes, contacts, and tasks. Read content from existing notes, contacts, and tasks and write to them using OAuth authentication. Supports text content manipulation for note creation and editing.',
docsLink: 'https://docs.simstudio.ai/tools/wealthbox',
category: 'tools',
bgColor: '#E0E0E0',
icon: WealthboxIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Read Note', id: 'read_note' },
{ label: 'Write Note', id: 'write_note' },
{ label: 'Read Contact', id: 'read_contact' },
{ label: 'Write Contact', id: 'write_contact' },
{ label: 'Read Task', id: 'read_task' },
{ label: 'Write Task', id: 'write_task' },
],
},
{
id: 'credential',
title: 'Wealthbox Account',
type: 'oauth-input',
layout: 'full',
provider: 'wealthbox',
serviceId: 'wealthbox',
requiredScopes: ['login', 'data'],
placeholder: 'Select Wealthbox account',
},
{
id: 'noteId',
title: 'Note ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter Note ID (optional)',
condition: { field: 'operation', value: ['read_note'] },
},
{
id: 'contactId',
title: 'Select Contact',
type: 'file-selector',
provider: 'wealthbox',
serviceId: 'wealthbox',
requiredScopes: ['login', 'data'],
layout: 'full',
placeholder: 'Enter Contact ID',
condition: { field: 'operation', value: ['read_contact', 'write_task', 'write_note'] },
},
{
id: 'taskId',
title: 'Select Task',
type: 'short-input',
provider: 'wealthbox',
serviceId: 'wealthbox',
requiredScopes: ['login', 'data'],
layout: 'full',
placeholder: 'Enter Task ID',
condition: { field: 'operation', value: ['read_task'] },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
layout: 'full',
placeholder: 'Enter Title',
condition: { field: 'operation', value: ['write_task'] },
},
{
id: 'dueDate',
title: 'Due Date',
type: 'short-input',
layout: 'full',
placeholder: 'Enter due date (e.g., 2015-05-24 11:00 AM -0400)',
condition: { field: 'operation', value: ['write_task'] },
},
{
id: 'firstName',
title: 'First Name',
type: 'short-input',
layout: 'full',
placeholder: 'Enter First Name',
condition: { field: 'operation', value: ['write_contact'] },
},
{
id: 'lastName',
title: 'Last Name',
type: 'short-input',
layout: 'full',
placeholder: 'Enter Last Name',
condition: { field: 'operation', value: ['write_contact'] },
},
{
id: 'emailAddress',
title: 'Email Address',
type: 'short-input',
layout: 'full',
placeholder: 'Enter Email Address',
condition: { field: 'operation', value: ['write_contact'] },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
layout: 'full',
placeholder: 'Enter Content',
condition: { field: 'operation', value: ['write_note', 'write_event', 'write_task'] },
},
{
id: 'backgroundInformation',
title: 'Background Information',
type: 'long-input',
layout: 'full',
placeholder: 'Enter Background Information',
condition: { field: 'operation', value: ['write_contact'] },
},
],
tools: {
access: [
'wealthbox_read_note',
'wealthbox_write_note',
'wealthbox_read_contact',
'wealthbox_write_contact',
'wealthbox_read_task',
'wealthbox_write_task',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'read_note':
return 'wealthbox_read_note'
case 'write_note':
return 'wealthbox_write_note'
case 'read_contact':
return 'wealthbox_read_contact'
case 'write_contact':
return 'wealthbox_write_contact'
case 'read_task':
return 'wealthbox_read_task'
case 'write_task':
return 'wealthbox_write_task'
default:
throw new Error(`Unknown operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, operation, ...rest } = params
// Build the parameters based on operation type
const baseParams = {
...rest,
credential,
}
// For note operations, we need noteId
if (operation === 'read_note' || operation === 'write_note') {
return {
...baseParams,
noteId: params.noteId,
}
}
// For contact operations, we need contactId
if (operation === 'read_contact') {
if (!params.contactId) {
throw new Error('Contact ID is required for contact operations')
}
return {
...baseParams,
contactId: params.contactId,
}
}
// For task operations, we need taskId
if (operation === 'read_task') {
return {
...baseParams,
taskId: params.taskId,
}
}
return baseParams
},
},
},
inputs: {
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
noteId: { type: 'string', required: false },
contactId: { type: 'string', required: false },
taskId: { type: 'string', required: false },
content: { type: 'string', required: false },
firstName: { type: 'string', required: false },
lastName: { type: 'string', required: false },
emailAddress: { type: 'string', required: false },
backgroundInformation: { type: 'string', required: false },
title: { type: 'string', required: false },
dueDate: { type: 'string', required: false },
},
outputs: {
note: 'any',
notes: 'any',
contact: 'any',
contacts: 'any',
task: 'any',
tasks: 'any',
metadata: 'json',
success: 'any',
},
}

View File

@@ -58,6 +58,7 @@ import { TranslateBlock } from './blocks/translate'
import { TwilioSMSBlock } from './blocks/twilio'
import { TypeformBlock } from './blocks/typeform'
import { VisionBlock } from './blocks/vision'
import { WealthboxBlock } from './blocks/wealthbox'
import { WhatsAppBlock } from './blocks/whatsapp'
import { WorkflowBlock } from './blocks/workflow'
import { XBlock } from './blocks/x'
@@ -121,6 +122,7 @@ export const registry: Record<string, BlockConfig> = {
twilio_sms: TwilioSMSBlock,
typeform: TypeformBlock,
vision: VisionBlock,
wealthbox: WealthboxBlock,
whatsapp: WhatsAppBlock,
workflow: WorkflowBlock,
x: XBlock,

View File

@@ -2991,3 +2991,20 @@ export const OllamaIcon = (props: SVGProps<SVGSVGElement>) => (
<path d='M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z' />
</svg>
)
export function WealthboxIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns='http://www.w3.org/2000/svg'
version='1.0'
width='200'
height='200'
viewBox='50 -50 200 200'
>
<g fill='#106ED4' stroke='none' transform='translate(0, 200) scale(0.15, -0.15)'>
<path d='M764 1542 c-110 -64 -230 -134 -266 -156 -42 -24 -71 -49 -78 -65 -7 -19 -10 -126 -8 -334 3 -291 4 -307 23 -326 11 -11 103 -67 205 -126 102 -59 219 -127 261 -151 42 -24 85 -44 96 -44 23 0 527 288 561 320 22 22 22 23 22 340 0 288 -2 320 -17 338 -32 37 -537 322 -569 321 -18 0 -107 -46 -230 -117z m445 -144 c108 -62 206 -123 219 -135 22 -22 22 -26 22 -261 0 -214 -2 -242 -17 -260 -23 -26 -414 -252 -437 -252 -9 0 -70 31 -134 69 -64 37 -161 94 -215 125 l-97 57 2 261 3 261 210 123 c116 67 219 123 229 123 10 1 107 -50 215 -111z' />
<path d='M700 1246 l-55 -32 -3 -211 -2 -211 37 -23 c21 -12 52 -30 69 -40 l30 -18 103 59 c56 33 109 60 117 60 8 0 62 -27 119 -60 l104 -60 63 37 c35 21 66 42 70 48 4 5 8 101 8 212 l0 202 -62 35 -63 35 -3 -197 c-1 -108 -6 -200 -11 -205 -5 -5 -54 17 -114 52 -58 34 -108 61 -111 61 -2 0 -51 -27 -107 -60 -56 -32 -106 -57 -111 -54 -4 3 -8 95 -8 205 0 109 -3 199 -7 199 -5 -1 -33 -16 -63 -34z' />
</g>
</svg>
)
}

View File

@@ -468,6 +468,41 @@ export const auth = betterAuth({
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/outlook`,
},
{
providerId: 'wealthbox',
clientId: env.WEALTHBOX_CLIENT_ID as string,
clientSecret: env.WEALTHBOX_CLIENT_SECRET as string,
authorizationUrl: 'https://app.crmworkspace.com/oauth/authorize',
tokenUrl: 'https://app.crmworkspace.com/oauth/token',
userInfoUrl: 'https://dummy-not-used.wealthbox.com', // Dummy URL since no user info endpoint exists
scopes: ['login', 'data'],
responseType: 'code',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/wealthbox`,
getUserInfo: async (tokens) => {
try {
logger.info('Creating Wealthbox user profile from token data')
// Generate a unique identifier since we can't fetch user info
const uniqueId = `wealthbox-${Date.now()}`
const now = new Date()
// Create a synthetic user profile
return {
id: uniqueId,
name: 'Wealthbox User',
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@wealthbox.user`,
image: null,
emailVerified: false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error creating Wealthbox user profile:', { error })
return null
}
},
},
// Supabase provider
{
providerId: 'supabase',

View File

@@ -99,6 +99,8 @@ export const env = createEnv({
MICROSOFT_CLIENT_SECRET: z.string().optional(),
HUBSPOT_CLIENT_ID: z.string().optional(),
HUBSPOT_CLIENT_SECRET: z.string().optional(),
WEALTHBOX_CLIENT_ID: z.string().optional(),
WEALTHBOX_CLIENT_SECRET: z.string().optional(),
DOCKER_BUILD: z.boolean().optional(),
LINEAR_CLIENT_ID: z.string().optional(),
LINEAR_CLIENT_SECRET: z.string().optional(),

View File

@@ -20,6 +20,7 @@ import {
RedditIcon,
SlackIcon,
SupabaseIcon,
WealthboxIcon,
xIcon,
} from '@/components/icons'
import { createLogger } from '@/lib/logs/console-logger'
@@ -41,6 +42,7 @@ export type OAuthProvider =
| 'linear'
| 'slack'
| 'reddit'
| 'wealthbox'
| string
export type OAuthService =
@@ -64,6 +66,7 @@ export type OAuthService =
| 'linear'
| 'slack'
| 'reddit'
| 'wealthbox'
export interface OAuthProviderConfig {
id: OAuthProvider
@@ -407,6 +410,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'reddit',
},
wealthbox: {
id: 'wealthbox',
name: 'Wealthbox',
icon: (props) => WealthboxIcon(props),
services: {
wealthbox: {
id: 'wealthbox',
name: 'Wealthbox',
description: 'Manage contacts, notes, and tasks in your Wealthbox CRM.',
providerId: 'wealthbox',
icon: (props) => WealthboxIcon(props),
baseProviderIcon: (props) => WealthboxIcon(props),
scopes: ['login', 'data'],
},
},
defaultService: 'wealthbox',
},
}
// Helper function to get a service by provider and service ID
@@ -475,6 +495,10 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
return 'linear'
} else if (provider === 'slack') {
return 'slack'
} else if (provider === 'reddit') {
return 'reddit'
} else if (provider === 'wealthbox') {
return 'wealthbox'
}
return providerConfig.defaultService
@@ -727,6 +751,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
useBasicAuth: true,
}
}
case 'wealthbox': {
const { clientId, clientSecret } = getCredentials(
env.WEALTHBOX_CLIENT_ID,
env.WEALTHBOX_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://app.crmworkspace.com/oauth/token',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: true,
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}
@@ -806,6 +843,7 @@ export async function refreshOAuthToken(
error: errorText,
parsedError: errorData,
provider,
providerId,
})
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
}

View File

@@ -95,6 +95,14 @@ import { sendSMSTool } from './twilio'
import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './typeform'
import type { ToolConfig } from './types'
import { visionTool } from './vision'
import {
wealthboxReadContactTool,
wealthboxReadNoteTool,
wealthboxReadTaskTool,
wealthboxWriteContactTool,
wealthboxWriteNoteTool,
wealthboxWriteTaskTool,
} from './wealthbox'
import { whatsappSendMessageTool } from './whatsapp'
import { workflowExecutorTool } from './workflow'
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
@@ -216,4 +224,10 @@ export const tools: Record<string, ToolConfig> = {
google_calendar_quick_add: googleCalendarQuickAddTool,
google_calendar_invite: googleCalendarInviteTool,
workflow_executor: workflowExecutorTool,
wealthbox_read_contact: wealthboxReadContactTool,
wealthbox_write_contact: wealthboxWriteContactTool,
wealthbox_read_task: wealthboxReadTaskTool,
wealthbox_write_task: wealthboxWriteTaskTool,
wealthbox_read_note: wealthboxReadNoteTool,
wealthbox_write_note: wealthboxWriteNoteTool,
}

View File

@@ -0,0 +1,13 @@
import { wealthboxReadContactTool } from './read_contact'
import { wealthboxReadNoteTool } from './read_note'
import { wealthboxReadTaskTool } from './read_task'
import { wealthboxWriteContactTool } from './write_contact'
import { wealthboxWriteNoteTool } from './write_note'
import { wealthboxWriteTaskTool } from './write_task'
export { wealthboxReadNoteTool }
export { wealthboxWriteNoteTool }
export { wealthboxReadContactTool }
export { wealthboxWriteContactTool }
export { wealthboxReadTaskTool }
export { wealthboxWriteTaskTool }

View File

@@ -0,0 +1,133 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxReadParams, WealthboxReadResponse } from './types'
const logger = createLogger('WealthboxReadContact')
export const wealthboxReadContactTool: ToolConfig<WealthboxReadParams, WealthboxReadResponse> = {
id: 'wealthbox_read_contact',
name: 'Read Wealthbox Contact',
description: 'Read content from a Wealthbox contact',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Wealthbox API',
},
contactId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The ID of the contact to read',
},
},
request: {
url: (params) => {
const contactId = params.contactId?.trim()
let url = 'https://api.crmworkspace.com/v1/contacts'
if (contactId) {
url = `https://api.crmworkspace.com/v1/contacts/${contactId}`
}
return url
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: WealthboxReadParams) => {
if (!response.ok) {
const errorText = await response.text()
logger.error(
`Wealthbox contact API error: ${response.status} ${response.statusText}`,
errorText
)
throw new Error(
`Failed to read Wealthbox contact: ${response.status} ${response.statusText} - ${errorText}`
)
}
const data = await response.json()
if (!data) {
return {
success: true,
output: {
contact: undefined,
metadata: {
operation: 'read_contact' as const,
contactId: params?.contactId || '',
itemType: 'contact' as const,
},
},
}
}
// Format contact information into readable content
const contact = data
let content = `Contact: ${contact.first_name || ''} ${contact.last_name || ''}`.trim()
if (contact.company_name) {
content += `\nCompany: ${contact.company_name}`
}
if (contact.background_information) {
content += `\nBackground: ${contact.background_information}`
}
if (contact.email_addresses && contact.email_addresses.length > 0) {
content += '\nEmail Addresses:'
contact.email_addresses.forEach((email: any) => {
content += `\n - ${email.address}${email.principal ? ' (Primary)' : ''} (${email.kind})`
})
}
if (contact.phone_numbers && contact.phone_numbers.length > 0) {
content += '\nPhone Numbers:'
contact.phone_numbers.forEach((phone: any) => {
content += `\n - ${phone.address}${phone.extension ? ` ext. ${phone.extension}` : ''}${phone.principal ? ' (Primary)' : ''} (${phone.kind})`
})
}
return {
success: true,
output: {
content,
contact,
metadata: {
operation: 'read_contact' as const,
contactId: params?.contactId || contact.id?.toString() || '',
itemType: 'contact' as const,
},
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Wealthbox contact'
},
}

View File

@@ -0,0 +1,146 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxReadParams, WealthboxReadResponse } from './types'
const logger = createLogger('WealthboxReadNote')
export const wealthboxReadNoteTool: ToolConfig<WealthboxReadParams, WealthboxReadResponse> = {
id: 'wealthbox_read_note',
name: 'Read Wealthbox Note',
description: 'Read content from a Wealthbox note',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Wealthbox API',
visibility: 'hidden',
},
noteId: {
type: 'string',
required: false,
description: 'The ID of the note to read',
visibility: 'user-only',
},
},
request: {
url: (params) => {
const noteId = params.noteId?.trim()
let url = 'https://api.crmworkspace.com/v1/notes'
if (noteId) {
url = `https://api.crmworkspace.com/v1/notes/${noteId}`
}
return url
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: WealthboxReadParams) => {
if (!response.ok) {
const errorText = await response.text()
logger.error(`Wealthbox note API error: ${response.status} ${response.statusText}`, errorText)
// Provide more specific error messages
if (response.status === 404) {
throw new Error(
`Note with ID ${params?.noteId} not found. Please check the note ID and try again.`
)
}
if (response.status === 403) {
throw new Error(
`Access denied to note with ID ${params?.noteId}. Please check your permissions.`
)
}
throw new Error(
`Failed to read Wealthbox note: ${response.status} ${response.statusText} - ${errorText}`
)
}
const data = await response.json()
if (!data) {
return {
success: false,
output: {
note: undefined,
metadata: {
operation: 'read_note' as const,
noteId: params?.noteId || '',
itemType: 'note' as const,
},
},
}
}
// Format note information into readable content
const note = data
let content = `Note Content: ${note.content || 'No content available'}`
if (note.created_at) {
content += `\nCreated: ${new Date(note.created_at).toLocaleString()}`
}
if (note.updated_at) {
content += `\nUpdated: ${new Date(note.updated_at).toLocaleString()}`
}
if (note.visible_to) {
content += `\nVisible to: ${note.visible_to}`
}
if (note.linked_to && note.linked_to.length > 0) {
content += '\nLinked to:'
note.linked_to.forEach((link: any) => {
content += `\n - ${link.name} (${link.type})`
})
}
if (note.tags && note.tags.length > 0) {
content += '\nTags:'
note.tags.forEach((tag: any) => {
content += `\n - ${tag.name}`
})
}
return {
success: true,
output: {
content,
note,
metadata: {
operation: 'read_note' as const,
noteId: params?.noteId || note.id?.toString() || '',
itemType: 'note' as const,
},
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Wealthbox note'
},
}

View File

@@ -0,0 +1,147 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxReadParams, WealthboxReadResponse } from './types'
const logger = createLogger('WealthboxReadTask')
export const wealthboxReadTaskTool: ToolConfig<WealthboxReadParams, WealthboxReadResponse> = {
id: 'wealthbox_read_task',
name: 'Read Wealthbox Task',
description: 'Read content from a Wealthbox task',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Wealthbox API',
visibility: 'hidden',
},
taskId: {
type: 'string',
required: false,
description: 'The ID of the task to read',
visibility: 'user-only',
},
},
request: {
url: (params) => {
const taskId = params.taskId?.trim()
let url = 'https://api.crmworkspace.com/v1/tasks'
if (taskId) {
url = `https://api.crmworkspace.com/v1/tasks/${taskId}`
}
return url
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: WealthboxReadParams) => {
if (!response.ok) {
const errorText = await response.text()
logger.error(`Wealthbox task API error: ${response.status} ${response.statusText}`, errorText)
// Provide more specific error messages
if (response.status === 404) {
throw new Error(
`Task with ID ${params?.taskId} not found. Please check the task ID and try again.`
)
}
if (response.status === 403) {
throw new Error(
`Access denied to task with ID ${params?.taskId}. Please check your permissions.`
)
}
throw new Error(
`Failed to read Wealthbox task: ${response.status} ${response.statusText} - ${errorText}`
)
}
const data = await response.json()
if (!data) {
return {
success: true,
output: {
task: undefined,
metadata: {
operation: 'read_task' as const,
taskId: params?.taskId || '',
itemType: 'task' as const,
},
},
}
}
// Format task information into readable content
const task = data
let content = `Task: ${task.name || 'Unnamed task'}`
if (task.due_date) {
content += `\nDue Date: ${new Date(task.due_date).toLocaleDateString()}`
}
if (task.complete !== undefined) {
content += `\nStatus: ${task.complete ? 'Complete' : 'Incomplete'}`
}
if (task.priority) {
content += `\nPriority: ${task.priority}`
}
if (task.category) {
content += `\nCategory: ${task.category}`
}
if (task.visible_to) {
content += `\nVisible to: ${task.visible_to}`
}
if (task.linked_to && task.linked_to.length > 0) {
content += '\nLinked to:'
task.linked_to.forEach((link: any) => {
content += `\n - ${link.name} (${link.type})`
})
}
return {
success: true,
output: {
content,
task,
metadata: {
operation: 'read_task' as const,
taskId: params?.taskId || task.id?.toString() || '',
itemType: 'task' as const,
},
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Wealthbox task'
},
}

View File

@@ -0,0 +1,142 @@
import type { ToolResponse } from '../types'
// Entity type definitions based on Wealthbox API responses
export interface WealthboxNote {
id: number
creator: number
created_at: string
updated_at: string
content: string
linked_to: Array<{
id: number
type: string
name: string
}>
visible_to: string
tags: Array<{
id: number
name: string
}>
}
export interface WealthboxContact {
id: number
first_name: string
last_name: string
company_name?: string
background_information?: string
email_addresses?: Array<{
address: string
principal: boolean
kind: string
}>
phone_numbers?: Array<{
address: string
principal: boolean
extension?: string
kind: string
}>
}
export interface WealthboxTask {
id: number
name: string
due_date: string
description?: string
complete?: boolean
category?: number
priority?: 'Low' | 'Medium' | 'High'
linked_to?: Array<{
id: number
type: string
name: string
}>
visible_to?: string
}
// Unified metadata structure
export interface WealthboxMetadata {
operation:
| 'read_note'
| 'write_note'
| 'read_contact'
| 'write_contact'
| 'read_task'
| 'write_task'
itemId?: string
contactId?: string
itemType: 'note' | 'contact' | 'task'
totalItems?: number
}
// Unified output structure for all operations
interface WealthboxUniformOutput {
// Single items (for write operations and single reads)
note?: WealthboxNote
contact?: WealthboxContact
task?: WealthboxTask
// Arrays (for bulk read operations)
notes?: WealthboxNote[]
contacts?: WealthboxContact[]
tasks?: WealthboxTask[]
// Operation result indicators
success?: boolean
metadata: WealthboxMetadata
}
// Both response types use identical structure
export interface WealthboxReadResponse extends ToolResponse {
output: WealthboxUniformOutput
}
export interface WealthboxWriteResponse extends ToolResponse {
output: WealthboxUniformOutput
}
// Unified parameter types
export interface WealthboxReadParams {
accessToken: string
operation: 'read_note' | 'read_contact' | 'read_task'
noteId?: string
contactId?: string
taskId?: string
}
export interface WealthboxWriteParams {
accessToken: string
operation: 'write_note' | 'write_contact' | 'write_task'
// IDs (optional for creating new items)
noteId?: string
contactId?: string
taskId?: string
// Note fields
content?: string
linkedTo?: Array<{
id: number
type: string
name: string
}>
visibleTo?: string
tags?: Array<{
id: number
name: string
}>
// Contact fields
firstName?: string
lastName?: string
backgroundInformation?: string
emailAddress?: string
// Task fields
title?: string
description?: string
dueDate?: string
complete?: boolean
category?: number
priority?: 'Low' | 'Medium' | 'High'
}

View File

@@ -0,0 +1,274 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxWriteParams, WealthboxWriteResponse } from './types'
const logger = createLogger('WealthboxWriteContact')
// Utility function to safely convert to string and trim
const safeStringify = (value: any): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'string') {
return value
}
return JSON.stringify(value)
}
// Utility function to validate parameters and build contact body
const validateAndBuildContactBody = (params: WealthboxWriteParams): Record<string, any> => {
// Validate required fields with safe stringification
const firstName = safeStringify(params.firstName).trim()
const lastName = safeStringify(params.lastName).trim()
if (!firstName) {
throw new Error('First name is required')
}
if (!lastName) {
throw new Error('Last name is required')
}
const body: Record<string, any> = {
first_name: firstName,
last_name: lastName,
}
// Add optional fields with safe stringification
const emailAddress = safeStringify(params.emailAddress).trim()
if (emailAddress) {
body.email_addresses = [
{
address: emailAddress,
kind: 'email',
principal: true,
},
]
}
const backgroundInformation = safeStringify(params.backgroundInformation).trim()
if (backgroundInformation) {
body.background_information = backgroundInformation
}
return body
}
export const wealthboxWriteContactTool: ToolConfig<WealthboxWriteParams, WealthboxWriteResponse> = {
id: 'wealthbox_write_contact',
name: 'Write Wealthbox Contact',
description: 'Create a new Wealthbox contact',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Wealthbox API',
visibility: 'hidden',
},
firstName: {
type: 'string',
required: true,
description: 'The first name of the contact',
visibility: 'user-or-llm',
},
lastName: {
type: 'string',
required: true,
description: 'The last name of the contact',
visibility: 'user-or-llm',
},
emailAddress: {
type: 'string',
required: false,
description: 'The email address of the contact',
visibility: 'user-or-llm',
},
backgroundInformation: {
type: 'string',
required: false,
description: 'Background information about the contact',
visibility: 'user-or-llm',
},
},
request: {
url: 'https://api.crmworkspace.com/v1/contacts',
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return validateAndBuildContactBody(params)
},
},
directExecution: async (params: WealthboxWriteParams) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
const body = validateAndBuildContactBody(params)
const response = await fetch('https://api.crmworkspace.com/v1/contacts', {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error(
`Wealthbox contact write API error: ${response.status} ${response.statusText}`,
errorText
)
throw new Error(
`Failed to create Wealthbox contact: ${response.status} ${response.statusText} - ${errorText}`
)
}
const data = await response.json()
if (!data) {
return {
success: true,
output: {
contact: undefined,
metadata: {
operation: 'write_contact' as const,
itemType: 'contact' as const,
},
},
}
}
// Format contact information into readable content
const contact = data
let content = `Contact created: ${contact.first_name || ''} ${contact.last_name || ''}`.trim()
if (contact.background_information) {
content += `\nBackground: ${contact.background_information}`
}
if (contact.email_addresses && contact.email_addresses.length > 0) {
content += '\nEmail Addresses:'
contact.email_addresses.forEach((email: any) => {
content += `\n - ${email.address}${email.principal ? ' (Primary)' : ''} (${email.kind})`
})
}
if (contact.phone_numbers && contact.phone_numbers.length > 0) {
content += '\nPhone Numbers:'
contact.phone_numbers.forEach((phone: any) => {
content += `\n - ${phone.address}${phone.extension ? ` ext. ${phone.extension}` : ''}${phone.principal ? ' (Primary)' : ''} (${phone.kind})`
})
}
return {
success: true,
output: {
content,
contact,
success: true,
metadata: {
operation: 'write_contact' as const,
contactId: contact.id?.toString() || '',
itemType: 'contact' as const,
},
},
}
},
transformResponse: async (response: Response, params?: WealthboxWriteParams) => {
if (!response.ok) {
const errorText = await response.text()
logger.error(
`Wealthbox contact write API error: ${response.status} ${response.statusText}`,
errorText
)
throw new Error(
`Failed to create Wealthbox contact: ${response.status} ${response.statusText} - ${errorText}`
)
}
const data = await response.json()
if (!data) {
return {
success: true,
output: {
contact: undefined,
metadata: {
operation: 'write_contact' as const,
itemType: 'contact' as const,
},
},
}
}
// Format contact information into readable content
const contact = data
let content = `Contact created: ${contact.first_name || ''} ${contact.last_name || ''}`.trim()
if (contact.background_information) {
content += `\nBackground: ${contact.background_information}`
}
if (contact.email_addresses && contact.email_addresses.length > 0) {
content += '\nEmail Addresses:'
contact.email_addresses.forEach((email: any) => {
content += `\n - ${email.address}${email.principal ? ' (Primary)' : ''} (${email.kind})`
})
}
if (contact.phone_numbers && contact.phone_numbers.length > 0) {
content += '\nPhone Numbers:'
contact.phone_numbers.forEach((phone: any) => {
content += `\n - ${phone.address}${phone.extension ? ` ext. ${phone.extension}` : ''}${phone.principal ? ' (Primary)' : ''} (${phone.kind})`
})
}
return {
success: true,
output: {
content,
contact,
success: true,
metadata: {
operation: 'write_contact' as const,
contactId: contact.id?.toString() || '',
itemType: 'contact' as const,
},
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while creating Wealthbox contact'
},
}

View File

@@ -0,0 +1,181 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxWriteParams, WealthboxWriteResponse } from './types'
const logger = createLogger('WealthboxWriteNote')
// Utility function to validate parameters and build note body
const validateAndBuildNoteBody = (params: WealthboxWriteParams): Record<string, any> => {
// Handle content conversion - stringify if not already a string
let content: string
if (params.content === null || params.content === undefined) {
throw new Error('Note content is required')
}
if (typeof params.content === 'string') {
content = params.content
} else {
content = JSON.stringify(params.content)
}
content = content.trim()
if (!content) {
throw new Error('Note content is required')
}
const body: Record<string, any> = {
content: content,
}
// Handle contact linking
if (params.contactId?.trim()) {
body.linked_to = [
{
id: Number.parseInt(params.contactId.trim()),
type: 'Contact',
},
]
}
return body
}
// Utility function to handle API errors
const handleApiError = (response: Response, errorText: string): never => {
logger.error(
`Wealthbox note write API error: ${response.status} ${response.statusText}`,
errorText
)
throw new Error(
`Failed to create Wealthbox note: ${response.status} ${response.statusText} - ${errorText}`
)
}
// Utility function to format note response
const formatNoteResponse = (data: any): WealthboxWriteResponse => {
if (!data) {
return {
success: false,
output: {
note: undefined,
metadata: {
operation: 'write_note' as const,
itemType: 'note' as const,
},
},
}
}
return {
success: true,
output: {
note: data,
success: true,
metadata: {
operation: 'write_note' as const,
itemId: data.id?.toString() || '',
itemType: 'note' as const,
},
},
}
}
export const wealthboxWriteNoteTool: ToolConfig<WealthboxWriteParams, WealthboxWriteResponse> = {
id: 'wealthbox_write_note',
name: 'Write Wealthbox Note',
description: 'Create or update a Wealthbox note',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Wealthbox API',
visibility: 'hidden',
},
content: {
type: 'string',
required: true,
description: 'The main body of the note',
visibility: 'user-or-llm',
},
contactId: {
type: 'string',
required: false,
description: 'ID of contact to link to this note',
visibility: 'user-only',
},
},
request: {
url: 'https://api.crmworkspace.com/v1/notes',
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return validateAndBuildNoteBody(params)
},
},
directExecution: async (params: WealthboxWriteParams) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
const body = validateAndBuildNoteBody(params)
const response = await fetch('https://api.crmworkspace.com/v1/notes', {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
handleApiError(response, errorText)
}
const data = await response.json()
return formatNoteResponse(data)
},
transformResponse: async (response: Response, params?: WealthboxWriteParams) => {
if (!response.ok) {
const errorText = await response.text()
handleApiError(response, errorText)
}
const data = await response.json()
return formatNoteResponse(data)
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while writing Wealthbox note'
},
}

View File

@@ -0,0 +1,244 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { WealthboxWriteParams, WealthboxWriteResponse } from './types'
const logger = createLogger('WealthboxWriteTask')
// Interface for Wealthbox task request body to replace Record<string, any>
interface WealthboxTaskRequestBody {
name: string
due_date: string
description?: string // Add this field
complete?: boolean
category?: number
linked_to?: Array<{
id: number
type: string
}>
}
// Utility function to safely convert to string and trim
const safeStringify = (value: any): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'string') {
return value
}
return JSON.stringify(value)
}
// Utility function to validate parameters and build task body
const validateAndBuildTaskBody = (params: WealthboxWriteParams): WealthboxTaskRequestBody => {
// Validate required fields with safe stringification
const title = safeStringify(params.title).trim()
const dueDate = safeStringify(params.dueDate).trim()
if (!title) {
throw new Error('Task title is required')
}
if (!dueDate) {
throw new Error('Due date is required')
}
const body: WealthboxTaskRequestBody = {
name: title,
due_date: dueDate,
}
// Add optional fields with safe stringification
const description = safeStringify(params.description).trim()
if (description) {
body.description = description
}
if (params.complete !== undefined) {
body.complete = params.complete
}
if (params.category !== undefined) {
body.category = params.category
}
// Handle contact linking with safe stringification
const contactId = safeStringify(params.contactId).trim()
if (contactId) {
body.linked_to = [
{
id: Number.parseInt(contactId),
type: 'Contact',
},
]
}
return body
}
// Utility function to handle API errors
const handleApiError = (response: Response, errorText: string): never => {
logger.error(
`Wealthbox task write API error: ${response.status} ${response.statusText}`,
errorText
)
throw new Error(
`Failed to write Wealthbox task: ${response.status} ${response.statusText} - ${errorText}`
)
}
// Utility function to format task response
const formatTaskResponse = (data: any, params?: WealthboxWriteParams): WealthboxWriteResponse => {
if (!data) {
return {
success: false,
output: {
task: undefined,
metadata: {
operation: 'write_task' as const,
itemType: 'task' as const,
},
},
}
}
return {
success: true,
output: {
task: data,
success: true,
metadata: {
operation: 'write_task' as const,
itemId: data.id?.toString() || params?.taskId || '',
itemType: 'task' as const,
},
},
}
}
export const wealthboxWriteTaskTool: ToolConfig<WealthboxWriteParams, WealthboxWriteResponse> = {
id: 'wealthbox_write_task',
name: 'Write Wealthbox Task',
description: 'Create or update a Wealthbox task',
version: '1.1',
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Wealthbox API',
visibility: 'hidden',
},
title: {
type: 'string',
required: true,
description: 'The name/title of the task',
visibility: 'user-or-llm',
},
dueDate: {
type: 'string',
required: true,
description:
'The due date and time of the task (format: "YYYY-MM-DD HH:MM AM/PM -HHMM", e.g., "2015-05-24 11:00 AM -0400")',
visibility: 'user-or-llm',
},
contactId: {
type: 'string',
required: false,
description: 'ID of contact to link to this task',
visibility: 'user-only',
},
description: {
type: 'string',
required: false,
description: 'Description or notes about the task',
visibility: 'user-or-llm',
},
},
request: {
url: (params) => {
const taskId = params.taskId?.trim()
if (taskId) {
return `https://api.crmworkspace.com/v1/tasks/${taskId}`
}
return 'https://api.crmworkspace.com/v1/tasks'
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return validateAndBuildTaskBody(params)
},
},
directExecution: async (params: WealthboxWriteParams) => {
// Debug logging to see what parameters we're receiving
logger.info('WealthboxWriteTask received parameters:', {
hasAccessToken: !!params.accessToken,
hasTitle: !!params.title,
hasDueDate: !!params.dueDate,
title: params.title,
dueDate: params.dueDate,
allParams: Object.keys(params),
})
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
const body = validateAndBuildTaskBody(params)
const url = `https://api.crmworkspace.com/v1/tasks`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
handleApiError(response, errorText)
}
const data = await response.json()
return formatTaskResponse(data, params)
},
transformResponse: async (response: Response, params?: WealthboxWriteParams) => {
if (!response.ok) {
const errorText = await response.text()
handleApiError(response, errorText)
}
const data = await response.json()
return formatTaskResponse(data, params)
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while writing Wealthbox task'
},
}