mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -48,6 +48,7 @@
|
||||
"twilio_sms",
|
||||
"typeform",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
"whatsapp",
|
||||
"x",
|
||||
"youtube"
|
||||
|
||||
190
apps/docs/content/docs/tools/wealthbox.mdx
Normal file
190
apps/docs/content/docs/tools/wealthbox.mdx
Normal 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`
|
||||
153
apps/sim/app/api/auth/oauth/wealthbox/item/route.ts
Normal file
153
apps/sim/app/api/auth/oauth/wealthbox/item/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
168
apps/sim/app/api/auth/oauth/wealthbox/items/route.ts
Normal file
168
apps/sim/app/api/auth/oauth/wealthbox/items/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
132
apps/sim/app/api/tools/wealthbox/item/route.ts
Normal file
132
apps/sim/app/api/tools/wealthbox/item/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
166
apps/sim/app/api/tools/wealthbox/items/route.ts
Normal file
166
apps/sim/app/api/tools/wealthbox/items/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
221
apps/sim/blocks/blocks/wealthbox.ts
Normal file
221
apps/sim/blocks/blocks/wealthbox.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
13
apps/sim/tools/wealthbox/index.ts
Normal file
13
apps/sim/tools/wealthbox/index.ts
Normal 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 }
|
||||
133
apps/sim/tools/wealthbox/read_contact.ts
Normal file
133
apps/sim/tools/wealthbox/read_contact.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
146
apps/sim/tools/wealthbox/read_note.ts
Normal file
146
apps/sim/tools/wealthbox/read_note.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
147
apps/sim/tools/wealthbox/read_task.ts
Normal file
147
apps/sim/tools/wealthbox/read_task.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
142
apps/sim/tools/wealthbox/types.ts
Normal file
142
apps/sim/tools/wealthbox/types.ts
Normal 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'
|
||||
}
|
||||
274
apps/sim/tools/wealthbox/write_contact.ts
Normal file
274
apps/sim/tools/wealthbox/write_contact.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
181
apps/sim/tools/wealthbox/write_note.ts
Normal file
181
apps/sim/tools/wealthbox/write_note.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
244
apps/sim/tools/wealthbox/write_task.ts
Normal file
244
apps/sim/tools/wealthbox/write_task.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user