improvement(tools): removed transformError, isInternalRoute, directExecution (#928)

* standardized response format for transformError

* removed trasnformError, moved error handling to executeTool for all different error formats

* remove isInternalRoute, make it implicit in executeTool

* removed directExecution, everything on the server nothing on the client

* fix supabase

* fix(tag-dropdown): fix values for parallel & loop blocks (#929)

* fix(search-modal): add parallel and loop blocks to search modal

* reordered tool params

* update docs
This commit is contained in:
Waleed Latif
2025-08-10 17:19:46 -07:00
committed by GitHub
parent df16382a19
commit 56ede1c980
186 changed files with 4692 additions and 7975 deletions

View File

@@ -416,8 +416,8 @@ In addition, you will need to update the registries:
Your tool should export a constant with a naming convention of `{toolName}Tool`. The tool ID should follow the format `{provider}_{tool_name}`. For example:
```typescript:/apps/sim/tools/pinecone/fetch.ts
import { ToolConfig, ToolResponse } from '../types'
import { PineconeParams, PineconeResponse } from './types'
import { ToolConfig, ToolResponse } from '@/tools/types'
import { PineconeParams, PineconeResponse } from '@/tools/pinecone/types'
export const fetchTool: ToolConfig<PineconeParams, PineconeResponse> = {
id: 'pinecone_fetch', // Follow the {provider}_{tool_name} format
@@ -448,9 +448,6 @@ In addition, you will need to update the registries:
transformResponse: async (response: Response) => {
// Transform response
},
transformError: (error) => {
// Handle errors
},
}
```
@@ -458,7 +455,7 @@ In addition, you will need to update the registries:
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
```typescript:/apps/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from './pinecone'
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
// ... other imports
export const tools: Record<string, ToolConfig> = {

View File

@@ -151,8 +151,6 @@ Update multiple existing records in an Airtable table
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID or name of the table |
| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object |
| `fields` | string | No | No description |
| `fields` | string | No | No description |
#### Output

View File

@@ -82,9 +82,10 @@ Runs a browser automation task using BrowserUse
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | json | Browser automation task results including task ID, success status, output data, and execution steps |
| `error` | string | Error message if the operation failed |
| `id` | string | Task execution identifier |
| `success` | boolean | Task completion status |
| `output` | json | Task output data |
| `steps` | json | Execution steps taken |

View File

@@ -62,7 +62,7 @@ Convert TTS using ElevenLabs voices
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `audioUrl` | string | Generated audio URL |
| `audioUrl` | string | The URL of the generated audio |

View File

@@ -71,8 +71,8 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | json | Array of parsed file objects with content, metadata, and file properties |
| `combinedContent` | string | All file contents merged into a single text string |
| `files` | array | Array of parsed files |
| `combinedContent` | string | Combined content of all parsed files |

View File

@@ -101,8 +101,8 @@ Query data from a Supabase table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Query operation results |
| `message` | string | Operation status message |
| `results` | array | Array of records returned from the query |
### `supabase_insert`
@@ -121,8 +121,8 @@ Insert data into a Supabase table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Insert operation results |
| `message` | string | Operation status message |
| `results` | array | Array of inserted records |
### `supabase_get_row`
@@ -141,8 +141,8 @@ Get a single row from a Supabase table based on filter criteria
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Get row operation results |
| `message` | string | Operation status message |
| `results` | object | The row data if found, null if not found |
### `supabase_update`
@@ -162,8 +162,8 @@ Update rows in a Supabase table based on filter criteria
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Update operation results |
| `message` | string | Operation status message |
| `results` | array | Array of updated records |
### `supabase_delete`
@@ -182,8 +182,8 @@ Delete rows from a Supabase table based on filter criteria
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Delete operation results |
| `message` | string | Operation status message |
| `results` | array | Array of deleted records |

View File

@@ -235,42 +235,8 @@ export async function POST(request: Request) {
error: result.error || 'Unknown error',
})
if (tool.transformError) {
try {
const errorResult = tool.transformError(result)
// Handle both string and Promise return types
if (typeof errorResult === 'string') {
throw new Error(errorResult)
}
// It's a Promise, await it
const transformedError = await errorResult
// If it's a string or has an error property, use it
if (typeof transformedError === 'string') {
throw new Error(transformedError)
}
if (
transformedError &&
typeof transformedError === 'object' &&
'error' in transformedError
) {
throw new Error(transformedError.error || 'Tool returned an error')
}
// Fallback
throw new Error('Tool returned an error')
} catch (transformError) {
logger.error(`[${requestId}] Error transformation failed for ${toolId}`, {
error:
transformError instanceof Error ? transformError.message : String(transformError),
})
if (transformError instanceof Error) {
throw transformError
}
throw new Error('Tool returned an error')
}
} else {
throw new Error('Tool returned an error')
}
// Let the main executeTool handle error transformation to avoid double transformation
throw new Error(result.error || 'Tool execution failed')
}
const endTime = new Date()

View File

@@ -0,0 +1,147 @@
import { NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console/logger'
import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('JiraUpdateAPI')
export async function PUT(request: Request) {
try {
const {
domain,
accessToken,
issueKey,
summary,
title, // Support both summary and title for backwards compatibility
description,
status,
priority,
assignee,
cloudId: providedCloudId,
} = await request.json()
// Validate required parameters
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueKey) {
logger.error('Missing issue key in request')
return NextResponse.json({ error: 'Issue key is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
logger.info('Using cloud ID:', cloudId)
// Build the URL using cloudId for Jira API
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}`
logger.info('Updating Jira issue at:', url)
// Map the summary from either summary or title field
const summaryValue = summary || title
const fields: Record<string, any> = {}
if (summaryValue) {
fields.summary = summaryValue
}
if (description) {
fields.description = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: description,
},
],
},
],
}
}
if (status) {
fields.status = {
name: status,
}
}
if (priority) {
fields.priority = {
name: priority,
}
}
if (assignee) {
fields.assignee = {
id: assignee,
}
}
const body = { fields }
// Make the request to Jira API
const response = await fetch(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Jira API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `Jira API error: ${response.status} ${response.statusText}`, details: errorText },
{ status: response.status }
)
}
// Note: Jira update API typically returns 204 No Content on success
const responseData = response.status === 204 ? {} : await response.json()
logger.info('Successfully updated Jira issue:', issueKey)
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueKey: responseData.key || issueKey,
summary: responseData.fields?.summary || 'Issue updated',
success: true,
},
})
} catch (error: any) {
logger.error('Error updating Jira issue:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,162 @@
import { NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console/logger'
import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('JiraWriteAPI')
export async function POST(request: Request) {
try {
const {
domain,
accessToken,
projectId,
summary,
description,
priority,
assignee,
cloudId: providedCloudId,
issueType,
parent,
} = await request.json()
// Validate required parameters
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!projectId) {
logger.error('Missing project ID in request')
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
}
if (!summary) {
logger.error('Missing summary in request')
return NextResponse.json({ error: 'Summary is required' }, { status: 400 })
}
if (!issueType) {
logger.error('Missing issue type in request')
return NextResponse.json({ error: 'Issue type is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
logger.info('Using cloud ID:', cloudId)
// Build the URL using cloudId for Jira API
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue`
logger.info('Creating Jira issue at:', url)
// Construct fields object with only the necessary fields
const fields: Record<string, any> = {
project: {
id: projectId,
},
issuetype: {
name: issueType,
},
summary: summary,
}
// Only add description if it exists
if (description) {
fields.description = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: description,
},
],
},
],
}
}
// Only add parent if it exists
if (parent) {
fields.parent = parent
}
// Only add priority if it exists
if (priority) {
fields.priority = {
name: priority,
}
}
// Only add assignee if it exists
if (assignee) {
fields.assignee = {
id: assignee,
}
}
const body = { fields }
// Make the request to Jira API
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Jira API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `Jira API error: ${response.status} ${response.statusText}`, details: errorText },
{ status: response.status }
)
}
const responseData = await response.json()
logger.info('Successfully created Jira issue:', responseData.key)
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueKey: responseData.key || 'unknown',
summary: responseData.fields?.summary || 'Issue created',
success: true,
url: `https://${domain}/browse/${responseData.key}`,
},
})
} catch (error: any) {
logger.error('Error creating Jira issue:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,53 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import type { ThinkingToolParams, ThinkingToolResponse } from '@/tools/thinking/types'
const logger = createLogger('ThinkingToolAPI')
export const dynamic = 'force-dynamic'
/**
* POST - Process a thinking tool request
* Simply acknowledges the thought by returning it in the output
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
const body: ThinkingToolParams = await request.json()
logger.info(`[${requestId}] Processing thinking tool request`)
// Validate the required parameter
if (!body.thought || typeof body.thought !== 'string') {
logger.warn(`[${requestId}] Missing or invalid 'thought' parameter`)
return NextResponse.json(
{
success: false,
error: 'The thought parameter is required and must be a string',
},
{ status: 400 }
)
}
// Simply acknowledge the thought by returning it in the output
const response: ThinkingToolResponse = {
success: true,
output: {
acknowledgedThought: body.thought,
},
}
logger.info(`[${requestId}] Thinking tool processed successfully`)
return NextResponse.json(response)
} catch (error) {
logger.error(`[${requestId}] Error processing thinking tool:`, error)
return NextResponse.json(
{
success: false,
error: 'Failed to process thinking tool request',
},
{ status: 500 }
)
}
}

View File

@@ -3,7 +3,17 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow } from 'lucide-react'
import {
BookOpen,
Building2,
LibraryBig,
RepeatIcon,
ScrollText,
Search,
Shapes,
SplitIcon,
Workflow,
} from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -119,7 +129,7 @@ export function SearchModal({
if (!isOnWorkflowPage) return []
const allBlocks = getAllBlocks()
return allBlocks
const regularBlocks = allBlocks
.filter(
(block) =>
block.type !== 'starter' &&
@@ -137,7 +147,28 @@ export function SearchModal({
type: block.type,
})
)
.sort((a, b) => a.name.localeCompare(b.name))
// Add special blocks (loop and parallel)
const specialBlocks: BlockItem[] = [
{
id: 'loop',
name: 'Loop',
description: 'Create a Loop',
icon: RepeatIcon,
bgColor: '#2FB3FF',
type: 'loop',
},
{
id: 'parallel',
name: 'Parallel',
description: 'Parallel Execution',
icon: SplitIcon,
bgColor: '#FEE12B',
type: 'parallel',
},
]
return [...regularBlocks, ...specialBlocks].sort((a, b) => a.name.localeCompare(b.name))
}, [isOnWorkflowPage])
// Get all available tools - only when on workflow page

View File

@@ -271,14 +271,6 @@ export interface Tool<P = any, O = Record<string, any>> {
output: O
error?: string
}>
transformError?: (error: any) =>
| string
| Promise<{
success: boolean
output: O
error?: string
}> // Function to format error messages
}
/**

View File

@@ -259,8 +259,13 @@ async function parseWithMistralOCR(
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.MISTRAL_OCR_API)
try {
const method =
typeof mistralParserTool.request!.method === 'function'
? mistralParserTool.request!.method(requestBody as any)
: mistralParserTool.request!.method
const res = await fetch(url, {
method: mistralParserTool.request!.method,
method,
headers,
body: JSON.stringify(requestBody),
signal: controller.signal,

View File

@@ -151,7 +151,7 @@ export class ToolTester<P = any, R = any> {
this.mockFetch = createErrorFetch(errorMessage, status)
global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch
// Create an error object that the transformError function can use
// Create an error object for direct error handling
this.error = new Error(errorMessage)
this.error.message = errorMessage
this.error.status = status
@@ -170,7 +170,7 @@ export class ToolTester<P = any, R = any> {
return this
}
// Store the error for transformError to use
// Store the error for direct error handling
private error: any = null
/**
@@ -196,37 +196,23 @@ export class ToolTester<P = any, R = any> {
})
if (!response.ok) {
if (this.tool.transformError) {
// Create a more detailed error object that simulates a real error
const data = await response.json().catch(() => ({}))
// Extract error message directly from response
const data = await response.json().catch(() => ({}))
// Build an error object with all the needed properties
const error: any = new Error(data.error || data.message || 'Request failed')
error.response = response
error.status = response.status
error.data = data
// Extract meaningful error message from the response
let errorMessage = data.error || data.message || response.statusText || 'Request failed'
// Add the status code to the message to help with identifying the error type
if (response.status === 404) {
error.message = 'Not Found'
} else if (response.status === 401) {
error.message = 'Unauthorized'
}
// Use the tool's transformError which matches the real implementation
const errorMessage = await this.tool.transformError(error)
return {
success: false,
output: {},
error: errorMessage || error.message,
}
// Add specific error messages for common status codes
if (response.status === 404) {
errorMessage = data.error || data.message || 'Not Found'
} else if (response.status === 401) {
errorMessage = data.error || data.message || 'Unauthorized'
}
// If there's no transformError function, return a generic error
return {
success: false,
output: {},
error: `HTTP error ${response.status}`,
error: errorMessage,
}
}
@@ -234,19 +220,25 @@ export class ToolTester<P = any, R = any> {
return await this.handleSuccessfulResponse(response, params)
} catch (error) {
// Handle thrown errors (network errors, etc.)
if (this.tool.transformError) {
const errorToUse = this.error || error
const errorMessage = await this.tool.transformError(errorToUse)
return {
success: false,
output: {},
error: typeof errorMessage === 'string' ? errorMessage : 'Network error',
}
const errorToUse = this.error || error
// Extract error message directly from error object
let errorMessage = 'Network error'
if (errorToUse instanceof Error) {
errorMessage = errorToUse.message
} else if (typeof errorToUse === 'string') {
errorMessage = errorToUse
} else if (errorToUse && typeof errorToUse === 'object') {
// Try to extract error message from error object structure
errorMessage =
errorToUse.error || errorToUse.message || errorToUse.statusText || 'Network error'
}
return {
success: false,
output: {},
error: error instanceof Error ? error.message : 'Network error',
error: errorMessage,
}
}
}

View File

@@ -52,9 +52,6 @@ export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, Airtabl
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to create Airtable records')
}
return {
success: true,
output: {
@@ -66,10 +63,6 @@ export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, Airtabl
}
},
transformError: (error: any) => {
return `Failed to create Airtable records: ${error.message || 'Unknown error'}`
},
outputs: {
records: {
type: 'json',

View File

@@ -53,10 +53,6 @@ export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetRes
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
// logger.error('Airtable API error:', data)
throw new Error(data.error?.message || 'Failed to get Airtable record')
}
return {
success: true,
output: {
@@ -68,11 +64,6 @@ export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetRes
}
},
transformError: (error: any) => {
// logger.error('Airtable tool error:', error)
return `Failed to get Airtable record: ${error.message || 'Unknown error'}`
},
outputs: {
record: {
type: 'json',

View File

@@ -71,9 +71,6 @@ export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableLis
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to fetch Airtable records')
}
return {
success: true,
output: {
@@ -86,10 +83,6 @@ export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableLis
}
},
transformError: (error: any) => {
return `Failed to list Airtable records: ${error.message || 'Unknown error'}`
},
outputs: {
records: {
type: 'json',

View File

@@ -4,8 +4,6 @@ import type {
} from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
// import { logger } from '@/utils/logger' // Removed logger due to import issues
export const airtableUpdateMultipleRecordsTool: ToolConfig<
AirtableUpdateMultipleParams,
AirtableUpdateMultipleResponse
@@ -44,9 +42,7 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
required: true,
visibility: 'user-or-llm',
description: 'Array of records to update, each with an `id` and a `fields` object',
// Example: [{ id: "rec123", fields: { "Status": "Done" } }, { id: "rec456", fields: { "Priority": "High" } }]
},
// TODO: Add typecast, performUpsert parameters
},
request: {
@@ -57,16 +53,11 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
// Body should contain { records: [...] } and optionally { typecast: true, performUpsert: {...} }
body: (params) => ({ records: params.records }),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
// logger.error('Airtable API error:', data)
throw new Error(data.error?.message || 'Failed to update Airtable records')
}
return {
success: true,
output: {
@@ -79,11 +70,6 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
}
},
transformError: (error: any) => {
// logger.error('Airtable tool error:', error)
return `Failed to update multiple Airtable records: ${error.message || 'Unknown error'}`
},
outputs: {
records: {
type: 'json',

View File

@@ -1,8 +1,6 @@
import type { AirtableUpdateParams, AirtableUpdateResponse } from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
// import { logger } from '@/utils/logger' // Removed logger due to import issues
export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, AirtableUpdateResponse> = {
id: 'airtable_update_record',
name: 'Airtable Update Record',
@@ -56,16 +54,11 @@ export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, Airtable
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
// Body should contain { fields: {...} } and optionally { typecast: true }
body: (params) => ({ fields: params.fields }),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
// logger.error('Airtable API error:', data)
throw new Error(data.error?.message || 'Failed to update Airtable record')
}
return {
success: true,
output: {
@@ -78,11 +71,6 @@ export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, Airtable
}
},
transformError: (error: any) => {
// logger.error('Airtable tool error:', error)
return `Failed to update Airtable record: ${error.message || 'Unknown error'}`
},
outputs: {
record: {
type: 'json',

View File

@@ -11,35 +11,6 @@ export const getAuthorPapersTool: ToolConfig<
description: 'Search for papers by a specific author on ArXiv.',
version: '1.0.0',
outputs: {
authorPapers: {
type: 'json',
description: 'Array of papers authored by the specified author',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of papers found for the author',
},
},
params: {
authorName: {
type: 'string',
@@ -77,10 +48,6 @@ export const getAuthorPapersTool: ToolConfig<
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
}
const xmlText = await response.text()
// Parse XML response
@@ -97,9 +64,32 @@ export const getAuthorPapersTool: ToolConfig<
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while searching for author papers on ArXiv'
outputs: {
authorPapers: {
type: 'json',
description: 'Array of papers authored by the specified author',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of papers found for the author',
},
},
}

View File

@@ -8,31 +8,6 @@ export const getPaperTool: ToolConfig<ArxivGetPaperParams, ArxivGetPaperResponse
description: 'Get detailed information about a specific ArXiv paper by its ID.',
version: '1.0.0',
outputs: {
paper: {
type: 'json',
description: 'Detailed information about the requested ArXiv paper',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
},
params: {
paperId: {
type: 'string',
@@ -63,30 +38,39 @@ export const getPaperTool: ToolConfig<ArxivGetPaperParams, ArxivGetPaperResponse
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
}
const xmlText = await response.text()
// Parse XML response
const papers = parseArxivXML(xmlText)
if (papers.length === 0) {
throw new Error('Paper not found')
}
return {
success: true,
output: {
paper: papers[0],
paper: papers[0] || null,
},
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while retrieving the ArXiv paper'
outputs: {
paper: {
type: 'json',
description: 'Detailed information about the requested ArXiv paper',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
},
}

View File

@@ -8,35 +8,6 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
description: 'Search for academic papers on ArXiv by keywords, authors, titles, or other fields.',
version: '1.0.0',
outputs: {
papers: {
type: 'json',
description: 'Array of papers matching the search query',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of results found for the search query',
},
},
params: {
searchQuery: {
type: 'string',
@@ -107,10 +78,6 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
}
const xmlText = await response.text()
// Parse XML response
@@ -127,7 +94,32 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'An error occurred while searching ArXiv'
outputs: {
papers: {
type: 'json',
description: 'Array of papers matching the search query',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
summary: { type: 'string' },
authors: { type: 'string' },
published: { type: 'string' },
updated: { type: 'string' },
link: { type: 'string' },
pdfLink: { type: 'string' },
categories: { type: 'string' },
primaryCategory: { type: 'string' },
comment: { type: 'string' },
journalRef: { type: 'string' },
doi: { type: 'string' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of results found for the search query',
},
},
}

View File

@@ -45,15 +45,6 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
description: 'API key for BrowserUse API',
},
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'json',
description:
'Browser automation task results including task ID, success status, output data, and execution steps',
},
error: { type: 'string', description: 'Error message if the operation failed' },
},
request: {
url: 'https://api.browser-use.com/api/v1/run-task',
method: 'POST',
@@ -109,14 +100,6 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
},
transformResponse: async (response: Response) => {
if (!response.ok) {
if (response.status === 422) {
const errorData = await response.json()
throw new Error(JSON.stringify(errorData))
}
throw new Error(`Request failed with status ${response.status}`)
}
const data = (await response.json()) as { id: string }
return {
success: true,
@@ -242,19 +225,10 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
}
},
transformError: (error) => {
try {
const errorData = JSON.parse(error.message)
if (errorData.detail && Array.isArray(errorData.detail)) {
const formattedError = errorData.detail
.map((item: any) => `${item.loc.join('.')}: ${item.msg}`)
.join(', ')
return `Validation error: ${formattedError}`
}
} catch {
// Not a JSON string, use the regular error message
}
return `Failed to run BrowserUse task: ${error.message || 'Unknown error'}`
outputs: {
id: { type: 'string', description: 'Task execution identifier' },
success: { type: 'boolean', description: 'Task completion status' },
output: { type: 'json', description: 'Task output data' },
steps: { type: 'json', description: 'Execution steps taken' },
},
}

View File

@@ -28,13 +28,7 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
description: 'Auth token for Clay webhook authentication',
},
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'json',
description: 'Clay populate operation results including response data from Clay webhook',
},
},
request: {
url: (params: ClayPopulateParams) => params.webhookURL,
method: 'POST',
@@ -53,15 +47,8 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
if (contentType?.includes('application/json')) {
data = await response.json()
if (!data.ok) {
throw new Error(data.error || 'Clay API error')
}
} else {
// Handle text response
data = await response.text()
if (data !== 'OK' && !response.ok) {
throw new Error(data || 'Clay API error')
}
}
return {
@@ -72,8 +59,11 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
}
},
transformError: (error: any) => {
const message = error.message || 'Clay populate failed'
return message
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'json',
description: 'Clay populate operation results including response data from Clay webhook',
},
},
}

View File

@@ -1,4 +1,5 @@
import type { ConfluenceRetrieveParams, ConfluenceRetrieveResponse } from '@/tools/confluence/types'
import { transformPageData } from '@/tools/confluence/utils'
import type { ToolConfig } from '@/tools/types'
export const confluenceRetrieveTool: ToolConfig<
@@ -15,13 +16,6 @@ export const confluenceRetrieveTool: ToolConfig<
provider: 'confluence',
},
outputs: {
ts: { type: 'string', description: 'Timestamp of retrieval' },
pageId: { type: 'string', description: 'Confluence page ID' },
content: { type: 'string', description: 'Page content with HTML tags stripped' },
title: { type: 'string', description: 'Page title' },
},
params: {
accessToken: {
type: 'string',
@@ -73,54 +67,14 @@ export const confluenceRetrieveTool: ToolConfig<
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
errorData?.error ||
`Failed to retrieve Confluence page: ${response.status} ${response.statusText}`
)
}
const data = await response.json()
return transformPageData(data)
},
transformError: (error: any) => {
return error.message || 'Failed to retrieve Confluence page'
outputs: {
ts: { type: 'string', description: 'Timestamp of retrieval' },
pageId: { type: 'string', description: 'Confluence page ID' },
content: { type: 'string', description: 'Page content with HTML tags stripped' },
title: { type: 'string', description: 'Page title' },
},
}
function transformPageData(data: any) {
// More lenient check - only require id and title
if (!data || !data.id || !data.title) {
throw new Error('Invalid response format from Confluence API - missing required fields')
}
// Get content from wherever we can find it
const content =
data.body?.view?.value ||
data.body?.storage?.value ||
data.body?.atlas_doc_format?.value ||
data.content ||
data.description ||
`Content for page ${data.title}`
const cleanContent = content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/\s+/g, ' ')
.trim()
return {
success: true,
output: {
ts: new Date().toISOString(),
pageId: data.id,
content: cleanContent,
title: data.title,
},
}
}

View File

@@ -12,13 +12,6 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
provider: 'confluence',
},
outputs: {
ts: { type: 'string', description: 'Timestamp of update' },
pageId: { type: 'string', description: 'Confluence page ID' },
title: { type: 'string', description: 'Updated page title' },
success: { type: 'boolean', description: 'Update operation success status' },
},
params: {
accessToken: {
type: 'string',
@@ -100,19 +93,6 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => null)
console.error('Update tool error response:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
console.error(
errorData?.error ||
`Failed to update Confluence page: ${response.status} ${response.statusText}`
)
}
const data = await response.json()
return {
success: true,
@@ -126,7 +106,10 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
}
},
transformError: (error: any) => {
return error.message || 'Failed to update Confluence page'
outputs: {
ts: { type: 'string', description: 'Timestamp of update' },
pageId: { type: 'string', description: 'Confluence page ID' },
title: { type: 'string', description: 'Updated page title' },
success: { type: 'boolean', description: 'Update operation success status' },
},
}

View File

@@ -27,3 +27,33 @@ export async function getConfluenceCloudId(domain: string, accessToken: string):
throw new Error('No Confluence resources found')
}
export function transformPageData(data: any) {
// Get content from wherever we can find it
const content =
data.body?.view?.value ||
data.body?.storage?.value ||
data.body?.atlas_doc_format?.value ||
data.content ||
data.description ||
`Content for page ${data.title || 'Unknown'}`
const cleanContent = content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/\s+/g, ' ')
.trim()
return {
success: true,
output: {
ts: new Date().toISOString(),
pageId: data.id || '',
content: cleanContent,
title: data.title || '',
},
}
}

View File

@@ -1,14 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
DiscordAPIError,
DiscordGetMessagesParams,
DiscordGetMessagesResponse,
DiscordMessage,
} from '@/tools/discord/types'
import type { DiscordGetMessagesParams, DiscordGetMessagesResponse } from '@/tools/discord/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('DiscordGetMessages')
export const discordGetMessagesTool: ToolConfig<
DiscordGetMessagesParams,
DiscordGetMessagesResponse
@@ -59,36 +51,7 @@ export const discordGetMessagesTool: ToolConfig<
},
transformResponse: async (response) => {
if (!response.ok) {
let errorMessage = `Failed to get Discord messages: ${response.status} ${response.statusText}`
try {
const errorData = (await response.json()) as DiscordAPIError
errorMessage = `Failed to get Discord messages: ${errorData.message || response.statusText}`
logger.error('Discord API error', { status: response.status, error: errorData })
} catch (e) {
logger.error('Error parsing Discord API response', { status: response.status, error: e })
}
return {
success: false,
output: {
message: errorMessage,
},
error: errorMessage,
}
}
let messages: DiscordMessage[]
try {
messages = await response.json()
} catch (_e) {
return {
success: false,
error: 'Failed to parse messages',
output: { message: 'Failed to parse messages' },
}
}
const messages = await response.json()
return {
success: true,
output: {
@@ -101,11 +64,6 @@ export const discordGetMessagesTool: ToolConfig<
}
},
transformError: (error) => {
logger.error('Error retrieving Discord messages', { error })
return `Error retrieving Discord messages: ${error instanceof Error ? error.message : String(error)}`
},
outputs: {
message: { type: 'string', description: 'Success or error message' },
messages: {

View File

@@ -1,14 +1,10 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
DiscordAPIError,
DiscordGetServerParams,
DiscordGetServerResponse,
DiscordGuild,
} from '@/tools/discord/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('DiscordGetServer')
export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGetServerResponse> = {
id: 'discord_get_server',
name: 'Discord Get Server',
@@ -48,36 +44,7 @@ export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGet
},
transformResponse: async (response: Response) => {
let responseData: any
try {
responseData = await response.json()
} catch (e) {
logger.error('Error parsing Discord API response', { status: response.status, error: e })
return {
success: false,
error: 'Failed to parse server data',
output: { message: 'Failed to parse server data' },
}
}
if (!response.ok) {
const errorData = responseData as DiscordAPIError
const errorMessage = `Discord API error: ${errorData.message || response.statusText}`
logger.error('Discord API error', {
status: response.status,
error: errorData,
})
return {
success: false,
output: {
message: errorMessage,
},
error: errorMessage,
}
}
const responseData = await response.json()
return {
success: true,
@@ -88,11 +55,6 @@ export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGet
}
},
transformError: (error: Error | unknown): string => {
logger.error('Error fetching Discord server', { error })
return `Error fetching Discord server: ${error instanceof Error ? error.message : String(error)}`
},
outputs: {
message: { type: 'string', description: 'Success or error message' },
data: {

View File

@@ -1,14 +1,10 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
DiscordAPIError,
DiscordGetUserParams,
DiscordGetUserResponse,
DiscordUser,
} from '@/tools/discord/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('DiscordGetUser')
export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUserResponse> = {
id: 'discord_get_user',
name: 'Discord Get User',
@@ -47,36 +43,7 @@ export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUser
},
transformResponse: async (response) => {
if (!response.ok) {
let errorMessage = `Failed to get Discord user: ${response.status} ${response.statusText}`
try {
const errorData = (await response.json()) as DiscordAPIError
errorMessage = `Failed to get Discord user: ${errorData.message || response.statusText}`
logger.error('Discord API error', { status: response.status, error: errorData })
} catch (e) {
logger.error('Error parsing Discord API response', { status: response.status, error: e })
}
return {
success: false,
output: {
message: errorMessage,
},
error: errorMessage,
}
}
let data: DiscordUser
try {
data = await response.clone().json()
} catch (_e) {
return {
success: false,
error: 'Failed to parse user data',
output: { message: 'Failed to parse user data' },
}
}
const data: DiscordUser = await response.json()
return {
success: true,
@@ -87,11 +54,6 @@ export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUser
}
},
transformError: (error) => {
logger.error('Error retrieving Discord user information', { error })
return `Error retrieving Discord user information: ${error.error}`
},
outputs: {
message: { type: 'string', description: 'Success or error message' },
data: {

View File

@@ -1,14 +1,10 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
DiscordAPIError,
DiscordMessage,
DiscordSendMessageParams,
DiscordSendMessageResponse,
} from '@/tools/discord/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('DiscordSendMessage')
export const discordSendMessageTool: ToolConfig<
DiscordSendMessageParams,
DiscordSendMessageResponse
@@ -76,50 +72,14 @@ export const discordSendMessageTool: ToolConfig<
},
transformResponse: async (response) => {
if (!response.ok) {
let errorMessage = `Failed to send Discord message: ${response.status} ${response.statusText}`
try {
const errorData = (await response.json()) as DiscordAPIError
errorMessage = `Failed to send Discord message: ${errorData.message || response.statusText}`
logger.error('Discord API error', { status: response.status, error: errorData })
} catch (e) {
logger.error('Error parsing Discord API response', { status: response.status, error: e })
}
return {
success: false,
output: {
message: errorMessage,
},
error: errorMessage,
}
const data = (await response.json()) as DiscordMessage
return {
success: true,
output: {
message: 'Discord message sent successfully',
data,
},
}
try {
const data = (await response.json()) as DiscordMessage
return {
success: true,
output: {
message: 'Discord message sent successfully',
data,
},
}
} catch (e) {
logger.error('Error parsing successful Discord response', { error: e })
return {
success: false,
error: 'Failed to parse Discord response',
output: {
message: 'Failed to parse Discord response',
},
}
}
},
transformError: (error: Error | unknown): string => {
logger.error('Error sending Discord message', { error })
return `Error sending Discord message: ${error instanceof Error ? error.message : String(error)}`
},
outputs: {

View File

@@ -1,9 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ElevenLabsTtsParams, ElevenLabsTtsResponse } from '@/tools/elevenlabs/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ElevenLabsTool')
export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsResponse> = {
id: 'elevenlabs_tts',
name: 'ElevenLabs TTS',
@@ -49,14 +46,9 @@ export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsRes
voiceId: params.voiceId,
modelId: params.modelId || 'eleven_monolingual_v1',
}),
isInternalRoute: true,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}`)
}
const data = await response.json()
return {
@@ -67,8 +59,7 @@ export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsRes
}
},
transformError: (error) => {
logger.error('ElevenLabs TTS error:', error)
return `Error generating speech: ${error instanceof Error ? error.message : String(error)}`
outputs: {
audioUrl: { type: 'string', description: 'The URL of the generated audio' },
},
}

View File

@@ -28,29 +28,9 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
},
},
outputs: {
answer: {
type: 'string',
description: 'AI-generated answer to the question',
},
citations: {
type: 'array',
description: 'Sources and citations for the answer',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the cited source' },
url: { type: 'string', description: 'The URL of the cited source' },
text: { type: 'string', description: 'Relevant text from the cited source' },
},
},
},
},
request: {
url: 'https://api.exa.ai/answer',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
'x-api-key': params.apiKey,
@@ -70,10 +50,6 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || data.error || 'Failed to generate answer')
}
return {
success: true,
output: {
@@ -89,7 +65,22 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'An error occurred while generating an answer'
outputs: {
answer: {
type: 'string',
description: 'AI-generated answer to the question',
},
citations: {
type: 'array',
description: 'Sources and citations for the answer',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the cited source' },
url: { type: 'string', description: 'The URL of the cited source' },
text: { type: 'string', description: 'Relevant text from the cited source' },
},
},
},
},
}

View File

@@ -38,32 +38,9 @@ export const findSimilarLinksTool: ToolConfig<
},
},
outputs: {
similarLinks: {
type: 'array',
description: 'Similar links found with titles, URLs, and text snippets',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the similar webpage' },
url: { type: 'string', description: 'The URL of the similar webpage' },
text: {
type: 'string',
description: 'Text snippet or full content from the similar webpage',
},
score: {
type: 'number',
description: 'Similarity score indicating how similar the page is',
},
},
},
},
},
request: {
url: 'https://api.exa.ai/findSimilar',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
'x-api-key': params.apiKey,
@@ -90,10 +67,6 @@ export const findSimilarLinksTool: ToolConfig<
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || data.error || 'Failed to find similar links')
}
return {
success: true,
output: {
@@ -107,7 +80,25 @@ export const findSimilarLinksTool: ToolConfig<
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'An error occurred while finding similar links'
outputs: {
similarLinks: {
type: 'array',
description: 'Similar links found with titles, URLs, and text snippets',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the similar webpage' },
url: { type: 'string', description: 'The URL of the similar webpage' },
text: {
type: 'string',
description: 'Text snippet or full content from the similar webpage',
},
score: {
type: 'number',
description: 'Similarity score indicating how similar the page is',
},
},
},
},
},
}

View File

@@ -36,26 +36,9 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
},
},
outputs: {
results: {
type: 'array',
description: 'Retrieved content from URLs with title, text, and summaries',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL that content was retrieved from' },
title: { type: 'string', description: 'The title of the webpage' },
text: { type: 'string', description: 'The full text content of the webpage' },
summary: { type: 'string', description: 'AI-generated summary of the webpage content' },
},
},
},
},
request: {
url: 'https://api.exa.ai/contents',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
'x-api-key': params.apiKey,
@@ -91,10 +74,6 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || data.error || 'Failed to retrieve webpage contents')
}
return {
success: true,
output: {
@@ -108,9 +87,19 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while retrieving webpage contents'
outputs: {
results: {
type: 'array',
description: 'Retrieved content from URLs with title, text, and summaries',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL that content was retrieved from' },
title: { type: 'string', description: 'The title of the webpage' },
text: { type: 'string', description: 'The full text content of the webpage' },
summary: { type: 'string', description: 'AI-generated summary of the webpage content' },
},
},
},
},
}

View File

@@ -34,28 +34,9 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
},
},
outputs: {
research: {
type: 'array',
description: 'Comprehensive research findings with citations and summaries',
items: {
type: 'object',
properties: {
title: { type: 'string' },
url: { type: 'string' },
summary: { type: 'string' },
text: { type: 'string' },
publishedDate: { type: 'string' },
author: { type: 'string' },
score: { type: 'number' },
},
},
},
},
request: {
url: 'https://api.exa.ai/research/v0/tasks',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
'x-api-key': params.apiKey,
@@ -92,13 +73,10 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || data.error || 'Failed to create research task')
}
return {
success: true,
output: {
@@ -183,14 +161,23 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
error: `Research task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
}
},
transformError: (error) => {
const errorMessage = error?.message || ''
if (errorMessage.includes('401')) {
return new Error('Invalid API key. Please check your Exa AI API key.')
}
if (errorMessage.includes('429')) {
return new Error('Rate limit exceeded. Please try again later.')
}
return error
outputs: {
research: {
type: 'array',
description: 'Comprehensive research findings with citations and summaries',
items: {
type: 'object',
properties: {
title: { type: 'string' },
url: { type: 'string' },
summary: { type: 'string' },
text: { type: 'string' },
publishedDate: { type: 'string' },
author: { type: 'string' },
score: { type: 'number' },
},
},
},
},
}

View File

@@ -41,31 +41,9 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
},
},
outputs: {
results: {
type: 'array',
description: 'Search results with titles, URLs, and text snippets',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the search result' },
url: { type: 'string', description: 'The URL of the search result' },
publishedDate: { type: 'string', description: 'Date when the content was published' },
author: { type: 'string', description: 'The author of the content' },
summary: { type: 'string', description: 'A brief summary of the content' },
favicon: { type: 'string', description: "URL of the site's favicon" },
image: { type: 'string', description: 'URL of a representative image from the page' },
text: { type: 'string', description: 'Text snippet or full content from the page' },
score: { type: 'number', description: 'Relevance score for the search result' },
},
},
},
},
request: {
url: 'https://api.exa.ai/search',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
'x-api-key': params.apiKey,
@@ -87,10 +65,6 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || data.error || 'Failed to perform Exa search')
}
return {
success: true,
output: {
@@ -109,9 +83,24 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Exa search'
outputs: {
results: {
type: 'array',
description: 'Search results with titles, URLs, and text snippets',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'The title of the search result' },
url: { type: 'string', description: 'The URL of the search result' },
publishedDate: { type: 'string', description: 'Date when the content was published' },
author: { type: 'string', description: 'The author of the content' },
summary: { type: 'string', description: 'A brief summary of the content' },
favicon: { type: 'string', description: "URL of the site's favicon" },
image: { type: 'string', description: 'URL of a representative image from the page' },
text: { type: 'string', description: 'Text snippet or full content from the page' },
score: { type: 'number', description: 'Relevance score for the search result' },
},
},
},
},
}

View File

@@ -87,95 +87,67 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
fileType: determinedFileType,
}
},
isInternalRoute: true,
},
transformResponse: async (response: Response): Promise<FileParserOutput> => {
logger.info('Received response status:', response.status)
try {
const result = await response.json()
logger.info('Response parsed successfully')
const result = await response.json()
logger.info('Response parsed successfully')
if (!response.ok) {
const errorMsg = result.error || 'File parsing failed'
logger.error('Error in response:', errorMsg)
throw new Error(errorMsg)
}
// Handle multiple files response
if (result.results) {
logger.info('Processing multiple files response')
// Handle multiple files response
if (result.results) {
logger.info('Processing multiple files response')
// Extract individual file results
const fileResults = result.results.map((fileResult: any) => {
return fileResult.output || fileResult
})
// Extract individual file results
const fileResults = result.results.map((fileResult: any) => {
if (!fileResult.success) {
logger.warn(`Error parsing file ${fileResult.filePath}: ${fileResult.error}`)
return {
content: `Error parsing file: ${fileResult.error || 'Unknown error'}`,
fileType: 'text/plain',
size: 0,
name: fileResult.filePath.split('/').pop() || 'unknown',
binary: false,
}
}
// Combine all file contents with clear dividers
const combinedContent = fileResults
.map((file: FileParseResult, index: number) => {
const divider = `\n${'='.repeat(80)}\n`
return fileResult.output
return file.content + (index < fileResults.length - 1 ? divider : '')
})
.join('\n')
// Combine all file contents with clear dividers
const combinedContent = fileResults
.map((file: FileParseResult, index: number) => {
const divider = `\n${'='.repeat(80)}\n`
return file.content + (index < fileResults.length - 1 ? divider : '')
})
.join('\n')
// Create the base output
const output: FileParserOutputData = {
files: fileResults,
combinedContent,
}
// Add named properties for each file for dropdown access
fileResults.forEach((file: FileParseResult, index: number) => {
output[`file${index + 1}`] = file
})
return {
success: true,
output,
}
// Create the base output
const output: FileParserOutputData = {
files: fileResults,
combinedContent,
}
// Handle single file response
if (result.success) {
logger.info('Successfully parsed file:', result.output.name)
// Add named properties for each file for dropdown access
fileResults.forEach((file: FileParseResult, index: number) => {
output[`file${index + 1}`] = file
})
// For a single file, create the output with both array and named property
const output: FileParserOutputData = {
files: [result.output],
combinedContent: result.output.content,
file1: result.output,
}
return {
success: true,
output,
}
return {
success: true,
output,
}
}
// Handle error response
throw new Error(result.error || 'File parsing failed')
} catch (error) {
logger.error('Error processing response:', error)
throw error
// Handle single file response
logger.info('Successfully parsed file:', result.output?.name || 'unknown')
// For a single file, create the output with both array and named property
const output: FileParserOutputData = {
files: [result.output || result],
combinedContent: result.output?.content || result.content || '',
file1: result.output || result,
}
return {
success: true,
output,
}
},
transformError: (error: any) => {
logger.error('Error occurred:', error)
return error.message || 'File parsing failed'
outputs: {
files: { type: 'array', description: 'Array of parsed files' },
combinedContent: { type: 'string', description: 'Combined content of all parsed files' },
},
}

View File

@@ -41,7 +41,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
request: {
url: 'https://api.firecrawl.dev/v1/crawl',
method: 'POST',
isInternalRoute: false,
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
@@ -58,10 +57,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || data.message || 'Failed to create crawl job')
}
return {
success: true,
output: {
@@ -140,19 +135,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
error: `Crawl job did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
}
},
transformError: (error) => {
const errorMessage = error?.message || ''
if (errorMessage.includes('401')) {
return new Error('Invalid API key. Please check your Firecrawl API key.')
}
if (errorMessage.includes('429')) {
return new Error('Rate limit exceeded. Please try again later.')
}
if (errorMessage.includes('402')) {
return new Error('Insufficient credits. Please check your Firecrawl account.')
}
return error
},
outputs: {
pages: {

View File

@@ -45,10 +45,6 @@ export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error?.message || 'Unknown error occurred')
}
return {
success: true,
output: {
@@ -59,12 +55,6 @@ export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
}
},
transformError: (error) => {
const message = error.error?.message || error.message
const code = error.error?.type || error.code
return `${message} (${code})`
},
outputs: {
markdown: { type: 'string', description: 'Page content in markdown format' },
html: { type: 'string', description: 'Raw HTML content of the page' },

View File

@@ -37,10 +37,6 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error?.message || 'Unknown error occurred')
}
return {
success: true,
output: {
@@ -50,12 +46,6 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
}
},
transformError: (error) => {
const message = error.error?.message || error.message
const code = error.error?.type || error.code
return `${message} (${code})`
},
outputs: {
data: {
type: 'array',

View File

@@ -68,30 +68,11 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
isCustomTool: params.isCustomTool || false,
}
},
isInternalRoute: true,
},
transformResponse: async (response: Response): Promise<CodeExecutionOutput> => {
const result = await response.json()
if (!response.ok || !result.success) {
// Create enhanced error with debug information if available
const error = new Error(result.error || 'Code execution failed')
// Add debug information to the error object if available
if (result.debug) {
Object.assign(error, {
line: result.debug.line,
column: result.debug.column,
errorType: result.debug.errorType,
stack: result.debug.stack,
enhancedError: true,
})
}
throw error
}
return {
success: true,
output: {
@@ -101,11 +82,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
}
},
transformError: (error: any) => {
// If we have enhanced error information, create a more detailed message
if (error.enhancedError && error.line) {
return `Line ${error.line}${error.column ? `:${error.column}` : ''} - ${error.message}`
}
return error.message || 'Code execution failed'
outputs: {
result: { type: 'string', description: 'The result of the code execution' },
stdout: { type: 'string', description: 'The standard output of the code execution' },
},
}

View File

@@ -131,10 +131,6 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'Failed to create comment'
},
outputs: {
content: { type: 'string', description: 'Human-readable comment confirmation' },
metadata: {

View File

@@ -48,10 +48,6 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
},
transformResponse: async (response, params) => {
if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`)
}
const data = await response.json()
// Create a human-readable content string
@@ -132,19 +128,6 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
}
},
transformError: (error) => {
if (error instanceof Error) {
if (error.message.includes('404')) {
return 'Commit or repository not found. Please check the owner, repository name, and branch.'
}
if (error.message.includes('401')) {
return 'Authentication failed. Please check your GitHub token.'
}
return error.message
}
return 'Failed to fetch commit information'
},
outputs: {
content: { type: 'string', description: 'Human-readable commit summary' },
metadata: {

View File

@@ -90,10 +90,6 @@ URL: ${pr.html_url}`
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'Failed to fetch PR details'
},
outputs: {
content: { type: 'string', description: 'Human-readable PR summary' },
metadata: {

View File

@@ -40,10 +40,6 @@ export const repoInfoTool: ToolConfig<BaseGitHubParams, RepoInfoResponse> = {
},
transformResponse: async (response) => {
if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`)
}
const data = await response.json()
// Create a human-readable content string
@@ -71,19 +67,6 @@ URL: ${data.html_url}`
}
},
transformError: (error) => {
if (error instanceof Error) {
if (error.message.includes('404')) {
return 'Repository not found. Please check the owner and repository name.'
}
if (error.message.includes('401')) {
return 'Authentication failed. Please check your GitHub token.'
}
return error.message
}
return 'Failed to fetch repository information'
},
outputs: {
content: { type: 'string', description: 'Human-readable repository summary' },
metadata: {

View File

@@ -9,26 +9,6 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
description: 'Draft emails using Gmail',
version: '1.0.0',
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Draft metadata',
properties: {
id: { type: 'string', description: 'Draft ID' },
message: {
type: 'object',
description: 'Message metadata',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
},
},
},
},
},
oauth: {
required: true,
provider: 'google-email',
@@ -109,10 +89,6 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to draft email')
}
return {
success: true,
output: {
@@ -129,16 +105,23 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while drafting email'
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Draft metadata',
properties: {
id: { type: 'string', description: 'Draft ID' },
message: {
type: 'object',
description: 'Message metadata',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
},
},
},
},
},
}

View File

@@ -1,13 +1,12 @@
import type {
GmailAttachment,
GmailMessage,
GmailReadParams,
GmailToolResponse,
} from '@/tools/gmail/types'
import type { GmailReadParams, GmailToolResponse } from '@/tools/gmail/types'
import {
createMessagesSummary,
GMAIL_API_BASE,
processMessage,
processMessageForSummary,
} from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
id: 'gmail_read',
name: 'Gmail Read',
@@ -23,12 +22,6 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
],
},
outputs: {
content: { type: 'string', description: 'Text content of the email' },
metadata: { type: 'json', description: 'Metadata of the email' },
attachments: { type: 'file[]', description: 'Attachments of the email' },
},
params: {
accessToken: {
type: 'string',
@@ -120,10 +113,6 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
transformResponse: async (response: Response, params?: GmailReadParams) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to read email')
}
// If we're fetching a single message directly (by ID)
if (params?.messageId) {
return await processMessage(data, params)
@@ -253,248 +242,9 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while reading email'
outputs: {
content: { type: 'string', description: 'Text content of the email' },
metadata: { type: 'json', description: 'Metadata of the email' },
attachments: { type: 'file[]', description: 'Attachments of the email' },
},
}
// Helper function to process a Gmail message
async function processMessage(
message: GmailMessage,
params?: GmailReadParams
): Promise<GmailToolResponse> {
// Check if message and payload exist
if (!message || !message.payload) {
return {
success: true,
output: {
content: 'Unable to process email: Invalid message format',
metadata: {
id: message?.id || '',
threadId: message?.threadId || '',
labelIds: message?.labelIds || [],
},
},
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || ''
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || ''
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value || ''
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
// Extract the message body
const body = extractMessageBody(message.payload)
// Check for attachments
const attachmentInfo = extractAttachmentInfo(message.payload)
const hasAttachments = attachmentInfo.length > 0
// Download attachments if requested
let attachments: GmailAttachment[] | undefined
if (params?.includeAttachments && hasAttachments && params.accessToken) {
try {
attachments = await downloadAttachments(message.id, attachmentInfo, params.accessToken)
} catch (error) {
// Continue without attachments rather than failing the entire request
}
}
const result: GmailToolResponse = {
success: true,
output: {
content: body || 'No content found in email',
metadata: {
id: message.id || '',
threadId: message.threadId || '',
labelIds: message.labelIds || [],
from,
to,
subject,
date,
hasAttachments,
attachmentCount: attachmentInfo.length,
},
// Always include attachments array (empty if none downloaded)
attachments: attachments || [],
},
}
return result
}
// Helper function to process a message for summary (without full content)
function processMessageForSummary(message: GmailMessage): any {
if (!message || !message.payload) {
return {
id: message?.id || '',
threadId: message?.threadId || '',
subject: 'Unknown Subject',
from: 'Unknown Sender',
date: '',
snippet: message?.snippet || '',
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
return {
id: message.id,
threadId: message.threadId,
subject,
from,
date,
snippet: message.snippet || '',
}
}
// Helper function to create a summary of multiple messages
function createMessagesSummary(messages: any[]): string {
if (messages.length === 0) {
return 'No messages found.'
}
let summary = `Found ${messages.length} messages:\n\n`
messages.forEach((msg, index) => {
summary += `${index + 1}. Subject: ${msg.subject}\n`
summary += ` From: ${msg.from}\n`
summary += ` Date: ${msg.date}\n`
summary += ` Preview: ${msg.snippet}\n\n`
})
summary += `To read a specific message, use the messageId parameter with one of these IDs: ${messages.map((m) => m.id).join(', ')}`
return summary
}
// Helper function to recursively extract message body from MIME parts
function extractMessageBody(payload: any): string {
// If the payload has a body with data, decode it
if (payload.body?.data) {
return Buffer.from(payload.body.data, 'base64').toString()
}
// If there are no parts, return empty string
if (!payload.parts || !Array.isArray(payload.parts) || payload.parts.length === 0) {
return ''
}
// First try to find a text/plain part
const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain')
if (textPart?.body?.data) {
return Buffer.from(textPart.body.data, 'base64').toString()
}
// If no text/plain, try to find text/html
const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html')
if (htmlPart?.body?.data) {
return Buffer.from(htmlPart.body.data, 'base64').toString()
}
// If we have multipart/alternative or other complex types, recursively check parts
for (const part of payload.parts) {
if (part.parts) {
const nestedBody = extractMessageBody(part)
if (nestedBody) {
return nestedBody
}
}
}
// If we couldn't find any text content, return empty string
return ''
}
// Helper function to extract attachment information from message payload
function extractAttachmentInfo(
payload: any
): Array<{ attachmentId: string; filename: string; mimeType: string; size: number }> {
const attachments: Array<{
attachmentId: string
filename: string
mimeType: string
size: number
}> = []
function processPayloadPart(part: any) {
// Check if this part has an attachment
if (part.body?.attachmentId && part.filename) {
attachments.push({
attachmentId: part.body.attachmentId,
filename: part.filename,
mimeType: part.mimeType || 'application/octet-stream',
size: part.body.size || 0,
})
}
// Recursively process nested parts
if (part.parts && Array.isArray(part.parts)) {
part.parts.forEach(processPayloadPart)
}
}
// Process the main payload
processPayloadPart(payload)
return attachments
}
// Helper function to download attachments from Gmail API
async function downloadAttachments(
messageId: string,
attachmentInfo: Array<{ attachmentId: string; filename: string; mimeType: string; size: number }>,
accessToken: string
): Promise<GmailAttachment[]> {
const downloadedAttachments: GmailAttachment[] = []
for (const attachment of attachmentInfo) {
try {
// Download attachment from Gmail API
const attachmentResponse = await fetch(
`${GMAIL_API_BASE}/messages/${messageId}/attachments/${attachment.attachmentId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!attachmentResponse.ok) {
continue
}
const attachmentData = (await attachmentResponse.json()) as { data: string; size: number }
// Decode base64url data to buffer
// Gmail API returns data in base64url format (URL-safe base64)
const base64Data = attachmentData.data.replace(/-/g, '+').replace(/_/g, '/')
const buffer = Buffer.from(base64Data, 'base64')
downloadedAttachments.push({
name: attachment.filename,
data: buffer,
mimeType: attachment.mimeType,
size: attachment.size,
})
} catch (error) {
// Continue with other attachments
}
}
return downloadedAttachments
}

View File

@@ -1,39 +1,17 @@
import type { GmailSearchParams, GmailToolResponse } from '@/tools/gmail/types'
import {
createMessagesSummary,
GMAIL_API_BASE,
processMessageForSummary,
} from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> = {
id: 'gmail_search',
name: 'Gmail Search',
description: 'Search emails in Gmail',
version: '1.0.0',
outputs: {
content: { type: 'string', description: 'Search results summary' },
metadata: {
type: 'object',
description: 'Search metadata',
properties: {
results: {
type: 'array',
description: 'Array of search results',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
subject: { type: 'string', description: 'Email subject' },
from: { type: 'string', description: 'Sender email address' },
date: { type: 'string', description: 'Email date' },
snippet: { type: 'string', description: 'Email snippet/preview' },
},
},
},
},
},
},
oauth: {
required: true,
provider: 'google-email',
@@ -80,10 +58,6 @@ export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> =
transformResponse: async (response, params) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to search emails')
}
if (!data.messages || data.messages.length === 0) {
return {
success: true,
@@ -151,65 +125,28 @@ export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> =
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while searching emails'
outputs: {
content: { type: 'string', description: 'Search results summary' },
metadata: {
type: 'object',
description: 'Search metadata',
properties: {
results: {
type: 'array',
description: 'Array of search results',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
subject: { type: 'string', description: 'Email subject' },
from: { type: 'string', description: 'Sender email address' },
date: { type: 'string', description: 'Email date' },
snippet: { type: 'string', description: 'Email snippet/preview' },
},
},
},
},
},
},
}
// Helper function to process a message for summary (without full content)
function processMessageForSummary(message: any): any {
if (!message || !message.payload) {
return {
id: message?.id || '',
threadId: message?.threadId || '',
subject: 'Unknown Subject',
from: 'Unknown Sender',
date: '',
snippet: message?.snippet || '',
}
}
const headers = message.payload.headers || []
const subject =
headers.find((h: any) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
const from = headers.find((h: any) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
const date = headers.find((h: any) => h.name.toLowerCase() === 'date')?.value || ''
return {
id: message.id,
threadId: message.threadId,
subject,
from,
date,
snippet: message.snippet || '',
}
}
// Helper function to create a summary of multiple messages
function createMessagesSummary(messages: any[]): string {
if (messages.length === 0) {
return 'No messages found.'
}
let summary = `Found ${messages.length} messages:\n\n`
messages.forEach((msg, index) => {
summary += `${index + 1}. Subject: ${msg.subject}\n`
summary += ` From: ${msg.from}\n`
summary += ` Date: ${msg.date}\n`
summary += ` Preview: ${msg.snippet}\n\n`
})
summary += `To read full content of a specific message, use the gmail_read tool with messageId: ${messages.map((m) => m.id).join(', ')}`
return summary
}

View File

@@ -1,27 +1,13 @@
import type { GmailSendParams, GmailToolResponse } from '@/tools/gmail/types'
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
id: 'gmail_send',
name: 'Gmail Send',
description: 'Send emails using Gmail',
version: '1.0.0',
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Email metadata',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
},
},
},
oauth: {
required: true,
provider: 'google-email',
@@ -100,10 +86,6 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to send email')
}
return {
success: true,
output: {
@@ -117,16 +99,16 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while sending email'
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Email metadata',
properties: {
id: { type: 'string', description: 'Gmail message ID' },
threadId: { type: 'string', description: 'Gmail thread ID' },
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
},
},
},
}

View File

@@ -0,0 +1,240 @@
import type {
GmailAttachment,
GmailMessage,
GmailReadParams,
GmailToolResponse,
} from '@/tools/gmail/types'
export const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
// Helper function to process a Gmail message
export async function processMessage(
message: GmailMessage,
params?: GmailReadParams
): Promise<GmailToolResponse> {
// Check if message and payload exist
if (!message || !message.payload) {
return {
success: true,
output: {
content: 'Unable to process email: Invalid message format',
metadata: {
id: message?.id || '',
threadId: message?.threadId || '',
labelIds: message?.labelIds || [],
},
},
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || ''
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || ''
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value || ''
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
// Extract the message body
const body = extractMessageBody(message.payload)
// Check for attachments
const attachmentInfo = extractAttachmentInfo(message.payload)
const hasAttachments = attachmentInfo.length > 0
// Download attachments if requested
let attachments: GmailAttachment[] | undefined
if (params?.includeAttachments && hasAttachments && params.accessToken) {
try {
attachments = await downloadAttachments(message.id, attachmentInfo, params.accessToken)
} catch (error) {
// Continue without attachments rather than failing the entire request
}
}
const result: GmailToolResponse = {
success: true,
output: {
content: body || 'No content found in email',
metadata: {
id: message.id || '',
threadId: message.threadId || '',
labelIds: message.labelIds || [],
from,
to,
subject,
date,
hasAttachments,
attachmentCount: attachmentInfo.length,
},
// Always include attachments array (empty if none downloaded)
attachments: attachments || [],
},
}
return result
}
// Helper function to process a message for summary (without full content)
export function processMessageForSummary(message: GmailMessage): any {
if (!message || !message.payload) {
return {
id: message?.id || '',
threadId: message?.threadId || '',
subject: 'Unknown Subject',
from: 'Unknown Sender',
date: '',
snippet: message?.snippet || '',
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
return {
id: message.id,
threadId: message.threadId,
subject,
from,
date,
snippet: message.snippet || '',
}
}
// Helper function to recursively extract message body from MIME parts
export function extractMessageBody(payload: any): string {
// If the payload has a body with data, decode it
if (payload.body?.data) {
return Buffer.from(payload.body.data, 'base64').toString()
}
// If there are no parts, return empty string
if (!payload.parts || !Array.isArray(payload.parts) || payload.parts.length === 0) {
return ''
}
// First try to find a text/plain part
const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain')
if (textPart?.body?.data) {
return Buffer.from(textPart.body.data, 'base64').toString()
}
// If no text/plain, try to find text/html
const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html')
if (htmlPart?.body?.data) {
return Buffer.from(htmlPart.body.data, 'base64').toString()
}
// If we have multipart/alternative or other complex types, recursively check parts
for (const part of payload.parts) {
if (part.parts) {
const nestedBody = extractMessageBody(part)
if (nestedBody) {
return nestedBody
}
}
}
// If we couldn't find any text content, return empty string
return ''
}
// Helper function to extract attachment information from message payload
export function extractAttachmentInfo(
payload: any
): Array<{ attachmentId: string; filename: string; mimeType: string; size: number }> {
const attachments: Array<{
attachmentId: string
filename: string
mimeType: string
size: number
}> = []
function processPayloadPart(part: any) {
// Check if this part has an attachment
if (part.body?.attachmentId && part.filename) {
attachments.push({
attachmentId: part.body.attachmentId,
filename: part.filename,
mimeType: part.mimeType || 'application/octet-stream',
size: part.body.size || 0,
})
}
// Recursively process nested parts
if (part.parts && Array.isArray(part.parts)) {
part.parts.forEach(processPayloadPart)
}
}
// Process the main payload
processPayloadPart(payload)
return attachments
}
// Helper function to download attachments from Gmail API
export async function downloadAttachments(
messageId: string,
attachmentInfo: Array<{ attachmentId: string; filename: string; mimeType: string; size: number }>,
accessToken: string
): Promise<GmailAttachment[]> {
const downloadedAttachments: GmailAttachment[] = []
for (const attachment of attachmentInfo) {
try {
// Download attachment from Gmail API
const attachmentResponse = await fetch(
`${GMAIL_API_BASE}/messages/${messageId}/attachments/${attachment.attachmentId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!attachmentResponse.ok) {
continue
}
const attachmentData = (await attachmentResponse.json()) as { data: string; size: number }
// Decode base64url data to buffer
// Gmail API returns data in base64url format (URL-safe base64)
const base64Data = attachmentData.data.replace(/-/g, '+').replace(/_/g, '/')
const buffer = Buffer.from(base64Data, 'base64')
downloadedAttachments.push({
name: attachment.filename,
data: buffer,
mimeType: attachment.mimeType,
size: attachment.size,
})
} catch (error) {
// Continue with other attachments
}
}
return downloadedAttachments
}
// Helper function to create a summary of multiple messages
export function createMessagesSummary(messages: any[]): string {
if (messages.length === 0) {
return 'No messages found.'
}
let summary = `Found ${messages.length} messages:\n\n`
messages.forEach((msg, index) => {
summary += `${index + 1}. Subject: ${msg.subject}\n`
summary += ` From: ${msg.from}\n`
summary += ` Date: ${msg.date}\n`
summary += ` Preview: ${msg.snippet}\n\n`
})
summary += `To read full content of a specific message, use the gmail_read tool with messageId: ${messages.map((m) => m.id).join(', ')}`
return summary
}

View File

@@ -34,6 +34,46 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
},
},
request: {
url: (params: GoogleSearchParams) => {
const baseUrl = 'https://www.googleapis.com/customsearch/v1'
const searchParams = new URLSearchParams()
// Add required parameters
searchParams.append('key', params.apiKey)
searchParams.append('q', params.query)
searchParams.append('cx', params.searchEngineId)
// Add optional parameter
if (params.num) {
searchParams.append('num', params.num.toString())
}
return `${baseUrl}?${searchParams.toString()}`
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
items: data.items || [],
searchInformation: data.searchInformation || {
totalResults: '0',
searchTime: 0,
formattedSearchTime: '0',
formattedTotalResults: '0',
},
},
}
},
outputs: {
items: {
type: 'array',
@@ -63,55 +103,4 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
},
},
},
request: {
url: (params: GoogleSearchParams) => {
const baseUrl = 'https://www.googleapis.com/customsearch/v1'
const searchParams = new URLSearchParams()
// Add required parameters
searchParams.append('key', params.apiKey)
searchParams.append('q', params.query)
searchParams.append('cx', params.searchEngineId)
// Add optional parameter
if (params.num) {
searchParams.append('num', params.num.toString())
}
return `${baseUrl}?${searchParams.toString()}`
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to perform Google search')
}
const data = await response.json()
return {
success: true,
output: {
items: data.items || [],
searchInformation: data.searchInformation || {
totalResults: '0',
searchTime: 0,
formattedSearchTime: '0',
formattedTotalResults: '0',
},
},
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Google search'
},
}

View File

@@ -148,20 +148,7 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
},
},
outputs: {
content: { type: 'string', description: 'Event creation confirmation message' },
metadata: {
type: 'json',
description: 'Created event metadata including ID, status, and details',
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to create calendar event')
}
const data: GoogleCalendarApiEventResponse = await response.json()
return {
@@ -185,19 +172,11 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while creating the calendar event'
outputs: {
content: { type: 'string', description: 'Event creation confirmation message' },
metadata: {
type: 'json',
description: 'Created event metadata including ID, status, and details',
},
},
}

View File

@@ -51,20 +51,7 @@ export const getTool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetRespo
}),
},
outputs: {
content: { type: 'string', description: 'Event retrieval confirmation message' },
metadata: {
type: 'json',
description: 'Event details including ID, status, times, and attendees',
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to get calendar event')
}
const data: GoogleCalendarApiEventResponse = await response.json()
return {
@@ -88,25 +75,11 @@ export const getTool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetRespo
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
if (
error.error.message.includes('Event not found') ||
error.error.message.includes('Not Found')
) {
return 'Event not found. Please check the event ID.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while retrieving the calendar event'
outputs: {
content: { type: 'string', description: 'Event retrieval confirmation message' },
metadata: {
type: 'json',
description: 'Event details including ID, status, times, and attendees',
},
},
}

View File

@@ -56,17 +56,6 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
},
},
outputs: {
content: {
type: 'string',
description: 'Attendee invitation confirmation message with email delivery status',
},
metadata: {
type: 'json',
description: 'Updated event metadata including attendee list and details',
},
},
request: {
url: (params: GoogleCalendarInviteParams) => {
const calendarId = params.calendarId || 'primary'
@@ -82,10 +71,6 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
if (!response.ok) {
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
}
// Validate required fields exist
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
@@ -258,30 +243,14 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
if (
error.error.message.includes('Event not found') ||
error.error.message.includes('Not Found')
) {
return 'Event not found. Please check the event ID.'
}
if (error.error.message.includes('Failed to fetch existing event')) {
return `Unable to retrieve existing event details: ${error.error.message}`
}
return error.error.message
}
return (
error.message || 'An unexpected error occurred while inviting attendees to the calendar event'
)
outputs: {
content: {
type: 'string',
description: 'Attendee invitation confirmation message with email delivery status',
},
metadata: {
type: 'json',
description: 'Updated event metadata including attendee list and details',
},
},
}

View File

@@ -80,20 +80,7 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
}),
},
outputs: {
content: { type: 'string', description: 'Summary of found events count' },
metadata: {
type: 'json',
description: 'List of events with pagination tokens and event details',
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to list calendar events')
}
const data: GoogleCalendarApiListResponse = await response.json()
const events = data.items || []
const eventsCount = events.length
@@ -124,19 +111,11 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while listing calendar events'
outputs: {
content: { type: 'string', description: 'Summary of found events count' },
metadata: {
type: 'json',
description: 'List of events with pagination tokens and event details',
},
},
}

View File

@@ -74,21 +74,9 @@ export const quickAddTool: ToolConfig<
}),
},
outputs: {
content: {
type: 'string',
description: 'Event creation confirmation message from natural language',
},
metadata: { type: 'json', description: 'Created event metadata including parsed details' },
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to create calendar event from text')
}
// Handle attendees if provided
let finalEventData = data
if (params?.attendees) {
@@ -171,22 +159,11 @@ export const quickAddTool: ToolConfig<
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
if (error.error.message.includes('parse')) {
return 'Could not parse the natural language text. Please try a different format.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while creating the calendar event'
outputs: {
content: {
type: 'string',
description: 'Event creation confirmation message from natural language',
},
metadata: { type: 'json', description: 'Created event metadata including parsed details' },
},
}

View File

@@ -86,14 +86,6 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
},
},
outputs: {
content: { type: 'string', description: 'Event update confirmation message' },
metadata: {
type: 'json',
description: 'Updated event metadata including ID, status, and details',
},
},
request: {
url: (params: GoogleCalendarUpdateParams) => {
const calendarId = params.calendarId || 'primary'
@@ -109,10 +101,6 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
if (!response.ok) {
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
}
// Start with the complete existing event to preserve all fields
const updatedEvent = { ...existingEvent }
@@ -257,28 +245,11 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
if (
error.error.message.includes('Event not found') ||
error.error.message.includes('Not Found')
) {
return 'Event not found. Please check the event ID.'
}
if (error.error.message.includes('Failed to fetch existing event')) {
return `Unable to retrieve existing event details: ${error.error.message}`
}
return error.error.message
}
return error.message || 'An unexpected error occurred while updating the calendar event'
outputs: {
content: { type: 'string', description: 'Event update confirmation message' },
metadata: {
type: 'json',
description: 'Updated event metadata including ID, status, and details',
},
},
}

View File

@@ -9,11 +9,13 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
name: 'Create Google Docs Document',
description: 'Create a new Google Docs document',
version: '1.0',
oauth: {
required: true,
provider: 'google-docs',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -46,6 +48,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
description: 'The ID of the folder to create the document in (internal use)',
},
},
request: {
url: () => {
return 'https://www.googleapis.com/drive/v3/files'
@@ -81,6 +84,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
return requestBody
},
},
postProcess: async (result, params, executeTool) => {
if (!result.success) {
return result
@@ -113,27 +117,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
return result
},
outputs: {
metadata: {
type: 'json',
description: 'Created document metadata including ID, title, and URL',
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let errorText = ''
try {
const responseClone = response.clone()
const responseText = await responseClone.text()
errorText = responseText
} catch (_e) {
errorText = 'Unable to read error response'
}
throw new Error(`Failed to create Google Docs document (${response.status}): ${errorText}`)
}
try {
// Get the response data
const responseText = await response.text()
@@ -162,23 +146,11 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
throw error
}
},
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 Google Docs document'
outputs: {
metadata: {
type: 'json',
description: 'Created document metadata including ID, title, and URL',
},
},
}

View File

@@ -1,4 +1,5 @@
import type { GoogleDocsReadResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
import { extractTextFromDocument } from '@/tools/google_docs/utils'
import type { ToolConfig } from '@/tools/types'
export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse> = {
@@ -6,11 +7,13 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
name: 'Read Google Docs Document',
description: 'Read content from a Google Docs document',
version: '1.0',
oauth: {
required: true,
provider: 'google-docs',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -25,6 +28,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
description: 'The ID of the document to read',
},
},
request: {
url: (params) => {
// Ensure documentId is valid
@@ -48,17 +52,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
},
},
outputs: {
content: { type: 'string', description: 'Extracted document text content' },
metadata: { type: 'json', description: 'Document metadata including ID, title, and URL' },
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to read Google Docs document: ${errorText}`)
}
const data = await response.json()
// Extract document content from the response
@@ -83,62 +77,9 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
},
}
},
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 Google Docs document'
outputs: {
content: { type: 'string', description: 'Extracted document text content' },
metadata: { type: 'json', description: 'Document metadata including ID, title, and URL' },
},
}
// Helper function to extract text content from Google Docs document structure
function extractTextFromDocument(document: any): string {
let text = ''
if (!document.body || !document.body.content) {
return text
}
// Process each structural element in the document
for (const element of document.body.content) {
if (element.paragraph) {
for (const paragraphElement of element.paragraph.elements) {
if (paragraphElement.textRun?.content) {
text += paragraphElement.textRun.content
}
}
} else if (element.table) {
// Process tables if needed
for (const tableRow of element.table.tableRows) {
for (const tableCell of tableRow.tableCells) {
if (tableCell.content) {
for (const cellContent of tableCell.content) {
if (cellContent.paragraph) {
for (const paragraphElement of cellContent.paragraph.elements) {
if (paragraphElement.textRun?.content) {
text += paragraphElement.textRun.content
}
}
}
}
}
}
}
}
}
return text
}

View File

@@ -0,0 +1,38 @@
// Helper function to extract text content from Google Docs document structure
export function extractTextFromDocument(document: any): string {
let text = ''
if (!document.body || !document.body.content) {
return text
}
// Process each structural element in the document
for (const element of document.body.content) {
if (element.paragraph) {
for (const paragraphElement of element.paragraph.elements) {
if (paragraphElement.textRun?.content) {
text += paragraphElement.textRun.content
}
}
} else if (element.table) {
// Process tables if needed
for (const tableRow of element.table.tableRows) {
for (const tableCell of tableRow.tableCells) {
if (tableCell.content) {
for (const cellContent of tableCell.content) {
if (cellContent.paragraph) {
for (const paragraphElement of cellContent.paragraph.elements) {
if (paragraphElement.textRun?.content) {
text += paragraphElement.textRun.content
}
}
}
}
}
}
}
}
}
return text
}

View File

@@ -87,18 +87,6 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let errorText = ''
try {
const responseClone = response.clone()
const responseText = await responseClone.text()
errorText = responseText
} catch (_e) {
errorText = 'Unable to read error response'
}
throw new Error(`Failed to write to Google Docs document (${response.status}): ${errorText}`)
}
const responseText = await response.text()
// Parse the response if it's not empty
@@ -133,23 +121,4 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
},
}
},
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 to Google Docs'
},
}

View File

@@ -6,11 +6,13 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
name: 'Create Folder in Google Drive',
description: 'Create a new folder in Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -37,6 +39,7 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
description: 'ID of the parent folder (internal use)',
},
},
request: {
url: 'https://www.googleapis.com/drive/v3/files',
method: 'POST',
@@ -64,13 +67,6 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
},
},
outputs: {
file: {
type: 'json',
description: 'Created folder metadata including ID, name, and parent information',
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
@@ -95,7 +91,11 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while creating folder in Google Drive'
outputs: {
file: {
type: 'json',
description: 'Created folder metadata including ID, name, and parent information',
},
},
}

View File

@@ -14,11 +14,13 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
description:
'Get content from a file in Google Drive (exports Google Workspace files automatically)',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -40,17 +42,6 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
},
},
outputs: {
content: {
type: 'string',
description: 'File content as text (Google Workspace files are exported)',
},
metadata: {
type: 'json',
description: 'File metadata including ID, name, MIME type, and links',
},
},
request: {
url: (params) =>
`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=id,name,mimeType`,
@@ -178,11 +169,15 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
throw error
}
},
transformError: (error) => {
logger.error('Download error', {
message: error.message,
stack: error.stack,
})
return error.message || 'An error occurred while getting content from Google Drive'
outputs: {
content: {
type: 'string',
description: 'File content as text (Google Workspace files are exported)',
},
metadata: {
type: 'json',
description: 'File metadata including ID, name, MIME type, and links',
},
},
}

View File

@@ -6,11 +6,13 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
name: 'List Google Drive Files',
description: 'List files and folders in Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -49,6 +51,7 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
description: 'The page token to use for pagination',
},
},
request: {
url: (params) => {
const url = new URL('https://www.googleapis.com/drive/v3/files')
@@ -87,13 +90,6 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
}),
},
outputs: {
files: {
type: 'json',
description: 'Array of file metadata objects from the specified folder',
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
@@ -119,7 +115,11 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while listing Google Drive files'
outputs: {
files: {
type: 'json',
description: 'Array of file metadata objects from the specified folder',
},
},
}

View File

@@ -10,11 +10,13 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
name: 'Upload to Google Drive',
description: 'Upload a file to Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: {
type: 'string',
@@ -53,6 +55,7 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
description: 'The ID of the folder to upload the file to (internal use)',
},
},
request: {
url: 'https://www.googleapis.com/drive/v3/files',
method: 'POST',
@@ -80,10 +83,6 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
},
},
outputs: {
file: { type: 'json', description: 'Uploaded file metadata including ID, name, and links' },
},
transformResponse: async (response: Response, params?: GoogleDriveToolParams) => {
try {
const data = await response.json()
@@ -202,11 +201,8 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
throw error
}
},
transformError: (error) => {
logger.error('Upload error', {
error: error.message,
stack: error.stack,
})
return error.message || 'An error occurred while uploading to Google Drive'
outputs: {
file: { type: 'json', description: 'Uploaded file metadata including ID, name, and links' },
},
}

View File

@@ -9,11 +9,13 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
name: 'Append to Google Sheets',
description: 'Append data to the end of a Google Sheets spreadsheet',
version: '1.0',
oauth: {
required: true,
provider: 'google-sheets',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
@@ -58,6 +60,7 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
description: 'Whether to include the appended values in the response',
},
},
request: {
url: (params) => {
// If range is not provided, use a default range for the first sheet
@@ -177,21 +180,7 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
},
},
outputs: {
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to append data to Google Sheets: ${errorText}`)
}
const data = await response.json()
// Extract spreadsheet ID from the URL
@@ -222,23 +211,13 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
return result
},
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 appending to Google Sheets'
outputs: {
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
}

View File

@@ -6,11 +6,13 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
name: 'Read from Google Sheets',
description: 'Read data from a Google Sheets spreadsheet',
version: '1.0',
oauth: {
required: true,
provider: 'google-sheets',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
@@ -31,6 +33,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
description: 'The range of cells to read from',
},
},
request: {
url: (params) => {
// Ensure spreadsheetId is valid
@@ -60,21 +63,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
},
},
outputs: {
data: { type: 'json', description: 'Sheet data including range and cell values' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
const errorText =
errorJson.error && typeof errorJson.error === 'object'
? errorJson.error.message || JSON.stringify(errorJson.error)
: errorJson.error || response.statusText
throw new Error(`Failed to read Google Sheets data: ${errorText}`)
}
const data = await response.json()
// Extract spreadsheet ID from the URL
@@ -105,41 +94,9 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
return result
},
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) {
// Handle Google API error response format
if (error.error) {
if (typeof error.error === 'string') {
return error.error
}
// Google API often returns error objects with error.error.message
if (typeof error.error === 'object' && error.error.message) {
return error.error.message
}
// If error.error is an object but doesn't have a message property
return JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
// If we have a complex object, stringify it for debugging
try {
return `Google Sheets API error: ${JSON.stringify(error)}`
} catch (_e) {
// In case the error object can't be stringified (e.g., circular references)
return 'Google Sheets API error: Unable to parse error details'
}
}
// Default fallback message
return 'An error occurred while reading from Google Sheets'
outputs: {
data: { type: 'json', description: 'Sheet data including range and cell values' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
}

View File

@@ -9,11 +9,13 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
name: 'Update Google Sheets',
description: 'Update data in a Google Sheets spreadsheet',
version: '1.0',
oauth: {
required: true,
provider: 'google-sheets',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
@@ -52,6 +54,7 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
description: 'Whether to include the updated values in the response',
},
},
request: {
url: (params) => {
// If range is not provided, use a default range for the first sheet, second row to preserve headers
@@ -131,20 +134,7 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
},
},
outputs: {
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to update data in Google Sheets: ${errorText}`)
}
const data = await response.json()
// Extract spreadsheet ID from the URL
@@ -174,23 +164,12 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
return result
},
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 updating Google Sheets'
outputs: {
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
}

View File

@@ -6,11 +6,13 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
name: 'Write to Google Sheets',
description: 'Write data to a Google Sheets spreadsheet',
version: '1.0',
oauth: {
required: true,
provider: 'google-sheets',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
@@ -49,6 +51,7 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
description: 'Whether to include the written values in the response',
},
},
request: {
url: (params) => {
// If range is not provided, use a default range for the first sheet, second row to preserve headers
@@ -128,20 +131,7 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
},
},
outputs: {
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to write data to Google Sheets: ${errorText}`)
}
const data = await response.json()
// Extract spreadsheet ID from the URL
@@ -171,23 +161,12 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
return result
},
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 to Google Sheets'
outputs: {
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
},
}

View File

@@ -1,176 +1,6 @@
import { getEnv } from '@/lib/env'
import { isTest } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import type { RequestParams, RequestResponse } from '@/tools/http/types'
import type { HttpMethod, TableRow, ToolConfig } from '@/tools/types'
const logger = createLogger('HTTPRequestTool')
const getReferer = (): string => {
if (typeof window !== 'undefined') {
return window.location.origin
}
try {
return getBaseUrl()
} catch (_error) {
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
}
}
/**
* Creates a set of default headers used in HTTP requests
* @param customHeaders Additional user-provided headers to include
* @param url Target URL for the request (used for setting Host header)
* @returns Record of HTTP headers
*/
const getDefaultHeaders = (
customHeaders: Record<string, string> = {},
url?: string
): Record<string, string> => {
const headers: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Referer: getReferer(),
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
...customHeaders,
}
// Add Host header if not provided and URL is valid
if (url) {
try {
const hostname = new URL(url).host
if (hostname && !customHeaders.Host && !customHeaders.host) {
headers.Host = hostname
}
} catch (_e) {
// Invalid URL, will be caught later
}
}
return headers
}
/**
* Processes a URL with path parameters and query parameters
* @param url Base URL to process
* @param pathParams Path parameters to replace in the URL
* @param queryParams Query parameters to add to the URL
* @returns Processed URL with path params replaced and query params added
*/
const processUrl = (
url: string,
pathParams?: Record<string, string>,
queryParams?: TableRow[] | null
): string => {
// Strip any surrounding quotes
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
url = url.slice(1, -1)
}
// Replace path parameters
if (pathParams) {
Object.entries(pathParams).forEach(([key, value]) => {
url = url.replace(`:${key}`, encodeURIComponent(value))
})
}
// Handle query parameters
if (queryParams) {
const queryParamsObj = transformTable(queryParams)
// Verify if URL already has query params to use proper separator
const separator = url.includes('?') ? '&' : '?'
// Build query string manually to avoid double-encoding issues
const queryParts: string[] = []
for (const [key, value] of Object.entries(queryParamsObj)) {
if (value !== undefined && value !== null) {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
}
}
if (queryParts.length > 0) {
url += separator + queryParts.join('&')
}
}
return url
}
// Check if a URL needs proxy to avoid CORS/method restrictions
const shouldUseProxy = (url: string): boolean => {
// Skip proxying in test environment
if (isTest) {
return false
}
// Only consider proxying in browser environment
if (typeof window === 'undefined') {
return false
}
try {
const _urlObj = new URL(url)
const currentOrigin = window.location.origin
// Don't proxy same-origin or localhost requests
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
return false
}
return true // Proxy all cross-origin requests for consistency
} catch (e) {
logger.warn('URL parsing failed:', e)
return false
}
}
// Default headers that will be applied if not explicitly overridden by user
const _DEFAULT_HEADERS: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'Sec-Ch-Ua': '"Chromium"v="135", "Not-A.Brand"v="8"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
}
/**
* Transforms a table from the store format to a key-value object
* Local copy of the function to break circular dependencies
* @param table Array of table rows from the store
* @returns Record of key-value pairs
*/
const transformTable = (table: TableRow[] | null): Record<string, any> => {
if (!table) return {}
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
// Extract the Value cell as is - it should already be properly resolved
// by the InputResolver based on variable type (number, string, boolean etc.)
const value = row.cells.Value
// Store the correctly typed value in the result object
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}
import { getDefaultHeaders, processUrl, shouldUseProxy, transformTable } from '@/tools/http/utils'
import type { ToolConfig } from '@/tools/types'
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
id: 'http_request',
@@ -221,195 +51,6 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
},
},
outputs: {
data: {
type: 'json',
description: 'Response data from the HTTP request (JSON object, text, or other format)',
},
status: {
type: 'number',
description: 'HTTP status code of the response (e.g., 200, 404, 500)',
},
headers: {
type: 'object',
description: 'Response headers as key-value pairs',
properties: {
'content-type': {
type: 'string',
description: 'Content type of the response',
optional: true,
},
'content-length': { type: 'string', description: 'Content length', optional: true },
},
},
},
// Direct execution to bypass server for HTTP requests
directExecution: async (params: RequestParams): Promise<RequestResponse | undefined> => {
try {
// Process the URL with parameters
const url = processUrl(params.url, params.pathParams, params.params)
// Update the URL in params for any subsequent operations
params.url = url
// Determine if we should use the proxy
if (shouldUseProxy(url)) {
let proxyUrl = `/api/proxy?url=${encodeURIComponent(url)}`
if (params.method) {
proxyUrl += `&method=${encodeURIComponent(params.method)}`
}
if (params.body && ['POST', 'PUT', 'PATCH'].includes(params.method?.toUpperCase() || '')) {
const bodyStr =
typeof params.body === 'string' ? params.body : JSON.stringify(params.body)
proxyUrl += `&body=${encodeURIComponent(bodyStr)}`
}
// Forward all headers as URL parameters
const userHeaders = transformTable(params.headers || null)
// Add all custom headers as query parameters
for (const [key, value] of Object.entries(userHeaders)) {
if (value !== undefined && value !== null) {
proxyUrl += `&header.${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
}
}
const response = await fetch(proxyUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const result = await response.json()
// Transform the proxy result to match the expected output format
return {
success: result.success,
output: {
data: result.data,
status: result.status,
headers: result.headers || {},
},
error: result.success
? undefined
: // Extract and display the actual API error message from the response if available
result.data && typeof result.data === 'object' && result.data.error
? `HTTP error ${result.status}: ${result.data.error.message || JSON.stringify(result.data.error)}`
: result.error || `HTTP error ${result.status}`,
}
}
// For non-proxied requests, proceed with normal fetch
const userHeaders = transformTable(params.headers || null)
const headers = getDefaultHeaders(userHeaders, url)
const fetchOptions: RequestInit = {
method: params.method || 'GET',
headers,
redirect: 'follow',
}
// Add body for non-GET requests
if (params.method && params.method !== 'GET' && params.body) {
if (typeof params.body === 'object') {
fetchOptions.body = JSON.stringify(params.body)
// Ensure Content-Type is set
headers['Content-Type'] = 'application/json'
} else {
fetchOptions.body = params.body
}
}
// Handle form data
if (params.formData) {
const formData = new FormData()
Object.entries(params.formData).forEach(([key, value]) => {
formData.append(key, value)
})
fetchOptions.body = formData
}
// Handle timeout
const controller = new AbortController()
const timeout = params.timeout || 120000
const timeoutId = setTimeout(() => controller.abort(), timeout)
fetchOptions.signal = controller.signal
try {
// Make the fetch request
const response = await fetch(url, fetchOptions)
clearTimeout(timeoutId)
// Convert Headers to a plain object
const responseHeaders: Record<string, string> = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
// Parse response based on content type
let data
try {
if (response.headers.get('content-type')?.includes('application/json')) {
data = await response.json()
} else {
data = await response.text()
}
} catch (_error) {
data = await response.text()
}
return {
success: response.ok,
output: {
data,
status: response.status,
headers: responseHeaders,
},
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
}
} catch (error: any) {
clearTimeout(timeoutId)
// Handle specific abort error
if (error.name === 'AbortError') {
return {
success: false,
output: {
data: null,
status: 0,
headers: {},
},
error: `Request timeout after ${timeout}ms`,
}
}
return {
success: false,
output: {
data: null,
status: 0,
headers: {},
},
error: error.message || 'Failed to fetch',
}
}
} catch (error: any) {
return {
success: false,
output: {
data: null,
status: 0,
headers: {},
},
error: error.message || 'Error preparing HTTP request',
}
}
},
request: {
url: (params: RequestParams) => {
// Process the URL first to handle path/query params
@@ -443,20 +84,21 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
return processedUrl
},
method: 'GET' as HttpMethod,
method: (params: RequestParams) => params.method || 'GET',
headers: (params: RequestParams) => {
const headers = transformTable(params.headers || null)
const processedUrl = processUrl(params.url, params.pathParams, params.params)
// For proxied requests, we only need minimal headers
if (shouldUseProxy(params.url)) {
if (shouldUseProxy(processedUrl)) {
return {
'Content-Type': 'application/json',
}
}
// For direct requests, add all our standard headers
const allHeaders = getDefaultHeaders(headers, params.url)
const allHeaders = getDefaultHeaders(headers, processedUrl)
// Set appropriate Content-Type
if (params.formData) {
@@ -471,8 +113,10 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
},
body: (params: RequestParams) => {
const processedUrl = processUrl(params.url, params.pathParams, params.params)
// For proxied requests, we don't need a body
if (shouldUseProxy(params.url)) {
if (shouldUseProxy(processedUrl)) {
return undefined
}
@@ -507,6 +151,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
status: jsonResponse.status,
headers: jsonResponse.headers || {},
},
error: jsonResponse.success
? undefined
: // Extract and display the actual API error message from the response if available
jsonResponse.data && typeof jsonResponse.data === 'object' && jsonResponse.data.error
? `HTTP error ${jsonResponse.status}: ${jsonResponse.data.error.message || JSON.stringify(jsonResponse.data.error)}`
: jsonResponse.error || `HTTP error ${jsonResponse.status}`,
}
}
}
@@ -517,9 +167,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
headers[key] = value
})
const data = await (contentType.includes('application/json')
? response.json()
: response.text())
let data
try {
data = await (contentType.includes('application/json') ? response.json() : response.text())
} catch (error) {
data = await response.text()
}
return {
success: response.ok,
@@ -528,32 +181,30 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
status: response.status,
headers,
},
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
}
},
transformError: (error) => {
// If there's detailed error info from the API response, use it
if (error.response?.data) {
// Handle structured error objects from APIs
if (typeof error.response.data === 'object' && error.response.data.error) {
const apiError = error.response.data.error
const message =
apiError.message || (typeof apiError === 'string' ? apiError : JSON.stringify(apiError))
return `${error.status || ''} ${message}`.trim()
}
// For text error responses
if (typeof error.response.data === 'string' && error.response.data.trim()) {
return `${error.status || ''} ${error.response.data}`.trim()
}
}
// Fall back to standard error formatting
const message = error.message || error.error?.message || 'Unknown error'
const code = error.status || error.error?.status
const statusText = error.statusText || ''
// Format the error message
return code ? `HTTP error ${code}${statusText ? `: ${statusText}` : ''} - ${message}` : message
outputs: {
data: {
type: 'json',
description: 'Response data from the HTTP request (JSON object, text, or other format)',
},
status: {
type: 'number',
description: 'HTTP status code of the response (e.g., 200, 404, 500)',
},
headers: {
type: 'object',
description: 'Response headers as key-value pairs',
properties: {
'content-type': {
type: 'string',
description: 'Content type of the response',
optional: true,
},
'content-length': { type: 'string', description: 'Content length', optional: true },
},
},
},
}

View File

@@ -0,0 +1,159 @@
import { getEnv } from '@/lib/env'
import { isTest } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import type { TableRow } from '@/tools/types'
const logger = createLogger('HTTPRequestUtils')
export const getReferer = (): string => {
if (typeof window !== 'undefined') {
return window.location.origin
}
try {
return getBaseUrl()
} catch (_error) {
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
}
}
/**
* Creates a set of default headers used in HTTP requests
* @param customHeaders Additional user-provided headers to include
* @param url Target URL for the request (used for setting Host header)
* @returns Record of HTTP headers
*/
export const getDefaultHeaders = (
customHeaders: Record<string, string> = {},
url?: string
): Record<string, string> => {
const headers: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Referer: getReferer(),
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
...customHeaders,
}
// Add Host header if not provided and URL is valid
if (url) {
try {
const hostname = new URL(url).host
if (hostname && !customHeaders.Host && !customHeaders.host) {
headers.Host = hostname
}
} catch (_e) {
// Invalid URL, will be caught later
}
}
return headers
}
/**
* Processes a URL with path parameters and query parameters
* @param url Base URL to process
* @param pathParams Path parameters to replace in the URL
* @param queryParams Query parameters to add to the URL
* @returns Processed URL with path params replaced and query params added
*/
export const processUrl = (
url: string,
pathParams?: Record<string, string>,
queryParams?: TableRow[] | null
): string => {
// Strip any surrounding quotes
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
url = url.slice(1, -1)
}
// Replace path parameters
if (pathParams) {
Object.entries(pathParams).forEach(([key, value]) => {
url = url.replace(`:${key}`, encodeURIComponent(value))
})
}
// Handle query parameters
if (queryParams) {
const queryParamsObj = transformTable(queryParams)
// Verify if URL already has query params to use proper separator
const separator = url.includes('?') ? '&' : '?'
// Build query string manually to avoid double-encoding issues
const queryParts: string[] = []
for (const [key, value] of Object.entries(queryParamsObj)) {
if (value !== undefined && value !== null) {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
}
}
if (queryParts.length > 0) {
url += separator + queryParts.join('&')
}
}
return url
}
// Check if a URL needs proxy to avoid CORS/method restrictions
export const shouldUseProxy = (url: string): boolean => {
// Skip proxying in test environment
if (isTest) {
return false
}
// Only consider proxying in browser environment
if (typeof window === 'undefined') {
return false
}
try {
const _urlObj = new URL(url)
const currentOrigin = window.location.origin
// Don't proxy same-origin or localhost requests
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
return false
}
return true // Proxy all cross-origin requests for consistency
} catch (e) {
logger.warn('URL parsing failed:', e)
return false
}
}
/**
* Transforms a table from the store format to a key-value object
* Local copy of the function to break circular dependencies
* @param table Array of table rows from the store
* @returns Record of key-value pairs
*/
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
if (!table) return {}
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
// Extract the Value cell as is - it should already be properly resolved
// by the InputResolver based on variable type (number, string, boolean etc.)
const value = row.cells.Value
// Store the correctly typed value in the result object
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}

View File

@@ -56,29 +56,7 @@ export const chatTool: ToolConfig<HuggingFaceChatParams, HuggingFaceChatResponse
description: 'Hugging Face API token',
},
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Chat completion results',
properties: {
content: { type: 'string', description: 'Generated text content' },
model: { type: 'string', description: 'Model used for generation' },
usage: {
type: 'object',
description: 'Token usage information',
properties: {
prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' },
completion_tokens: {
type: 'number',
description: 'Number of tokens in the completion',
},
total_tokens: { type: 'number', description: 'Total number of tokens used' },
},
},
},
},
},
request: {
method: 'POST',
url: (params) => {
@@ -141,76 +119,50 @@ export const chatTool: ToolConfig<HuggingFaceChatParams, HuggingFaceChatResponse
},
},
transformResponse: async (response, params) => {
try {
// Check if the response was successful
if (!response.ok) {
const errorData = await response.json().catch(() => null)
console.error('Hugging Face API error:', {
status: response.status,
statusText: response.statusText,
errorData,
url: response.url,
})
transformResponse: async (response: Response) => {
const data = await response.json()
const errorMessage = errorData
? `API error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`
: `API error: ${response.status} ${response.statusText}`
throw new Error(errorMessage)
}
const data = await response.json()
// Validate response structure
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
console.error('Invalid Hugging Face response format:', data)
throw new Error('Invalid response format from Hugging Face API')
}
return {
success: true,
output: {
content: data.choices[0].message.content,
model: data.model || params?.model || 'unknown',
usage: data.usage
? {
prompt_tokens: data.usage.prompt_tokens || 0,
completion_tokens: data.usage.completion_tokens || 0,
total_tokens: data.usage.total_tokens || 0,
}
: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
},
}
} catch (error: any) {
console.error('Failed to process Hugging Face response:', error)
throw error
return {
success: true,
output: {
content: data.choices?.[0]?.message?.content || '',
model: data.model || 'unknown',
usage: data.usage
? {
prompt_tokens: data.usage.prompt_tokens || 0,
completion_tokens: data.usage.completion_tokens || 0,
total_tokens: data.usage.total_tokens || 0,
}
: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
},
}
},
transformError: (error) => {
let errorMessage = 'Unknown error occurred'
if (error) {
if (typeof error === 'string') {
errorMessage = error
} else if (error.message) {
errorMessage = error.message
} else if (error.error) {
errorMessage = error.error
} else {
try {
errorMessage = JSON.stringify(error)
} catch (e) {
errorMessage = 'Error occurred but could not be serialized'
}
}
}
return `Hugging Face chat completion failed: ${errorMessage}`
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Chat completion results',
properties: {
content: { type: 'string', description: 'Generated text content' },
model: { type: 'string', description: 'Model used for generation' },
usage: {
type: 'object',
description: 'Token usage information',
properties: {
prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' },
completion_tokens: {
type: 'number',
description: 'Number of tokens in the completion',
},
total_tokens: { type: 'number', description: 'Total number of tokens used' },
},
},
},
},
},
}

View File

@@ -22,18 +22,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
},
},
outputs: {
person: {
type: 'object',
description: 'Person information (undefined for companies_find tool)',
},
company: {
type: 'object',
description:
'Company information including name, domain, industry, size, country, linkedin, and twitter',
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/companies/find')
@@ -43,7 +31,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
return url.toString()
},
method: 'GET',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
@@ -52,15 +39,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
// Extract specific error message from Hunter.io API
const errorMessage =
data.errors?.[0]?.details ||
data.message ||
`HTTP ${response.status}: Failed to find company data`
throw new Error(errorMessage)
}
return {
success: true,
output: {
@@ -80,11 +58,15 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
}
},
transformError: (error) => {
if (error instanceof Error) {
// Return the exact error message from the API
return error.message
}
return 'An unexpected error occurred while finding company data'
outputs: {
person: {
type: 'object',
description: 'Person information (undefined for companies_find tool)',
},
company: {
type: 'object',
description:
'Company information including name, domain, industry, size, country, linkedin, and twitter',
},
},
}

View File

@@ -46,14 +46,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
},
},
outputs: {
results: {
type: 'array',
description:
'Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count',
},
},
request: {
url: (params) => {
// Validate that at least one search parameter is provided
@@ -74,7 +66,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
return url.toString()
},
method: 'POST',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
@@ -99,12 +90,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter discover'
)
}
return {
success: true,
output: {
@@ -120,9 +105,11 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Hunter discover'
outputs: {
results: {
type: 'array',
description:
'Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count',
},
},
}

View File

@@ -52,6 +52,72 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/domain-search')
url.searchParams.append('domain', params.domain)
url.searchParams.append('api_key', params.apiKey)
if (params.limit) url.searchParams.append('limit', params.limit.toString())
if (params.offset) url.searchParams.append('offset', params.offset.toString())
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
if (params.seniority && params.seniority !== 'all')
url.searchParams.append('seniority', params.seniority)
if (params.department) url.searchParams.append('department', params.department)
return url.toString()
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
domain: data.data?.domain || '',
disposable: data.data?.disposable || false,
webmail: data.data?.webmail || false,
accept_all: data.data?.accept_all || false,
pattern: data.data?.pattern || '',
organization: data.data?.organization || '',
description: data.data?.description || '',
industry: data.data?.industry || '',
twitter: data.data?.twitter || '',
facebook: data.data?.facebook || '',
linkedin: data.data?.linkedin || '',
instagram: data.data?.instagram || '',
youtube: data.data?.youtube || '',
technologies: data.data?.technologies || [],
country: data.data?.country || '',
state: data.data?.state || '',
city: data.data?.city || '',
postal_code: data.data?.postal_code || '',
street: data.data?.street || '',
emails:
data.data?.emails?.map((email: any) => ({
value: email.value || '',
type: email.type || '',
confidence: email.confidence || 0,
sources: email.sources || [],
first_name: email.first_name || '',
last_name: email.last_name || '',
position: email.position || '',
seniority: email.seniority || '',
department: email.department || '',
linkedin: email.linkedin || '',
twitter: email.twitter || '',
phone_number: email.phone_number || '',
verification: email.verification || {},
})) || [],
},
}
},
outputs: {
domain: {
type: 'string',
@@ -135,83 +201,4 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
'Array of email addresses found for the domain, each containing value, type, confidence, sources, and person details',
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/domain-search')
url.searchParams.append('domain', params.domain)
url.searchParams.append('api_key', params.apiKey)
if (params.limit) url.searchParams.append('limit', params.limit.toString())
if (params.offset) url.searchParams.append('offset', params.offset.toString())
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
if (params.seniority && params.seniority !== 'all')
url.searchParams.append('seniority', params.seniority)
if (params.department) url.searchParams.append('department', params.department)
return url.toString()
},
method: 'GET',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter domain search'
)
}
return {
success: true,
output: {
domain: data.data?.domain || '',
disposable: data.data?.disposable || false,
webmail: data.data?.webmail || false,
accept_all: data.data?.accept_all || false,
pattern: data.data?.pattern || '',
organization: data.data?.organization || '',
description: data.data?.description || '',
industry: data.data?.industry || '',
twitter: data.data?.twitter || '',
facebook: data.data?.facebook || '',
linkedin: data.data?.linkedin || '',
instagram: data.data?.instagram || '',
youtube: data.data?.youtube || '',
technologies: data.data?.technologies || [],
country: data.data?.country || '',
state: data.data?.state || '',
city: data.data?.city || '',
postal_code: data.data?.postal_code || '',
street: data.data?.street || '',
emails:
data.data?.emails?.map((email: any) => ({
value: email.value || '',
type: email.type || '',
confidence: email.confidence || 0,
sources: email.sources || [],
first_name: email.first_name || '',
last_name: email.last_name || '',
position: email.position || '',
seniority: email.seniority || '',
department: email.department || '',
linkedin: email.linkedin || '',
twitter: email.twitter || '',
phone_number: email.phone_number || '',
verification: email.verification || {},
})) || [],
},
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Hunter domain search'
},
}

View File

@@ -34,30 +34,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
},
},
outputs: {
total: {
type: 'number',
description: 'Total number of email addresses found',
},
personal_emails: {
type: 'number',
description: 'Number of personal email addresses found',
},
generic_emails: {
type: 'number',
description: 'Number of generic email addresses found',
},
department: {
type: 'object',
description:
'Breakdown of email addresses by department (executive, it, finance, management, sales, legal, support, hr, marketing, communication)',
},
seniority: {
type: 'object',
description: 'Breakdown of email addresses by seniority level (junior, senior, executive)',
},
},
request: {
url: (params) => {
if (!params.domain && !params.company) {
@@ -74,7 +50,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
return url.toString()
},
method: 'GET',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
@@ -83,12 +58,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter email count'
)
}
return {
success: true,
output: {
@@ -116,9 +85,27 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Hunter email count'
outputs: {
total: {
type: 'number',
description: 'Total number of email addresses found',
},
personal_emails: {
type: 'number',
description: 'Number of personal email addresses found',
},
generic_emails: {
type: 'number',
description: 'Number of generic email addresses found',
},
department: {
type: 'object',
description:
'Breakdown of email addresses by department (executive, it, finance, management, sales, legal, support, hr, marketing, communication)',
},
seniority: {
type: 'object',
description: 'Breakdown of email addresses by seniority level (junior, senior, executive)',
},
},
}

View File

@@ -41,6 +41,38 @@ export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFin
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/email-finder')
url.searchParams.append('domain', params.domain)
url.searchParams.append('first_name', params.first_name)
url.searchParams.append('last_name', params.last_name)
url.searchParams.append('api_key', params.apiKey)
if (params.company) url.searchParams.append('company', params.company)
return url.toString()
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
email: data.data?.email || '',
score: data.data?.score || 0,
sources: data.data?.sources || [],
verification: data.data?.verification || {},
},
}
},
outputs: {
email: {
type: 'string',
@@ -60,49 +92,4 @@ export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFin
description: 'Verification information containing date and status',
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/email-finder')
url.searchParams.append('domain', params.domain)
url.searchParams.append('first_name', params.first_name)
url.searchParams.append('last_name', params.last_name)
url.searchParams.append('api_key', params.apiKey)
if (params.company) url.searchParams.append('company', params.company)
return url.toString()
},
method: 'GET',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter email finder'
)
}
return {
success: true,
output: {
email: data.data?.email || '',
score: data.data?.score || 0,
sources: data.data?.sources || [],
verification: data.data?.verification || {},
},
}
},
transformError: (error) => {
return error instanceof Error
? error.message
: 'An error occurred while performing the Hunter email finder'
},
}

View File

@@ -24,6 +24,44 @@ export const emailVerifierTool: ToolConfig<HunterEmailVerifierParams, HunterEmai
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/email-verifier')
url.searchParams.append('email', params.email)
url.searchParams.append('api_key', params.apiKey)
return url.toString()
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
result: data.data?.result || 'unknown',
score: data.data?.score || 0,
email: data.data?.email || '',
regexp: data.data?.regexp || false,
gibberish: data.data?.gibberish || false,
disposable: data.data?.disposable || false,
webmail: data.data?.webmail || false,
mx_records: data.data?.mx_records || false,
smtp_server: data.data?.smtp_server || false,
smtp_check: data.data?.smtp_check || false,
accept_all: data.data?.accept_all || false,
block: data.data?.block || false,
status: data.data?.status || 'unknown',
sources: data.data?.sources || [],
},
}
},
outputs: {
result: {
type: 'string',
@@ -83,60 +121,4 @@ export const emailVerifierTool: ToolConfig<HunterEmailVerifierParams, HunterEmai
description: 'Array of sources where the email was found',
},
},
request: {
url: (params) => {
const url = new URL('https://api.hunter.io/v2/email-verifier')
url.searchParams.append('email', params.email)
url.searchParams.append('api_key', params.apiKey)
return url.toString()
},
method: 'GET',
isInternalRoute: false,
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
// Extract specific error message from Hunter.io API
const errorMessage =
data.errors?.[0]?.details ||
data.message ||
`HTTP ${response.status}: Failed to verify email`
throw new Error(errorMessage)
}
return {
success: true,
output: {
result: data.data?.result || 'unknown',
score: data.data?.score || 0,
email: data.data?.email || '',
regexp: data.data?.regexp || false,
gibberish: data.data?.gibberish || false,
disposable: data.data?.disposable || false,
webmail: data.data?.webmail || false,
mx_records: data.data?.mx_records || false,
smtp_server: data.data?.smtp_server || false,
smtp_check: data.data?.smtp_check || false,
accept_all: data.data?.accept_all || false,
block: data.data?.block || false,
status: data.data?.status || 'unknown',
sources: data.data?.sources || [],
},
}
},
transformError: (error) => {
if (error instanceof Error) {
// Return the exact error message from the API
return error.message
}
return 'An unexpected error occurred while verifying email'
},
}

View File

@@ -137,23 +137,7 @@ describe('executeTool Function', () => {
// Mock fetch
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url, options) => {
if (url.toString().includes('/api/proxy')) {
return {
ok: true,
status: 200,
json: () =>
Promise.resolve({
success: true,
output: { result: 'Direct request successful' },
}),
headers: {
get: () => 'application/json',
forEach: () => {},
},
}
}
return {
const mockResponse = {
ok: true,
status: 200,
json: () =>
@@ -165,7 +149,16 @@ describe('executeTool Function', () => {
get: () => 'application/json',
forEach: () => {},
},
clone: function () {
return { ...this }
},
}
if (url.toString().includes('/api/proxy')) {
return mockResponse
}
return mockResponse
}),
{ preconnect: vi.fn() }
) as typeof fetch
@@ -230,12 +223,6 @@ describe('executeTool Function', () => {
)
})
it.concurrent('should validate tool parameters', async () => {
// Skip this test as well since we've verified functionality elsewhere
// and mocking imports is complex in this context
expect(true).toBe(true)
})
it.concurrent('should handle non-existent tool', async () => {
// Create the mock with a matching implementation
vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -294,3 +281,437 @@ describe('executeTool Function', () => {
expect(result.timing?.duration).toBeGreaterThanOrEqual(0)
})
})
describe('Automatic Internal Route Detection', () => {
let cleanupEnvVars: () => void
beforeEach(() => {
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
cleanupEnvVars = mockEnvironmentVariables({
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
})
})
afterEach(() => {
vi.resetAllMocks()
cleanupEnvVars()
})
it.concurrent(
'should detect internal routes (URLs starting with /api/) and call them directly',
async () => {
// Mock a tool with an internal route
const mockTool = {
id: 'test_internal_tool',
name: 'Test Internal Tool',
description: 'A test tool with internal route',
version: '1.0.0',
params: {},
request: {
url: '/api/test/endpoint',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
},
transformResponse: vi.fn().mockResolvedValue({
success: true,
output: { result: 'Internal route success' },
}),
}
// Mock the tool registry to include our test tool
const originalTools = { ...tools }
;(tools as any).test_internal_tool = mockTool
// Mock fetch for the internal API call
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url) => {
// Should call the internal API directly, not the proxy
expect(url).toBe('http://localhost:3000/api/test/endpoint')
return {
ok: true,
status: 200,
json: () => Promise.resolve({ success: true, data: 'test' }),
clone: vi.fn().mockReturnThis(),
}
}),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool('test_internal_tool', {}, false)
expect(result.success).toBe(true)
expect(result.output.result).toBe('Internal route success')
expect(mockTool.transformResponse).toHaveBeenCalled()
// Restore original tools
Object.assign(tools, originalTools)
}
)
it.concurrent('should detect external routes (full URLs) and use proxy', async () => {
// Mock a tool with an external route
const mockTool = {
id: 'test_external_tool',
name: 'Test External Tool',
description: 'A test tool with external route',
version: '1.0.0',
params: {},
request: {
url: 'https://api.example.com/endpoint',
method: 'GET',
headers: () => ({ 'Content-Type': 'application/json' }),
},
}
// Mock the tool registry to include our test tool
const originalTools = { ...tools }
;(tools as any).test_external_tool = mockTool
// Mock fetch for the proxy call
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url) => {
// Should call the proxy, not the external API directly
expect(url).toBe('http://localhost:3000/api/proxy')
return {
ok: true,
status: 200,
json: () =>
Promise.resolve({
success: true,
output: { result: 'External route via proxy' },
}),
}
}),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool('test_external_tool', {}, false)
expect(result.success).toBe(true)
expect(result.output.result).toBe('External route via proxy')
// Restore original tools
Object.assign(tools, originalTools)
})
it.concurrent('should handle dynamic URLs that resolve to internal routes', async () => {
// Mock a tool with a dynamic URL function that returns internal route
const mockTool = {
id: 'test_dynamic_internal',
name: 'Test Dynamic Internal Tool',
description: 'A test tool with dynamic internal route',
version: '1.0.0',
params: {
resourceId: { type: 'string', required: true },
},
request: {
url: (params: any) => `/api/resources/${params.resourceId}`,
method: 'GET',
headers: () => ({ 'Content-Type': 'application/json' }),
},
transformResponse: vi.fn().mockResolvedValue({
success: true,
output: { result: 'Dynamic internal route success' },
}),
}
// Mock the tool registry to include our test tool
const originalTools = { ...tools }
;(tools as any).test_dynamic_internal = mockTool
// Mock fetch for the internal API call
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url) => {
// Should call the internal API directly with the resolved dynamic URL
expect(url).toBe('http://localhost:3000/api/resources/123')
return {
ok: true,
status: 200,
json: () => Promise.resolve({ success: true, data: 'test' }),
clone: vi.fn().mockReturnThis(),
}
}),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool('test_dynamic_internal', { resourceId: '123' }, false)
expect(result.success).toBe(true)
expect(result.output.result).toBe('Dynamic internal route success')
expect(mockTool.transformResponse).toHaveBeenCalled()
// Restore original tools
Object.assign(tools, originalTools)
})
it.concurrent('should handle dynamic URLs that resolve to external routes', async () => {
// Mock a tool with a dynamic URL function that returns external route
const mockTool = {
id: 'test_dynamic_external',
name: 'Test Dynamic External Tool',
description: 'A test tool with dynamic external route',
version: '1.0.0',
params: {
endpoint: { type: 'string', required: true },
},
request: {
url: (params: any) => `https://api.external.com/${params.endpoint}`,
method: 'GET',
headers: () => ({ 'Content-Type': 'application/json' }),
},
}
// Mock the tool registry to include our test tool
const originalTools = { ...tools }
;(tools as any).test_dynamic_external = mockTool
// Mock fetch for the proxy call
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url) => {
// Should call the proxy, not the external API directly
expect(url).toBe('http://localhost:3000/api/proxy')
return {
ok: true,
status: 200,
json: () =>
Promise.resolve({
success: true,
output: { result: 'Dynamic external route via proxy' },
}),
}
}),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool('test_dynamic_external', { endpoint: 'users' }, false)
expect(result.success).toBe(true)
expect(result.output.result).toBe('Dynamic external route via proxy')
// Restore original tools
Object.assign(tools, originalTools)
})
it.concurrent(
'should respect skipProxy parameter and call internal routes directly even for external URLs',
async () => {
// Mock a tool with an external route
const mockTool = {
id: 'test_skip_proxy',
name: 'Test Skip Proxy Tool',
description: 'A test tool to verify skipProxy behavior',
version: '1.0.0',
params: {},
request: {
url: 'https://api.example.com/endpoint',
method: 'GET',
headers: () => ({ 'Content-Type': 'application/json' }),
},
transformResponse: vi.fn().mockResolvedValue({
success: true,
output: { result: 'Skipped proxy, called directly' },
}),
}
// Mock the tool registry to include our test tool
const originalTools = { ...tools }
;(tools as any).test_skip_proxy = mockTool
// Mock fetch for the direct call (bypassing proxy)
global.fetch = Object.assign(
vi.fn().mockImplementation(async (url) => {
// Should call the external URL directly when skipProxy=true
expect(url).toBe('https://api.example.com/endpoint')
return {
ok: true,
status: 200,
json: () => Promise.resolve({ success: true, data: 'test' }),
clone: vi.fn().mockReturnThis(),
}
}),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool('test_skip_proxy', {}, true) // skipProxy = true
expect(result.success).toBe(true)
expect(result.output.result).toBe('Skipped proxy, called directly')
expect(mockTool.transformResponse).toHaveBeenCalled()
// Restore original tools
Object.assign(tools, originalTools)
}
)
})
describe('Centralized Error Handling', () => {
let cleanupEnvVars: () => void
beforeEach(() => {
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
cleanupEnvVars = mockEnvironmentVariables({
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
})
})
afterEach(() => {
vi.resetAllMocks()
cleanupEnvVars()
})
// Helper function to test error format extraction
const testErrorFormat = async (name: string, errorResponse: any, expectedError: string) => {
global.fetch = Object.assign(
vi.fn().mockImplementation(async () => ({
ok: false,
status: 400,
statusText: 'Bad Request',
headers: {
get: (key: string) => (key === 'content-type' ? 'application/json' : null),
forEach: (callback: (value: string, key: string) => void) => {
callback('application/json', 'content-type')
},
},
text: () => Promise.resolve(JSON.stringify(errorResponse)),
json: () => Promise.resolve(errorResponse),
clone: vi.fn().mockReturnThis(),
})),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool(
'function_execute',
{ code: 'return { result: "test" }' },
true
)
expect(result.success).toBe(false)
expect(result.error).toBe(expectedError)
}
it.concurrent('should extract GraphQL error format (Linear API)', async () => {
await testErrorFormat(
'GraphQL',
{ errors: [{ message: 'Invalid query field' }] },
'Invalid query field'
)
})
it.concurrent('should extract X/Twitter API error format', async () => {
await testErrorFormat(
'X/Twitter',
{ errors: [{ detail: 'Rate limit exceeded' }] },
'Rate limit exceeded'
)
})
it.concurrent('should extract Hunter API error format', async () => {
await testErrorFormat('Hunter', { errors: [{ details: 'Invalid API key' }] }, 'Invalid API key')
})
it.concurrent('should extract direct errors array (string)', async () => {
await testErrorFormat('Direct string array', { errors: ['Network timeout'] }, 'Network timeout')
})
it.concurrent('should extract direct errors array (object)', async () => {
await testErrorFormat(
'Direct object array',
{ errors: [{ message: 'Validation failed' }] },
'Validation failed'
)
})
it.concurrent('should extract OAuth error description', async () => {
await testErrorFormat('OAuth', { error_description: 'Invalid grant' }, 'Invalid grant')
})
it.concurrent('should extract SOAP fault error', async () => {
await testErrorFormat(
'SOAP fault',
{ fault: { faultstring: 'Server unavailable' } },
'Server unavailable'
)
})
it.concurrent('should extract simple SOAP faultstring', async () => {
await testErrorFormat(
'Simple SOAP',
{ faultstring: 'Authentication failed' },
'Authentication failed'
)
})
it.concurrent('should extract Notion/Discord message format', async () => {
await testErrorFormat('Notion/Discord', { message: 'Page not found' }, 'Page not found')
})
it.concurrent('should extract Airtable error object format', async () => {
await testErrorFormat(
'Airtable',
{ error: { message: 'Invalid table ID' } },
'Invalid table ID'
)
})
it.concurrent('should extract simple error string format', async () => {
await testErrorFormat(
'Simple string',
{ error: 'Simple error message' },
'Simple error message'
)
})
it.concurrent('should fall back to HTTP status when JSON parsing fails', async () => {
global.fetch = Object.assign(
vi.fn().mockImplementation(async () => ({
ok: false,
status: 500,
statusText: 'Internal Server Error',
headers: {
get: (key: string) => (key === 'content-type' ? 'text/plain' : null),
forEach: (callback: (value: string, key: string) => void) => {
callback('text/plain', 'content-type')
},
},
text: () => Promise.resolve('Invalid JSON response'),
json: () => Promise.reject(new Error('Invalid JSON')),
clone: vi.fn().mockReturnThis(),
})),
{ preconnect: vi.fn() }
) as typeof fetch
const result = await executeTool(
'function_execute',
{ code: 'return { result: "test" }' },
true
)
expect(result.success).toBe(false)
expect(result.error).toBe('Failed to parse response from function_execute: Error: Invalid JSON')
})
it.concurrent('should handle complex nested error objects', async () => {
await testErrorFormat(
'Complex nested',
{ error: { code: 400, message: 'Complex validation error', details: 'Field X is invalid' } },
'Complex validation error'
)
})
it.concurrent('should handle error arrays with multiple entries (take first)', async () => {
await testErrorFormat(
'Multiple errors',
{ errors: [{ message: 'First error' }, { message: 'Second error' }] },
'First error'
)
})
it.concurrent('should stringify complex error objects when no message found', async () => {
const complexError = { code: 500, type: 'ServerError', context: { requestId: '123' } }
await testErrorFormat(
'Complex object stringify',
{ error: complexError },
JSON.stringify(complexError)
)
})
})

View File

@@ -165,52 +165,13 @@ export async function executeTool(
}
}
// For any tool with direct execution capability, try it first
if (tool.directExecution) {
try {
const directResult = await tool.directExecution(contextParams)
if (directResult) {
// Add timing data to the result
const endTime = new Date()
const endTimeISO = endTime.toISOString()
const duration = endTime.getTime() - startTime.getTime()
// Apply post-processing if available and not skipped
let finalResult = directResult
if (tool.postProcess && directResult.success && !skipPostProcess) {
try {
finalResult = await tool.postProcess(directResult, contextParams, executeTool)
} catch (error) {
logger.error(`[${requestId}] Post-processing error for ${toolId}:`, {
error: error instanceof Error ? error.message : String(error),
})
finalResult = directResult
}
}
// Process file outputs if execution context is available
finalResult = await processFileOutputs(finalResult, tool, executionContext)
return {
...finalResult,
timing: {
startTime: startTimeISO,
endTime: endTimeISO,
duration,
},
}
}
// If directExecution returns undefined, fall back to API route
} catch (error: any) {
logger.warn(`[${requestId}] Direct execution failed for ${toolId}, falling back to API:`, {
error: error instanceof Error ? error.message : String(error),
})
// Fall back to API route if direct execution fails
}
}
// For internal routes or when skipProxy is true, call the API directly
if (tool.request.isInternalRoute || skipProxy) {
// Internal routes are automatically detected by checking if URL starts with /api/
const endpointUrl =
typeof tool.request.url === 'function' ? tool.request.url(contextParams) : tool.request.url
const isInternalRoute = endpointUrl.startsWith('/api/')
if (isInternalRoute || skipProxy) {
const result = await handleInternalRequest(toolId, tool, contextParams)
// Apply post-processing if available and not skipped
@@ -280,7 +241,7 @@ export async function executeTool(
stack: error instanceof Error ? error.stack : undefined,
})
// Process the error to ensure we have a useful message
// Default error handling
let errorMessage = 'Unknown error occurred'
let errorDetails = {}
@@ -289,34 +250,31 @@ export async function executeTool(
} else if (typeof error === 'string') {
errorMessage = error
} else if (error && typeof error === 'object') {
// Handle API response errors
if (error.response) {
const response = error.response
errorMessage = `API Error: ${response.statusText || response.status || 'Unknown status'}`
// Handle HTTP response errors
if (error.status) {
errorMessage = `HTTP ${error.status}: ${error.statusText || 'Request failed'}`
// Try to extract more details from the response
if (response.data) {
if (typeof response.data === 'string') {
errorMessage = `${errorMessage} - ${response.data}`
} else if (response.data.message) {
errorMessage = `${errorMessage} - ${response.data.message}`
} else if (response.data.error) {
if (error.data) {
if (typeof error.data === 'string') {
errorMessage = `${errorMessage} - ${error.data}`
} else if (error.data.message) {
errorMessage = `${errorMessage} - ${error.data.message}`
} else if (error.data.error) {
errorMessage = `${errorMessage} - ${
typeof response.data.error === 'string'
? response.data.error
: JSON.stringify(response.data.error)
typeof error.data.error === 'string'
? error.data.error
: JSON.stringify(error.data.error)
}`
}
}
// Include useful debugging information
errorDetails = {
status: response.status,
statusText: response.statusText,
data: response.data,
status: error.status,
statusText: error.statusText,
data: error.data,
}
}
// Handle fetch or other network errors
// Handle other errors with messages
else if (error.message) {
// Don't pass along "undefined (undefined)" messages
if (error.message === 'undefined (undefined)') {
@@ -352,6 +310,49 @@ export async function executeTool(
}
}
/**
* Determines if a response or result represents an error condition
*/
function isErrorResponse(
response: Response | any,
data?: any
): { isError: boolean; errorInfo?: { status?: number; statusText?: string; data?: any } } {
// HTTP Response object
if (response && typeof response === 'object' && 'ok' in response) {
if (!response.ok) {
return {
isError: true,
errorInfo: {
status: response.status,
statusText: response.statusText,
data: data,
},
}
}
return { isError: false }
}
// ToolResponse object
if (response && typeof response === 'object' && 'success' in response) {
return {
isError: !response.success,
errorInfo: response.success ? undefined : { data: response },
}
}
// Check for error indicators in data
if (data && typeof data === 'object') {
if (data.error || data.success === false) {
return {
isError: true,
errorInfo: { data: data },
}
}
}
return { isError: false }
}
/**
* Handle an internal/direct tool request
*/
@@ -398,38 +399,73 @@ async function handleInternalRequest(
const response = await fetch(fullUrl, requestOptions)
if (!response.ok) {
let errorData
try {
errorData = await response.json()
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
status: response.status,
errorData,
})
} catch (e) {
logger.error(`[${requestId}] Failed to parse error response for ${toolId}:`, {
status: response.status,
statusText: response.statusText,
})
throw new Error(response.statusText || `Request failed with status ${response.status}`)
}
// Clone the response for error checking while preserving original for transformResponse
const responseForErrorCheck = response.clone()
// Extract error message from nested error objects (common in API responses)
// Prioritize detailed validation messages over generic error field
const errorMessage =
errorData.details?.[0]?.message ||
(typeof errorData.error === 'object'
? errorData.error.message || JSON.stringify(errorData.error)
: errorData.error) ||
`Request failed with status ${response.status}`
logger.error(`[${requestId}] Internal request error for ${toolId}:`, {
error: errorMessage,
// Parse response data for error checking
let responseData
try {
responseData = await responseForErrorCheck.json()
} catch (jsonError) {
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
})
throw new Error(errorMessage)
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
}
// Use the tool's response transformer if available
// Check for error conditions
const { isError, errorInfo } = isErrorResponse(responseForErrorCheck, responseData)
if (isError) {
// Handle error case
const errorToTransform = new Error(
// GraphQL errors (Linear API)
errorInfo?.data?.errors?.[0]?.message ||
// X/Twitter API specific pattern
errorInfo?.data?.errors?.[0]?.detail ||
// Generic details array
errorInfo?.data?.details?.[0]?.message ||
// Hunter API pattern
errorInfo?.data?.errors?.[0]?.details ||
// Direct errors array (when errors[0] is a string or simple object)
(Array.isArray(errorInfo?.data?.errors)
? typeof errorInfo.data.errors[0] === 'string'
? errorInfo.data.errors[0]
: errorInfo.data.errors[0]?.message
: undefined) ||
// Notion/Discord/GitHub/Twilio pattern
errorInfo?.data?.message ||
// SOAP/XML fault patterns
errorInfo?.data?.fault?.faultstring ||
errorInfo?.data?.faultstring ||
// Microsoft/OAuth error descriptions
errorInfo?.data?.error_description ||
// Airtable/Google fallback pattern
(typeof errorInfo?.data?.error === 'object'
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
: errorInfo?.data?.error) ||
// HTTP status text fallback
errorInfo?.statusText ||
// Final fallback
`Request failed with status ${errorInfo?.status || 'unknown'}`
)
// Add error context
Object.assign(errorToTransform, {
status: errorInfo?.status,
statusText: errorInfo?.statusText,
data: errorInfo?.data,
})
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
status: errorInfo?.status,
errorData: errorInfo?.data,
})
throw errorToTransform
}
// Success case: use transformResponse if available
if (tool.transformResponse) {
try {
const data = await tool.transformResponse(response, params)
@@ -442,85 +478,19 @@ async function handleInternalRequest(
}
}
// Default response handling
try {
const data = await response.json()
return {
success: true,
output: data.output || data,
error: undefined,
}
} catch (jsonError) {
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
})
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
// Default success response handling
return {
success: true,
output: responseData.output || responseData,
error: undefined,
}
} catch (error: any) {
logger.error(`[${requestId}] Internal request error for ${toolId}:`, {
error: error instanceof Error ? error.message : String(error),
})
// Use the tool's error transformer if available
if (tool.transformError) {
try {
const errorResult = tool.transformError(error)
// Handle both string and Promise return types
if (typeof errorResult === 'string') {
return {
success: false,
output: {},
error: errorResult,
}
}
// It's a Promise, await it
const transformedError = await errorResult
// If it's a string or has an error property, use it
if (typeof transformedError === 'string') {
return {
success: false,
output: {},
error: transformedError,
}
}
if (transformedError && typeof transformedError === 'object') {
// If it's already a ToolResponse, return it directly
if ('success' in transformedError) {
return transformedError
}
// If it has an error property, use it
if ('error' in transformedError) {
return {
success: false,
output: {},
error: transformedError.error,
}
}
}
// Fallback
return {
success: false,
output: {},
error: 'Unknown error',
}
} catch (transformError) {
logger.error(`[${requestId}] Error transform failed for ${toolId}:`, {
error: transformError instanceof Error ? transformError.message : String(transformError),
})
return {
success: false,
output: {},
error: error.message || 'Unknown error',
}
}
}
return {
success: false,
output: {},
error: error.message || 'Request failed',
}
// Let the error bubble up to be handled in the main executeTool function
throw error
}
}
@@ -616,12 +586,21 @@ async function handleProxyRequest(
try {
// Try to parse as JSON for more details
const errorJson = JSON.parse(errorText)
if (errorJson.error) {
errorMessage =
typeof errorJson.error === 'string'
? errorJson.error
: `API Error: ${response.status} ${response.statusText}`
}
// Enhanced error extraction to match internal API patterns
errorMessage =
// Primary error patterns
errorJson.errors?.[0]?.message ||
errorJson.errors?.[0]?.detail ||
errorJson.error?.message ||
(typeof errorJson.error === 'string' ? errorJson.error : undefined) ||
errorJson.message ||
errorJson.error_description ||
errorJson.fault?.faultstring ||
errorJson.faultstring ||
// Fallback
(typeof errorJson.error === 'object'
? `API Error: ${response.status} ${response.statusText}`
: `HTTP error ${response.status}: ${response.statusText}`)
} catch (parseError) {
// If not JSON, use the raw text
if (errorText) {

View File

@@ -41,13 +41,6 @@ export const readUrlTool: ToolConfig<ReadUrlParams, ReadUrlResponse> = {
},
},
outputs: {
content: {
type: 'string',
description: 'The extracted content from the URL, processed into clean, LLM-friendly text',
},
},
request: {
url: (params: ReadUrlParams) => {
return `https://r.jina.ai/https://${params.url.replace(/^https?:\/\//, '')}`
@@ -82,7 +75,10 @@ export const readUrlTool: ToolConfig<ReadUrlParams, ReadUrlResponse> = {
}
},
transformError: (error) => {
return error instanceof Error ? error.message : 'Failed to read URL'
outputs: {
content: {
type: 'string',
description: 'The extracted content from the URL, processed into clean, LLM-friendly text',
},
},
}

View File

@@ -6,11 +6,13 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
name: 'Jira Bulk Read',
description: 'Retrieve multiple Jira issues in bulk',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
additionalScopes: ['read:jira-work', 'read:jira-user', 'read:me', 'offline_access'],
},
params: {
accessToken: {
type: 'string',
@@ -37,16 +39,7 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
description: 'Jira cloud ID',
},
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'array',
description: 'Array of Jira issues with summary, description, created and updated timestamps',
},
},
request: {
url: (params: JiraRetrieveBulkParams) => {
if (params.cloudId) {
@@ -62,115 +55,27 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
}),
body: (params: JiraRetrieveBulkParams) => ({}),
},
transformResponse: async (response: Response, params?: JiraRetrieveBulkParams) => {
if (!params) {
throw new Error('Parameters are required for Jira bulk issue retrieval')
}
// If we don't have a cloudId, we need to fetch it first
if (!params?.cloudId) {
const accessibleResources = await response.json()
const normalizedInput = `https://${params?.domain}`.toLowerCase()
const matchedResource = accessibleResources.find(
(r: any) => r.url.toLowerCase() === normalizedInput
)
try {
// If we don't have a cloudId, we need to fetch it first
if (!params.cloudId) {
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to fetch accessible resources: ${response.status} ${response.statusText}`
)
}
// First get issue keys from picker
const pickerUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/picker?currentJQL=project=${params?.projectId}`
const pickerResponse = await fetch(pickerUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
Accept: 'application/json',
},
})
const accessibleResources = await response.json()
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
throw new Error('No accessible Jira resources found for this account')
}
const normalizedInput = `https://${params.domain}`.toLowerCase()
const matchedResource = accessibleResources.find(
(r) => r.url.toLowerCase() === normalizedInput
)
if (!matchedResource) {
throw new Error(`Could not find matching Jira site for domain: ${params.domain}`)
}
// First get issue keys from picker
const pickerUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/picker?currentJQL=project=${params.projectId}`
const pickerResponse = await fetch(pickerUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
},
})
if (!pickerResponse.ok) {
const errorData = await pickerResponse.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve issue keys: ${pickerResponse.status} ${pickerResponse.statusText}`
)
}
const pickerData = await pickerResponse.json()
const issueKeys = pickerData.sections
.flatMap((section: any) => section.issues || [])
.map((issue: any) => issue.key)
if (issueKeys.length === 0) {
return {
success: true,
output: [],
}
}
// Now use bulkfetch to get the full issue details
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/bulkfetch`
const bulkfetchResponse = await fetch(bulkfetchUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
expand: ['names'],
fields: ['summary', 'description', 'created', 'updated'],
fieldsByKeys: false,
issueIdsOrKeys: issueKeys,
properties: [],
}),
})
if (!bulkfetchResponse.ok) {
const errorData = await bulkfetchResponse.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`
)
}
const data = await bulkfetchResponse.json()
return {
success: true,
output: data.issues.map((issue: any) => ({
ts: new Date().toISOString(),
summary: issue.fields.summary,
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
created: issue.fields.created,
updated: issue.fields.updated,
})),
}
}
// If we have a cloudId, this response is from the issue picker
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve issue keys: ${response.status} ${response.statusText}`
)
}
const pickerData = await response.json()
const pickerData = await pickerResponse.json()
const issueKeys = pickerData.sections
.flatMap((section: any) => section.issues || [])
.map((issue: any) => issue.key)
@@ -182,12 +87,12 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
}
}
// Use bulkfetch to get the full issue details
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/bulkfetch`
// Now use bulkfetch to get the full issue details
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/bulkfetch`
const bulkfetchResponse = await fetch(bulkfetchUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
Authorization: `Bearer ${params?.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
@@ -200,14 +105,6 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
}),
})
if (!bulkfetchResponse.ok) {
const errorData = await bulkfetchResponse.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`
)
}
const data = await bulkfetchResponse.json()
return {
success: true,
@@ -219,11 +116,60 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
updated: issue.fields.updated,
})),
}
} catch (error) {
throw error instanceof Error ? error : new Error(String(error))
}
// If we have a cloudId, this response is from the issue picker
const pickerData = await response.json()
const issueKeys = pickerData.sections
.flatMap((section: any) => section.issues || [])
.map((issue: any) => issue.key)
if (issueKeys.length === 0) {
return {
success: true,
output: [],
}
}
// Use bulkfetch to get the full issue details
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/issue/bulkfetch`
const bulkfetchResponse = await fetch(bulkfetchUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
expand: ['names'],
fields: ['summary', 'description', 'created', 'updated'],
fieldsByKeys: false,
issueIdsOrKeys: issueKeys,
properties: [],
}),
})
const data = await bulkfetchResponse.json()
return {
success: true,
output: data.issues.map((issue: any) => ({
ts: new Date().toISOString(),
summary: issue.fields.summary,
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
created: issue.fields.created,
updated: issue.fields.updated,
})),
}
},
transformError: (error: any) => {
return error.message || 'Failed to retrieve Jira issues'
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'array',
description: 'Array of Jira issues with summary, description, created and updated timestamps',
},
},
}

View File

@@ -12,6 +12,7 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
provider: 'jira',
additionalScopes: ['read:jira-work', 'read:jira-user', 'read:me', 'offline_access'],
},
params: {
accessToken: {
type: 'string',
@@ -46,17 +47,6 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description:
'Jira issue details with issue key, summary, description, created and updated timestamps',
},
},
request: {
url: (params: JiraRetrieveParams) => {
@@ -76,85 +66,25 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
},
transformResponse: async (response: Response, params?: JiraRetrieveParams) => {
if (!params) {
throw new Error('Parameters are required for Jira issue retrieval')
}
// If we don't have a cloudId, we need to fetch it first
if (!params?.cloudId) {
const accessibleResources = await response.json()
const normalizedInput = `https://${params?.domain}`.toLowerCase()
const matchedResource = accessibleResources.find(
(r: any) => r.url.toLowerCase() === normalizedInput
)
try {
// If we don't have a cloudId, we need to fetch it first
if (!params.cloudId) {
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to fetch accessible resources: ${response.status} ${response.statusText}`
)
}
const accessibleResources = await response.json()
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
throw new Error('No accessible Jira resources found for this account')
}
const normalizedInput = `https://${params.domain}`.toLowerCase()
const matchedResource = accessibleResources.find(
(r) => r.url.toLowerCase() === normalizedInput
)
if (!matchedResource) {
throw new Error(`Could not find matching Jira site for domain: ${params.domain}`)
}
// Now fetch the actual issue with the found cloudId
const issueUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog`
const issueResponse = await fetch(issueUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${params.accessToken}`,
},
})
if (!issueResponse.ok) {
const errorData = await issueResponse.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve Jira issue: ${issueResponse.status} ${issueResponse.statusText}`
)
}
const data = await issueResponse.json()
if (!data || !data.fields) {
throw new Error('Invalid response format from Jira API')
}
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: data.key,
summary: data.fields.summary,
description: data.fields.description,
created: data.fields.created,
updated: data.fields.updated,
},
}
}
// If we have a cloudId, this response is the issue data
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
errorData?.message ||
`Failed to retrieve Jira issue: ${response.status} ${response.statusText}`
)
}
const data = await response.json()
if (!data || !data.fields) {
throw new Error('Invalid response format from Jira API')
}
// Now fetch the actual issue with the found cloudId
const issueUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/${params?.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog`
const issueResponse = await fetch(issueUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${params?.accessToken}`,
},
})
const data = await issueResponse.json()
return {
success: true,
output: {
@@ -166,12 +96,32 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
updated: data.fields.updated,
},
}
} catch (error) {
throw error instanceof Error ? error : new Error(String(error))
}
// If we have a cloudId, this response is the issue data
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: data.key,
summary: data.fields.summary,
description: data.fields.description,
created: data.fields.created,
updated: data.fields.updated,
},
}
},
transformError: (error: any) => {
return error.message || 'Failed to retrieve Jira issue'
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description:
'Jira issue details with issue key, summary, description, created and updated timestamps',
},
},
}

View File

@@ -1,5 +1,4 @@
import type { JiraUpdateParams, JiraUpdateResponse } from '@/tools/jira/types'
import { getJiraCloudId } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> = {
@@ -78,6 +77,65 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: '/api/tools/jira/update',
method: 'PUT',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => {
// Pass all parameters to the internal API route
return {
domain: params.domain,
accessToken: params.accessToken,
issueKey: params.issueKey,
summary: params.summary,
title: params.title, // Support both for backwards compatibility
description: params.description,
status: params.status,
priority: params.priority,
assignee: params.assignee,
cloudId: params.cloudId,
}
},
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue updated successfully',
success: true,
},
}
}
const data = JSON.parse(responseText)
// The internal API route already returns the correct format
if (data.success && data.output) {
return data
}
// Fallback for unexpected response format
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue updated',
success: false,
},
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
@@ -89,147 +147,4 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
'Updated Jira issue details with timestamp, issue key, summary, and success status',
},
},
directExecution: async (params) => {
// Pre-fetch the cloudId if not provided
if (!params.cloudId) {
params.cloudId = await getJiraCloudId(params.domain, params.accessToken)
}
return undefined // Let the regular request handling take over
},
request: {
url: (params) => {
const { domain, issueKey, cloudId } = params
if (!domain || !issueKey || !cloudId) {
throw new Error('Domain, issueKey, and cloudId are required')
}
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}`
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
body: (params) => {
// Map the summary from either summary or title field
const summaryValue = params.summary || params.title
const descriptionValue = params.description
const fields: Record<string, any> = {}
if (summaryValue) {
fields.summary = summaryValue
}
if (descriptionValue) {
fields.description = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: descriptionValue,
},
],
},
],
}
}
if (params.status) {
fields.status = {
name: params.status,
}
}
if (params.priority) {
fields.priority = {
name: params.priority,
}
}
if (params.assignee) {
fields.assignee = {
id: params.assignee,
}
}
return { fields }
},
},
transformResponse: async (response: Response, params?: JiraUpdateParams) => {
// Log the response details for debugging
const responseText = await response.text()
if (!response.ok) {
try {
if (responseText) {
const data = JSON.parse(responseText)
throw new Error(
data.errorMessages?.[0] ||
data.errors?.[Object.keys(data.errors)[0]] ||
data.message ||
'Failed to update Jira issue'
)
}
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`)
} catch (e) {
if (e instanceof SyntaxError) {
// If we can't parse the response as JSON, return the raw text
throw new Error(`Jira API error (${response.status}): ${responseText}`)
}
throw e
}
}
// For successful responses
try {
if (!responseText) {
// Some successful PUT requests might return no content
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: params?.issueKey || 'unknown',
summary: 'Issue updated successfully',
success: true,
},
}
}
const data = JSON.parse(responseText)
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: data.key || params?.issueKey || 'unknown',
summary: data.fields?.summary || 'Issue updated',
success: true,
},
}
} catch (_e) {
// If we can't parse the response but it was successful, still return success
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: params?.issueKey || 'unknown',
summary: 'Issue updated (response parsing failed)',
success: true,
},
}
}
},
transformError: (error: any) => {
return error.message || 'Failed to update Jira issue'
},
}

View File

@@ -1,5 +1,4 @@
import type { JiraWriteParams, JiraWriteResponse } from '@/tools/jira/types'
import { getJiraCloudId } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
@@ -81,6 +80,66 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
description: 'Type of issue to create (e.g., Task, Story)',
},
},
request: {
url: '/api/tools/jira/write',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => {
// Pass all parameters to the internal API route
return {
domain: params.domain,
accessToken: params.accessToken,
projectId: params.projectId,
summary: params.summary,
description: params.description,
priority: params.priority,
assignee: params.assignee,
cloudId: params.cloudId,
issueType: params.issueType,
parent: params.parent,
}
},
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue created successfully',
success: true,
url: '',
},
}
}
const data = JSON.parse(responseText)
// The internal API route already returns the correct format
if (data.success && data.output) {
return data
}
// Fallback for unexpected response format
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue created',
success: false,
},
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
@@ -92,150 +151,4 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
'Created Jira issue details with timestamp, issue key, summary, success status, and URL',
},
},
directExecution: async (params) => {
// Pre-fetch the cloudId if not provided
if (!params.cloudId) {
params.cloudId = await getJiraCloudId(params.domain, params.accessToken)
}
return undefined // Let the regular request handling take over
},
request: {
url: (params) => {
const { domain, cloudId } = params
if (!domain || !cloudId) {
throw new Error('Domain and cloudId are required')
}
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue`
return url
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
body: (params) => {
// Validate required fields
if (!params.projectId) {
throw new Error('Project ID is required')
}
if (!params.summary) {
throw new Error('Summary is required')
}
if (!params.issueType) {
throw new Error('Issue type is required')
}
// Construct fields object with only the necessary fields
const fields: Record<string, any> = {
project: {
id: params.projectId,
},
issuetype: {
name: params.issueType,
},
summary: params.summary, // Use the summary field directly
}
// Only add description if it exists
if (params.description) {
fields.description = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: params.description,
},
],
},
],
}
}
// Only add parent if it exists
if (params.parent) {
fields.parent = params.parent
}
const body = { fields }
return body
},
},
transformResponse: async (response: Response, params?: JiraWriteParams) => {
// Log the response details for debugging
const responseText = await response.text()
if (!response.ok) {
try {
if (responseText) {
const data = JSON.parse(responseText)
throw new Error(
data.errorMessages?.[0] ||
data.errors?.[Object.keys(data.errors)[0]] ||
data.message ||
'Failed to create Jira issue'
)
}
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`)
} catch (e) {
if (e instanceof SyntaxError) {
// If we can't parse the response as JSON, return the raw text
throw new Error(`Jira API error (${response.status}): ${responseText}`)
}
throw e
}
}
// For successful responses
try {
if (!responseText) {
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue created successfully',
success: true,
url: '',
},
}
}
const data = JSON.parse(responseText)
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: data.key || 'unknown',
summary: data.fields?.summary || 'Issue created',
success: true,
url: `https://${params?.domain}/browse/${data.key}`,
},
}
} catch (_e) {
return {
success: true,
output: {
ts: new Date().toISOString(),
issueKey: 'unknown',
summary: 'Issue created (response parsing failed)',
success: true,
url: '',
},
}
}
},
transformError: (error: any) => {
return error.message || 'Failed to create Jira issue'
},
}

View File

@@ -6,6 +6,7 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
name: 'Knowledge Create Document',
description: 'Create a new document in a knowledge base',
version: '1.0.0',
params: {
knowledgeBaseId: {
type: 'string',
@@ -64,29 +65,6 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
},
},
outputs: {
data: {
type: 'object',
description: 'Information about the created document',
properties: {
id: { type: 'string', description: 'Document ID' },
name: { type: 'string', description: 'Document name' },
type: { type: 'string', description: 'Document type' },
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last update timestamp' },
},
},
message: {
type: 'string',
description: 'Success or error message describing the operation result',
},
documentId: {
type: 'string',
description: 'ID of the created document',
},
},
request: {
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents`,
method: 'POST',
@@ -168,94 +146,57 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
return requestBody
},
isInternalRoute: true,
},
transformResponse: async (response): Promise<KnowledgeCreateDocumentResponse> => {
try {
const result = await response.json()
const result = await response.json()
const data = result.data || result
const documentsCreated = data.documentsCreated || []
if (!response.ok) {
const errorMessage = result.error?.message || result.message || 'Failed to create document'
throw new Error(errorMessage)
}
const data = result.data || result
const documentsCreated = data.documentsCreated || []
// Handle multiple documents response
const uploadCount = documentsCreated.length
const firstDocument = documentsCreated[0]
return {
success: true,
output: {
data: {
id: firstDocument?.documentId || firstDocument?.id || '',
name:
uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
type: 'document',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
enabled: true,
},
message:
uploadCount > 1
? `Successfully created ${uploadCount} documents in knowledge base`
: `Successfully created document in knowledge base`,
documentId: firstDocument?.documentId || firstDocument?.id || '',
},
}
} catch (error: any) {
return {
success: false,
output: {
data: {
id: '',
name: '',
type: '',
enabled: true,
createdAt: '',
updatedAt: '',
},
message: `Failed to create document: ${error.message || 'Unknown error'}`,
documentId: '',
},
error: `Failed to create document: ${error.message || 'Unknown error'}`,
}
}
},
transformError: async (error): Promise<KnowledgeCreateDocumentResponse> => {
let errorMessage = 'Failed to create document'
if (error.message) {
if (error.message.includes('Document name')) {
errorMessage = `Document name error: ${error.message}`
} else if (error.message.includes('Document content')) {
errorMessage = `Document content error: ${error.message}`
} else if (error.message.includes('invalid characters')) {
errorMessage = `${error.message}. Please use a valid filename.`
} else if (error.message.includes('maximum size')) {
errorMessage = `${error.message}. Consider breaking large content into smaller documents.`
} else {
errorMessage = `Failed to create document: ${error.message}`
}
}
// Handle multiple documents response
const uploadCount = documentsCreated.length
const firstDocument = documentsCreated[0]
return {
success: false,
success: true,
output: {
data: {
id: '',
name: '',
type: '',
id: firstDocument?.documentId || firstDocument?.id || '',
name: uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
type: 'document',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
enabled: true,
createdAt: '',
updatedAt: '',
},
message: errorMessage,
documentId: '',
message:
uploadCount > 1
? `Successfully created ${uploadCount} documents in knowledge base`
: `Successfully created document in knowledge base`,
documentId: firstDocument?.documentId || firstDocument?.id || '',
},
error: errorMessage,
}
},
outputs: {
data: {
type: 'object',
description: 'Information about the created document',
properties: {
id: { type: 'string', description: 'Document ID' },
name: { type: 'string', description: 'Document name' },
type: { type: 'string', description: 'Document type' },
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last update timestamp' },
},
},
message: {
type: 'string',
description: 'Success or error message describing the operation result',
},
documentId: {
type: 'string',
description: 'ID of the created document',
},
},
}

View File

@@ -6,6 +6,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
name: 'Knowledge Search',
description: 'Search for similar content in a knowledge base using vector similarity',
version: '1.0.0',
params: {
knowledgeBaseId: {
type: 'string',
@@ -29,37 +30,6 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
},
},
outputs: {
results: {
type: 'array',
description: 'Array of search results from the knowledge base',
items: {
type: 'object',
properties: {
id: { type: 'string' },
content: { type: 'string' },
documentId: { type: 'string' },
chunkIndex: { type: 'number' },
similarity: { type: 'number' },
metadata: { type: 'object' },
},
},
},
query: {
type: 'string',
description: 'The search query that was executed',
},
totalResults: {
type: 'number',
description: 'Total number of results found',
},
cost: {
type: 'object',
description: 'Cost information for the search operation',
optional: true,
},
},
request: {
url: () => '/api/knowledge/search',
method: 'POST',
@@ -117,53 +87,50 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
return requestBody
},
isInternalRoute: true,
},
transformResponse: async (response): Promise<KnowledgeSearchResponse> => {
try {
const result = await response.json()
if (!response.ok) {
const errorMessage = result.error || result.message || 'Failed to perform search'
throw new Error(errorMessage)
}
const data = result.data || result
return {
success: true,
output: {
results: data.results || [],
query: data.query,
totalResults: data.totalResults || 0,
cost: data.cost,
},
}
} catch (error: any) {
return {
success: false,
output: {
results: [],
query: '',
totalResults: 0,
cost: undefined,
},
error: error.message || 'Failed to perform vector search',
}
}
},
transformError: async (error): Promise<KnowledgeSearchResponse> => {
const errorMessage = error.message || 'Failed to perform search'
const result = await response.json()
const data = result.data || result
return {
success: false,
success: true,
output: {
results: [],
query: '',
totalResults: 0,
cost: undefined,
results: data.results || [],
query: data.query,
totalResults: data.totalResults || 0,
cost: data.cost,
},
error: errorMessage,
}
},
outputs: {
results: {
type: 'array',
description: 'Array of search results from the knowledge base',
items: {
type: 'object',
properties: {
id: { type: 'string' },
content: { type: 'string' },
documentId: { type: 'string' },
chunkIndex: { type: 'number' },
similarity: { type: 'number' },
metadata: { type: 'object' },
},
},
},
query: {
type: 'string',
description: 'The search query that was executed',
},
totalResults: {
type: 'number',
description: 'Total number of results found',
},
cost: {
type: 'object',
description: 'Cost information for the search operation',
optional: true,
},
},
}

View File

@@ -6,6 +6,7 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
name: 'Knowledge Upload Chunk',
description: 'Upload a new chunk to a document in a knowledge base',
version: '1.0.0',
params: {
knowledgeBaseId: {
type: 'string',
@@ -24,6 +25,50 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
},
},
request: {
url: (params) =>
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => {
const workflowId = params._context?.workflowId
const requestBody = {
content: params.content,
enabled: true,
...(workflowId && { workflowId }),
}
return requestBody
},
},
transformResponse: async (response): Promise<KnowledgeUploadChunkResponse> => {
const result = await response.json()
const data = result.data || result
return {
success: true,
output: {
data: {
id: data.id,
chunkIndex: data.chunkIndex || 0,
content: data.content,
contentLength: data.contentLength || data.content?.length || 0,
tokenCount: data.tokenCount || 0,
enabled: data.enabled !== undefined ? data.enabled : true,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
message: `Successfully uploaded chunk to document`,
documentId: data.documentId,
cost: data.cost,
},
}
},
outputs: {
data: {
type: 'object',
@@ -53,96 +98,4 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
optional: true,
},
},
request: {
url: (params) =>
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => {
const workflowId = params._context?.workflowId
const requestBody = {
content: params.content,
enabled: true,
...(workflowId && { workflowId }),
}
return requestBody
},
isInternalRoute: true,
},
transformResponse: async (response): Promise<KnowledgeUploadChunkResponse> => {
try {
const result = await response.json()
if (!response.ok) {
const errorMessage = result.error?.message || result.message || 'Failed to upload chunk'
throw new Error(errorMessage)
}
const data = result.data || result
return {
success: true,
output: {
data: {
id: data.id,
chunkIndex: data.chunkIndex || 0,
content: data.content,
contentLength: data.contentLength || data.content?.length || 0,
tokenCount: data.tokenCount || 0,
enabled: data.enabled !== undefined ? data.enabled : true,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
message: `Successfully uploaded chunk to document`,
documentId: data.documentId,
cost: data.cost,
},
}
} catch (error: any) {
return {
success: false,
output: {
data: {
id: '',
chunkIndex: 0,
content: '',
contentLength: 0,
tokenCount: 0,
enabled: true,
createdAt: '',
updatedAt: '',
},
message: `Failed to upload chunk: ${error.message || 'Unknown error'}`,
documentId: '',
},
error: `Failed to upload chunk: ${error.message || 'Unknown error'}`,
}
}
},
transformError: async (error): Promise<KnowledgeUploadChunkResponse> => {
const errorMessage = `Failed to upload chunk: ${error.message || 'Unknown error'}`
return {
success: false,
output: {
data: {
id: '',
chunkIndex: 0,
content: '',
contentLength: 0,
tokenCount: 0,
enabled: true,
createdAt: '',
updatedAt: '',
},
message: errorMessage,
documentId: '',
},
error: errorMessage,
}
},
}

View File

@@ -7,10 +7,12 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
name: 'Linear Issue Writer',
description: 'Create a new issue in Linear',
version: '1.0.0',
oauth: {
required: true,
provider: 'linear',
},
params: {
teamId: {
type: 'string',
@@ -38,21 +40,6 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
},
},
outputs: {
issue: {
type: 'object',
description:
'The created issue containing id, title, description, state, teamId, and projectId',
properties: {
id: { type: 'string', description: 'Issue ID' },
title: { type: 'string', description: 'Issue title' },
description: { type: 'string', description: 'Issue description' },
state: { type: 'string', description: 'Issue state' },
teamId: { type: 'string', description: 'Team ID' },
projectId: { type: 'string', description: 'Project ID' },
},
},
},
request: {
url: 'https://api.linear.app/graphql',
method: 'POST',
@@ -100,12 +87,10 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
}
},
},
transformResponse: async (response) => {
const data = await response.json()
const issue = data.data.issueCreate.issue
if (!issue) {
throw new Error('Failed to create issue: No issue returned from Linear API')
}
return {
success: true,
output: {
@@ -120,20 +105,20 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
},
}
},
transformError: (error) => {
if (error instanceof Error) {
return error.message
}
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
}
}
return 'Failed to create Linear issue'
outputs: {
issue: {
type: 'object',
description:
'The created issue containing id, title, description, state, teamId, and projectId',
properties: {
id: { type: 'string', description: 'Issue ID' },
title: { type: 'string', description: 'Issue title' },
description: { type: 'string', description: 'Issue description' },
state: { type: 'string', description: 'Issue state' },
teamId: { type: 'string', description: 'Team ID' },
projectId: { type: 'string', description: 'Project ID' },
},
},
},
}

View File

@@ -10,10 +10,12 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
name: 'Linear Issue Reader',
description: 'Fetch and filter issues from Linear',
version: '1.0.0',
oauth: {
required: true,
provider: 'linear',
},
params: {
teamId: {
type: 'string',
@@ -29,24 +31,6 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
},
},
outputs: {
issues: {
type: 'array',
description:
'Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Issue ID' },
title: { type: 'string', description: 'Issue title' },
description: { type: 'string', description: 'Issue description' },
state: { type: 'string', description: 'Issue state' },
teamId: { type: 'string', description: 'Team ID' },
projectId: { type: 'string', description: 'Project ID' },
},
},
},
},
request: {
url: 'https://api.linear.app/graphql',
method: 'POST',
@@ -85,15 +69,9 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
},
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (data.errors) {
return {
success: false,
output: { issues: [] },
error: data.errors.map((e: any) => e.message).join('; '),
}
}
return {
success: true,
output: {
@@ -108,23 +86,23 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
},
}
},
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 'Failed to fetch Linear issues'
outputs: {
issues: {
type: 'array',
description:
'Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Issue ID' },
title: { type: 'string', description: 'Issue title' },
description: { type: 'string', description: 'Issue description' },
state: { type: 'string', description: 'Issue state' },
teamId: { type: 'string', description: 'Team ID' },
projectId: { type: 'string', description: 'Project ID' },
},
},
},
},
}

View File

@@ -38,18 +38,6 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
},
},
outputs: {
answer: {
type: 'string',
description: 'The sourced answer to the search query',
},
sources: {
type: 'array',
description:
'Array of sources used to compile the answer, each containing name, url, and snippet',
},
},
request: {
url: 'https://api.linkup.so/v1/search',
method: 'POST',
@@ -71,11 +59,6 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Linkup API error: ${response.status} ${errorText}`)
}
const data: LinkupSearchResponse = await response.json()
return {
@@ -87,7 +70,15 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
}
},
transformError: (error) => {
return `Error searching with Linkup: ${error.message}`
outputs: {
answer: {
type: 'string',
description: 'The sourced answer to the search query',
},
sources: {
type: 'array',
description:
'Array of sources used to compile the answer, each containing name, url, and snippet',
},
},
}

View File

@@ -6,6 +6,7 @@ export const mem0AddMemoriesTool: ToolConfig = {
name: 'Add Memories',
description: 'Add memories to Mem0 for persistent storage and retrieval',
version: '1.0.0',
params: {
userId: {
type: 'string',
@@ -27,16 +28,6 @@ export const mem0AddMemoriesTool: ToolConfig = {
},
},
outputs: {
ids: {
type: 'array',
description: 'Array of memory IDs that were created',
},
memories: {
type: 'array',
description: 'Array of memory objects that were created',
},
},
request: {
url: 'https://api.mem0.ai/v1/memories/',
method: 'POST',
@@ -76,6 +67,7 @@ export const mem0AddMemoriesTool: ToolConfig = {
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
@@ -122,13 +114,15 @@ export const mem0AddMemoriesTool: ToolConfig = {
},
}
},
transformError: async (error) => {
return {
success: false,
output: {
ids: [],
memories: [],
},
}
outputs: {
ids: {
type: 'array',
description: 'Array of memory IDs that were created',
},
memories: {
type: 'array',
description: 'Array of memory objects that were created',
},
},
}

View File

@@ -6,6 +6,7 @@ export const mem0GetMemoriesTool: ToolConfig = {
name: 'Get Memories',
description: 'Retrieve memories from Mem0 by ID or filter criteria',
version: '1.0.0',
params: {
userId: {
type: 'string',
@@ -46,16 +47,6 @@ export const mem0GetMemoriesTool: ToolConfig = {
},
},
outputs: {
memories: {
type: 'array',
description: 'Array of retrieved memory objects',
},
ids: {
type: 'array',
description: 'Array of memory IDs that were retrieved',
},
},
request: {
url: (params: Record<string, any>) => {
// For a specific memory ID, use the get single memory endpoint
@@ -113,43 +104,29 @@ export const mem0GetMemoriesTool: ToolConfig = {
return body
},
},
transformResponse: async (response, params) => {
try {
// Get raw response for debugging
const responseText = await response.clone().text()
// Parse the response
const data = JSON.parse(responseText)
transformResponse: async (response: Response) => {
const data = await response.json()
const memories = Array.isArray(data) ? data : [data]
const ids = memories.map((memory) => memory.id).filter(Boolean)
// Format the memories for display
const memories = Array.isArray(data) ? data : [data]
// Extract IDs if available
const ids = memories.map((memory) => memory.id).filter(Boolean)
return {
success: true,
output: {
memories,
ids,
},
}
} catch (error: any) {
return {
success: false,
output: {
error: `Failed to process get memories response: ${error.message}`,
},
}
}
},
transformError: async (error) => {
return {
success: false,
success: true,
output: {
ids: [],
memories: [],
memories,
ids,
},
}
},
outputs: {
memories: {
type: 'array',
description: 'Array of retrieved memory objects',
},
ids: {
type: 'array',
description: 'Array of memory IDs that were retrieved',
},
},
}

View File

@@ -7,6 +7,7 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
name: 'Search Memories',
description: 'Search for memories in Mem0 using semantic search',
version: '1.0.0',
params: {
userId: {
type: 'string',
@@ -35,16 +36,6 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
},
},
outputs: {
searchResults: {
type: 'array',
description: 'Array of search results with memory data, each containing id, data, and score',
},
ids: {
type: 'array',
description: 'Array of memory IDs found in the search results',
},
},
request: {
url: 'https://api.mem0.ai/v2/memories/search/',
method: 'POST',
@@ -65,67 +56,54 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
return body
},
},
transformResponse: async (response) => {
try {
// Get raw response for debugging
const responseText = await response.clone().text()
const data = await response.json()
// Parse the response
const data = JSON.parse(responseText)
// Handle empty results
if (!data || (Array.isArray(data) && data.length === 0)) {
return {
success: true,
output: {
searchResults: [],
ids: [],
},
}
}
// For array results (standard format)
if (Array.isArray(data)) {
const searchResults = data.map((item) => ({
id: item.id,
data: { memory: item.memory || '' },
score: item.score || 0,
}))
const ids = data.map((item) => item.id).filter(Boolean)
return {
success: true,
output: {
searchResults,
ids,
},
}
}
// Fallback for unexpected response format
if (!data || (Array.isArray(data) && data.length === 0)) {
return {
success: true,
output: {
searchResults: [],
},
}
} catch (error: any) {
return {
success: false,
output: {
error: `Failed to process search response: ${error.message}`,
ids: [],
},
}
}
},
transformError: async (error) => {
if (Array.isArray(data)) {
const searchResults = data.map((item) => ({
id: item.id,
data: { memory: item.memory || '' },
score: item.score || 0,
}))
const ids = data.map((item) => item.id).filter(Boolean)
return {
success: true,
output: {
searchResults,
ids,
},
}
}
return {
success: false,
success: true,
output: {
ids: [],
searchResults: [],
},
}
},
outputs: {
searchResults: {
type: 'array',
description: 'Array of search results with memory data, each containing id, data, and score',
},
ids: {
type: 'array',
description: 'Array of memory IDs found in the search results',
},
},
}

View File

@@ -6,6 +6,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
name: 'Add Memory',
description: 'Add a new memory to the database or append to existing memory with the same ID.',
version: '1.0.0',
params: {
id: {
type: 'string',
@@ -24,14 +25,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
description: 'Content for agent memory',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the memory was added successfully' },
memories: {
type: 'array',
description: 'Array of memory objects including the new or updated memory',
},
error: { type: 'string', description: 'Error message if operation failed' },
},
request: {
url: '/api/memory',
method: 'POST',
@@ -85,44 +79,29 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
return body
},
isInternalRoute: true,
},
transformResponse: async (response): Promise<MemoryResponse> => {
try {
const result = await response.json()
const errorMessage = result.error?.message || 'Failed to add memory'
const result = await response.json()
const data = result.data || result
const data = result.data || result
// For agent memories, return the full array of message objects
const memories = Array.isArray(data.data) ? data.data : [data.data]
// For agent memories, return the full array of message objects
const memories = Array.isArray(data.data) ? data.data : [data.data]
return {
success: true,
output: {
memories,
},
error: errorMessage,
}
} catch (error: any) {
return {
success: false,
output: {
memories: undefined,
},
error,
}
return {
success: true,
output: {
memories,
},
}
},
transformError: async (error): Promise<MemoryResponse> => {
const errorMessage = `Memory operation failed: ${error.message || 'Unknown error occurred'}`
return {
success: false,
output: {
memories: undefined,
message: `Memory operation failed: ${error.message || 'Unknown error occurred'}`,
},
error: errorMessage,
}
outputs: {
success: { type: 'boolean', description: 'Whether the memory was added successfully' },
memories: {
type: 'array',
description: 'Array of memory objects including the new or updated memory',
},
error: { type: 'string', description: 'Error message if operation failed' },
},
}

View File

@@ -6,6 +6,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
name: 'Delete Memory',
description: 'Delete a specific memory by its ID',
version: '1.0.0',
params: {
id: {
type: 'string',
@@ -13,11 +14,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
description: 'Identifier for the memory to delete',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the memory was deleted successfully' },
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
request: {
url: (params): any => {
// Get workflowId from context (set by workflow execution)
@@ -44,41 +41,21 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
headers: () => ({
'Content-Type': 'application/json',
}),
isInternalRoute: true,
},
transformResponse: async (response): Promise<MemoryResponse> => {
try {
const result = await response.json()
const result = await response.json()
if (!response.ok) {
const errorMessage = result.error?.message || 'Failed to delete memory'
throw new Error(errorMessage)
}
return {
success: true,
output: {
message: 'Memory deleted successfully.',
},
}
} catch (error: any) {
return {
success: false,
output: {
message: `Failed to delete memory: ${error.message || 'Unknown error'}`,
},
error: `Failed to delete memory: ${error.message || 'Unknown error'}`,
}
return {
success: true,
output: {
message: 'Memory deleted successfully.',
},
}
},
transformError: async (error): Promise<MemoryResponse> => {
const errorMessage = `Memory deletion failed: ${error.message || 'Unknown error'}`
return {
success: false,
output: {
message: errorMessage,
},
error: errorMessage,
}
outputs: {
success: { type: 'boolean', description: 'Whether the memory was deleted successfully' },
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
}

View File

@@ -6,6 +6,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
name: 'Get Memory',
description: 'Retrieve a specific memory by its ID',
version: '1.0.0',
params: {
id: {
type: 'string',
@@ -13,12 +14,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
description: 'Identifier for the memory to retrieve',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the memory was retrieved successfully' },
memories: { type: 'array', description: 'Array of memory data for the requested ID' },
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
request: {
url: (params): any => {
// Get workflowId from context (set by workflow execution)
@@ -45,46 +41,25 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
headers: () => ({
'Content-Type': 'application/json',
}),
isInternalRoute: true,
},
transformResponse: async (response): Promise<MemoryResponse> => {
try {
const result = await response.json()
const result = await response.json()
const data = result.data || result
if (!response.ok) {
const errorMessage = result.error?.message || 'Failed to retrieve memory'
throw new Error(errorMessage)
}
const data = result.data || result
return {
success: true,
output: {
memories: data.data,
message: 'Memory retrieved successfully',
},
}
} catch (error: any) {
return {
success: false,
output: {
memories: undefined,
message: `Failed to retrieve memory: ${error.message || 'Unknown error'}`,
},
error: `Failed to retrieve memory: ${error.message || 'Unknown error'}`,
}
return {
success: true,
output: {
memories: data.data,
message: 'Memory retrieved successfully',
},
}
},
transformError: async (error): Promise<MemoryResponse> => {
const errorMessage = `Memory retrieval failed: ${error.message || 'Unknown error'}`
return {
success: false,
output: {
memories: undefined,
message: errorMessage,
},
error: errorMessage,
}
outputs: {
success: { type: 'boolean', description: 'Whether the memory was retrieved successfully' },
memories: { type: 'array', description: 'Array of memory data for the requested ID' },
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
}

View File

@@ -6,16 +6,9 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
name: 'Get All Memories',
description: 'Retrieve all memories from the database',
version: '1.0.0',
params: {},
outputs: {
success: { type: 'boolean', description: 'Whether all memories were retrieved successfully' },
memories: {
type: 'array',
description: 'Array of all memory objects with keys, types, and data',
},
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
request: {
url: (params): any => {
// Get workflowId from context (set by workflow execution)
@@ -42,55 +35,38 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
headers: () => ({
'Content-Type': 'application/json',
}),
isInternalRoute: true,
},
transformResponse: async (response): Promise<MemoryResponse> => {
try {
const result = await response.json()
const result = await response.json()
if (!response.ok) {
const errorMessage = result.error?.message || 'Failed to retrieve memories'
throw new Error(errorMessage)
}
// Extract memories from the response
const data = result.data || result
const rawMemories = data.memories || data || []
// Extract memories from the response
const data = result.data || result
const rawMemories = data.memories || data || []
// Transform memories to return them with their keys and types for better context
const memories = rawMemories.map((memory: any) => ({
key: memory.key,
type: memory.type,
data: memory.data,
}))
// Transform memories to return them with their keys and types for better context
const memories = rawMemories.map((memory: any) => ({
key: memory.key,
type: memory.type,
data: memory.data,
}))
return {
success: true,
output: {
memories,
message: 'Memories retrieved successfully',
},
}
} catch (error: any) {
return {
success: false,
output: {
memories: [],
message: `Failed to retrieve memories: ${error.message || 'Unknown error'}`,
},
error: `Failed to retrieve memories: ${error.message || 'Unknown error'}`,
}
return {
success: true,
output: {
memories,
message: 'Memories retrieved successfully',
},
}
},
transformError: async (error): Promise<MemoryResponse> => {
const errorMessage = `Memory retrieval failed: ${error.message || 'Unknown error'}`
return {
success: false,
output: {
memories: [],
message: errorMessage,
},
error: errorMessage,
}
outputs: {
success: { type: 'boolean', description: 'Whether all memories were retrieved successfully' },
memories: {
type: 'array',
description: 'Array of all memory objects with keys, types, and data',
},
message: { type: 'string', description: 'Success or error message' },
error: { type: 'string', description: 'Error message if operation failed' },
},
}

View File

@@ -9,11 +9,13 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
name: 'Read from Microsoft Excel',
description: 'Read data from a Microsoft Excel spreadsheet',
version: '1.0',
oauth: {
required: true,
provider: 'microsoft-excel',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
@@ -34,31 +36,7 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
description: 'The range of cells to read from',
},
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Excel spreadsheet data and metadata',
properties: {
data: {
type: 'object',
description: 'Range data from the spreadsheet',
properties: {
range: { type: 'string', description: 'The range that was read' },
values: { type: 'array', description: 'Array of rows containing cell values' },
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
@@ -93,16 +71,8 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
const errorText =
errorJson.error && typeof errorJson.error === 'object'
? errorJson.error.message || JSON.stringify(errorJson.error)
: errorJson.error || response.statusText
throw new Error(`Failed to read Microsoft Excel data: ${errorText}`)
}
transformResponse: async (response: Response) => {
const data = await response.json()
const urlParts = response.url.split('/drive/items/')
@@ -130,33 +100,30 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
return result
},
transformError: (error) => {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'object' && error !== null) {
if (error.error) {
if (typeof error.error === 'string') {
return error.error
}
if (typeof error.error === 'object' && error.error.message) {
return error.error.message
}
return JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
try {
return `Microsoft Excel API error: ${JSON.stringify(error)}`
} catch (_e) {
return 'Microsoft Excel API error: Unable to parse error details'
}
}
return 'An error occurred while reading from Microsoft Excel'
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Excel spreadsheet data and metadata',
properties: {
data: {
type: 'object',
description: 'Range data from the spreadsheet',
properties: {
range: { type: 'string', description: 'The range that was read' },
values: { type: 'array', description: 'Array of rows containing cell values' },
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
},
},
}

Some files were not shown because too many files have changed in this diff Show More