chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Tools Scope
|
||||
|
||||
These rules apply to integration tool definitions under `apps/sim/tools/**`.
|
||||
|
||||
- Start from the service API docs before adding or changing a tool.
|
||||
- Keep each service under `tools/{service}/` with `index.ts`, `types.ts`, and one file per action.
|
||||
- Tool IDs must use `snake_case` and match registry keys exactly.
|
||||
- Use `visibility: 'hidden'` only for system-injected params such as OAuth access tokens.
|
||||
- Use `visibility: 'user-only'` for credentials and account-specific values the user must provide.
|
||||
- Use `visibility: 'user-or-llm'` for ordinary operation parameters.
|
||||
- In `transformResponse`, extract meaningful fields instead of dumping raw JSON.
|
||||
- Use `?? null` for nullable response fields and `?? []` for optional arrays where appropriate.
|
||||
- Register every tool in `tools/registry.ts` and keep entries aligned with the exported tool IDs.
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const a2aCancelTaskTool: ToolConfig<A2ACancelTaskParams, A2ACancelTaskResponse> = {
|
||||
id: 'a2a_cancel_task',
|
||||
name: 'A2A Cancel Task',
|
||||
description: 'Request cancellation of an in-progress A2A task.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
agentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The A2A agent endpoint URL',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The task ID to cancel',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/a2a/cancel-task',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
agentUrl: params.agentUrl,
|
||||
taskId: params.taskId,
|
||||
}
|
||||
if (params.apiKey) body.apiKey = params.apiKey
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => response.json(),
|
||||
|
||||
outputs: {
|
||||
taskId: { type: 'string', description: 'Task identifier' },
|
||||
state: { type: 'string', description: 'Task lifecycle state after cancellation' },
|
||||
canceled: { type: 'boolean', description: 'Whether the task was canceled' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { A2AAgentCardResponse, A2AGetAgentCardParams } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const a2aGetAgentCardTool: ToolConfig<A2AGetAgentCardParams, A2AAgentCardResponse> = {
|
||||
id: 'a2a_get_agent_card',
|
||||
name: 'A2A Get Agent Card',
|
||||
description: 'Fetch the Agent Card (discovery document) for an external A2A agent.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
agentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The A2A agent endpoint URL',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/a2a/get-agent-card',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = { agentUrl: params.agentUrl }
|
||||
if (params.apiKey) body.apiKey = params.apiKey
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => response.json(),
|
||||
|
||||
outputs: {
|
||||
name: { type: 'string', description: 'Agent display name' },
|
||||
description: { type: 'string', description: 'Agent description' },
|
||||
url: { type: 'string', description: 'Agent endpoint URL' },
|
||||
version: { type: 'string', description: "The agent's own version" },
|
||||
protocolVersion: { type: 'string', description: 'A2A protocol version the agent exposes' },
|
||||
capabilities: {
|
||||
type: 'json',
|
||||
description: 'Agent capability flags',
|
||||
properties: {
|
||||
streaming: { type: 'boolean', description: 'Supports streaming responses' },
|
||||
pushNotifications: { type: 'boolean', description: 'Supports push notifications' },
|
||||
extendedAgentCard: { type: 'boolean', description: 'Provides an extended agent card' },
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
type: 'array',
|
||||
description: 'Skills the agent can perform',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Skill identifier' },
|
||||
name: { type: 'string', description: 'Skill name' },
|
||||
description: { type: 'string', description: 'Skill description' },
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultInputModes: {
|
||||
type: 'array',
|
||||
description: 'Default accepted input media types',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
defaultOutputModes: {
|
||||
type: 'array',
|
||||
description: 'Default produced output media types',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { A2A_TASK_OUTPUTS, type A2AGetTaskParams, type A2ATaskResponse } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const a2aGetTaskTool: ToolConfig<A2AGetTaskParams, A2ATaskResponse> = {
|
||||
id: 'a2a_get_task',
|
||||
name: 'A2A Get Task',
|
||||
description: 'Retrieve the current state and result of an A2A task.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
agentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The A2A agent endpoint URL',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The task ID to retrieve',
|
||||
},
|
||||
historyLength: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of history messages to include',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/a2a/get-task',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
agentUrl: params.agentUrl,
|
||||
taskId: params.taskId,
|
||||
}
|
||||
if (params.historyLength !== undefined) body.historyLength = params.historyLength
|
||||
if (params.apiKey) body.apiKey = params.apiKey
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => response.json(),
|
||||
|
||||
outputs: A2A_TASK_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { a2aCancelTaskTool } from './cancel_task'
|
||||
export { a2aGetAgentCardTool } from './get_agent_card'
|
||||
export { a2aGetTaskTool } from './get_task'
|
||||
export { a2aSendMessageTool } from './send_message'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
A2A_TASK_OUTPUTS,
|
||||
type A2ASendMessageParams,
|
||||
type A2ATaskResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const a2aSendMessageTool: ToolConfig<A2ASendMessageParams, A2ATaskResponse> = {
|
||||
id: 'a2a_send_message',
|
||||
name: 'A2A Send Message',
|
||||
description: 'Send a message to an external A2A agent and return its response.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
agentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The A2A agent endpoint URL',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The message text to send',
|
||||
},
|
||||
data: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional structured JSON data to attach',
|
||||
},
|
||||
files: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional files to attach',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Existing task ID to continue',
|
||||
},
|
||||
contextId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Conversation context ID to continue',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/a2a/send-message',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
agentUrl: params.agentUrl,
|
||||
message: params.message,
|
||||
}
|
||||
if (params.data) body.data = params.data
|
||||
if (params.files) body.files = params.files
|
||||
if (params.taskId) body.taskId = params.taskId
|
||||
if (params.contextId) body.contextId = params.contextId
|
||||
if (params.apiKey) body.apiKey = params.apiKey
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => response.json(),
|
||||
|
||||
outputs: A2A_TASK_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { A2AAgentCardOutput, A2ATaskOutput } from '@/lib/a2a/client'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface A2ABaseParams {
|
||||
agentUrl: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export interface A2ASendMessageParams extends A2ABaseParams {
|
||||
message: string
|
||||
data?: unknown
|
||||
files?: UserFile[]
|
||||
taskId?: string
|
||||
contextId?: string
|
||||
}
|
||||
|
||||
export interface A2AGetTaskParams extends A2ABaseParams {
|
||||
taskId: string
|
||||
historyLength?: number
|
||||
}
|
||||
|
||||
export interface A2ACancelTaskParams extends A2ABaseParams {
|
||||
taskId: string
|
||||
}
|
||||
|
||||
export type A2AGetAgentCardParams = A2ABaseParams
|
||||
|
||||
export interface A2ATaskResponse extends ToolResponse {
|
||||
output: A2ATaskOutput
|
||||
}
|
||||
|
||||
export interface A2ACancelTaskResponse extends ToolResponse {
|
||||
output: {
|
||||
taskId: string
|
||||
state: string
|
||||
canceled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface A2AAgentCardResponse extends ToolResponse {
|
||||
output: A2AAgentCardOutput
|
||||
}
|
||||
|
||||
/** Shared output schema for the task-returning operations (send, get). */
|
||||
export const A2A_TASK_OUTPUTS = {
|
||||
content: { type: 'string', description: 'Agent response text' },
|
||||
taskId: { type: 'string', description: 'Task identifier' },
|
||||
contextId: { type: 'string', description: 'Conversation/context identifier' },
|
||||
state: { type: 'string', description: 'Task lifecycle state' },
|
||||
artifacts: {
|
||||
type: 'array',
|
||||
description: 'Structured task output artifacts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Artifact name' },
|
||||
description: { type: 'string', description: 'Artifact description' },
|
||||
content: { type: 'string', description: 'Artifact text content' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { CreateDraftParams, CreateDraftResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailCreateDraftTool: ToolConfig<CreateDraftParams, CreateDraftResult> = {
|
||||
id: 'agentmail_create_draft',
|
||||
name: 'Create Draft',
|
||||
description: 'Create a new email draft in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to create the draft in',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Recipient email addresses (comma-separated)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Draft subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plain text draft body',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HTML draft body',
|
||||
},
|
||||
cc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CC recipient email addresses (comma-separated)',
|
||||
},
|
||||
bcc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'BCC recipient email addresses (comma-separated)',
|
||||
},
|
||||
inReplyTo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of message being replied to',
|
||||
},
|
||||
sendAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 8601 timestamp to schedule sending',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
|
||||
if (params.subject) body.subject = params.subject
|
||||
if (params.text) body.text = params.text
|
||||
if (params.html) body.html = params.html
|
||||
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
|
||||
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
|
||||
if (params.inReplyTo) body.in_reply_to = params.inReplyTo
|
||||
if (params.sendAt) body.send_at = params.sendAt
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<CreateDraftResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to create draft',
|
||||
output: {
|
||||
draftId: '',
|
||||
inboxId: '',
|
||||
subject: null,
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
text: null,
|
||||
html: null,
|
||||
preview: null,
|
||||
labels: [],
|
||||
inReplyTo: null,
|
||||
sendStatus: null,
|
||||
sendAt: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
draftId: data.draft_id ?? '',
|
||||
inboxId: data.inbox_id ?? '',
|
||||
subject: data.subject ?? null,
|
||||
to: data.to ?? [],
|
||||
cc: data.cc ?? [],
|
||||
bcc: data.bcc ?? [],
|
||||
text: data.text ?? null,
|
||||
html: data.html ?? null,
|
||||
preview: data.preview ?? null,
|
||||
labels: data.labels ?? [],
|
||||
inReplyTo: data.in_reply_to ?? null,
|
||||
sendStatus: data.send_status ?? null,
|
||||
sendAt: data.send_at ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
draftId: { type: 'string', description: 'Unique identifier for the draft' },
|
||||
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
|
||||
subject: { type: 'string', description: 'Draft subject', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
text: { type: 'string', description: 'Plain text content', optional: true },
|
||||
html: { type: 'string', description: 'HTML content', optional: true },
|
||||
preview: { type: 'string', description: 'Draft preview text', optional: true },
|
||||
labels: { type: 'array', description: 'Labels assigned to the draft' },
|
||||
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
|
||||
sendStatus: {
|
||||
type: 'string',
|
||||
description: 'Send status (scheduled, sending, failed)',
|
||||
optional: true,
|
||||
},
|
||||
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { CreateInboxParams, CreateInboxResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailCreateInboxTool: ToolConfig<CreateInboxParams, CreateInboxResult> = {
|
||||
id: 'agentmail_create_inbox',
|
||||
name: 'Create Inbox',
|
||||
description: 'Create a new email inbox with AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Username for the inbox email address',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Domain for the inbox email address',
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Display name for the inbox',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentmail.to/v0/inboxes',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
...(params.username && { username: params.username }),
|
||||
...(params.domain && { domain: params.domain }),
|
||||
...(params.displayName && { display_name: params.displayName }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<CreateInboxResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to create inbox',
|
||||
output: {
|
||||
inboxId: '',
|
||||
email: '',
|
||||
displayName: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inboxId: data.inbox_id ?? '',
|
||||
email: data.email ?? '',
|
||||
displayName: data.display_name ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
|
||||
email: { type: 'string', description: 'Email address of the inbox' },
|
||||
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { DeleteDraftParams, DeleteDraftResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailDeleteDraftTool: ToolConfig<DeleteDraftParams, DeleteDraftResult> = {
|
||||
id: 'agentmail_delete_draft',
|
||||
name: 'Delete Draft',
|
||||
description: 'Delete an email draft in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the draft',
|
||||
},
|
||||
draftId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the draft to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<DeleteDraftResult> => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to delete draft',
|
||||
output: { deleted: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { deleted: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the draft was successfully deleted' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { DeleteInboxParams, DeleteInboxResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailDeleteInboxTool: ToolConfig<DeleteInboxParams, DeleteInboxResult> = {
|
||||
id: 'agentmail_delete_inbox',
|
||||
name: 'Delete Inbox',
|
||||
description: 'Delete an email inbox in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<DeleteInboxResult> => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to delete inbox',
|
||||
output: { deleted: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { deleted: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the inbox was successfully deleted' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { DeleteThreadParams, DeleteThreadResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailDeleteThreadTool: ToolConfig<DeleteThreadParams, DeleteThreadResult> = {
|
||||
id: 'agentmail_delete_thread',
|
||||
name: 'Delete Thread',
|
||||
description:
|
||||
'Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the thread',
|
||||
},
|
||||
threadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the thread to delete',
|
||||
},
|
||||
permanent: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Force permanent deletion instead of moving to trash',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.permanent) query.set('permanent', 'true')
|
||||
const qs = query.toString()
|
||||
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<DeleteThreadResult> => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to delete thread',
|
||||
output: { deleted: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { deleted: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the thread was successfully deleted' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { ForwardMessageParams, ForwardMessageResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailForwardMessageTool: ToolConfig<ForwardMessageParams, ForwardMessageResult> = {
|
||||
id: 'agentmail_forward_message',
|
||||
name: 'Forward Message',
|
||||
description: 'Forward an email message to new recipients in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the message',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the message to forward',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Recipient email addresses (comma-separated)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Override subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional plain text to prepend',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional HTML to prepend',
|
||||
},
|
||||
cc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CC recipient email addresses (comma-separated)',
|
||||
},
|
||||
bcc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'BCC recipient email addresses (comma-separated)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}/forward`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
to: params.to.split(',').map((e) => e.trim()),
|
||||
}
|
||||
if (params.subject) body.subject = params.subject
|
||||
if (params.text) body.text = params.text
|
||||
if (params.html) body.html = params.html
|
||||
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
|
||||
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ForwardMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to forward message',
|
||||
output: { messageId: '', threadId: '' },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messageId: data.message_id ?? '',
|
||||
threadId: data.thread_id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messageId: { type: 'string', description: 'ID of the forwarded message' },
|
||||
threadId: { type: 'string', description: 'ID of the thread' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { GetDraftParams, GetDraftResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailGetDraftTool: ToolConfig<GetDraftParams, GetDraftResult> = {
|
||||
id: 'agentmail_get_draft',
|
||||
name: 'Get Draft',
|
||||
description: 'Get details of a specific email draft in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox the draft belongs to',
|
||||
},
|
||||
draftId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the draft to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<GetDraftResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to get draft',
|
||||
output: {
|
||||
draftId: '',
|
||||
inboxId: '',
|
||||
subject: null,
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
text: null,
|
||||
html: null,
|
||||
preview: null,
|
||||
labels: [],
|
||||
inReplyTo: null,
|
||||
sendStatus: null,
|
||||
sendAt: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
draftId: data.draft_id ?? '',
|
||||
inboxId: data.inbox_id ?? '',
|
||||
subject: data.subject ?? null,
|
||||
to: data.to ?? [],
|
||||
cc: data.cc ?? [],
|
||||
bcc: data.bcc ?? [],
|
||||
text: data.text ?? null,
|
||||
html: data.html ?? null,
|
||||
preview: data.preview ?? null,
|
||||
labels: data.labels ?? [],
|
||||
inReplyTo: data.in_reply_to ?? null,
|
||||
sendStatus: data.send_status ?? null,
|
||||
sendAt: data.send_at ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
draftId: { type: 'string', description: 'Unique identifier for the draft' },
|
||||
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
|
||||
subject: { type: 'string', description: 'Draft subject', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
text: { type: 'string', description: 'Plain text content', optional: true },
|
||||
html: { type: 'string', description: 'HTML content', optional: true },
|
||||
preview: { type: 'string', description: 'Draft preview text', optional: true },
|
||||
labels: { type: 'array', description: 'Labels assigned to the draft' },
|
||||
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
|
||||
sendStatus: {
|
||||
type: 'string',
|
||||
description: 'Send status (scheduled, sending, failed)',
|
||||
optional: true,
|
||||
},
|
||||
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { GetInboxParams, GetInboxResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailGetInboxTool: ToolConfig<GetInboxParams, GetInboxResult> = {
|
||||
id: 'agentmail_get_inbox',
|
||||
name: 'Get Inbox',
|
||||
description: 'Get details of a specific email inbox in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<GetInboxResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to get inbox',
|
||||
output: {
|
||||
inboxId: '',
|
||||
email: '',
|
||||
displayName: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inboxId: data.inbox_id ?? '',
|
||||
email: data.email ?? '',
|
||||
displayName: data.display_name ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
|
||||
email: { type: 'string', description: 'Email address of the inbox' },
|
||||
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { GetMessageParams, GetMessageResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailGetMessageTool: ToolConfig<GetMessageParams, GetMessageResult> = {
|
||||
id: 'agentmail_get_message',
|
||||
name: 'Get Message',
|
||||
description: 'Get details of a specific email message in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the message',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the message to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<GetMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to get message',
|
||||
output: {
|
||||
messageId: '',
|
||||
threadId: '',
|
||||
from: null,
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: null,
|
||||
text: null,
|
||||
html: null,
|
||||
labels: [],
|
||||
timestamp: null,
|
||||
createdAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messageId: data.message_id ?? '',
|
||||
threadId: data.thread_id ?? '',
|
||||
from: data.from ?? null,
|
||||
to: data.to ?? [],
|
||||
cc: data.cc ?? [],
|
||||
bcc: data.bcc ?? [],
|
||||
subject: data.subject ?? null,
|
||||
text: data.text ?? null,
|
||||
html: data.html ?? null,
|
||||
labels: data.labels ?? [],
|
||||
timestamp: data.timestamp ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messageId: { type: 'string', description: 'Unique identifier for the message' },
|
||||
threadId: { type: 'string', description: 'ID of the thread this message belongs to' },
|
||||
from: { type: 'string', description: 'Sender email address', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
subject: { type: 'string', description: 'Message subject', optional: true },
|
||||
text: { type: 'string', description: 'Plain text content', optional: true },
|
||||
html: { type: 'string', description: 'HTML content', optional: true },
|
||||
labels: { type: 'array', description: 'Labels assigned to the message' },
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Time the message was sent or drafted',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { GetThreadParams, GetThreadResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailGetThreadTool: ToolConfig<GetThreadParams, GetThreadResult> = {
|
||||
id: 'agentmail_get_thread',
|
||||
name: 'Get Thread',
|
||||
description: 'Get details of a specific email thread including messages in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the thread',
|
||||
},
|
||||
threadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the thread to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<GetThreadResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to get thread',
|
||||
output: {
|
||||
threadId: '',
|
||||
subject: null,
|
||||
senders: [],
|
||||
recipients: [],
|
||||
messageCount: 0,
|
||||
labels: [],
|
||||
lastMessageAt: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
threadId: data.thread_id ?? '',
|
||||
subject: data.subject ?? null,
|
||||
senders: data.senders ?? [],
|
||||
recipients: data.recipients ?? [],
|
||||
messageCount: data.message_count ?? 0,
|
||||
labels: data.labels ?? [],
|
||||
lastMessageAt: data.timestamp ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
messages: (data.messages ?? []).map((msg: Record<string, unknown>) => ({
|
||||
messageId: msg.message_id ?? '',
|
||||
from: (msg.from as string) ?? null,
|
||||
to: (msg.to as string[]) ?? [],
|
||||
cc: (msg.cc as string[]) ?? [],
|
||||
bcc: (msg.bcc as string[]) ?? [],
|
||||
subject: (msg.subject as string) ?? null,
|
||||
text: (msg.text as string) ?? null,
|
||||
html: (msg.html as string) ?? null,
|
||||
labels: (msg.labels as string[]) ?? [],
|
||||
timestamp: (msg.timestamp as string) ?? null,
|
||||
createdAt: (msg.created_at as string) ?? '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
threadId: { type: 'string', description: 'Unique identifier for the thread' },
|
||||
subject: { type: 'string', description: 'Thread subject', optional: true },
|
||||
senders: { type: 'array', description: 'List of sender email addresses' },
|
||||
recipients: { type: 'array', description: 'List of recipient email addresses' },
|
||||
messageCount: { type: 'number', description: 'Number of messages in the thread' },
|
||||
labels: { type: 'array', description: 'Labels assigned to the thread' },
|
||||
lastMessageAt: { type: 'string', description: 'Timestamp of last message', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Messages in the thread',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageId: { type: 'string', description: 'Unique identifier for the message' },
|
||||
from: { type: 'string', description: 'Sender email address', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
subject: { type: 'string', description: 'Message subject', optional: true },
|
||||
text: { type: 'string', description: 'Plain text content', optional: true },
|
||||
html: { type: 'string', description: 'HTML content', optional: true },
|
||||
labels: { type: 'array', description: 'Labels assigned to the message' },
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Time the message was sent or drafted',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export { agentmailCreateDraftTool } from '@/tools/agentmail/create_draft'
|
||||
export { agentmailCreateInboxTool } from '@/tools/agentmail/create_inbox'
|
||||
export { agentmailDeleteDraftTool } from '@/tools/agentmail/delete_draft'
|
||||
export { agentmailDeleteInboxTool } from '@/tools/agentmail/delete_inbox'
|
||||
export { agentmailDeleteThreadTool } from '@/tools/agentmail/delete_thread'
|
||||
export { agentmailForwardMessageTool } from '@/tools/agentmail/forward_message'
|
||||
export { agentmailGetDraftTool } from '@/tools/agentmail/get_draft'
|
||||
export { agentmailGetInboxTool } from '@/tools/agentmail/get_inbox'
|
||||
export { agentmailGetMessageTool } from '@/tools/agentmail/get_message'
|
||||
export { agentmailGetThreadTool } from '@/tools/agentmail/get_thread'
|
||||
export { agentmailListDraftsTool } from '@/tools/agentmail/list_drafts'
|
||||
export { agentmailListInboxesTool } from '@/tools/agentmail/list_inboxes'
|
||||
export { agentmailListMessagesTool } from '@/tools/agentmail/list_messages'
|
||||
export { agentmailListThreadsTool } from '@/tools/agentmail/list_threads'
|
||||
export { agentmailReplyMessageTool } from '@/tools/agentmail/reply_message'
|
||||
export { agentmailSendDraftTool } from '@/tools/agentmail/send_draft'
|
||||
export { agentmailSendMessageTool } from '@/tools/agentmail/send_message'
|
||||
export { agentmailUpdateDraftTool } from '@/tools/agentmail/update_draft'
|
||||
export { agentmailUpdateInboxTool } from '@/tools/agentmail/update_inbox'
|
||||
export { agentmailUpdateMessageTool } from '@/tools/agentmail/update_message'
|
||||
export { agentmailUpdateThreadTool } from '@/tools/agentmail/update_thread'
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { ListDraftsParams, ListDraftsResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailListDraftsTool: ToolConfig<ListDraftsParams, ListDraftsResult> = {
|
||||
id: 'agentmail_list_drafts',
|
||||
name: 'List Drafts',
|
||||
description: 'List email drafts in an inbox in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to list drafts from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of drafts to return',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination token for next page of results',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.limit) query.set('limit', String(params.limit))
|
||||
if (params.pageToken) query.set('page_token', params.pageToken)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ListDraftsResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to list drafts',
|
||||
output: { drafts: [], count: 0, nextPageToken: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
drafts: (data.drafts ?? []).map((draft: Record<string, unknown>) => ({
|
||||
draftId: draft.draft_id ?? '',
|
||||
inboxId: draft.inbox_id ?? '',
|
||||
subject: (draft.subject as string) ?? null,
|
||||
to: (draft.to as string[]) ?? [],
|
||||
cc: (draft.cc as string[]) ?? [],
|
||||
bcc: (draft.bcc as string[]) ?? [],
|
||||
preview: (draft.preview as string) ?? null,
|
||||
sendStatus: (draft.send_status as string) ?? null,
|
||||
sendAt: (draft.send_at as string) ?? null,
|
||||
createdAt: (draft.created_at as string) ?? '',
|
||||
updatedAt: (draft.updated_at as string) ?? '',
|
||||
})),
|
||||
count: data.count ?? 0,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
drafts: {
|
||||
type: 'array',
|
||||
description: 'List of drafts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
draftId: { type: 'string', description: 'Unique identifier for the draft' },
|
||||
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
|
||||
subject: { type: 'string', description: 'Draft subject', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
preview: { type: 'string', description: 'Draft preview text', optional: true },
|
||||
sendStatus: {
|
||||
type: 'string',
|
||||
description: 'Send status (scheduled, sending, failed)',
|
||||
optional: true,
|
||||
},
|
||||
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Total number of drafts' },
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for retrieving the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { ListInboxesParams, ListInboxesResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailListInboxesTool: ToolConfig<ListInboxesParams, ListInboxesResult> = {
|
||||
id: 'agentmail_list_inboxes',
|
||||
name: 'List Inboxes',
|
||||
description: 'List all email inboxes in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of inboxes to return',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination token for next page of results',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.limit) query.set('limit', String(params.limit))
|
||||
if (params.pageToken) query.set('page_token', params.pageToken)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentmail.to/v0/inboxes${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ListInboxesResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to list inboxes',
|
||||
output: { inboxes: [], count: 0, nextPageToken: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inboxes: (data.inboxes ?? []).map((inbox: Record<string, unknown>) => ({
|
||||
inboxId: inbox.inbox_id ?? '',
|
||||
email: inbox.email ?? '',
|
||||
displayName: inbox.display_name ?? null,
|
||||
createdAt: inbox.created_at ?? '',
|
||||
updatedAt: inbox.updated_at ?? '',
|
||||
})),
|
||||
count: data.count ?? 0,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inboxes: {
|
||||
type: 'array',
|
||||
description: 'List of inboxes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
|
||||
email: { type: 'string', description: 'Email address of the inbox' },
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: 'Display name of the inbox',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Total number of inboxes' },
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for retrieving the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ListMessagesParams, ListMessagesResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailListMessagesTool: ToolConfig<ListMessagesParams, ListMessagesResult> = {
|
||||
id: 'agentmail_list_messages',
|
||||
name: 'List Messages',
|
||||
description: 'List messages in an inbox in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to list messages from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of messages to return',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination token for next page of results',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.limit) query.set('limit', String(params.limit))
|
||||
if (params.pageToken) query.set('page_token', params.pageToken)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ListMessagesResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to list messages',
|
||||
output: { messages: [], count: 0, nextPageToken: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messages: (data.messages ?? []).map((msg: Record<string, unknown>) => ({
|
||||
messageId: msg.message_id ?? '',
|
||||
from: (msg.from as string) ?? null,
|
||||
to: (msg.to as string[]) ?? [],
|
||||
subject: (msg.subject as string) ?? null,
|
||||
preview: (msg.preview as string) ?? null,
|
||||
timestamp: (msg.timestamp as string) ?? null,
|
||||
createdAt: (msg.created_at as string) ?? '',
|
||||
})),
|
||||
count: data.count ?? 0,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'List of messages in the inbox',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageId: { type: 'string', description: 'Unique identifier for the message' },
|
||||
from: { type: 'string', description: 'Sender email address', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
subject: { type: 'string', description: 'Message subject', optional: true },
|
||||
preview: { type: 'string', description: 'Message preview text', optional: true },
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Time the message was sent or drafted',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Total number of messages' },
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for retrieving the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ListThreadsParams, ListThreadsResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailListThreadsTool: ToolConfig<ListThreadsParams, ListThreadsResult> = {
|
||||
id: 'agentmail_list_threads',
|
||||
name: 'List Threads',
|
||||
description: 'List email threads in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to list threads from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of threads to return',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination token for next page of results',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated labels to filter threads by',
|
||||
},
|
||||
before: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter threads before this ISO 8601 timestamp',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter threads after this ISO 8601 timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.limit) query.set('limit', String(params.limit))
|
||||
if (params.pageToken) query.set('page_token', params.pageToken)
|
||||
if (params.labels) {
|
||||
for (const label of params.labels.split(',')) {
|
||||
query.append('labels', label.trim())
|
||||
}
|
||||
}
|
||||
if (params.before) query.set('before', params.before)
|
||||
if (params.after) query.set('after', params.after)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ListThreadsResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to list threads',
|
||||
output: { threads: [], count: 0, nextPageToken: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
threads: (data.threads ?? []).map((thread: Record<string, unknown>) => ({
|
||||
threadId: thread.thread_id ?? '',
|
||||
subject: (thread.subject as string) ?? null,
|
||||
senders: (thread.senders as string[]) ?? [],
|
||||
recipients: (thread.recipients as string[]) ?? [],
|
||||
messageCount: (thread.message_count as number) ?? 0,
|
||||
lastMessageAt: (thread.timestamp as string) ?? null,
|
||||
createdAt: (thread.created_at as string) ?? '',
|
||||
updatedAt: (thread.updated_at as string) ?? '',
|
||||
})),
|
||||
count: data.count ?? 0,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
threads: {
|
||||
type: 'array',
|
||||
description: 'List of email threads',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
threadId: { type: 'string', description: 'Unique identifier for the thread' },
|
||||
subject: { type: 'string', description: 'Thread subject', optional: true },
|
||||
senders: { type: 'array', description: 'List of sender email addresses' },
|
||||
recipients: { type: 'array', description: 'List of recipient email addresses' },
|
||||
messageCount: { type: 'number', description: 'Number of messages in the thread' },
|
||||
lastMessageAt: {
|
||||
type: 'string',
|
||||
description: 'Timestamp of last message',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Total number of threads' },
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for retrieving the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { ReplyMessageParams, ReplyMessageResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailReplyMessageTool: ToolConfig<ReplyMessageParams, ReplyMessageResult> = {
|
||||
id: 'agentmail_reply_message',
|
||||
name: 'Reply to Message',
|
||||
description: 'Reply to an existing email message in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to reply from',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the message to reply to',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plain text reply body',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HTML reply body',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Override recipient email addresses (comma-separated)',
|
||||
},
|
||||
cc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CC email addresses (comma-separated)',
|
||||
},
|
||||
bcc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'BCC email addresses (comma-separated)',
|
||||
},
|
||||
replyAll: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reply to all recipients of the original message',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const endpoint = params.replyAll ? 'reply-all' : 'reply'
|
||||
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}/${endpoint}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.text) body.text = params.text
|
||||
if (params.html) body.html = params.html
|
||||
// /reply-all endpoint auto-determines recipients; only /reply accepts to/cc/bcc
|
||||
if (!params.replyAll) {
|
||||
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
|
||||
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
|
||||
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<ReplyMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to reply to message',
|
||||
output: { messageId: '', threadId: '' },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messageId: data.message_id ?? '',
|
||||
threadId: data.thread_id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messageId: { type: 'string', description: 'ID of the sent reply message' },
|
||||
threadId: { type: 'string', description: 'ID of the thread' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { SendDraftParams, SendDraftResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailSendDraftTool: ToolConfig<SendDraftParams, SendDraftResult> = {
|
||||
id: 'agentmail_send_draft',
|
||||
name: 'Send Draft',
|
||||
description: 'Send an existing email draft in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the draft',
|
||||
},
|
||||
draftId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the draft to send',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}/send`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SendDraftResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to send draft',
|
||||
output: { messageId: '', threadId: '' },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messageId: data.message_id ?? '',
|
||||
threadId: data.thread_id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messageId: { type: 'string', description: 'ID of the sent message' },
|
||||
threadId: { type: 'string', description: 'ID of the thread' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { SendMessageParams, SendMessageResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailSendMessageTool: ToolConfig<SendMessageParams, SendMessageResult> = {
|
||||
id: 'agentmail_send_message',
|
||||
name: 'Send Message',
|
||||
description: 'Send an email message from an AgentMail inbox',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to send from',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Recipient email address (comma-separated for multiple)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plain text email body',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HTML email body',
|
||||
},
|
||||
cc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CC recipient email addresses (comma-separated)',
|
||||
},
|
||||
bcc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'BCC recipient email addresses (comma-separated)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/send`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
to: params.to.split(',').map((e) => e.trim()),
|
||||
subject: params.subject,
|
||||
}
|
||||
if (params.text) body.text = params.text
|
||||
if (params.html) body.html = params.html
|
||||
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
|
||||
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<SendMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to send message',
|
||||
output: {
|
||||
threadId: '',
|
||||
messageId: '',
|
||||
subject: '',
|
||||
to: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
threadId: data.thread_id ?? '',
|
||||
messageId: data.message_id ?? '',
|
||||
subject: params?.subject ?? '',
|
||||
to: params?.to ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
threadId: { type: 'string', description: 'ID of the created thread' },
|
||||
messageId: { type: 'string', description: 'ID of the sent message' },
|
||||
subject: { type: 'string', description: 'Email subject line' },
|
||||
to: { type: 'string', description: 'Recipient email address' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/** Create Inbox */
|
||||
export interface CreateInboxParams {
|
||||
apiKey: string
|
||||
username?: string
|
||||
domain?: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export interface CreateInboxResult extends ToolResponse {
|
||||
output: {
|
||||
inboxId: string
|
||||
email: string
|
||||
displayName: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** List Inboxes */
|
||||
export interface ListInboxesParams {
|
||||
apiKey: string
|
||||
limit?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface ListInboxesResult extends ToolResponse {
|
||||
output: {
|
||||
inboxes: Array<{
|
||||
inboxId: string
|
||||
email: string
|
||||
displayName: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}>
|
||||
count: number
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Inbox */
|
||||
export interface GetInboxParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
}
|
||||
|
||||
export interface GetInboxResult extends ToolResponse {
|
||||
output: {
|
||||
inboxId: string
|
||||
email: string
|
||||
displayName: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Inbox */
|
||||
export interface UpdateInboxParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface UpdateInboxResult extends ToolResponse {
|
||||
output: {
|
||||
inboxId: string
|
||||
email: string
|
||||
displayName: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete Inbox */
|
||||
export interface DeleteInboxParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
}
|
||||
|
||||
export interface DeleteInboxResult extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Send Message (Create Thread) */
|
||||
export interface SendMessageParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
to: string
|
||||
subject: string
|
||||
text?: string
|
||||
html?: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
}
|
||||
|
||||
export interface SendMessageResult extends ToolResponse {
|
||||
output: {
|
||||
threadId: string
|
||||
messageId: string
|
||||
subject: string
|
||||
to: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Reply to Message */
|
||||
export interface ReplyMessageParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
messageId: string
|
||||
text?: string
|
||||
html?: string
|
||||
to?: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
replyAll?: boolean
|
||||
}
|
||||
|
||||
export interface ReplyMessageResult extends ToolResponse {
|
||||
output: {
|
||||
messageId: string
|
||||
threadId: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Forward Message */
|
||||
export interface ForwardMessageParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
messageId: string
|
||||
to: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
}
|
||||
|
||||
export interface ForwardMessageResult extends ToolResponse {
|
||||
output: {
|
||||
messageId: string
|
||||
threadId: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Message Labels */
|
||||
export interface UpdateMessageParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
messageId: string
|
||||
addLabels?: string
|
||||
removeLabels?: string
|
||||
}
|
||||
|
||||
export interface UpdateMessageResult extends ToolResponse {
|
||||
output: {
|
||||
messageId: string
|
||||
labels: string[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Create Draft */
|
||||
export interface CreateDraftParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
to?: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
inReplyTo?: string
|
||||
sendAt?: string
|
||||
}
|
||||
|
||||
export interface CreateDraftResult extends ToolResponse {
|
||||
output: {
|
||||
draftId: string
|
||||
inboxId: string
|
||||
subject: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
text: string | null
|
||||
html: string | null
|
||||
preview: string | null
|
||||
labels: string[]
|
||||
inReplyTo: string | null
|
||||
sendStatus: string | null
|
||||
sendAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Draft */
|
||||
export interface UpdateDraftParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
draftId: string
|
||||
to?: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
sendAt?: string
|
||||
}
|
||||
|
||||
export interface UpdateDraftResult extends ToolResponse {
|
||||
output: {
|
||||
draftId: string
|
||||
inboxId: string
|
||||
subject: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
text: string | null
|
||||
html: string | null
|
||||
preview: string | null
|
||||
labels: string[]
|
||||
inReplyTo: string | null
|
||||
sendStatus: string | null
|
||||
sendAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete Draft */
|
||||
export interface DeleteDraftParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
draftId: string
|
||||
}
|
||||
|
||||
export interface DeleteDraftResult extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Send Draft */
|
||||
export interface SendDraftParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
draftId: string
|
||||
}
|
||||
|
||||
export interface SendDraftResult extends ToolResponse {
|
||||
output: {
|
||||
messageId: string
|
||||
threadId: string
|
||||
}
|
||||
}
|
||||
|
||||
/** List Drafts */
|
||||
export interface ListDraftsParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
limit?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface ListDraftsResult extends ToolResponse {
|
||||
output: {
|
||||
drafts: Array<{
|
||||
draftId: string
|
||||
inboxId: string
|
||||
subject: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
preview: string | null
|
||||
sendStatus: string | null
|
||||
sendAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}>
|
||||
count: number
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Draft */
|
||||
export interface GetDraftParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
draftId: string
|
||||
}
|
||||
|
||||
export interface GetDraftResult extends ToolResponse {
|
||||
output: {
|
||||
draftId: string
|
||||
inboxId: string
|
||||
subject: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
text: string | null
|
||||
html: string | null
|
||||
preview: string | null
|
||||
labels: string[]
|
||||
inReplyTo: string | null
|
||||
sendStatus: string | null
|
||||
sendAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** List Threads */
|
||||
export interface ListThreadsParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
limit?: number
|
||||
pageToken?: string
|
||||
labels?: string
|
||||
before?: string
|
||||
after?: string
|
||||
}
|
||||
|
||||
export interface ListThreadsResult extends ToolResponse {
|
||||
output: {
|
||||
threads: Array<{
|
||||
threadId: string
|
||||
subject: string | null
|
||||
senders: string[]
|
||||
recipients: string[]
|
||||
messageCount: number
|
||||
lastMessageAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}>
|
||||
count: number
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Thread */
|
||||
export interface GetThreadParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
threadId: string
|
||||
}
|
||||
|
||||
export interface GetThreadResult extends ToolResponse {
|
||||
output: {
|
||||
threadId: string
|
||||
subject: string | null
|
||||
senders: string[]
|
||||
recipients: string[]
|
||||
messageCount: number
|
||||
labels: string[]
|
||||
lastMessageAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
messages: Array<{
|
||||
messageId: string
|
||||
from: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
subject: string | null
|
||||
text: string | null
|
||||
html: string | null
|
||||
labels: string[]
|
||||
timestamp: string | null
|
||||
createdAt: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Thread Labels */
|
||||
export interface UpdateThreadParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
threadId: string
|
||||
addLabels?: string
|
||||
removeLabels?: string
|
||||
}
|
||||
|
||||
export interface UpdateThreadResult extends ToolResponse {
|
||||
output: {
|
||||
threadId: string
|
||||
labels: string[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete Thread */
|
||||
export interface DeleteThreadParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
threadId: string
|
||||
permanent?: boolean
|
||||
}
|
||||
|
||||
export interface DeleteThreadResult extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** List Messages */
|
||||
export interface ListMessagesParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
limit?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface ListMessagesResult extends ToolResponse {
|
||||
output: {
|
||||
messages: Array<{
|
||||
messageId: string
|
||||
from: string | null
|
||||
to: string[]
|
||||
subject: string | null
|
||||
preview: string | null
|
||||
timestamp: string | null
|
||||
createdAt: string
|
||||
}>
|
||||
count: number
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Message */
|
||||
export interface GetMessageParams {
|
||||
apiKey: string
|
||||
inboxId: string
|
||||
messageId: string
|
||||
}
|
||||
|
||||
export interface GetMessageResult extends ToolResponse {
|
||||
output: {
|
||||
messageId: string
|
||||
threadId: string
|
||||
from: string | null
|
||||
to: string[]
|
||||
cc: string[]
|
||||
bcc: string[]
|
||||
subject: string | null
|
||||
text: string | null
|
||||
html: string | null
|
||||
labels: string[]
|
||||
timestamp: string | null
|
||||
createdAt: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { UpdateDraftParams, UpdateDraftResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailUpdateDraftTool: ToolConfig<UpdateDraftParams, UpdateDraftResult> = {
|
||||
id: 'agentmail_update_draft',
|
||||
name: 'Update Draft',
|
||||
description: 'Update an existing email draft in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the draft',
|
||||
},
|
||||
draftId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the draft to update',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Recipient email addresses (comma-separated)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Draft subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plain text draft body',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HTML draft body',
|
||||
},
|
||||
cc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CC recipient email addresses (comma-separated)',
|
||||
},
|
||||
bcc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'BCC recipient email addresses (comma-separated)',
|
||||
},
|
||||
sendAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 8601 timestamp to schedule sending',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
|
||||
if (params.subject) body.subject = params.subject
|
||||
if (params.text) body.text = params.text
|
||||
if (params.html) body.html = params.html
|
||||
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
|
||||
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
|
||||
if (params.sendAt) body.send_at = params.sendAt
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<UpdateDraftResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to update draft',
|
||||
output: {
|
||||
draftId: '',
|
||||
inboxId: '',
|
||||
subject: null,
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
text: null,
|
||||
html: null,
|
||||
preview: null,
|
||||
labels: [],
|
||||
inReplyTo: null,
|
||||
sendStatus: null,
|
||||
sendAt: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
draftId: data.draft_id ?? '',
|
||||
inboxId: data.inbox_id ?? '',
|
||||
subject: data.subject ?? null,
|
||||
to: data.to ?? [],
|
||||
cc: data.cc ?? [],
|
||||
bcc: data.bcc ?? [],
|
||||
text: data.text ?? null,
|
||||
html: data.html ?? null,
|
||||
preview: data.preview ?? null,
|
||||
labels: data.labels ?? [],
|
||||
inReplyTo: data.in_reply_to ?? null,
|
||||
sendStatus: data.send_status ?? null,
|
||||
sendAt: data.send_at ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
draftId: { type: 'string', description: 'Unique identifier for the draft' },
|
||||
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
|
||||
subject: { type: 'string', description: 'Draft subject', optional: true },
|
||||
to: { type: 'array', description: 'Recipient email addresses' },
|
||||
cc: { type: 'array', description: 'CC email addresses' },
|
||||
bcc: { type: 'array', description: 'BCC email addresses' },
|
||||
text: { type: 'string', description: 'Plain text content', optional: true },
|
||||
html: { type: 'string', description: 'HTML content', optional: true },
|
||||
preview: { type: 'string', description: 'Draft preview text', optional: true },
|
||||
labels: { type: 'array', description: 'Labels assigned to the draft' },
|
||||
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
|
||||
sendStatus: {
|
||||
type: 'string',
|
||||
description: 'Send status (scheduled, sending, failed)',
|
||||
optional: true,
|
||||
},
|
||||
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { UpdateInboxParams, UpdateInboxResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailUpdateInboxTool: ToolConfig<UpdateInboxParams, UpdateInboxResult> = {
|
||||
id: 'agentmail_update_inbox',
|
||||
name: 'Update Inbox',
|
||||
description: 'Update the display name of an email inbox in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox to update',
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New display name for the inbox',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
display_name: params.displayName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<UpdateInboxResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to update inbox',
|
||||
output: {
|
||||
inboxId: '',
|
||||
email: '',
|
||||
displayName: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inboxId: data.inbox_id ?? '',
|
||||
email: data.email ?? '',
|
||||
displayName: data.display_name ?? null,
|
||||
createdAt: data.created_at ?? '',
|
||||
updatedAt: data.updated_at ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
|
||||
email: { type: 'string', description: 'Email address of the inbox' },
|
||||
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { UpdateMessageParams, UpdateMessageResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailUpdateMessageTool: ToolConfig<UpdateMessageParams, UpdateMessageResult> = {
|
||||
id: 'agentmail_update_message',
|
||||
name: 'Update Message',
|
||||
description: 'Add or remove labels on an email message in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the message',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the message to update',
|
||||
},
|
||||
addLabels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated labels to add to the message',
|
||||
},
|
||||
removeLabels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated labels to remove from the message',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.addLabels) {
|
||||
body.add_labels = params.addLabels.split(',').map((l) => l.trim())
|
||||
}
|
||||
if (params.removeLabels) {
|
||||
body.remove_labels = params.removeLabels.split(',').map((l) => l.trim())
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<UpdateMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to update message',
|
||||
output: { messageId: '', labels: [] },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messageId: data.message_id ?? '',
|
||||
labels: data.labels ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messageId: { type: 'string', description: 'Unique identifier for the message' },
|
||||
labels: { type: 'array', description: 'Current labels on the message' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { UpdateThreadParams, UpdateThreadResult } from '@/tools/agentmail/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentmailUpdateThreadTool: ToolConfig<UpdateThreadParams, UpdateThreadResult> = {
|
||||
id: 'agentmail_update_thread',
|
||||
name: 'Update Thread Labels',
|
||||
description: 'Add or remove labels on an email thread in AgentMail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentMail API key',
|
||||
},
|
||||
inboxId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the inbox containing the thread',
|
||||
},
|
||||
threadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the thread to update',
|
||||
},
|
||||
addLabels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated labels to add to the thread',
|
||||
},
|
||||
removeLabels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated labels to remove from the thread',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
...(params.addLabels && {
|
||||
add_labels: params.addLabels.split(',').map((l) => l.trim()),
|
||||
}),
|
||||
...(params.removeLabels && {
|
||||
remove_labels: params.removeLabels.split(',').map((l) => l.trim()),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<UpdateThreadResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message ?? 'Failed to update thread',
|
||||
output: { threadId: '', labels: [] },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
threadId: data.thread_id ?? '',
|
||||
labels: data.labels ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
threadId: { type: 'string', description: 'Unique identifier for the thread' },
|
||||
labels: { type: 'array', description: 'Current labels on the thread' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import type {
|
||||
AgentPhoneCreateCallParams,
|
||||
AgentPhoneCreateCallResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneCreateCallTool: ToolConfig<
|
||||
AgentPhoneCreateCallParams,
|
||||
AgentPhoneCreateCallResult
|
||||
> = {
|
||||
id: 'agentphone_create_call',
|
||||
name: 'Create Outbound Call',
|
||||
description: 'Initiate an outbound voice call from an AgentPhone agent',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
agentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Agent that will handle the call',
|
||||
},
|
||||
toNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number to call in E.164 format (e.g. +14155551234)',
|
||||
},
|
||||
fromNumberId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent's first assigned number is used.",
|
||||
},
|
||||
initialGreeting: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional greeting spoken when the recipient answers',
|
||||
},
|
||||
voice: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Voice ID override for this call (defaults to the agent's configured voice)",
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentphone.to/v1/calls',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
agentId: params.agentId,
|
||||
toNumber: params.toNumber,
|
||||
}
|
||||
if (params.fromNumberId) body.fromNumberId = params.fromNumberId
|
||||
if (params.initialGreeting) body.initialGreeting = params.initialGreeting
|
||||
if (params.voice) body.voice = params.voice
|
||||
if (params.systemPrompt) body.systemPrompt = params.systemPrompt
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneCreateCallResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create call',
|
||||
output: {
|
||||
id: '',
|
||||
agentId: null,
|
||||
status: null,
|
||||
toNumber: null,
|
||||
fromNumber: null,
|
||||
phoneNumberId: null,
|
||||
direction: null,
|
||||
startedAt: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? data.callId ?? '',
|
||||
agentId: data.agentId ?? null,
|
||||
status: data.status ?? null,
|
||||
toNumber: data.toNumber ?? null,
|
||||
fromNumber: data.fromNumber ?? null,
|
||||
phoneNumberId: data.phoneNumberId ?? null,
|
||||
direction: data.direction ?? null,
|
||||
startedAt: data.startedAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique call identifier' },
|
||||
agentId: { type: 'string', description: 'Agent handling the call', optional: true },
|
||||
status: { type: 'string', description: 'Initial call status', optional: true },
|
||||
toNumber: { type: 'string', description: 'Destination phone number', optional: true },
|
||||
fromNumber: { type: 'string', description: 'Caller ID used for the call', optional: true },
|
||||
phoneNumberId: {
|
||||
type: 'string',
|
||||
description: 'ID of the phone number used as caller ID',
|
||||
optional: true,
|
||||
},
|
||||
direction: { type: 'string', description: 'Call direction (outbound)', optional: true },
|
||||
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
AgentPhoneCreateContactParams,
|
||||
AgentPhoneCreateContactResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneCreateContactTool: ToolConfig<
|
||||
AgentPhoneCreateContactParams,
|
||||
AgentPhoneCreateContactResult
|
||||
> = {
|
||||
id: 'agentphone_create_contact',
|
||||
name: 'Create Contact',
|
||||
description: 'Create a new contact in AgentPhone',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number in E.164 format (e.g. +14155551234)',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Contact's full name",
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Contact's email address",
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Freeform notes stored on the contact',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentphone.to/v1/contacts',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
phoneNumber: params.phoneNumber,
|
||||
name: params.name,
|
||||
}
|
||||
if (params.email) body.email = params.email
|
||||
if (params.notes) body.notes = params.notes
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneCreateContactResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create contact',
|
||||
output: {
|
||||
id: '',
|
||||
phoneNumber: '',
|
||||
name: '',
|
||||
email: null,
|
||||
notes: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
name: data.name ?? '',
|
||||
email: data.email ?? null,
|
||||
notes: data.notes ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Contact ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
|
||||
name: { type: 'string', description: 'Contact name' },
|
||||
email: { type: 'string', description: 'Contact email address', optional: true },
|
||||
notes: { type: 'string', description: 'Freeform notes', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
AgentPhoneCreateNumberParams,
|
||||
AgentPhoneCreateNumberResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneCreateNumberTool: ToolConfig<
|
||||
AgentPhoneCreateNumberParams,
|
||||
AgentPhoneCreateNumberResult
|
||||
> = {
|
||||
id: 'agentphone_create_number',
|
||||
name: 'Create Phone Number',
|
||||
description: 'Provision a new SMS- and voice-enabled phone number',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Two-letter country code (e.g. US, CA). Defaults to US.',
|
||||
},
|
||||
areaCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Preferred area code (US/CA only, e.g. "415"). Best-effort — may be ignored if unavailable.',
|
||||
},
|
||||
agentId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optionally attach the number to an agent immediately',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentphone.to/v1/numbers',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.country) body.country = params.country
|
||||
if (params.areaCode) body.areaCode = params.areaCode
|
||||
if (params.agentId) body.agentId = params.agentId
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneCreateNumberResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create phone number',
|
||||
output: {
|
||||
id: '',
|
||||
phoneNumber: '',
|
||||
country: '',
|
||||
status: '',
|
||||
type: '',
|
||||
agentId: null,
|
||||
createdAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
country: data.country ?? '',
|
||||
status: data.status ?? '',
|
||||
type: data.type ?? '',
|
||||
agentId: data.agentId ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique phone number ID' },
|
||||
phoneNumber: { type: 'string', description: 'Provisioned phone number in E.164 format' },
|
||||
country: { type: 'string', description: 'Two-letter country code' },
|
||||
status: { type: 'string', description: 'Number status (e.g. active)' },
|
||||
type: { type: 'string', description: 'Number type (e.g. sms)', optional: true },
|
||||
agentId: {
|
||||
type: 'string',
|
||||
description: 'Agent the number is attached to',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 timestamp when the number was created' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type {
|
||||
AgentPhoneDeleteContactParams,
|
||||
AgentPhoneDeleteContactResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneDeleteContactTool: ToolConfig<
|
||||
AgentPhoneDeleteContactParams,
|
||||
AgentPhoneDeleteContactResult
|
||||
> = {
|
||||
id: 'agentphone_delete_contact',
|
||||
name: 'Delete Contact',
|
||||
description: 'Delete a contact by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Contact ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<AgentPhoneDeleteContactResult> => {
|
||||
const contactId = params?.contactId?.trim() ?? ''
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to delete contact'
|
||||
try {
|
||||
const data = await response.json()
|
||||
errorMessage = data?.detail?.[0]?.msg ?? data?.message ?? errorMessage
|
||||
} catch {
|
||||
// Response body may be empty; ignore parse failures.
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: { id: contactId, deleted: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { id: contactId, deleted: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'ID of the deleted contact' },
|
||||
deleted: { type: 'boolean', description: 'Whether the contact was deleted successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type {
|
||||
AgentPhoneGetCallParams,
|
||||
AgentPhoneGetCallResult,
|
||||
AgentPhoneTranscriptTurn,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetCallTool: ToolConfig<AgentPhoneGetCallParams, AgentPhoneGetCallResult> = {
|
||||
id: 'agentphone_get_call',
|
||||
name: 'Get Call',
|
||||
description: 'Fetch a call and its full transcript',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
callId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the call to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/calls/${params.callId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetCallResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch call',
|
||||
output: {
|
||||
id: '',
|
||||
agentId: null,
|
||||
phoneNumberId: null,
|
||||
phoneNumber: null,
|
||||
fromNumber: '',
|
||||
toNumber: '',
|
||||
direction: '',
|
||||
status: '',
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
durationSeconds: null,
|
||||
lastTranscriptSnippet: null,
|
||||
recordingUrl: null,
|
||||
recordingAvailable: null,
|
||||
transcripts: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const transcripts: AgentPhoneTranscriptTurn[] = (data.transcripts ?? []).map(
|
||||
(turn: Record<string, unknown>) => ({
|
||||
id: (turn.id as string) ?? '',
|
||||
transcript: (turn.transcript as string) ?? '',
|
||||
confidence: (turn.confidence as number | null) ?? null,
|
||||
response: (turn.response as string | null) ?? null,
|
||||
createdAt: (turn.createdAt as string) ?? '',
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
agentId: data.agentId ?? null,
|
||||
phoneNumberId: data.phoneNumberId ?? null,
|
||||
phoneNumber: data.phoneNumber ?? null,
|
||||
fromNumber: data.fromNumber ?? '',
|
||||
toNumber: data.toNumber ?? '',
|
||||
direction: data.direction ?? '',
|
||||
status: data.status ?? '',
|
||||
startedAt: data.startedAt ?? null,
|
||||
endedAt: data.endedAt ?? null,
|
||||
durationSeconds: data.durationSeconds ?? null,
|
||||
lastTranscriptSnippet: data.lastTranscriptSnippet ?? null,
|
||||
recordingUrl: data.recordingUrl ?? null,
|
||||
recordingAvailable: data.recordingAvailable ?? null,
|
||||
transcripts,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Call ID' },
|
||||
agentId: { type: 'string', description: 'Agent that handled the call', optional: true },
|
||||
phoneNumberId: { type: 'string', description: 'Phone number ID', optional: true },
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
description: 'Phone number used for the call',
|
||||
optional: true,
|
||||
},
|
||||
fromNumber: { type: 'string', description: 'Caller phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
direction: { type: 'string', description: 'inbound or outbound', optional: true },
|
||||
status: { type: 'string', description: 'Call status' },
|
||||
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
|
||||
endedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
|
||||
durationSeconds: { type: 'number', description: 'Call duration in seconds', optional: true },
|
||||
lastTranscriptSnippet: {
|
||||
type: 'string',
|
||||
description: 'Last transcript snippet',
|
||||
optional: true,
|
||||
},
|
||||
recordingUrl: { type: 'string', description: 'Recording audio URL', optional: true },
|
||||
recordingAvailable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether a recording is available',
|
||||
optional: true,
|
||||
},
|
||||
transcripts: {
|
||||
type: 'array',
|
||||
description: 'Ordered transcript turns for the call',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Transcript turn ID' },
|
||||
transcript: { type: 'string', description: 'User utterance' },
|
||||
confidence: {
|
||||
type: 'number',
|
||||
description: 'Speech recognition confidence',
|
||||
optional: true,
|
||||
},
|
||||
response: {
|
||||
type: 'string',
|
||||
description: 'Agent response (when available)',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type {
|
||||
AgentPhoneGetCallTranscriptParams,
|
||||
AgentPhoneGetCallTranscriptResult,
|
||||
AgentPhoneTranscriptEntry,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetCallTranscriptTool: ToolConfig<
|
||||
AgentPhoneGetCallTranscriptParams,
|
||||
AgentPhoneGetCallTranscriptResult
|
||||
> = {
|
||||
id: 'agentphone_get_call_transcript',
|
||||
name: 'Get Call Transcript',
|
||||
description: 'Get the full ordered transcript for a call',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
callId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the call to retrieve the transcript for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/calls/${params.callId.trim()}/transcript`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<AgentPhoneGetCallTranscriptResult> => {
|
||||
const data = await response.json()
|
||||
const callId = params?.callId?.trim() ?? ''
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch transcript',
|
||||
output: { callId, transcript: [] },
|
||||
}
|
||||
}
|
||||
|
||||
const rawTurns = Array.isArray(data?.transcript)
|
||||
? data.transcript
|
||||
: Array.isArray(data)
|
||||
? data
|
||||
: []
|
||||
|
||||
const transcript: AgentPhoneTranscriptEntry[] = rawTurns.map(
|
||||
(turn: Record<string, unknown>) => ({
|
||||
role: (turn.role as string) ?? '',
|
||||
content: (turn.content as string) ?? '',
|
||||
createdAt: (turn.createdAt as string) ?? (turn.created_at as string) ?? null,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { callId: data?.callId ?? callId, transcript },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
callId: { type: 'string', description: 'Call ID' },
|
||||
transcript: {
|
||||
type: 'array',
|
||||
description: 'Ordered transcript turns for the call',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
role: {
|
||||
type: 'string',
|
||||
description: 'Speaker role (user or agent)',
|
||||
},
|
||||
content: { type: 'string', description: 'Turn content' },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type {
|
||||
AgentPhoneGetContactParams,
|
||||
AgentPhoneGetContactResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetContactTool: ToolConfig<
|
||||
AgentPhoneGetContactParams,
|
||||
AgentPhoneGetContactResult
|
||||
> = {
|
||||
id: 'agentphone_get_contact',
|
||||
name: 'Get Contact',
|
||||
description: 'Fetch a single contact by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Contact ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetContactResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch contact',
|
||||
output: {
|
||||
id: '',
|
||||
phoneNumber: '',
|
||||
name: '',
|
||||
email: null,
|
||||
notes: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
name: data.name ?? '',
|
||||
email: data.email ?? null,
|
||||
notes: data.notes ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Contact ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
|
||||
name: { type: 'string', description: 'Contact name' },
|
||||
email: { type: 'string', description: 'Contact email address', optional: true },
|
||||
notes: { type: 'string', description: 'Freeform notes', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import type {
|
||||
AgentPhoneConversationMessage,
|
||||
AgentPhoneGetConversationParams,
|
||||
AgentPhoneGetConversationResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetConversationTool: ToolConfig<
|
||||
AgentPhoneGetConversationParams,
|
||||
AgentPhoneGetConversationResult
|
||||
> = {
|
||||
id: 'agentphone_get_conversation',
|
||||
name: 'Get Conversation',
|
||||
description: 'Get a conversation along with its recent messages',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
conversationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Conversation ID',
|
||||
},
|
||||
messageLimit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of recent messages to include (default 50, max 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.messageLimit === 'number') {
|
||||
query.set('message_limit', String(params.messageLimit))
|
||||
}
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetConversationResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch conversation',
|
||||
output: {
|
||||
id: '',
|
||||
agentId: null,
|
||||
phoneNumberId: '',
|
||||
phoneNumber: '',
|
||||
participant: '',
|
||||
lastMessageAt: '',
|
||||
messageCount: 0,
|
||||
metadata: null,
|
||||
createdAt: '',
|
||||
messages: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const messages: AgentPhoneConversationMessage[] = (data.messages ?? []).map(
|
||||
(msg: Record<string, unknown>) => ({
|
||||
id: (msg.id as string) ?? '',
|
||||
body: (msg.body as string) ?? '',
|
||||
fromNumber: (msg.fromNumber as string) ?? '',
|
||||
toNumber: (msg.toNumber as string) ?? '',
|
||||
direction: (msg.direction as string) ?? '',
|
||||
channel: (msg.channel as string | null) ?? null,
|
||||
mediaUrl: (msg.mediaUrl as string | null) ?? null,
|
||||
mediaUrls: Array.isArray(msg.mediaUrls) ? (msg.mediaUrls as string[]) : [],
|
||||
receivedAt: (msg.receivedAt as string) ?? '',
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
agentId: data.agentId ?? null,
|
||||
phoneNumberId: data.phoneNumberId ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
participant: data.participant ?? '',
|
||||
lastMessageAt: data.lastMessageAt ?? '',
|
||||
messageCount: data.messageCount ?? 0,
|
||||
metadata: data.metadata ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
messages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Conversation ID' },
|
||||
agentId: { type: 'string', description: 'Agent ID', optional: true },
|
||||
phoneNumberId: { type: 'string', description: 'Phone number ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number' },
|
||||
participant: { type: 'string', description: 'External participant phone number' },
|
||||
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
messageCount: { type: 'number', description: 'Number of messages in the conversation' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Custom metadata stored on the conversation',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Recent messages in the conversation',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Message ID' },
|
||||
body: { type: 'string', description: 'Message text' },
|
||||
fromNumber: { type: 'string', description: 'Sender phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
direction: { type: 'string', description: 'inbound or outbound' },
|
||||
channel: { type: 'string', description: 'sms, mms, or imessage', optional: true },
|
||||
mediaUrl: { type: 'string', description: 'Attached media URL', optional: true },
|
||||
mediaUrls: {
|
||||
type: 'array',
|
||||
description: 'All attached media URLs',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type {
|
||||
AgentPhoneConversationMessage,
|
||||
AgentPhoneGetConversationMessagesParams,
|
||||
AgentPhoneGetConversationMessagesResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetConversationMessagesTool: ToolConfig<
|
||||
AgentPhoneGetConversationMessagesParams,
|
||||
AgentPhoneGetConversationMessagesResult
|
||||
> = {
|
||||
id: 'agentphone_get_conversation_messages',
|
||||
name: 'Get Conversation Messages',
|
||||
description: 'Get paginated messages for a conversation',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
conversationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Conversation ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of messages to return (default 50, max 200)',
|
||||
},
|
||||
before: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return messages received before this ISO 8601 timestamp',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return messages received after this ISO 8601 timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (params.before) query.set('before', params.before)
|
||||
if (params.after) query.set('after', params.after)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}/messages${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetConversationMessagesResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch messages',
|
||||
output: { data: [], hasMore: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(msg: Record<string, unknown>): AgentPhoneConversationMessage => ({
|
||||
id: (msg.id as string) ?? '',
|
||||
body: (msg.body as string) ?? '',
|
||||
fromNumber: (msg.fromNumber as string) ?? '',
|
||||
toNumber: (msg.toNumber as string) ?? '',
|
||||
direction: (msg.direction as string) ?? '',
|
||||
channel: (msg.channel as string | null) ?? null,
|
||||
mediaUrl: (msg.mediaUrl as string | null) ?? null,
|
||||
mediaUrls: Array.isArray(msg.mediaUrls) ? (msg.mediaUrls as string[]) : [],
|
||||
receivedAt: (msg.receivedAt as string) ?? '',
|
||||
})
|
||||
),
|
||||
hasMore: data.hasMore ?? false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Messages in the conversation',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Message ID' },
|
||||
body: { type: 'string', description: 'Message text' },
|
||||
fromNumber: { type: 'string', description: 'Sender phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
direction: { type: 'string', description: 'inbound or outbound' },
|
||||
channel: { type: 'string', description: 'sms, mms, or imessage', optional: true },
|
||||
mediaUrl: { type: 'string', description: 'Attached media URL', optional: true },
|
||||
mediaUrls: {
|
||||
type: 'array',
|
||||
description: 'All attached media URLs',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more messages are available' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type {
|
||||
AgentPhoneGetNumberMessagesParams,
|
||||
AgentPhoneGetNumberMessagesResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetNumberMessagesTool: ToolConfig<
|
||||
AgentPhoneGetNumberMessagesParams,
|
||||
AgentPhoneGetNumberMessagesResult
|
||||
> = {
|
||||
id: 'agentphone_get_number_messages',
|
||||
name: 'Get Phone Number Messages',
|
||||
description: 'Fetch messages received on a specific phone number',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
numberId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the phone number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of messages to return (default 50, max 200)',
|
||||
},
|
||||
before: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return messages received before this ISO 8601 timestamp',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return messages received after this ISO 8601 timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (params.before) query.set('before', params.before)
|
||||
if (params.after) query.set('after', params.after)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/numbers/${params.numberId.trim()}/messages${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetNumberMessagesResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch messages',
|
||||
output: { data: [], hasMore: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map((msg: Record<string, unknown>) => ({
|
||||
id: (msg.id as string) ?? '',
|
||||
from_: (msg.from_ as string) ?? '',
|
||||
to: (msg.to as string) ?? '',
|
||||
body: (msg.body as string) ?? '',
|
||||
direction: (msg.direction as string) ?? '',
|
||||
channel: (msg.channel as string | null) ?? null,
|
||||
receivedAt: (msg.receivedAt as string) ?? '',
|
||||
})),
|
||||
hasMore: data.hasMore ?? false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Messages received on the number',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Message ID' },
|
||||
from_: { type: 'string', description: 'Sender phone number (E.164)' },
|
||||
to: { type: 'string', description: 'Recipient phone number (E.164)' },
|
||||
body: { type: 'string', description: 'Message text' },
|
||||
direction: { type: 'string', description: 'inbound or outbound' },
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel (sms, mms, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more messages are available' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { AgentPhoneGetUsageParams, AgentPhoneGetUsageResult } from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetUsageTool: ToolConfig<
|
||||
AgentPhoneGetUsageParams,
|
||||
AgentPhoneGetUsageResult
|
||||
> = {
|
||||
id: 'agentphone_get_usage',
|
||||
name: 'Get Usage',
|
||||
description: 'Retrieve current usage statistics for the AgentPhone account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentphone.to/v1/usage',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetUsageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch usage',
|
||||
output: {
|
||||
plan: {
|
||||
name: '',
|
||||
limits: {
|
||||
numbers: null,
|
||||
messagesPerMonth: null,
|
||||
voiceMinutesPerMonth: null,
|
||||
maxCallDurationMinutes: null,
|
||||
concurrentCalls: null,
|
||||
},
|
||||
},
|
||||
numbers: { used: null, limit: null, remaining: null },
|
||||
stats: {
|
||||
totalMessages: null,
|
||||
messagesLast24h: null,
|
||||
messagesLast7d: null,
|
||||
messagesLast30d: null,
|
||||
totalCalls: null,
|
||||
callsLast24h: null,
|
||||
callsLast7d: null,
|
||||
callsLast30d: null,
|
||||
totalWebhookDeliveries: null,
|
||||
successfulWebhookDeliveries: null,
|
||||
failedWebhookDeliveries: null,
|
||||
},
|
||||
periodStart: '',
|
||||
periodEnd: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const planLimits = data?.plan?.limits ?? {}
|
||||
const numbers = data?.numbers ?? {}
|
||||
const stats = data?.stats ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
plan: {
|
||||
name: data?.plan?.name ?? '',
|
||||
limits: {
|
||||
numbers: planLimits.numbers ?? null,
|
||||
messagesPerMonth: planLimits.messagesPerMonth ?? null,
|
||||
voiceMinutesPerMonth: planLimits.voiceMinutesPerMonth ?? null,
|
||||
maxCallDurationMinutes: planLimits.maxCallDurationMinutes ?? null,
|
||||
concurrentCalls: planLimits.concurrentCalls ?? null,
|
||||
},
|
||||
},
|
||||
numbers: {
|
||||
used: numbers.used ?? null,
|
||||
limit: numbers.limit ?? null,
|
||||
remaining: numbers.remaining ?? null,
|
||||
},
|
||||
stats: {
|
||||
totalMessages: stats.totalMessages ?? null,
|
||||
messagesLast24h: stats.messagesLast24h ?? null,
|
||||
messagesLast7d: stats.messagesLast7d ?? null,
|
||||
messagesLast30d: stats.messagesLast30d ?? null,
|
||||
totalCalls: stats.totalCalls ?? null,
|
||||
callsLast24h: stats.callsLast24h ?? null,
|
||||
callsLast7d: stats.callsLast7d ?? null,
|
||||
callsLast30d: stats.callsLast30d ?? null,
|
||||
totalWebhookDeliveries: stats.totalWebhookDeliveries ?? null,
|
||||
successfulWebhookDeliveries: stats.successfulWebhookDeliveries ?? null,
|
||||
failedWebhookDeliveries: stats.failedWebhookDeliveries ?? null,
|
||||
},
|
||||
periodStart: data.periodStart ?? '',
|
||||
periodEnd: data.periodEnd ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
plan: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Plan name and limits (name, limits: numbers/messagesPerMonth/voiceMinutesPerMonth/maxCallDurationMinutes/concurrentCalls)',
|
||||
},
|
||||
numbers: {
|
||||
type: 'json',
|
||||
description: 'Phone number usage (used, limit, remaining)',
|
||||
},
|
||||
stats: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Usage stats: totalMessages, messagesLast24h/7d/30d, totalCalls, callsLast24h/7d/30d, totalWebhookDeliveries, successfulWebhookDeliveries, failedWebhookDeliveries',
|
||||
},
|
||||
periodStart: { type: 'string', description: 'Billing period start' },
|
||||
periodEnd: { type: 'string', description: 'Billing period end' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
AgentPhoneGetUsageDailyParams,
|
||||
AgentPhoneGetUsageDailyResult,
|
||||
AgentPhoneUsageDailyEntry,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetUsageDailyTool: ToolConfig<
|
||||
AgentPhoneGetUsageDailyParams,
|
||||
AgentPhoneGetUsageDailyResult
|
||||
> = {
|
||||
id: 'agentphone_get_usage_daily',
|
||||
name: 'Get Daily Usage',
|
||||
description: 'Get a daily breakdown of usage (messages, calls, webhooks) for the last N days',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of days to return (1-365, default 30)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.days === 'number') query.set('days', String(params.days))
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/usage/daily${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetUsageDailyResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch daily usage',
|
||||
output: { data: [], days: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(entry: Record<string, unknown>): AgentPhoneUsageDailyEntry => ({
|
||||
date: (entry.date as string) ?? '',
|
||||
messages: (entry.messages as number) ?? 0,
|
||||
calls: (entry.calls as number) ?? 0,
|
||||
webhooks: (entry.webhooks as number) ?? 0,
|
||||
})
|
||||
),
|
||||
days: data.days ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Daily usage entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Day (YYYY-MM-DD)' },
|
||||
messages: { type: 'number', description: 'Messages that day' },
|
||||
calls: { type: 'number', description: 'Calls that day' },
|
||||
webhooks: { type: 'number', description: 'Webhook deliveries that day' },
|
||||
},
|
||||
},
|
||||
},
|
||||
days: { type: 'number', description: 'Number of days returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
AgentPhoneGetUsageMonthlyParams,
|
||||
AgentPhoneGetUsageMonthlyResult,
|
||||
AgentPhoneUsageMonthlyEntry,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneGetUsageMonthlyTool: ToolConfig<
|
||||
AgentPhoneGetUsageMonthlyParams,
|
||||
AgentPhoneGetUsageMonthlyResult
|
||||
> = {
|
||||
id: 'agentphone_get_usage_monthly',
|
||||
name: 'Get Monthly Usage',
|
||||
description: 'Get monthly usage aggregation (messages, calls, webhooks) for the last N months',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
months: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of months to return (1-24, default 6)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.months === 'number') query.set('months', String(params.months))
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/usage/monthly${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneGetUsageMonthlyResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch monthly usage',
|
||||
output: { data: [], months: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(entry: Record<string, unknown>): AgentPhoneUsageMonthlyEntry => ({
|
||||
month: (entry.month as string) ?? '',
|
||||
messages: (entry.messages as number) ?? 0,
|
||||
calls: (entry.calls as number) ?? 0,
|
||||
webhooks: (entry.webhooks as number) ?? 0,
|
||||
})
|
||||
),
|
||||
months: data.months ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Monthly usage entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
month: { type: 'string', description: 'Month (YYYY-MM)' },
|
||||
messages: { type: 'number', description: 'Messages that month' },
|
||||
calls: { type: 'number', description: 'Calls that month' },
|
||||
webhooks: { type: 'number', description: 'Webhook deliveries that month' },
|
||||
},
|
||||
},
|
||||
},
|
||||
months: { type: 'number', description: 'Number of months returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export { agentphoneCreateCallTool } from '@/tools/agentphone/create_call'
|
||||
export { agentphoneCreateContactTool } from '@/tools/agentphone/create_contact'
|
||||
export { agentphoneCreateNumberTool } from '@/tools/agentphone/create_number'
|
||||
export { agentphoneDeleteContactTool } from '@/tools/agentphone/delete_contact'
|
||||
export { agentphoneGetCallTool } from '@/tools/agentphone/get_call'
|
||||
export { agentphoneGetCallTranscriptTool } from '@/tools/agentphone/get_call_transcript'
|
||||
export { agentphoneGetContactTool } from '@/tools/agentphone/get_contact'
|
||||
export { agentphoneGetConversationTool } from '@/tools/agentphone/get_conversation'
|
||||
export { agentphoneGetConversationMessagesTool } from '@/tools/agentphone/get_conversation_messages'
|
||||
export { agentphoneGetNumberMessagesTool } from '@/tools/agentphone/get_number_messages'
|
||||
export { agentphoneGetUsageTool } from '@/tools/agentphone/get_usage'
|
||||
export { agentphoneGetUsageDailyTool } from '@/tools/agentphone/get_usage_daily'
|
||||
export { agentphoneGetUsageMonthlyTool } from '@/tools/agentphone/get_usage_monthly'
|
||||
export { agentphoneListCallsTool } from '@/tools/agentphone/list_calls'
|
||||
export { agentphoneListContactsTool } from '@/tools/agentphone/list_contacts'
|
||||
export { agentphoneListConversationsTool } from '@/tools/agentphone/list_conversations'
|
||||
export { agentphoneListNumbersTool } from '@/tools/agentphone/list_numbers'
|
||||
export { agentphoneReactToMessageTool } from '@/tools/agentphone/react_to_message'
|
||||
export { agentphoneReleaseNumberTool } from '@/tools/agentphone/release_number'
|
||||
export { agentphoneSendMessageTool } from '@/tools/agentphone/send_message'
|
||||
export * from '@/tools/agentphone/types'
|
||||
export { agentphoneUpdateContactTool } from '@/tools/agentphone/update_contact'
|
||||
export { agentphoneUpdateConversationTool } from '@/tools/agentphone/update_conversation'
|
||||
@@ -0,0 +1,169 @@
|
||||
import type {
|
||||
AgentPhoneCallSummary,
|
||||
AgentPhoneListCallsParams,
|
||||
AgentPhoneListCallsResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneListCallsTool: ToolConfig<
|
||||
AgentPhoneListCallsParams,
|
||||
AgentPhoneListCallsResult
|
||||
> = {
|
||||
id: 'agentphone_list_calls',
|
||||
name: 'List Calls',
|
||||
description: 'List voice calls for this AgentPhone account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default 20, max 100)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip (min 0)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by status (completed, in-progress, failed)',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by direction (inbound, outbound)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by call type (pstn, web)',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search by phone number (matches fromNumber or toNumber)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
|
||||
if (params.status) query.set('status', params.status)
|
||||
if (params.direction) query.set('direction', params.direction)
|
||||
if (params.type) query.set('type', params.type)
|
||||
if (params.search) query.set('search', params.search)
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/calls${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneListCallsResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list calls',
|
||||
output: { data: [], hasMore: false, total: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(call: Record<string, unknown>): AgentPhoneCallSummary => ({
|
||||
id: (call.id as string) ?? '',
|
||||
agentId: (call.agentId as string | null) ?? null,
|
||||
phoneNumberId: (call.phoneNumberId as string | null) ?? null,
|
||||
phoneNumber: (call.phoneNumber as string | null) ?? null,
|
||||
fromNumber: (call.fromNumber as string) ?? '',
|
||||
toNumber: (call.toNumber as string) ?? '',
|
||||
direction: (call.direction as string) ?? '',
|
||||
status: (call.status as string) ?? '',
|
||||
startedAt: (call.startedAt as string | null) ?? null,
|
||||
endedAt: (call.endedAt as string | null) ?? null,
|
||||
durationSeconds: (call.durationSeconds as number | null) ?? null,
|
||||
lastTranscriptSnippet: (call.lastTranscriptSnippet as string | null) ?? null,
|
||||
recordingUrl: (call.recordingUrl as string | null) ?? null,
|
||||
recordingAvailable: (call.recordingAvailable as boolean | null) ?? null,
|
||||
})
|
||||
),
|
||||
hasMore: data.hasMore ?? false,
|
||||
total: data.total ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Calls',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Call ID' },
|
||||
agentId: {
|
||||
type: 'string',
|
||||
description: 'Agent that handled the call',
|
||||
optional: true,
|
||||
},
|
||||
phoneNumberId: {
|
||||
type: 'string',
|
||||
description: 'Phone number ID used for the call',
|
||||
optional: true,
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
description: 'Phone number used for the call',
|
||||
optional: true,
|
||||
},
|
||||
fromNumber: { type: 'string', description: 'Caller phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
direction: { type: 'string', description: 'inbound or outbound', optional: true },
|
||||
status: { type: 'string', description: 'Call status' },
|
||||
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
|
||||
endedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
|
||||
durationSeconds: {
|
||||
type: 'number',
|
||||
description: 'Call duration in seconds',
|
||||
optional: true,
|
||||
},
|
||||
lastTranscriptSnippet: {
|
||||
type: 'string',
|
||||
description: 'Last transcript snippet',
|
||||
optional: true,
|
||||
},
|
||||
recordingUrl: { type: 'string', description: 'Recording audio URL', optional: true },
|
||||
recordingAvailable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether a recording is available',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more results are available' },
|
||||
total: { type: 'number', description: 'Total number of matching calls' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
AgentPhoneContact,
|
||||
AgentPhoneListContactsParams,
|
||||
AgentPhoneListContactsResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneListContactsTool: ToolConfig<
|
||||
AgentPhoneListContactsParams,
|
||||
AgentPhoneListContactsResult
|
||||
> = {
|
||||
id: 'agentphone_list_contacts',
|
||||
name: 'List Contacts',
|
||||
description: 'List contacts for this AgentPhone account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by name or phone number (case-insensitive contains)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default 50, max 200)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip (min 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.search) query.set('search', params.search)
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/contacts${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneListContactsResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list contacts',
|
||||
output: { data: [], hasMore: false, total: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(c: Record<string, unknown>): AgentPhoneContact => ({
|
||||
id: (c.id as string) ?? '',
|
||||
phoneNumber: (c.phoneNumber as string) ?? '',
|
||||
name: (c.name as string) ?? '',
|
||||
email: (c.email as string | null) ?? null,
|
||||
notes: (c.notes as string | null) ?? null,
|
||||
createdAt: (c.createdAt as string) ?? '',
|
||||
updatedAt: (c.updatedAt as string) ?? '',
|
||||
})
|
||||
),
|
||||
hasMore: data.hasMore ?? false,
|
||||
total: data.total ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Contacts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Contact ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
|
||||
name: { type: 'string', description: 'Contact name' },
|
||||
email: { type: 'string', description: 'Contact email address', optional: true },
|
||||
notes: { type: 'string', description: 'Freeform notes', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more results are available' },
|
||||
total: { type: 'number', description: 'Total number of contacts' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
AgentPhoneConversationSummary,
|
||||
AgentPhoneListConversationsParams,
|
||||
AgentPhoneListConversationsResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneListConversationsTool: ToolConfig<
|
||||
AgentPhoneListConversationsParams,
|
||||
AgentPhoneListConversationsResult
|
||||
> = {
|
||||
id: 'agentphone_list_conversations',
|
||||
name: 'List Conversations',
|
||||
description: 'List conversations (message threads) for this AgentPhone account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default 20, max 100)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip (min 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/conversations${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneListConversationsResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list conversations',
|
||||
output: { data: [], hasMore: false, total: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map(
|
||||
(conv: Record<string, unknown>): AgentPhoneConversationSummary => ({
|
||||
id: (conv.id as string) ?? '',
|
||||
agentId: (conv.agentId as string | null) ?? null,
|
||||
phoneNumberId: (conv.phoneNumberId as string) ?? '',
|
||||
phoneNumber: (conv.phoneNumber as string) ?? '',
|
||||
participant: (conv.participant as string) ?? '',
|
||||
lastMessageAt: (conv.lastMessageAt as string) ?? '',
|
||||
lastMessagePreview: (conv.lastMessagePreview as string) ?? '',
|
||||
messageCount: (conv.messageCount as number) ?? 0,
|
||||
metadata: (conv.metadata as Record<string, unknown> | null) ?? null,
|
||||
createdAt: (conv.createdAt as string) ?? '',
|
||||
})
|
||||
),
|
||||
hasMore: data.hasMore ?? false,
|
||||
total: data.total ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Conversations',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Conversation ID' },
|
||||
agentId: { type: 'string', description: 'Agent ID', optional: true },
|
||||
phoneNumberId: { type: 'string', description: 'Phone number ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number' },
|
||||
participant: { type: 'string', description: 'External participant phone number' },
|
||||
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
lastMessagePreview: { type: 'string', description: 'Last message preview' },
|
||||
messageCount: { type: 'number', description: 'Number of messages in the conversation' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Custom metadata stored on the conversation',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more results are available' },
|
||||
total: { type: 'number', description: 'Total number of conversations' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type {
|
||||
AgentPhoneListNumbersParams,
|
||||
AgentPhoneListNumbersResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneListNumbersTool: ToolConfig<
|
||||
AgentPhoneListNumbersParams,
|
||||
AgentPhoneListNumbersResult
|
||||
> = {
|
||||
id: 'agentphone_list_numbers',
|
||||
name: 'List Phone Numbers',
|
||||
description: 'List all phone numbers provisioned for this AgentPhone account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default 20, max 100)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip (min 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams()
|
||||
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
|
||||
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
|
||||
const qs = query.toString()
|
||||
return `https://api.agentphone.to/v1/numbers${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneListNumbersResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list phone numbers',
|
||||
output: { data: [], hasMore: false, total: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: (data.data ?? []).map((num: Record<string, unknown>) => ({
|
||||
id: (num.id as string) ?? '',
|
||||
phoneNumber: (num.phoneNumber as string) ?? '',
|
||||
country: (num.country as string) ?? '',
|
||||
status: (num.status as string) ?? '',
|
||||
type: (num.type as string) ?? '',
|
||||
agentId: (num.agentId as string | null) ?? null,
|
||||
createdAt: (num.createdAt as string) ?? '',
|
||||
})),
|
||||
hasMore: data.hasMore ?? false,
|
||||
total: data.total ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Phone numbers',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Phone number ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
|
||||
country: { type: 'string', description: 'Two-letter country code' },
|
||||
status: { type: 'string', description: 'Number status' },
|
||||
type: { type: 'string', description: 'Number type (e.g. sms)', optional: true },
|
||||
agentId: { type: 'string', description: 'Attached agent ID', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more results are available' },
|
||||
total: { type: 'number', description: 'Total number of phone numbers' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
AgentPhoneReactToMessageParams,
|
||||
AgentPhoneReactToMessageResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneReactToMessageTool: ToolConfig<
|
||||
AgentPhoneReactToMessageParams,
|
||||
AgentPhoneReactToMessageResult
|
||||
> = {
|
||||
id: 'agentphone_react_to_message',
|
||||
name: 'React to Message',
|
||||
description: 'Send an iMessage tapback reaction to a message (iMessage only)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the message to react to',
|
||||
},
|
||||
reaction: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reaction type: love, like, dislike, laugh, emphasize, or question',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/messages/${params.messageId.trim()}/reactions`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ reaction: params.reaction }),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<AgentPhoneReactToMessageResult> => {
|
||||
const data = await response.json()
|
||||
const messageId = params?.messageId?.trim() ?? ''
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to send reaction',
|
||||
output: { id: '', reactionType: '', messageId, channel: '' },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
reactionType: data.reaction_type ?? '',
|
||||
messageId: data.message_id ?? messageId,
|
||||
channel: data.channel ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Reaction ID' },
|
||||
reactionType: { type: 'string', description: 'Reaction type applied' },
|
||||
messageId: { type: 'string', description: 'ID of the message that was reacted to' },
|
||||
channel: { type: 'string', description: 'Channel (imessage)' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type {
|
||||
AgentPhoneReleaseNumberParams,
|
||||
AgentPhoneReleaseNumberResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneReleaseNumberTool: ToolConfig<
|
||||
AgentPhoneReleaseNumberParams,
|
||||
AgentPhoneReleaseNumberResult
|
||||
> = {
|
||||
id: 'agentphone_release_number',
|
||||
name: 'Release Phone Number',
|
||||
description: 'Release (delete) a phone number. This action is irreversible.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
numberId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the phone number to release',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/numbers/${params.numberId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<AgentPhoneReleaseNumberResult> => {
|
||||
const numberId = params?.numberId?.trim() ?? ''
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to release phone number'
|
||||
try {
|
||||
const data = await response.json()
|
||||
errorMessage = data?.detail?.[0]?.msg ?? data?.message ?? errorMessage
|
||||
} catch {
|
||||
// Response body may be empty on DELETE errors; ignore parse failures.
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: { id: numberId, released: false },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { id: numberId, released: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'ID of the released phone number' },
|
||||
released: { type: 'boolean', description: 'Whether the number was released successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type {
|
||||
AgentPhoneSendMessageParams,
|
||||
AgentPhoneSendMessageResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneSendMessageTool: ToolConfig<
|
||||
AgentPhoneSendMessageParams,
|
||||
AgentPhoneSendMessageResult
|
||||
> = {
|
||||
id: 'agentphone_send_message',
|
||||
name: 'Send Message',
|
||||
description: 'Send an outbound SMS or iMessage from an AgentPhone agent',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
agentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Agent sending the message',
|
||||
},
|
||||
toNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Recipient phone number in E.164 format (e.g. +14155551234)',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Message text to send',
|
||||
},
|
||||
mediaUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional URL of an image, video, or file to attach',
|
||||
},
|
||||
numberId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
"Phone number ID to send from. If omitted, the agent's first assigned number is used.",
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.agentphone.to/v1/messages',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
agent_id: params.agentId,
|
||||
to_number: params.toNumber,
|
||||
body: params.body,
|
||||
}
|
||||
if (params.mediaUrl) body.media_url = params.mediaUrl
|
||||
if (params.numberId) body.number_id = params.numberId
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneSendMessageResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to send message',
|
||||
output: { id: '', status: '', channel: '', fromNumber: '', toNumber: '' },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
status: data.status ?? '',
|
||||
channel: data.channel ?? '',
|
||||
fromNumber: data.from_number ?? '',
|
||||
toNumber: data.to_number ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Message ID' },
|
||||
status: { type: 'string', description: 'Delivery status' },
|
||||
channel: { type: 'string', description: 'sms, mms, or imessage' },
|
||||
fromNumber: { type: 'string', description: 'Sender phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface AgentPhoneNumber {
|
||||
id: string
|
||||
phoneNumber: string
|
||||
country: string
|
||||
status: string
|
||||
type: string
|
||||
agentId: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface AgentPhoneNumberMessage {
|
||||
id: string
|
||||
from_: string
|
||||
to: string
|
||||
body: string
|
||||
direction: string
|
||||
channel: string | null
|
||||
receivedAt: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneConversationSummary {
|
||||
id: string
|
||||
agentId: string | null
|
||||
phoneNumberId: string
|
||||
phoneNumber: string
|
||||
participant: string
|
||||
lastMessageAt: string
|
||||
lastMessagePreview: string
|
||||
messageCount: number
|
||||
metadata: Record<string, unknown> | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneConversationMessage {
|
||||
id: string
|
||||
body: string
|
||||
fromNumber: string
|
||||
toNumber: string
|
||||
direction: string
|
||||
channel: string | null
|
||||
mediaUrl: string | null
|
||||
mediaUrls: string[]
|
||||
receivedAt: string
|
||||
}
|
||||
|
||||
interface AgentPhoneConversationDetail {
|
||||
id: string
|
||||
agentId: string | null
|
||||
phoneNumberId: string
|
||||
phoneNumber: string
|
||||
participant: string
|
||||
lastMessageAt: string
|
||||
messageCount: number
|
||||
metadata: Record<string, unknown> | null
|
||||
createdAt: string
|
||||
messages: AgentPhoneConversationMessage[]
|
||||
}
|
||||
|
||||
export interface AgentPhoneCallSummary {
|
||||
id: string
|
||||
agentId: string | null
|
||||
phoneNumberId: string | null
|
||||
phoneNumber: string | null
|
||||
fromNumber: string
|
||||
toNumber: string
|
||||
direction: string
|
||||
status: string
|
||||
startedAt: string | null
|
||||
endedAt: string | null
|
||||
durationSeconds: number | null
|
||||
lastTranscriptSnippet: string | null
|
||||
recordingUrl: string | null
|
||||
recordingAvailable: boolean | null
|
||||
}
|
||||
|
||||
export interface AgentPhoneTranscriptTurn {
|
||||
id: string
|
||||
transcript: string
|
||||
confidence: number | null
|
||||
response: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneTranscriptEntry {
|
||||
role: string
|
||||
content: string
|
||||
createdAt: string | null
|
||||
}
|
||||
|
||||
interface AgentPhoneCallDetail extends AgentPhoneCallSummary {
|
||||
transcripts: AgentPhoneTranscriptTurn[]
|
||||
}
|
||||
|
||||
export interface AgentPhoneContact {
|
||||
id: string
|
||||
phoneNumber: string
|
||||
name: string
|
||||
email: string | null
|
||||
notes: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateNumberParams {
|
||||
apiKey: string
|
||||
country?: string
|
||||
areaCode?: string
|
||||
agentId?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateNumberResult extends ToolResponse {
|
||||
output: AgentPhoneNumber
|
||||
}
|
||||
|
||||
export interface AgentPhoneListNumbersParams {
|
||||
apiKey: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneListNumbersResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneNumber[]
|
||||
hasMore: boolean
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneReleaseNumberParams {
|
||||
apiKey: string
|
||||
numberId: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneReleaseNumberResult extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
released: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetNumberMessagesParams {
|
||||
apiKey: string
|
||||
numberId: string
|
||||
limit?: number
|
||||
before?: string
|
||||
after?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetNumberMessagesResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneNumberMessage[]
|
||||
hasMore: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateCallParams {
|
||||
apiKey: string
|
||||
agentId: string
|
||||
toNumber: string
|
||||
fromNumberId?: string
|
||||
initialGreeting?: string
|
||||
voice?: string
|
||||
systemPrompt?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateCallResult extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
agentId: string | null
|
||||
status: string | null
|
||||
toNumber: string | null
|
||||
fromNumber: string | null
|
||||
phoneNumberId: string | null
|
||||
direction: string | null
|
||||
startedAt: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneListCallsParams {
|
||||
apiKey: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
status?: string
|
||||
direction?: string
|
||||
type?: string
|
||||
search?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneListCallsResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneCallSummary[]
|
||||
hasMore: boolean
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetCallParams {
|
||||
apiKey: string
|
||||
callId: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetCallResult extends ToolResponse {
|
||||
output: AgentPhoneCallDetail
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetCallTranscriptParams {
|
||||
apiKey: string
|
||||
callId: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetCallTranscriptResult extends ToolResponse {
|
||||
output: {
|
||||
callId: string
|
||||
transcript: AgentPhoneTranscriptEntry[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneListConversationsParams {
|
||||
apiKey: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneListConversationsResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneConversationSummary[]
|
||||
hasMore: boolean
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetConversationParams {
|
||||
apiKey: string
|
||||
conversationId: string
|
||||
messageLimit?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetConversationResult extends ToolResponse {
|
||||
output: AgentPhoneConversationDetail
|
||||
}
|
||||
|
||||
export interface AgentPhoneUpdateConversationParams {
|
||||
apiKey: string
|
||||
conversationId: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface AgentPhoneUpdateConversationResult extends ToolResponse {
|
||||
output: AgentPhoneConversationDetail
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetConversationMessagesParams {
|
||||
apiKey: string
|
||||
conversationId: string
|
||||
limit?: number
|
||||
before?: string
|
||||
after?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetConversationMessagesResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneConversationMessage[]
|
||||
hasMore: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneSendMessageParams {
|
||||
apiKey: string
|
||||
agentId: string
|
||||
toNumber: string
|
||||
body: string
|
||||
mediaUrl?: string
|
||||
numberId?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneSendMessageResult extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
status: string
|
||||
channel: string
|
||||
fromNumber: string
|
||||
toNumber: string
|
||||
}
|
||||
}
|
||||
|
||||
export type AgentPhoneReactionType =
|
||||
| 'love'
|
||||
| 'like'
|
||||
| 'dislike'
|
||||
| 'laugh'
|
||||
| 'emphasize'
|
||||
| 'question'
|
||||
|
||||
export interface AgentPhoneReactToMessageParams {
|
||||
apiKey: string
|
||||
messageId: string
|
||||
reaction: AgentPhoneReactionType
|
||||
}
|
||||
|
||||
export interface AgentPhoneReactToMessageResult extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
reactionType: string
|
||||
messageId: string
|
||||
channel: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateContactParams {
|
||||
apiKey: string
|
||||
phoneNumber: string
|
||||
name: string
|
||||
email?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneCreateContactResult extends ToolResponse {
|
||||
output: AgentPhoneContact
|
||||
}
|
||||
|
||||
export interface AgentPhoneListContactsParams {
|
||||
apiKey: string
|
||||
search?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneListContactsResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneContact[]
|
||||
hasMore: boolean
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetContactParams {
|
||||
apiKey: string
|
||||
contactId: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetContactResult extends ToolResponse {
|
||||
output: AgentPhoneContact
|
||||
}
|
||||
|
||||
export interface AgentPhoneUpdateContactParams {
|
||||
apiKey: string
|
||||
contactId: string
|
||||
phoneNumber?: string
|
||||
name?: string
|
||||
email?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneUpdateContactResult extends ToolResponse {
|
||||
output: AgentPhoneContact
|
||||
}
|
||||
|
||||
export interface AgentPhoneDeleteContactParams {
|
||||
apiKey: string
|
||||
contactId: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneDeleteContactResult extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface AgentPhoneUsagePlan {
|
||||
name: string
|
||||
limits: {
|
||||
numbers: number | null
|
||||
messagesPerMonth: number | null
|
||||
voiceMinutesPerMonth: number | null
|
||||
maxCallDurationMinutes: number | null
|
||||
concurrentCalls: number | null
|
||||
}
|
||||
}
|
||||
|
||||
interface AgentPhoneUsageStats {
|
||||
totalMessages: number | null
|
||||
messagesLast24h: number | null
|
||||
messagesLast7d: number | null
|
||||
messagesLast30d: number | null
|
||||
totalCalls: number | null
|
||||
callsLast24h: number | null
|
||||
callsLast7d: number | null
|
||||
callsLast30d: number | null
|
||||
totalWebhookDeliveries: number | null
|
||||
successfulWebhookDeliveries: number | null
|
||||
failedWebhookDeliveries: number | null
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageResult extends ToolResponse {
|
||||
output: {
|
||||
plan: AgentPhoneUsagePlan
|
||||
numbers: {
|
||||
used: number | null
|
||||
limit: number | null
|
||||
remaining: number | null
|
||||
}
|
||||
stats: AgentPhoneUsageStats
|
||||
periodStart: string
|
||||
periodEnd: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneUsageDailyEntry {
|
||||
date: string
|
||||
messages: number
|
||||
calls: number
|
||||
webhooks: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageDailyParams {
|
||||
apiKey: string
|
||||
days?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageDailyResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneUsageDailyEntry[]
|
||||
days: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentPhoneUsageMonthlyEntry {
|
||||
month: string
|
||||
messages: number
|
||||
calls: number
|
||||
webhooks: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageMonthlyParams {
|
||||
apiKey: string
|
||||
months?: number
|
||||
}
|
||||
|
||||
export interface AgentPhoneGetUsageMonthlyResult extends ToolResponse {
|
||||
output: {
|
||||
data: AgentPhoneUsageMonthlyEntry[]
|
||||
months: number
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
AgentPhoneUpdateContactParams,
|
||||
AgentPhoneUpdateContactResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneUpdateContactTool: ToolConfig<
|
||||
AgentPhoneUpdateContactParams,
|
||||
AgentPhoneUpdateContactResult
|
||||
> = {
|
||||
id: 'agentphone_update_contact',
|
||||
name: 'Update Contact',
|
||||
description: "Update a contact's fields",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Contact ID',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New phone number in E.164 format',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New contact name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New email address',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New freeform notes',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
|
||||
if (params.name) body.name = params.name
|
||||
if (params.email) body.email = params.email
|
||||
if (params.notes) body.notes = params.notes
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneUpdateContactResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to update contact',
|
||||
output: {
|
||||
id: '',
|
||||
phoneNumber: '',
|
||||
name: '',
|
||||
email: null,
|
||||
notes: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
name: data.name ?? '',
|
||||
email: data.email ?? null,
|
||||
notes: data.notes ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Contact ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
|
||||
name: { type: 'string', description: 'Contact name' },
|
||||
email: { type: 'string', description: 'Contact email address', optional: true },
|
||||
notes: { type: 'string', description: 'Freeform notes', optional: true },
|
||||
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import type {
|
||||
AgentPhoneConversationMessage,
|
||||
AgentPhoneUpdateConversationParams,
|
||||
AgentPhoneUpdateConversationResult,
|
||||
} from '@/tools/agentphone/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agentphoneUpdateConversationTool: ToolConfig<
|
||||
AgentPhoneUpdateConversationParams,
|
||||
AgentPhoneUpdateConversationResult
|
||||
> = {
|
||||
id: 'agentphone_update_conversation',
|
||||
name: 'Update Conversation',
|
||||
description: 'Update conversation metadata (stored state). Pass null to clear existing metadata.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AgentPhone API key',
|
||||
},
|
||||
conversationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Conversation ID',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Custom key-value metadata to store on the conversation. Pass null to clear existing metadata.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.metadata !== undefined) body.metadata = params.metadata
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<AgentPhoneUpdateConversationResult> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to update conversation',
|
||||
output: {
|
||||
id: '',
|
||||
agentId: null,
|
||||
phoneNumberId: '',
|
||||
phoneNumber: '',
|
||||
participant: '',
|
||||
lastMessageAt: '',
|
||||
messageCount: 0,
|
||||
metadata: null,
|
||||
createdAt: '',
|
||||
messages: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const rawMessages = Array.isArray(data?.messages) ? data.messages : []
|
||||
const messages: AgentPhoneConversationMessage[] = rawMessages.map(
|
||||
(message: Record<string, unknown>) => ({
|
||||
id: (message.id as string) ?? '',
|
||||
body: (message.body as string) ?? '',
|
||||
fromNumber: (message.fromNumber as string) ?? '',
|
||||
toNumber: (message.toNumber as string) ?? '',
|
||||
direction: (message.direction as string) ?? '',
|
||||
channel: (message.channel as string | null) ?? null,
|
||||
mediaUrl: (message.mediaUrl as string | null) ?? null,
|
||||
mediaUrls: Array.isArray(message.mediaUrls) ? (message.mediaUrls as string[]) : [],
|
||||
receivedAt: (message.receivedAt as string) ?? '',
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
agentId: data.agentId ?? null,
|
||||
phoneNumberId: data.phoneNumberId ?? '',
|
||||
phoneNumber: data.phoneNumber ?? '',
|
||||
participant: data.participant ?? '',
|
||||
lastMessageAt: data.lastMessageAt ?? '',
|
||||
messageCount: data.messageCount ?? 0,
|
||||
metadata: data.metadata ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
messages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Conversation ID' },
|
||||
agentId: { type: 'string', description: 'Agent ID', optional: true },
|
||||
phoneNumberId: { type: 'string', description: 'Phone number ID' },
|
||||
phoneNumber: { type: 'string', description: 'Phone number' },
|
||||
participant: { type: 'string', description: 'External participant phone number' },
|
||||
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
messageCount: { type: 'number', description: 'Number of messages' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Custom metadata stored on the conversation',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Messages in the conversation',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Message ID' },
|
||||
body: { type: 'string', description: 'Message body' },
|
||||
fromNumber: { type: 'string', description: 'Sender phone number' },
|
||||
toNumber: { type: 'string', description: 'Recipient phone number' },
|
||||
direction: { type: 'string', description: 'inbound or outbound' },
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel (sms, mms, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
mediaUrl: {
|
||||
type: 'string',
|
||||
description: 'Media URL if any',
|
||||
optional: true,
|
||||
},
|
||||
mediaUrls: {
|
||||
type: 'array',
|
||||
description: 'All attached media URLs',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { AgiloftAttachFileParams, AgiloftAttachFileResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftAttachFileTool: ToolConfig<AgiloftAttachFileParams, AgiloftAttachFileResponse> =
|
||||
{
|
||||
id: 'agiloft_attach_file',
|
||||
name: 'Agiloft Attach File',
|
||||
description: 'Attach a file to a field in an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to attach the file to',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File to attach',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name to assign to the file (defaults to original file name)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/agiloft/attach',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
file: params.file,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
recordId: '',
|
||||
fieldName: '',
|
||||
fileName: '',
|
||||
totalAttachments: 0,
|
||||
},
|
||||
error: data.error || 'Failed to attach file',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: data.output?.recordId ?? '',
|
||||
fieldName: data.output?.fieldName ?? '',
|
||||
fileName: data.output?.fileName ?? '',
|
||||
totalAttachments: data.output?.totalAttachments ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record the file was attached to',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
description: 'Name of the field the file was attached to',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
description: 'Name of the attached file',
|
||||
},
|
||||
totalAttachments: {
|
||||
type: 'number',
|
||||
description: 'Total number of files attached in the field after the operation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftAttachmentInfoResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftAttachmentInfoTool: ToolConfig<
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftAttachmentInfoResponse
|
||||
> = {
|
||||
id: 'agiloft_attachment_info',
|
||||
name: 'Agiloft Attachment Info',
|
||||
description: 'Get information about file attachments on a record field.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to check attachments on',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field to inspect',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/attachment_info',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
attachments: {
|
||||
type: 'array',
|
||||
description: 'List of attachments with position, name, and size',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Position index of the attachment in the field',
|
||||
},
|
||||
name: { type: 'string', description: 'File name of the attachment' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of attachments in the field',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { AgiloftCreateRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftCreateRecordTool: ToolConfig<AgiloftCreateRecordParams, AgiloftRecordResponse> =
|
||||
{
|
||||
id: 'agiloft_create_record',
|
||||
name: 'Agiloft Create Record',
|
||||
description: 'Create a new record in an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Record field values as a JSON object (e.g., {"first_name": "John", "status": "Active"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/create_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
data: params.data,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the created record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Field values of the created record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { AgiloftDeleteRecordParams, AgiloftDeleteResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftDeleteRecordTool: ToolConfig<AgiloftDeleteRecordParams, AgiloftDeleteResponse> =
|
||||
{
|
||||
id: 'agiloft_delete_record',
|
||||
name: 'Agiloft Delete Record',
|
||||
description: 'Delete a record from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/delete_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the deleted record',
|
||||
},
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the record was successfully deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftGetChoiceLineIdResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftGetChoiceLineIdTool: ToolConfig<
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftGetChoiceLineIdResponse
|
||||
> = {
|
||||
id: 'agiloft_get_choice_line_id',
|
||||
name: 'Agiloft Get Choice Line ID',
|
||||
description:
|
||||
'Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "case", "contracts")',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Choice field name (e.g., "priority", "status")',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Choice display value to resolve (e.g., "High", "Active")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/get_choice_line_id',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
fieldName: params.fieldName,
|
||||
value: params.value,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
choiceLineId: {
|
||||
type: 'number',
|
||||
description: 'Internal numeric line ID of the choice value',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file'
|
||||
export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info'
|
||||
export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record'
|
||||
export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record'
|
||||
export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id'
|
||||
export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record'
|
||||
export { agiloftReadRecordTool } from '@/tools/agiloft/read_record'
|
||||
export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment'
|
||||
export { agiloftRetrieveAttachmentTool } from '@/tools/agiloft/retrieve_attachment'
|
||||
export { agiloftSavedSearchTool } from '@/tools/agiloft/saved_search'
|
||||
export { agiloftSearchRecordsTool } from '@/tools/agiloft/search_records'
|
||||
export { agiloftSelectRecordsTool } from '@/tools/agiloft/select_records'
|
||||
export * from '@/tools/agiloft/types'
|
||||
export { agiloftUpdateRecordTool } from '@/tools/agiloft/update_record'
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { AgiloftLockRecordParams, AgiloftLockResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftLockRecordTool: ToolConfig<AgiloftLockRecordParams, AgiloftLockResponse> = {
|
||||
id: 'agiloft_lock_record',
|
||||
name: 'Agiloft Lock Record',
|
||||
description: 'Lock, unlock, or check the lock status of an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to lock, unlock, or check',
|
||||
},
|
||||
lockAction: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Action to perform: "lock", "unlock", or "check"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/lock_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
lockAction: params.lockAction,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Record ID',
|
||||
},
|
||||
lockStatus: {
|
||||
type: 'string',
|
||||
description: 'Lock status (e.g., "LOCKED", "UNLOCKED")',
|
||||
},
|
||||
lockedBy: {
|
||||
type: 'string',
|
||||
description: 'Username of the user who locked the record',
|
||||
optional: true,
|
||||
},
|
||||
lockExpiresInMinutes: {
|
||||
type: 'number',
|
||||
description: 'Minutes until the lock expires',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { AgiloftReadRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftReadRecordTool: ToolConfig<AgiloftReadRecordParams, AgiloftRecordResponse> = {
|
||||
id: 'agiloft_read_record',
|
||||
name: 'Agiloft Read Record',
|
||||
description: 'Read a record by ID from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to read',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field names to include in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/read_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fields: params.fields,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Field values of the record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRemoveAttachmentResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftRemoveAttachmentTool: ToolConfig<
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRemoveAttachmentResponse
|
||||
> = {
|
||||
id: 'agiloft_remove_attachment',
|
||||
name: 'Agiloft Remove Attachment',
|
||||
description: 'Remove an attached file from a field in an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record containing the attachment',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
position: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position index of the file to remove (starting from 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/remove_attachment',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
position: params.position,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
remainingAttachments: {
|
||||
type: 'number',
|
||||
description: 'Number of attachments remaining in the field after removal',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type {
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftRetrieveAttachmentTool: ToolConfig<
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentResponse
|
||||
> = {
|
||||
id: 'agiloft_retrieve_attachment',
|
||||
name: 'Agiloft Retrieve Attachment',
|
||||
description: 'Download an attached file from an Agiloft record field.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record containing the attachment',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
position: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position index of the file in the field (starting from 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/agiloft/retrieve',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
position: params.position,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
file: { name: '', mimeType: '', data: '', size: 0 },
|
||||
},
|
||||
error: data.error || 'Failed to retrieve attachment',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: data.output.file.name,
|
||||
mimeType: data.output.file.mimeType,
|
||||
data: data.output.file.data,
|
||||
size: data.output.file.size,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Downloaded attachment file',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { AgiloftSavedSearchParams, AgiloftSavedSearchResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSavedSearchTool: ToolConfig<
|
||||
AgiloftSavedSearchParams,
|
||||
AgiloftSavedSearchResponse
|
||||
> = {
|
||||
id: 'agiloft_saved_search',
|
||||
name: 'Agiloft Saved Search',
|
||||
description: 'List saved searches defined for an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name to list saved searches for (e.g., "contracts")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/saved_search',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searches: {
|
||||
type: 'array',
|
||||
description: 'List of saved searches for the table',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Saved search name' },
|
||||
label: { type: 'string', description: 'Saved search display label' },
|
||||
id: { type: 'number', description: 'Saved search database identifier' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Saved search description',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { AgiloftSearchRecordsParams, AgiloftSearchResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSearchRecordsTool: ToolConfig<
|
||||
AgiloftSearchRecordsParams,
|
||||
AgiloftSearchResponse
|
||||
> = {
|
||||
id: 'agiloft_search_records',
|
||||
name: 'Agiloft Search Records',
|
||||
description: 'Search for records in an Agiloft table using a query.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name to search in (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query using Agiloft query syntax (e.g., "status=\'Active\'" or "company_name~=\'Acme\'")',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field names to include in the results',
|
||||
},
|
||||
page: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for paginated results (starting from 0)',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of records to return per page',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/search_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
query: params.query,
|
||||
fields: params.fields,
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'json',
|
||||
description: 'Array of matching records with their field values',
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Current page number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Records per page',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { AgiloftSelectRecordsParams, AgiloftSelectResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSelectRecordsTool: ToolConfig<
|
||||
AgiloftSelectRecordsParams,
|
||||
AgiloftSelectResponse
|
||||
> = {
|
||||
id: 'agiloft_select_records',
|
||||
name: 'Agiloft Select Records',
|
||||
description: 'Select record IDs matching a SQL WHERE clause from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
where: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SQL WHERE clause using database column names (e.g., "summary like \'%new%\'" or "assigned_person=\'John Doe\'")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/select_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
where: params.where,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordIds: {
|
||||
type: 'array',
|
||||
description: 'Array of record IDs matching the query',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Base parameters shared by all Agiloft tools.
|
||||
* Agiloft authenticates via instance URL, KB name, and user credentials.
|
||||
*/
|
||||
export interface AgiloftBaseParams {
|
||||
instanceUrl: string
|
||||
knowledgeBase: string
|
||||
login: string
|
||||
password: string
|
||||
table: string
|
||||
}
|
||||
|
||||
export interface AgiloftCreateRecordParams extends AgiloftBaseParams {
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface AgiloftReadRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface AgiloftUpdateRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface AgiloftDeleteRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface AgiloftSearchRecordsParams extends AgiloftBaseParams {
|
||||
query: string
|
||||
fields?: string
|
||||
page?: string
|
||||
limit?: string
|
||||
}
|
||||
|
||||
export interface AgiloftSelectRecordsParams extends AgiloftBaseParams {
|
||||
where: string
|
||||
}
|
||||
|
||||
export type AgiloftSavedSearchParams = AgiloftBaseParams
|
||||
|
||||
export interface AgiloftAttachmentInfoParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
}
|
||||
|
||||
export interface AgiloftLockRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
lockAction: 'lock' | 'unlock' | 'check'
|
||||
}
|
||||
|
||||
export interface AgiloftRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string | null
|
||||
fields: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftDeleteResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
totalCount: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSelectResponse extends ToolResponse {
|
||||
output: {
|
||||
recordIds: string[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSavedSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
searches: Array<{
|
||||
name: string
|
||||
label: string
|
||||
id: string | number
|
||||
description: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftAttachmentInfoResponse extends ToolResponse {
|
||||
output: {
|
||||
attachments: Array<{
|
||||
position: number
|
||||
name: string
|
||||
size: number
|
||||
}>
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftLockResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
lockStatus: string
|
||||
lockedBy: string | null
|
||||
lockExpiresInMinutes: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftAttachFileParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
file?: unknown
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
export interface AgiloftAttachFileResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
fileName: string
|
||||
totalAttachments: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftRetrieveAttachmentParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
position: string
|
||||
}
|
||||
|
||||
export interface AgiloftRetrieveAttachmentResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftRemoveAttachmentParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
position: string
|
||||
}
|
||||
|
||||
export interface AgiloftRemoveAttachmentResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
remainingAttachments: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftGetChoiceLineIdParams extends AgiloftBaseParams {
|
||||
fieldName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface AgiloftGetChoiceLineIdResponse extends ToolResponse {
|
||||
output: {
|
||||
choiceLineId: number | null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AgiloftRecordResponse, AgiloftUpdateRecordParams } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftUpdateRecordTool: ToolConfig<AgiloftUpdateRecordParams, AgiloftRecordResponse> =
|
||||
{
|
||||
id: 'agiloft_update_record',
|
||||
name: 'Agiloft Update Record',
|
||||
description: 'Update an existing record in an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to update',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Updated field values as a JSON object (e.g., {"status": "Active", "priority": "High"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/update_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
data: params.data,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the updated record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Updated field values of the record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockValidateUrlWithDNS, mockSecureFetch } = vi.hoisted(() => ({
|
||||
mockValidateUrlWithDNS: vi.fn(),
|
||||
mockSecureFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/input-validation.server', () => ({
|
||||
validateUrlWithDNS: mockValidateUrlWithDNS,
|
||||
secureFetchWithPinnedIP: mockSecureFetch,
|
||||
}))
|
||||
|
||||
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
|
||||
|
||||
const baseParams = {
|
||||
instanceUrl: 'https://example.agiloft.com',
|
||||
knowledgeBase: 'demo',
|
||||
login: 'admin',
|
||||
password: 'secret',
|
||||
table: 'contracts',
|
||||
}
|
||||
|
||||
function mockResponse(body: { ok?: boolean; status?: number; json?: unknown; text?: string }) {
|
||||
return {
|
||||
ok: body.ok ?? true,
|
||||
status: body.status ?? 200,
|
||||
statusText: '',
|
||||
headers: { get: () => null, getSetCookie: () => [], toRecord: () => ({}) },
|
||||
body: null,
|
||||
text: async () => body.text ?? '',
|
||||
json: async () => body.json ?? {},
|
||||
arrayBuffer: async () => new ArrayBuffer(0),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockValidateUrlWithDNS.mockReset()
|
||||
mockSecureFetch.mockReset()
|
||||
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' })
|
||||
})
|
||||
|
||||
describe('executeAgiloftRequest', () => {
|
||||
it('resolves DNS once, logs in, runs the operation with the bearer token, then logs out — all pinned', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-1' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ json: { id: 42, fields: { name: 'foo' } } }))
|
||||
.mockResolvedValueOnce(mockResponse({}))
|
||||
|
||||
const result = await executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({
|
||||
url: `${base}/ewws/REST/demo/contracts/42`,
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
}),
|
||||
async (response) => {
|
||||
const data = (await response.json()) as { id: number; fields: Record<string, unknown> }
|
||||
return {
|
||||
success: response.ok,
|
||||
output: { id: String(data.id), fields: data.fields },
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({ success: true, output: { id: '42', fields: { name: 'foo' } } })
|
||||
|
||||
expect(mockValidateUrlWithDNS).toHaveBeenCalledWith(
|
||||
'https://example.agiloft.com',
|
||||
'instanceUrl'
|
||||
)
|
||||
|
||||
const calls = mockSecureFetch.mock.calls
|
||||
expect(calls).toHaveLength(3)
|
||||
expect(calls[0][0]).toBe(
|
||||
'https://example.agiloft.com/ewws/EWLogin?$KB=demo&$login=admin&$password=secret'
|
||||
)
|
||||
expect(calls[1][0]).toBe('https://example.agiloft.com/ewws/REST/demo/contracts/42')
|
||||
expect(calls[2][0]).toBe('https://example.agiloft.com/ewws/EWLogout?$KB=demo')
|
||||
|
||||
for (const call of calls) {
|
||||
expect(call[1]).toBe('203.0.113.10')
|
||||
}
|
||||
expect(calls[1][2]).toMatchObject({
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json', Authorization: 'Bearer tok-1' },
|
||||
})
|
||||
})
|
||||
|
||||
it('still logs out when the operation throws', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-2' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
|
||||
.mockResolvedValueOnce(mockResponse({}))
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async (response) => {
|
||||
if (!response.ok) throw new Error('operation failed')
|
||||
return { success: true, output: {} }
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('operation failed')
|
||||
|
||||
expect(mockSecureFetch).toHaveBeenCalledTimes(3)
|
||||
expect(mockSecureFetch.mock.calls[2][0]).toContain('/ewws/EWLogout')
|
||||
})
|
||||
|
||||
it('swallows logout failures (best-effort)', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-3' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ json: { ok: true } }))
|
||||
.mockRejectedValueOnce(new Error('logout network error'))
|
||||
|
||||
const result = await executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('throws when login does not return an access token', async () => {
|
||||
mockSecureFetch.mockResolvedValueOnce(mockResponse({ json: {} }))
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
).rejects.toThrow('Agiloft login did not return an access token')
|
||||
|
||||
expect(mockSecureFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('rejects an instance URL that resolves to a blocked IP without issuing any request', async () => {
|
||||
mockValidateUrlWithDNS.mockResolvedValue({
|
||||
isValid: false,
|
||||
error: 'instanceUrl resolves to a blocked IP address',
|
||||
})
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
{ ...baseParams, instanceUrl: 'https://internal.attacker.com' },
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
).rejects.toThrow(/blocked IP address/)
|
||||
|
||||
expect(mockSecureFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
type SecureFetchResponse,
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import type { AgiloftBaseParams } from '@/tools/agiloft/types'
|
||||
import type { HttpMethod, ToolResponse } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('AgiloftAuthServer')
|
||||
|
||||
interface AgiloftRequestConfig {
|
||||
url: string
|
||||
method: HttpMethod
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the Agiloft instance URL and resolves its DNS once, returning the
|
||||
* resolved IP so subsequent requests can pin to it. This prevents DNS-rebinding
|
||||
* (TOCTOU) SSRF where the hostname could resolve to a private IP on a later
|
||||
* lookup. Server-only — uses node:dns/promises.
|
||||
*/
|
||||
export async function resolveAgiloftInstance(instanceUrl: string): Promise<string> {
|
||||
const validation = await validateUrlWithDNS(instanceUrl, 'instanceUrl')
|
||||
if (!validation.isValid || !validation.resolvedIP) {
|
||||
throw new Error(validation.error || 'Invalid Agiloft instance URL')
|
||||
}
|
||||
return validation.resolvedIP
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS-pinned variant of agiloftLogin. Requires a pre-resolved IP so the
|
||||
* connection cannot be steered to a different host between validation and
|
||||
* the actual TCP connection.
|
||||
*/
|
||||
export async function agiloftLoginPinned(
|
||||
params: AgiloftBaseParams,
|
||||
resolvedIP: string
|
||||
): Promise<string> {
|
||||
const base = params.instanceUrl.replace(/\/$/, '')
|
||||
const kb = encodeURIComponent(params.knowledgeBase)
|
||||
const login = encodeURIComponent(params.login)
|
||||
const password = encodeURIComponent(params.password)
|
||||
|
||||
const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}`
|
||||
const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { access_token?: string }
|
||||
const token = data.access_token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Agiloft login did not return an access token')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS-pinned variant of agiloftLogout. Best-effort — failures are logged but
|
||||
* not thrown.
|
||||
*/
|
||||
export async function agiloftLogoutPinned(
|
||||
instanceUrl: string,
|
||||
knowledgeBase: string,
|
||||
token: string,
|
||||
resolvedIP: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const base = instanceUrl.replace(/\/$/, '')
|
||||
const kb = encodeURIComponent(knowledgeBase)
|
||||
await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn('Agiloft logout failed (best-effort)', { error })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared wrapper that handles the full Agiloft auth lifecycle behind the
|
||||
* codebase's SSRF-safe fetch path. The instance URL is validated and resolved
|
||||
* to a concrete IP once via `validateUrlWithDNS` (which rejects hostnames that
|
||||
* resolve to private/reserved addresses), and every hop — login, the operation
|
||||
* request, and logout — is issued through `secureFetchWithPinnedIP` so the
|
||||
* connection is pinned to that validated IP. This defeats DNS-rebinding (TOCTOU)
|
||||
* SSRF where a hostname could resolve to an internal address on a later lookup.
|
||||
*
|
||||
* 1. Validate + resolve the instance URL once.
|
||||
* 2. Login to obtain a Bearer token.
|
||||
* 3. Execute the operation request with the token.
|
||||
* 4. Logout to clean up the session (best-effort).
|
||||
*
|
||||
* The `buildRequest` callback receives the base URL and returns the request
|
||||
* config. The `transformResponse` callback converts the raw response into the
|
||||
* tool's output format.
|
||||
*
|
||||
* Server-only — uses node:dns/promises and node:http(s) via the pinned fetch.
|
||||
*/
|
||||
export async function executeAgiloftRequest<R extends ToolResponse>(
|
||||
params: AgiloftBaseParams,
|
||||
buildRequest: (base: string) => AgiloftRequestConfig,
|
||||
transformResponse: (response: SecureFetchResponse) => Promise<R>
|
||||
): Promise<R> {
|
||||
const resolvedIP = await resolveAgiloftInstance(params.instanceUrl)
|
||||
const token = await agiloftLoginPinned(params, resolvedIP)
|
||||
const base = params.instanceUrl.replace(/\/$/, '')
|
||||
|
||||
try {
|
||||
const req = buildRequest(base)
|
||||
const response = await secureFetchWithPinnedIP(req.url, resolvedIP, {
|
||||
method: req.method,
|
||||
headers: {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: req.body,
|
||||
})
|
||||
return await transformResponse(response)
|
||||
} finally {
|
||||
await agiloftLogoutPinned(params.instanceUrl, params.knowledgeBase, token, resolvedIP)
|
||||
}
|
||||
}
|
||||
|
||||
export type { SecureFetchResponse }
|
||||
@@ -0,0 +1,162 @@
|
||||
import type {
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftBaseParams,
|
||||
AgiloftDeleteRecordParams,
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftLockRecordParams,
|
||||
AgiloftReadRecordParams,
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftSavedSearchParams,
|
||||
AgiloftSearchRecordsParams,
|
||||
AgiloftSelectRecordsParams,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { HttpMethod } from '@/tools/types'
|
||||
|
||||
/** URL builders (credential-free -- auth is via Bearer token header) */
|
||||
|
||||
function encodeTable(params: AgiloftBaseParams) {
|
||||
return {
|
||||
kb: encodeURIComponent(params.knowledgeBase),
|
||||
table: encodeURIComponent(params.table),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCreateRecordUrl(base: string, params: AgiloftBaseParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
return `${base}/ewws/REST/${kb}/${table}?$lang=en`
|
||||
}
|
||||
|
||||
export function buildReadRecordUrl(base: string, params: AgiloftReadRecordParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
let url = `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
|
||||
if (params.fields) {
|
||||
const fieldList = params.fields
|
||||
.split(',')
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
for (const field of fieldList) {
|
||||
url += `&$fields=${encodeURIComponent(field)}`
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function buildUpdateRecordUrl(
|
||||
base: string,
|
||||
params: AgiloftBaseParams & { recordId: string }
|
||||
): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
}
|
||||
|
||||
export function buildDeleteRecordUrl(base: string, params: AgiloftDeleteRecordParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
}
|
||||
|
||||
function buildEwBaseQuery(params: AgiloftBaseParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
return `$KB=${kb}&$table=${table}&$lang=en`
|
||||
}
|
||||
|
||||
export function buildSearchRecordsUrl(base: string, params: AgiloftSearchRecordsParams): string {
|
||||
const query = encodeURIComponent(params.query)
|
||||
let url = `${base}/ewws/EWSearch/.json?${buildEwBaseQuery(params)}&query=${query}`
|
||||
|
||||
if (params.fields) {
|
||||
const fieldList = params.fields
|
||||
.split(',')
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
for (const field of fieldList) {
|
||||
url += `&field=${encodeURIComponent(field)}`
|
||||
}
|
||||
}
|
||||
|
||||
if (params.page) {
|
||||
url += `&page=${encodeURIComponent(params.page)}`
|
||||
}
|
||||
if (params.limit) {
|
||||
url += `&limit=${encodeURIComponent(params.limit)}`
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function buildSelectRecordsUrl(base: string, params: AgiloftSelectRecordsParams): string {
|
||||
const where = encodeURIComponent(params.where)
|
||||
return `${base}/ewws/EWSelect/.json?${buildEwBaseQuery(params)}&where=${where}`
|
||||
}
|
||||
|
||||
export function buildSavedSearchUrl(base: string, params: AgiloftSavedSearchParams): string {
|
||||
return `${base}/ewws/EWSavedSearch/.json?${buildEwBaseQuery(params)}`
|
||||
}
|
||||
|
||||
export function buildRetrieveAttachmentUrl(
|
||||
base: string,
|
||||
params: AgiloftRetrieveAttachmentParams
|
||||
): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const position = encodeURIComponent(params.position)
|
||||
return `${base}/ewws/EWRetrieve?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
|
||||
}
|
||||
|
||||
export function buildRemoveAttachmentUrl(
|
||||
base: string,
|
||||
params: AgiloftRemoveAttachmentParams
|
||||
): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const position = encodeURIComponent(params.position)
|
||||
return `${base}/ewws/EWRemoveAttachment?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
|
||||
}
|
||||
|
||||
export function buildAttachmentInfoUrl(base: string, params: AgiloftAttachmentInfoParams): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const fieldName = encodeURIComponent(params.fieldName.trim())
|
||||
return `${base}/ewws/EWAttachInfo/.json?${buildEwBaseQuery(params)}&id=${id}&field=${fieldName}`
|
||||
}
|
||||
|
||||
export function buildLockRecordUrl(base: string, params: AgiloftLockRecordParams): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/EWLock/.json?${buildEwBaseQuery(params)}&id=${id}`
|
||||
}
|
||||
|
||||
export function buildAttachFileUrl(
|
||||
base: string,
|
||||
params: AgiloftBaseParams & { recordId: string; fieldName: string },
|
||||
fileName: string
|
||||
): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const recordId = encodeURIComponent(params.recordId.trim())
|
||||
const fieldName = encodeURIComponent(params.fieldName.trim())
|
||||
const encodedFileName = encodeURIComponent(fileName)
|
||||
return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}`
|
||||
}
|
||||
|
||||
export function buildGetChoiceLineIdUrl(
|
||||
base: string,
|
||||
params: AgiloftGetChoiceLineIdParams
|
||||
): string {
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const value = encodeURIComponent(params.value.trim())
|
||||
return `${base}/ewws/EWGetChoiceLineId/.json?${buildEwBaseQuery(params)}&field=${field}&value=${value}`
|
||||
}
|
||||
|
||||
export function getLockHttpMethod(lockAction: string): HttpMethod {
|
||||
switch (lockAction) {
|
||||
case 'lock':
|
||||
return 'PUT'
|
||||
case 'unlock':
|
||||
return 'DELETE'
|
||||
default:
|
||||
return 'GET'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { AhrefsAnchorsParams, AhrefsAnchorsResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'anchor,links_to_target,dofollow_links,refdomains,first_seen,last_seen'
|
||||
|
||||
export const anchorsTool: ToolConfig<AhrefsAnchorsParams, AhrefsAnchorsResponse> = {
|
||||
id: 'ahrefs_anchors',
|
||||
name: 'Ahrefs Anchors',
|
||||
description:
|
||||
"Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text.",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
history: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Historical scope: "live" (currently live), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/anchors')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
url.searchParams.set('history', params.history || 'all_time')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get anchors')
|
||||
}
|
||||
|
||||
const anchors = (data.anchors || []).map((item: any) => ({
|
||||
anchor: item.anchor || '',
|
||||
backlinks: item.links_to_target ?? 0,
|
||||
dofollowBacklinks: item.dofollow_links ?? 0,
|
||||
referringDomains: item.refdomains ?? 0,
|
||||
firstSeen: item.first_seen || '',
|
||||
lastSeen: item.last_seen ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
anchors,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
anchors: {
|
||||
type: 'array',
|
||||
description: 'Anchor text distribution for the backlink profile',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
anchor: { type: 'string', description: 'The anchor text' },
|
||||
backlinks: { type: 'number', description: 'Total backlinks using this anchor text' },
|
||||
dofollowBacklinks: {
|
||||
type: 'number',
|
||||
description: 'Number of dofollow backlinks using this anchor text',
|
||||
},
|
||||
referringDomains: {
|
||||
type: 'number',
|
||||
description: 'Number of unique referring domains using this anchor text',
|
||||
},
|
||||
firstSeen: {
|
||||
type: 'string',
|
||||
description: 'When a link with this anchor was first found',
|
||||
},
|
||||
lastSeen: {
|
||||
type: 'string',
|
||||
description: 'When a backlink with this anchor was last seen (null if still live)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { AhrefsBacklinksParams, AhrefsBacklinksResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'url_from,url_to,anchor,domain_rating_source,is_dofollow,first_seen,last_visited'
|
||||
|
||||
export const backlinksTool: ToolConfig<AhrefsBacklinksParams, AhrefsBacklinksResponse> = {
|
||||
id: 'ahrefs_backlinks',
|
||||
name: 'Ahrefs Backlinks',
|
||||
description:
|
||||
'Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
history: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Historical scope: "live" (currently live backlinks), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date).',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/all-backlinks')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
url.searchParams.set('history', params.history || 'all_time')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get backlinks')
|
||||
}
|
||||
|
||||
const backlinks = (data.backlinks || []).map((link: any) => ({
|
||||
urlFrom: link.url_from || '',
|
||||
urlTo: link.url_to || '',
|
||||
anchor: link.anchor || '',
|
||||
domainRatingSource: link.domain_rating_source ?? 0,
|
||||
isDofollow: link.is_dofollow ?? false,
|
||||
firstSeen: link.first_seen || '',
|
||||
lastVisited: link.last_visited || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
backlinks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
backlinks: {
|
||||
type: 'array',
|
||||
description: 'List of backlinks pointing to the target',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
urlFrom: { type: 'string', description: 'The URL of the page containing the backlink' },
|
||||
urlTo: { type: 'string', description: 'The URL being linked to' },
|
||||
anchor: { type: 'string', description: 'The anchor text of the link' },
|
||||
domainRatingSource: {
|
||||
type: 'number',
|
||||
description: 'Domain Rating of the linking domain',
|
||||
},
|
||||
isDofollow: { type: 'boolean', description: 'Whether the link is dofollow' },
|
||||
firstSeen: { type: 'string', description: 'When the backlink was first discovered' },
|
||||
lastVisited: { type: 'string', description: 'When the backlink was last checked' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { AhrefsBacklinksStatsParams, AhrefsBacklinksStatsResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const backlinksStatsTool: ToolConfig<
|
||||
AhrefsBacklinksStatsParams,
|
||||
AhrefsBacklinksStatsResponse
|
||||
> = {
|
||||
id: 'ahrefs_backlinks_stats',
|
||||
name: 'Ahrefs Backlinks Stats',
|
||||
description:
|
||||
'Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks-stats')
|
||||
url.searchParams.set('target', params.target)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get backlinks stats')
|
||||
}
|
||||
|
||||
const metrics = data.metrics || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
stats: {
|
||||
liveBacklinks: metrics.live ?? 0,
|
||||
liveReferringDomains: metrics.live_refdomains ?? 0,
|
||||
allTimeBacklinks: metrics.all_time ?? 0,
|
||||
allTimeReferringDomains: metrics.all_time_refdomains ?? 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
stats: {
|
||||
type: 'object',
|
||||
description: 'Backlink and referring domain totals',
|
||||
properties: {
|
||||
liveBacklinks: { type: 'number', description: 'Number of currently live backlinks' },
|
||||
liveReferringDomains: {
|
||||
type: 'number',
|
||||
description: 'Number of currently live referring domains',
|
||||
},
|
||||
allTimeBacklinks: {
|
||||
type: 'number',
|
||||
description: 'Total backlinks ever discovered, including lost ones',
|
||||
},
|
||||
allTimeReferringDomains: {
|
||||
type: 'number',
|
||||
description: 'Total referring domains ever discovered, including lost ones',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import type { AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'url,domain_rating,ahrefs_rank,backlinks,refdomains,org_traffic,org_keywords,paid_traffic'
|
||||
|
||||
export const batchAnalysisTool: ToolConfig<AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse> =
|
||||
{
|
||||
id: 'ahrefs_batch_analysis',
|
||||
name: 'Ahrefs Batch Analysis',
|
||||
description:
|
||||
'Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
targets: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of domains or URLs to analyze. Example: "example.com,competitor.com"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
protocol: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Protocol applied to every target: "both" (default), "http", or "https"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
volumeMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.ahrefs.com/v3/batch-analysis/batch-analysis',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const targets = params.targets
|
||||
.split(',')
|
||||
.map((target) => target.trim())
|
||||
.filter((target) => target.length > 0)
|
||||
.map((url) => ({
|
||||
url,
|
||||
mode: params.mode || 'subdomains',
|
||||
protocol: params.protocol || 'both',
|
||||
}))
|
||||
|
||||
return {
|
||||
select: SELECT_FIELDS.split(','),
|
||||
targets,
|
||||
country: params.country || 'us',
|
||||
volume_mode: params.volumeMode || 'monthly',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to run batch analysis')
|
||||
}
|
||||
|
||||
const results = (data.targets || []).map((item: any) => ({
|
||||
url: item.url || '',
|
||||
index: item.index ?? 0,
|
||||
domainRating: item.domain_rating ?? null,
|
||||
ahrefsRank: item.ahrefs_rank ?? null,
|
||||
backlinks: item.backlinks ?? null,
|
||||
referringDomains: item.refdomains ?? null,
|
||||
organicTraffic: item.org_traffic ?? null,
|
||||
organicKeywords: item.org_keywords ?? null,
|
||||
paidTraffic: item.paid_traffic ?? null,
|
||||
error: item.error ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Bulk metrics for each analyzed target, in submission order',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The analyzed target URL or domain' },
|
||||
index: { type: 'number', description: 'Index of the target in the submitted list' },
|
||||
domainRating: {
|
||||
type: 'number',
|
||||
description: 'Domain Rating score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
ahrefsRank: {
|
||||
type: 'number',
|
||||
description: 'Ahrefs Rank (global ranking)',
|
||||
optional: true,
|
||||
},
|
||||
backlinks: {
|
||||
type: 'number',
|
||||
description: 'Total backlinks to the target',
|
||||
optional: true,
|
||||
},
|
||||
referringDomains: {
|
||||
type: 'number',
|
||||
description: 'Unique domains linking to the target',
|
||||
optional: true,
|
||||
},
|
||||
organicTraffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic traffic',
|
||||
optional: true,
|
||||
},
|
||||
organicKeywords: {
|
||||
type: 'number',
|
||||
description: 'Number of organic keywords ranked (top 100)',
|
||||
optional: true,
|
||||
},
|
||||
paidTraffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly paid search traffic',
|
||||
optional: true,
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
description: 'Error message if this target could not be analyzed',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
AhrefsBrokenBacklinksParams,
|
||||
AhrefsBrokenBacklinksResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'url_from,url_to,http_code_target,anchor,domain_rating_source'
|
||||
|
||||
export const brokenBacklinksTool: ToolConfig<
|
||||
AhrefsBrokenBacklinksParams,
|
||||
AhrefsBrokenBacklinksResponse
|
||||
> = {
|
||||
id: 'ahrefs_broken_backlinks',
|
||||
name: 'Ahrefs Broken Backlinks',
|
||||
description:
|
||||
'Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/broken-backlinks')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get broken backlinks')
|
||||
}
|
||||
|
||||
const brokenBacklinks = (data.backlinks || []).map((link: any) => ({
|
||||
urlFrom: link.url_from || '',
|
||||
urlTo: link.url_to || '',
|
||||
httpCode: link.http_code_target ?? null,
|
||||
anchor: link.anchor || '',
|
||||
domainRatingSource: link.domain_rating_source ?? 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
brokenBacklinks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
brokenBacklinks: {
|
||||
type: 'array',
|
||||
description: 'List of broken backlinks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
urlFrom: {
|
||||
type: 'string',
|
||||
description: 'The URL of the page containing the broken link',
|
||||
},
|
||||
urlTo: { type: 'string', description: 'The broken URL being linked to' },
|
||||
httpCode: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code of the broken target URL (e.g., 404, 410)',
|
||||
optional: true,
|
||||
},
|
||||
anchor: { type: 'string', description: 'The anchor text of the link' },
|
||||
domainRatingSource: {
|
||||
type: 'number',
|
||||
description: 'Domain Rating of the linking domain',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { AhrefsDomainRatingParams, AhrefsDomainRatingResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const domainRatingTool: ToolConfig<AhrefsDomainRatingParams, AhrefsDomainRatingResponse> = {
|
||||
id: 'ahrefs_domain_rating',
|
||||
name: 'Ahrefs Domain Rating',
|
||||
description:
|
||||
"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website's backlink profile on a scale from 0 to 100.",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain to analyze (e.g., example.com)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date for historical data in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/domain-rating')
|
||||
url.searchParams.set('target', params.target)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get domain rating')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
domainRating: data.domain_rating?.domain_rating ?? 0,
|
||||
ahrefsRank: data.domain_rating?.ahrefs_rank ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
domainRating: {
|
||||
type: 'number',
|
||||
description: 'Domain Rating score (0-100)',
|
||||
},
|
||||
ahrefsRank: {
|
||||
type: 'number',
|
||||
description: 'Ahrefs Rank - global ranking based on backlink profile strength',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type {
|
||||
AhrefsDomainRatingHistoryParams,
|
||||
AhrefsDomainRatingHistoryResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const domainRatingHistoryTool: ToolConfig<
|
||||
AhrefsDomainRatingHistoryParams,
|
||||
AhrefsDomainRatingHistoryResponse
|
||||
> = {
|
||||
id: 'ahrefs_domain_rating_history',
|
||||
name: 'Ahrefs Domain Rating History',
|
||||
description:
|
||||
'Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
dateFrom: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Start date of the historical period, in YYYY-MM-DD format',
|
||||
},
|
||||
dateTo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
historyGrouping: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/domain-rating-history')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('date_from', params.dateFrom)
|
||||
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
|
||||
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get domain rating history')
|
||||
}
|
||||
|
||||
const domainRatings = (data.domain_ratings || []).map((item: any) => ({
|
||||
date: item.date || '',
|
||||
domainRating: item.domain_rating ?? 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
domainRatings,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
domainRatings: {
|
||||
type: 'array',
|
||||
description: 'Historical Domain Rating data points',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'The date of the measurement' },
|
||||
domainRating: { type: 'number', description: 'Domain Rating score (0-100) on this date' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { anchorsTool } from '@/tools/ahrefs/anchors'
|
||||
import { backlinksTool } from '@/tools/ahrefs/backlinks'
|
||||
import { backlinksStatsTool } from '@/tools/ahrefs/backlinks_stats'
|
||||
import { batchAnalysisTool } from '@/tools/ahrefs/batch_analysis'
|
||||
import { brokenBacklinksTool } from '@/tools/ahrefs/broken_backlinks'
|
||||
import { domainRatingTool } from '@/tools/ahrefs/domain_rating'
|
||||
import { domainRatingHistoryTool } from '@/tools/ahrefs/domain_rating_history'
|
||||
import { keywordOverviewTool } from '@/tools/ahrefs/keyword_overview'
|
||||
import { keywordsHistoryTool } from '@/tools/ahrefs/keywords_history'
|
||||
import { metricsTool } from '@/tools/ahrefs/metrics'
|
||||
import { metricsHistoryTool } from '@/tools/ahrefs/metrics_history'
|
||||
import { organicCompetitorsTool } from '@/tools/ahrefs/organic_competitors'
|
||||
import { organicKeywordsTool } from '@/tools/ahrefs/organic_keywords'
|
||||
import { paidPagesTool } from '@/tools/ahrefs/paid_pages'
|
||||
import { rankTrackerCompetitorsOverviewTool } from '@/tools/ahrefs/rank_tracker_competitors_overview'
|
||||
import { rankTrackerCompetitorsStatsTool } from '@/tools/ahrefs/rank_tracker_competitors_stats'
|
||||
import { rankTrackerOverviewTool } from '@/tools/ahrefs/rank_tracker_overview'
|
||||
import { rankTrackerSerpOverviewTool } from '@/tools/ahrefs/rank_tracker_serp_overview'
|
||||
import { refdomainsHistoryTool } from '@/tools/ahrefs/refdomains_history'
|
||||
import { referringDomainsTool } from '@/tools/ahrefs/referring_domains'
|
||||
import { relatedTermsTool } from '@/tools/ahrefs/related_terms'
|
||||
import { siteAuditPageExplorerTool } from '@/tools/ahrefs/site_audit_page_explorer'
|
||||
import { topPagesTool } from '@/tools/ahrefs/top_pages'
|
||||
|
||||
export const ahrefsDomainRatingTool = domainRatingTool
|
||||
export const ahrefsBacklinksTool = backlinksTool
|
||||
export const ahrefsBacklinksStatsTool = backlinksStatsTool
|
||||
export const ahrefsReferringDomainsTool = referringDomainsTool
|
||||
export const ahrefsOrganicKeywordsTool = organicKeywordsTool
|
||||
export const ahrefsTopPagesTool = topPagesTool
|
||||
export const ahrefsKeywordOverviewTool = keywordOverviewTool
|
||||
export const ahrefsBrokenBacklinksTool = brokenBacklinksTool
|
||||
export const ahrefsMetricsTool = metricsTool
|
||||
export const ahrefsOrganicCompetitorsTool = organicCompetitorsTool
|
||||
export const ahrefsRankTrackerOverviewTool = rankTrackerOverviewTool
|
||||
export const ahrefsRankTrackerSerpOverviewTool = rankTrackerSerpOverviewTool
|
||||
export const ahrefsRankTrackerCompetitorsOverviewTool = rankTrackerCompetitorsOverviewTool
|
||||
export const ahrefsRankTrackerCompetitorsStatsTool = rankTrackerCompetitorsStatsTool
|
||||
export const ahrefsBatchAnalysisTool = batchAnalysisTool
|
||||
export const ahrefsSiteAuditPageExplorerTool = siteAuditPageExplorerTool
|
||||
export const ahrefsDomainRatingHistoryTool = domainRatingHistoryTool
|
||||
export const ahrefsMetricsHistoryTool = metricsHistoryTool
|
||||
export const ahrefsRefdomainsHistoryTool = refdomainsHistoryTool
|
||||
export const ahrefsKeywordsHistoryTool = keywordsHistoryTool
|
||||
export const ahrefsRelatedTermsTool = relatedTermsTool
|
||||
export const ahrefsAnchorsTool = anchorsTool
|
||||
export const ahrefsPaidPagesTool = paidPagesTool
|
||||
|
||||
export * from '@/tools/ahrefs/types'
|
||||
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
AhrefsKeywordOverviewParams,
|
||||
AhrefsKeywordOverviewResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'keyword,volume,difficulty,cpc,clicks,searches_pct_clicks_organic_only,parent_topic,traffic_potential,intents'
|
||||
|
||||
export const keywordOverviewTool: ToolConfig<
|
||||
AhrefsKeywordOverviewParams,
|
||||
AhrefsKeywordOverviewResponse
|
||||
> = {
|
||||
id: 'ahrefs_keyword_overview',
|
||||
name: 'Ahrefs Keyword Overview',
|
||||
description:
|
||||
'Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
keyword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The keyword to analyze',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for keyword data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/keywords-explorer/overview')
|
||||
url.searchParams.set('keywords', params.keyword)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get keyword overview')
|
||||
}
|
||||
|
||||
const result = (data.keywords || [])[0] || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
overview: {
|
||||
keyword: result.keyword || '',
|
||||
searchVolume: result.volume ?? 0,
|
||||
keywordDifficulty: result.difficulty ?? null,
|
||||
cpc: typeof result.cpc === 'number' ? result.cpc / 100 : null,
|
||||
clicks: result.clicks ?? null,
|
||||
clicksPercentage: result.searches_pct_clicks_organic_only ?? null,
|
||||
parentTopic: result.parent_topic ?? null,
|
||||
trafficPotential: result.traffic_potential ?? null,
|
||||
intents: result.intents ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
overview: {
|
||||
type: 'object',
|
||||
description: 'Keyword metrics overview',
|
||||
properties: {
|
||||
keyword: { type: 'string', description: 'The analyzed keyword' },
|
||||
searchVolume: { type: 'number', description: 'Monthly search volume' },
|
||||
keywordDifficulty: {
|
||||
type: 'number',
|
||||
description: 'Keyword difficulty score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
cpc: { type: 'number', description: 'Cost per click in USD', optional: true },
|
||||
clicks: { type: 'number', description: 'Estimated clicks per month', optional: true },
|
||||
clicksPercentage: {
|
||||
type: 'number',
|
||||
description: 'Percentage of searches that result in an organic click',
|
||||
optional: true,
|
||||
},
|
||||
parentTopic: {
|
||||
type: 'string',
|
||||
description: 'The parent topic for this keyword',
|
||||
optional: true,
|
||||
},
|
||||
trafficPotential: {
|
||||
type: 'number',
|
||||
description: 'Estimated traffic potential if ranking #1',
|
||||
optional: true,
|
||||
},
|
||||
intents: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Search intent flags (informational, navigational, commercial, transactional, branded, local)',
|
||||
optional: true,
|
||||
properties: {
|
||||
informational: { type: 'boolean', description: 'Query seeks information' },
|
||||
navigational: { type: 'boolean', description: 'Query seeks a specific site or page' },
|
||||
commercial: { type: 'boolean', description: 'Query researches a purchase decision' },
|
||||
transactional: { type: 'boolean', description: 'Query intends to complete a purchase' },
|
||||
branded: { type: 'boolean', description: 'Query references a specific brand' },
|
||||
local: { type: 'boolean', description: 'Query seeks local results' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type {
|
||||
AhrefsKeywordsHistoryParams,
|
||||
AhrefsKeywordsHistoryResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'date,top3,top4_10,top11_20,top21_50,top51_plus'
|
||||
|
||||
export const keywordsHistoryTool: ToolConfig<
|
||||
AhrefsKeywordsHistoryParams,
|
||||
AhrefsKeywordsHistoryResponse
|
||||
> = {
|
||||
id: 'ahrefs_keywords_history',
|
||||
name: 'Ahrefs Keywords History',
|
||||
description:
|
||||
'Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
dateFrom: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Start date of the historical period, in YYYY-MM-DD format',
|
||||
},
|
||||
dateTo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
historyGrouping: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/keywords-history')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('date_from', params.dateFrom)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
|
||||
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get keywords history')
|
||||
}
|
||||
|
||||
const keywordsHistory = (data.keywords || []).map((item: any) => ({
|
||||
date: item.date || '',
|
||||
top3: item.top3 ?? 0,
|
||||
top4To10: item.top4_10 ?? 0,
|
||||
top11To20: item.top11_20 ?? 0,
|
||||
top21To50: item.top21_50 ?? 0,
|
||||
top51Plus: item.top51_plus ?? 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
keywordsHistory,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
keywordsHistory: {
|
||||
type: 'array',
|
||||
description: 'Historical organic keyword ranking distribution',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Date of the record' },
|
||||
top3: { type: 'number', description: 'Keywords ranking in top 3 organic results' },
|
||||
top4To10: { type: 'number', description: 'Keywords ranking in positions 4-10' },
|
||||
top11To20: { type: 'number', description: 'Keywords ranking in positions 11-20' },
|
||||
top21To50: { type: 'number', description: 'Keywords ranking in positions 21-50' },
|
||||
top51Plus: { type: 'number', description: 'Keywords ranking in position 51 and beyond' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { AhrefsMetricsParams, AhrefsMetricsResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metricsTool: ToolConfig<AhrefsMetricsParams, AhrefsMetricsResponse> = {
|
||||
id: 'ahrefs_metrics',
|
||||
name: 'Ahrefs Metrics',
|
||||
description:
|
||||
'Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for traffic data. Example: "us", "gb", "de"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/metrics')
|
||||
url.searchParams.set('target', params.target)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get metrics')
|
||||
}
|
||||
|
||||
const metrics = data.metrics || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metrics: {
|
||||
organicTraffic: metrics.org_traffic ?? 0,
|
||||
organicKeywords: metrics.org_keywords ?? 0,
|
||||
organicKeywordsTop3: metrics.org_keywords_1_3 ?? 0,
|
||||
organicCost: typeof metrics.org_cost === 'number' ? metrics.org_cost / 100 : null,
|
||||
paidTraffic: metrics.paid_traffic ?? 0,
|
||||
paidKeywords: metrics.paid_keywords ?? 0,
|
||||
paidPages: metrics.paid_pages ?? 0,
|
||||
paidCost: typeof metrics.paid_cost === 'number' ? metrics.paid_cost / 100 : null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metrics: {
|
||||
type: 'object',
|
||||
description: 'Organic and paid search overview',
|
||||
properties: {
|
||||
organicTraffic: { type: 'number', description: 'Estimated monthly organic traffic' },
|
||||
organicKeywords: { type: 'number', description: 'Number of organic keywords ranked' },
|
||||
organicKeywordsTop3: {
|
||||
type: 'number',
|
||||
description: 'Number of organic keywords ranking in positions 1-3',
|
||||
},
|
||||
organicCost: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly cost to replicate organic traffic via ads (USD)',
|
||||
optional: true,
|
||||
},
|
||||
paidTraffic: { type: 'number', description: 'Estimated monthly paid search traffic' },
|
||||
paidKeywords: { type: 'number', description: 'Number of paid keywords targeted' },
|
||||
paidPages: { type: 'number', description: 'Number of pages receiving paid traffic' },
|
||||
paidCost: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly paid search spend (USD)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { AhrefsMetricsHistoryParams, AhrefsMetricsHistoryResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'date,org_traffic,org_cost,paid_traffic,paid_cost'
|
||||
|
||||
export const metricsHistoryTool: ToolConfig<
|
||||
AhrefsMetricsHistoryParams,
|
||||
AhrefsMetricsHistoryResponse
|
||||
> = {
|
||||
id: 'ahrefs_metrics_history',
|
||||
name: 'Ahrefs Metrics History',
|
||||
description:
|
||||
'Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
dateFrom: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Start date of the historical period, in YYYY-MM-DD format',
|
||||
},
|
||||
dateTo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
volumeMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
|
||||
},
|
||||
historyGrouping: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/metrics-history')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('date_from', params.dateFrom)
|
||||
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
|
||||
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get metrics history')
|
||||
}
|
||||
|
||||
const metrics = (data.metrics || []).map((item: any) => ({
|
||||
date: item.date || '',
|
||||
organicTraffic: item.org_traffic ?? 0,
|
||||
organicCost: typeof item.org_cost === 'number' ? item.org_cost / 100 : null,
|
||||
paidTraffic: item.paid_traffic ?? 0,
|
||||
paidCost: typeof item.paid_cost === 'number' ? item.paid_cost / 100 : null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metricsHistory: metrics,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metricsHistory: {
|
||||
type: 'array',
|
||||
description: 'Historical organic and paid traffic data points',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Date of the metric entry' },
|
||||
organicTraffic: { type: 'number', description: 'Estimated monthly organic visits' },
|
||||
organicCost: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly cost to replicate organic traffic via ads (USD)',
|
||||
optional: true,
|
||||
},
|
||||
paidTraffic: { type: 'number', description: 'Estimated monthly paid search visits' },
|
||||
paidCost: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly paid search spend (USD)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
AhrefsOrganicCompetitorsParams,
|
||||
AhrefsOrganicCompetitorsResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'competitor_domain,domain_rating,keywords_common,keywords_target,keywords_competitor,traffic'
|
||||
|
||||
export const organicCompetitorsTool: ToolConfig<
|
||||
AhrefsOrganicCompetitorsParams,
|
||||
AhrefsOrganicCompetitorsResponse
|
||||
> = {
|
||||
id: 'ahrefs_organic_competitors',
|
||||
name: 'Ahrefs Organic Competitors',
|
||||
description:
|
||||
'Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/organic-competitors')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get organic competitors')
|
||||
}
|
||||
|
||||
const competitors = (data.competitors || []).map((competitor: any) => ({
|
||||
domain: competitor.competitor_domain ?? null,
|
||||
domainRating: competitor.domain_rating ?? 0,
|
||||
commonKeywords: competitor.keywords_common ?? 0,
|
||||
targetKeywords: competitor.keywords_target ?? 0,
|
||||
competitorKeywords: competitor.keywords_competitor ?? 0,
|
||||
traffic: competitor.traffic ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
competitors,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
competitors: {
|
||||
type: 'array',
|
||||
description: 'List of organic search competitors ranked by keyword overlap',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The competitor domain',
|
||||
optional: true,
|
||||
},
|
||||
domainRating: { type: 'number', description: 'Domain Rating of the competitor' },
|
||||
commonKeywords: {
|
||||
type: 'number',
|
||||
description: 'Number of keywords the competitor and target both rank for',
|
||||
},
|
||||
targetKeywords: {
|
||||
type: 'number',
|
||||
description: 'Number of keywords the target ranks for',
|
||||
},
|
||||
competitorKeywords: {
|
||||
type: 'number',
|
||||
description: 'Number of keywords the competitor ranks for',
|
||||
},
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic traffic for the competitor',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type {
|
||||
AhrefsOrganicKeywordsParams,
|
||||
AhrefsOrganicKeywordsResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'keyword,volume,best_position,best_position_url,sum_traffic,keyword_difficulty'
|
||||
|
||||
export const organicKeywordsTool: ToolConfig<
|
||||
AhrefsOrganicKeywordsParams,
|
||||
AhrefsOrganicKeywordsResponse
|
||||
> = {
|
||||
id: 'ahrefs_organic_keywords',
|
||||
name: 'Ahrefs Organic Keywords',
|
||||
description:
|
||||
'Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/organic-keywords')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get organic keywords')
|
||||
}
|
||||
|
||||
const keywords = (data.keywords || []).map((kw: any) => ({
|
||||
keyword: kw.keyword || '',
|
||||
volume: kw.volume ?? 0,
|
||||
position: kw.best_position ?? null,
|
||||
url: kw.best_position_url ?? null,
|
||||
traffic: kw.sum_traffic ?? 0,
|
||||
keywordDifficulty: kw.keyword_difficulty ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
keywords,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
keywords: {
|
||||
type: 'array',
|
||||
description: 'List of organic keywords the target ranks for',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string', description: 'The keyword' },
|
||||
volume: { type: 'number', description: 'Monthly search volume' },
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Best ranking position for this keyword',
|
||||
optional: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'The URL that ranks at the best position for this keyword',
|
||||
optional: true,
|
||||
},
|
||||
traffic: { type: 'number', description: 'Estimated monthly organic traffic' },
|
||||
keywordDifficulty: {
|
||||
type: 'number',
|
||||
description: 'Keyword difficulty score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { AhrefsPaidPagesParams, AhrefsPaidPagesResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'url,sum_traffic,keywords,top_keyword,value,ads_count'
|
||||
|
||||
export const paidPagesTool: ToolConfig<AhrefsPaidPagesParams, AhrefsPaidPagesResponse> = {
|
||||
id: 'ahrefs_paid_pages',
|
||||
name: 'Ahrefs Paid Pages',
|
||||
description:
|
||||
"Get a target domain's pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/paid-pages')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get paid pages')
|
||||
}
|
||||
|
||||
const paidPages = (data.pages || []).map((page: any) => ({
|
||||
url: page.url ?? null,
|
||||
traffic: page.sum_traffic ?? null,
|
||||
keywords: page.keywords ?? null,
|
||||
topKeyword: page.top_keyword ?? null,
|
||||
value: typeof page.value === 'number' ? page.value / 100 : null,
|
||||
adsCount: page.ads_count ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
paidPages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
paidPages: {
|
||||
type: 'array',
|
||||
description: 'List of pages receiving paid search traffic',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The page URL', optional: true },
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly paid search traffic',
|
||||
optional: true,
|
||||
},
|
||||
keywords: {
|
||||
type: 'number',
|
||||
description: 'Number of paid keywords the page ranks for',
|
||||
optional: true,
|
||||
},
|
||||
topKeyword: {
|
||||
type: 'string',
|
||||
description: 'The top keyword driving paid traffic to this page',
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly paid traffic cost in USD',
|
||||
optional: true,
|
||||
},
|
||||
adsCount: {
|
||||
type: 'number',
|
||||
description: 'Number of unique ads shown for this page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import type {
|
||||
AhrefsRankTrackerCompetitorsOverviewParams,
|
||||
AhrefsRankTrackerCompetitorsOverviewResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'keyword,volume,keyword_difficulty,serp_features,competitors_list'
|
||||
|
||||
export const rankTrackerCompetitorsOverviewTool: ToolConfig<
|
||||
AhrefsRankTrackerCompetitorsOverviewParams,
|
||||
AhrefsRankTrackerCompetitorsOverviewResponse
|
||||
> = {
|
||||
id: 'ahrefs_rank_tracker_competitors_overview',
|
||||
name: 'Ahrefs Rank Tracker Competitors Overview',
|
||||
description:
|
||||
"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword's volume and difficulty alongside every competitor's position, traffic, and traffic value. This endpoint is free and does not consume API units.",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
projectId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report rankings for, in YYYY-MM-DD format',
|
||||
},
|
||||
device: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Rankings device type: "desktop" or "mobile"',
|
||||
},
|
||||
dateCompared: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Comparison date in YYYY-MM-DD format, to compute position/traffic deltas',
|
||||
},
|
||||
volumeMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/competitors-overview')
|
||||
url.searchParams.set('project_id', String(params.projectId))
|
||||
url.searchParams.set('date', params.date)
|
||||
url.searchParams.set('device', params.device)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.dateCompared) url.searchParams.set('date_compared', params.dateCompared)
|
||||
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message || data.error || 'Failed to get rank tracker competitors overview'
|
||||
)
|
||||
}
|
||||
|
||||
const competitorKeywords = (data.keywords || []).map((item: any) => ({
|
||||
keyword: item.keyword || '',
|
||||
volume: item.volume ?? null,
|
||||
keywordDifficulty: item.keyword_difficulty ?? null,
|
||||
serpFeatures: item.serp_features ?? [],
|
||||
competitorsList: (item.competitors_list || []).map((competitor: any) => ({
|
||||
url: competitor.url || '',
|
||||
position: competitor.position ?? null,
|
||||
bestPositionKind: competitor.best_position_kind ?? null,
|
||||
traffic: competitor.traffic ?? null,
|
||||
value: typeof competitor.value === 'number' ? competitor.value / 100 : null,
|
||||
})),
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
competitorKeywords,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
competitorKeywords: {
|
||||
type: 'array',
|
||||
description: 'Tracked keywords with competitor ranking data',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string', description: 'The tracked keyword' },
|
||||
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
|
||||
keywordDifficulty: {
|
||||
type: 'number',
|
||||
description: 'Keyword difficulty score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
serpFeatures: {
|
||||
type: 'array',
|
||||
description: 'SERP features present in the results',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
competitorsList: {
|
||||
type: 'array',
|
||||
description: 'Ranking data for each tracked competitor on this keyword',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: "The competitor's ranking URL" },
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Current ranking position',
|
||||
optional: true,
|
||||
},
|
||||
bestPositionKind: {
|
||||
type: 'string',
|
||||
description: 'Type of the best position achieved',
|
||||
optional: true,
|
||||
},
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated traffic to the competitor',
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
description: 'Estimated traffic value (USD)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import type {
|
||||
AhrefsRankTrackerCompetitorsStatsParams,
|
||||
AhrefsRankTrackerCompetitorsStatsResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'competitor,traffic,traffic_value,average_position,pos_1_3,pos_4_10,share_of_voice,share_of_traffic_value'
|
||||
|
||||
export const rankTrackerCompetitorsStatsTool: ToolConfig<
|
||||
AhrefsRankTrackerCompetitorsStatsParams,
|
||||
AhrefsRankTrackerCompetitorsStatsResponse
|
||||
> = {
|
||||
id: 'ahrefs_rank_tracker_competitors_stats',
|
||||
name: 'Ahrefs Rank Tracker Competitors Stats',
|
||||
description:
|
||||
"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor's traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
projectId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics for, in YYYY-MM-DD format',
|
||||
},
|
||||
device: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Rankings device type: "desktop" or "mobile"',
|
||||
},
|
||||
volumeMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/competitors-stats')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
url.searchParams.set('date', params.date)
|
||||
url.searchParams.set('device', params.device)
|
||||
url.searchParams.set('project_id', String(params.projectId))
|
||||
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message || data.error || 'Failed to get rank tracker competitors stats'
|
||||
)
|
||||
}
|
||||
|
||||
const competitorsStats = (data['competitors-metrics'] || []).map((item: any) => ({
|
||||
competitor: item.competitor || '',
|
||||
traffic: item.traffic ?? null,
|
||||
trafficValue: typeof item.traffic_value === 'number' ? item.traffic_value / 100 : null,
|
||||
averagePosition: item.average_position ?? null,
|
||||
pos1To3: item.pos_1_3 ?? 0,
|
||||
pos4To10: item.pos_4_10 ?? 0,
|
||||
shareOfVoice: item.share_of_voice ?? 0,
|
||||
shareOfTrafficValue: item.share_of_traffic_value ?? 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
competitorsStats,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
competitorsStats: {
|
||||
type: 'array',
|
||||
description: 'Aggregate stats for each tracked competitor',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
competitor: { type: 'string', description: "The competitor's URL" },
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic visits',
|
||||
optional: true,
|
||||
},
|
||||
trafficValue: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic traffic value (USD)',
|
||||
optional: true,
|
||||
},
|
||||
averagePosition: {
|
||||
type: 'number',
|
||||
description: 'Average top organic position across tracked keywords',
|
||||
optional: true,
|
||||
},
|
||||
pos1To3: { type: 'number', description: 'Keywords ranking in top 3 positions' },
|
||||
pos4To10: { type: 'number', description: 'Keywords ranking in positions 4-10' },
|
||||
shareOfVoice: { type: 'number', description: 'Organic traffic share percentage' },
|
||||
shareOfTrafficValue: {
|
||||
type: 'number',
|
||||
description: 'Organic traffic value share percentage',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type {
|
||||
AhrefsRankTrackerOverviewParams,
|
||||
AhrefsRankTrackerOverviewResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'keyword,position,volume,keyword_difficulty,url,traffic,serp_features,best_position_kind'
|
||||
|
||||
export const rankTrackerOverviewTool: ToolConfig<
|
||||
AhrefsRankTrackerOverviewParams,
|
||||
AhrefsRankTrackerOverviewResponse
|
||||
> = {
|
||||
id: 'ahrefs_rank_tracker_overview',
|
||||
name: 'Ahrefs Rank Tracker Overview',
|
||||
description:
|
||||
'Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
projectId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report rankings for, in YYYY-MM-DD format',
|
||||
},
|
||||
device: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Rankings device type: "desktop" or "mobile"',
|
||||
},
|
||||
dateCompared: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Comparison date in YYYY-MM-DD format, to compute position/traffic deltas',
|
||||
},
|
||||
volumeMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/overview')
|
||||
url.searchParams.set('project_id', String(params.projectId))
|
||||
url.searchParams.set('date', params.date)
|
||||
url.searchParams.set('device', params.device)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.dateCompared) url.searchParams.set('date_compared', params.dateCompared)
|
||||
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get rank tracker overview')
|
||||
}
|
||||
|
||||
const overviews = (data.overviews || []).map((item: any) => ({
|
||||
keyword: item.keyword || '',
|
||||
position: item.position ?? null,
|
||||
volume: item.volume ?? null,
|
||||
keywordDifficulty: item.keyword_difficulty ?? null,
|
||||
url: item.url ?? null,
|
||||
traffic: item.traffic ?? null,
|
||||
serpFeatures: item.serp_features ?? [],
|
||||
bestPositionKind: item.best_position_kind ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
overviews,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
overviews: {
|
||||
type: 'array',
|
||||
description: 'Ranking overview for each tracked keyword',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string', description: 'The tracked keyword' },
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Top organic search position',
|
||||
optional: true,
|
||||
},
|
||||
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
|
||||
keywordDifficulty: {
|
||||
type: 'number',
|
||||
description: 'Keyword difficulty score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
url: { type: 'string', description: 'Top-ranking URL', optional: true },
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic visits',
|
||||
optional: true,
|
||||
},
|
||||
serpFeatures: {
|
||||
type: 'array',
|
||||
description: 'SERP features present in the results',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
bestPositionKind: {
|
||||
type: 'string',
|
||||
description: 'Type of the top position (organic, paid, or SERP feature)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type {
|
||||
AhrefsRankTrackerSerpOverviewParams,
|
||||
AhrefsRankTrackerSerpOverviewResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const rankTrackerSerpOverviewTool: ToolConfig<
|
||||
AhrefsRankTrackerSerpOverviewParams,
|
||||
AhrefsRankTrackerSerpOverviewResponse
|
||||
> = {
|
||||
id: 'ahrefs_rank_tracker_serp_overview',
|
||||
name: 'Ahrefs Rank Tracker SERP Overview',
|
||||
description:
|
||||
'Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
projectId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
|
||||
},
|
||||
keyword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The tracked keyword to retrieve SERP data for',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for the tracked keyword. Example: "us", "gb", "de"',
|
||||
},
|
||||
device: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Rankings device type: "desktop" or "mobile"',
|
||||
},
|
||||
topPositions: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of top organic positions to return (defaults to all available)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format',
|
||||
},
|
||||
locationId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Location ID of the tracked keyword, if tracked at a specific location',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code of the tracked keyword',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/serp-overview')
|
||||
url.searchParams.set('project_id', String(params.projectId))
|
||||
url.searchParams.set('keyword', params.keyword)
|
||||
url.searchParams.set('country', params.country)
|
||||
url.searchParams.set('device', params.device)
|
||||
if (params.topPositions) url.searchParams.set('top_positions', String(params.topPositions))
|
||||
if (params.date) url.searchParams.set('date', params.date)
|
||||
if (params.locationId) url.searchParams.set('location_id', String(params.locationId))
|
||||
if (params.languageCode) url.searchParams.set('language_code', params.languageCode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get SERP overview')
|
||||
}
|
||||
|
||||
const positions = (data.positions || []).map((item: any) => ({
|
||||
position: item.position ?? 0,
|
||||
url: item.url || '',
|
||||
title: item.title || '',
|
||||
type: item.type ?? [],
|
||||
domainRating: item.domain_rating ?? 0,
|
||||
urlRating: item.url_rating ?? 0,
|
||||
backlinks: item.backlinks ?? 0,
|
||||
refdomains: item.refdomains ?? 0,
|
||||
traffic: item.traffic ?? 0,
|
||||
value: typeof item.value === 'number' ? item.value / 100 : null,
|
||||
topKeyword: item.top_keyword ?? null,
|
||||
topKeywordVolume: item.top_keyword_volume ?? null,
|
||||
updateDate: item.update_date || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
positions,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
positions: {
|
||||
type: 'array',
|
||||
description: 'Every ranking result on the SERP for the tracked keyword',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
position: { type: 'number', description: 'Position of the result in the SERP' },
|
||||
url: { type: 'string', description: 'URL of the ranking page' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
type: {
|
||||
type: 'array',
|
||||
description: 'The kind of the position: organic, paid, or a SERP feature',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
domainRating: { type: 'number', description: 'Domain Rating of the ranking domain' },
|
||||
urlRating: { type: 'number', description: 'URL Rating of the ranking page' },
|
||||
backlinks: { type: 'number', description: 'Total backlinks to the ranking domain' },
|
||||
refdomains: { type: 'number', description: 'Unique referring domains' },
|
||||
traffic: { type: 'number', description: 'Estimated monthly organic search traffic' },
|
||||
value: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly traffic value (USD)',
|
||||
optional: true,
|
||||
},
|
||||
topKeyword: {
|
||||
type: 'string',
|
||||
description: 'Highest-traffic keyword ranking for this page',
|
||||
optional: true,
|
||||
},
|
||||
topKeywordVolume: {
|
||||
type: 'number',
|
||||
description: 'Monthly search volume for the top keyword',
|
||||
optional: true,
|
||||
},
|
||||
updateDate: { type: 'string', description: 'Date the SERP was last checked' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
AhrefsRefdomainsHistoryParams,
|
||||
AhrefsRefdomainsHistoryResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const refdomainsHistoryTool: ToolConfig<
|
||||
AhrefsRefdomainsHistoryParams,
|
||||
AhrefsRefdomainsHistoryResponse
|
||||
> = {
|
||||
id: 'ahrefs_refdomains_history',
|
||||
name: 'Ahrefs Referring Domains History',
|
||||
description:
|
||||
'Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain or URL to analyze. Example: "example.com"',
|
||||
},
|
||||
dateFrom: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Start date of the historical period, in YYYY-MM-DD format',
|
||||
},
|
||||
dateTo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
historyGrouping: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/refdomains-history')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('date_from', params.dateFrom)
|
||||
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
|
||||
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message || data.error || 'Failed to get referring domains history'
|
||||
)
|
||||
}
|
||||
|
||||
const referringDomainsHistory = (data.refdomains || []).map((item: any) => ({
|
||||
date: item.date || '',
|
||||
referringDomains: item.refdomains ?? 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
referringDomainsHistory,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
referringDomainsHistory: {
|
||||
type: 'array',
|
||||
description: 'Historical referring domains count data points',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'The date of the data point' },
|
||||
referringDomains: {
|
||||
type: 'number',
|
||||
description: 'Total number of unique domains linking to the target on this date',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
AhrefsReferringDomainsParams,
|
||||
AhrefsReferringDomainsResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen'
|
||||
|
||||
export const referringDomainsTool: ToolConfig<
|
||||
AhrefsReferringDomainsParams,
|
||||
AhrefsReferringDomainsResponse
|
||||
> = {
|
||||
id: 'ahrefs_referring_domains',
|
||||
name: 'Ahrefs Referring Domains',
|
||||
description:
|
||||
'Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
history: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Historical scope: "live" (currently live), "all_time" (default, includes lost domains), or "since:YYYY-MM-DD" (domains found since a date).',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/refdomains')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
url.searchParams.set('history', params.history || 'all_time')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get referring domains')
|
||||
}
|
||||
|
||||
const referringDomains = (data.refdomains || []).map((domain: any) => ({
|
||||
domain: domain.domain || '',
|
||||
domainRating: domain.domain_rating ?? 0,
|
||||
backlinks: domain.links_to_target ?? 0,
|
||||
dofollowBacklinks: domain.dofollow_links ?? 0,
|
||||
firstSeen: domain.first_seen || '',
|
||||
lastVisited: domain.last_seen ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
referringDomains,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
referringDomains: {
|
||||
type: 'array',
|
||||
description: 'List of domains linking to the target',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
domain: { type: 'string', description: 'The referring domain' },
|
||||
domainRating: { type: 'number', description: 'Domain Rating of the referring domain' },
|
||||
backlinks: {
|
||||
type: 'number',
|
||||
description: 'Total number of backlinks from this domain to the target',
|
||||
},
|
||||
dofollowBacklinks: {
|
||||
type: 'number',
|
||||
description: 'Number of dofollow backlinks from this domain',
|
||||
},
|
||||
firstSeen: { type: 'string', description: 'When the domain was first seen linking' },
|
||||
lastVisited: {
|
||||
type: 'string',
|
||||
description: 'When the domain was last seen linking (null if never re-crawled)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { AhrefsRelatedTermsParams, AhrefsRelatedTermsResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'keyword,volume,difficulty,cpc,parent_topic,traffic_potential,intents,serp_features'
|
||||
|
||||
export const relatedTermsTool: ToolConfig<AhrefsRelatedTermsParams, AhrefsRelatedTermsResponse> = {
|
||||
id: 'ahrefs_related_terms',
|
||||
name: 'Ahrefs Related Terms',
|
||||
description:
|
||||
'Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for ("also rank for") or also discuss ("also talk about"), with volume, difficulty, and CPC.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
keyword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The seed keyword to find related terms for',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for keyword data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Type of related keywords to return: "also_rank_for", "also_talk_about", or "all" (default: "all")',
|
||||
},
|
||||
viewFor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether to derive related terms from the top 10 or top 100 ranking pages (default: "top_10")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/keywords-explorer/related-terms')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
url.searchParams.set('keywords', params.keyword)
|
||||
if (params.terms) url.searchParams.set('terms', params.terms)
|
||||
if (params.viewFor) url.searchParams.set('view_for', params.viewFor)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get related terms')
|
||||
}
|
||||
|
||||
const relatedTerms = (data.keywords || []).map((item: any) => ({
|
||||
keyword: item.keyword || '',
|
||||
volume: item.volume ?? null,
|
||||
keywordDifficulty: item.difficulty ?? null,
|
||||
cpc: typeof item.cpc === 'number' ? item.cpc / 100 : null,
|
||||
parentTopic: item.parent_topic ?? null,
|
||||
trafficPotential: item.traffic_potential ?? null,
|
||||
intents: item.intents ?? null,
|
||||
serpFeatures: item.serp_features ?? [],
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
relatedTerms,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
relatedTerms: {
|
||||
type: 'array',
|
||||
description: 'Related keyword ideas for the seed keyword',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string', description: 'The related keyword' },
|
||||
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
|
||||
keywordDifficulty: {
|
||||
type: 'number',
|
||||
description: 'Keyword difficulty score (0-100)',
|
||||
optional: true,
|
||||
},
|
||||
cpc: { type: 'number', description: 'Cost per click in USD', optional: true },
|
||||
parentTopic: {
|
||||
type: 'string',
|
||||
description: 'The parent topic for this keyword',
|
||||
optional: true,
|
||||
},
|
||||
trafficPotential: {
|
||||
type: 'number',
|
||||
description: 'Estimated traffic potential if ranking #1',
|
||||
optional: true,
|
||||
},
|
||||
intents: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Search intent flags (informational, navigational, commercial, transactional, branded, local)',
|
||||
optional: true,
|
||||
},
|
||||
serpFeatures: {
|
||||
type: 'array',
|
||||
description: 'SERP features present in the results',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type {
|
||||
AhrefsSiteAuditPageExplorerParams,
|
||||
AhrefsSiteAuditPageExplorerResponse,
|
||||
} from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS =
|
||||
'url,http_code,title,internal_links,external_links,backlinks,compliant,traffic'
|
||||
|
||||
export const siteAuditPageExplorerTool: ToolConfig<
|
||||
AhrefsSiteAuditPageExplorerParams,
|
||||
AhrefsSiteAuditPageExplorerResponse
|
||||
> = {
|
||||
id: 'ahrefs_site_audit_page_explorer',
|
||||
name: 'Ahrefs Site Audit Page Explorer',
|
||||
description:
|
||||
'Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
projectId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Site Audit project ID (found in the project URL in Ahrefs)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip, for pagination',
|
||||
},
|
||||
issueId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return pages affected by this issue ID',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-audit/page-explorer')
|
||||
url.searchParams.set('project_id', String(params.projectId))
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
if (params.date) url.searchParams.set('date', params.date)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
if (params.offset) url.searchParams.set('offset', String(params.offset))
|
||||
if (params.issueId) url.searchParams.set('issue_id', params.issueId)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get site audit pages')
|
||||
}
|
||||
|
||||
const auditPages = (data.pages || []).map((page: any) => ({
|
||||
url: page.url || '',
|
||||
httpCode: page.http_code ?? null,
|
||||
title: page.title ?? [],
|
||||
internalLinks: (page.internal_links || []).length,
|
||||
externalLinks: (page.external_links || []).length,
|
||||
backlinks: page.backlinks ?? null,
|
||||
compliant: page.compliant ?? null,
|
||||
traffic: page.traffic ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
auditPages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
auditPages: {
|
||||
type: 'array',
|
||||
description: 'List of crawled pages with health and SEO metrics',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The crawled page URL' },
|
||||
httpCode: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code returned by the URL',
|
||||
optional: true,
|
||||
},
|
||||
title: {
|
||||
type: 'array',
|
||||
description: 'Page title tag(s)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
internalLinks: { type: 'number', description: 'Number of internal outgoing links' },
|
||||
externalLinks: { type: 'number', description: 'Number of external outgoing links' },
|
||||
backlinks: {
|
||||
type: 'number',
|
||||
description: 'Number of incoming external links to the page',
|
||||
optional: true,
|
||||
},
|
||||
compliant: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the page is indexable (200 status, no canonical/noindex)',
|
||||
optional: true,
|
||||
},
|
||||
traffic: {
|
||||
type: 'number',
|
||||
description: 'Estimated monthly organic traffic to the page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { AhrefsTopPagesParams, AhrefsTopPagesResponse } from '@/tools/ahrefs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const SELECT_FIELDS = 'url,sum_traffic,keywords,top_keyword,value'
|
||||
|
||||
export const topPagesTool: ToolConfig<AhrefsTopPagesParams, AhrefsTopPagesResponse> = {
|
||||
id: 'ahrefs_top_pages',
|
||||
name: 'Ahrefs Top Pages',
|
||||
description:
|
||||
'Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target domain to analyze. Example: "example.com"',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Ahrefs API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.ahrefs.com/v3/site-explorer/top-pages')
|
||||
url.searchParams.set('target', params.target)
|
||||
url.searchParams.set('country', params.country || 'us')
|
||||
url.searchParams.set('select', SELECT_FIELDS)
|
||||
// Date is required - default to today if not provided
|
||||
const date = params.date || new Date().toISOString().split('T')[0]
|
||||
url.searchParams.set('date', date)
|
||||
if (params.mode) url.searchParams.set('mode', params.mode)
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get top pages')
|
||||
}
|
||||
|
||||
const pages = (data.pages || []).map((page: any) => ({
|
||||
url: page.url ?? null,
|
||||
traffic: page.sum_traffic ?? 0,
|
||||
keywords: page.keywords ?? null,
|
||||
topKeyword: page.top_keyword ?? null,
|
||||
value: typeof page.value === 'number' ? page.value / 100 : null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pages: {
|
||||
type: 'array',
|
||||
description: 'List of top pages by organic traffic',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The page URL', optional: true },
|
||||
traffic: { type: 'number', description: 'Estimated monthly organic traffic' },
|
||||
keywords: {
|
||||
type: 'number',
|
||||
description: 'Number of keywords the page ranks for',
|
||||
optional: true,
|
||||
},
|
||||
topKeyword: {
|
||||
type: 'string',
|
||||
description: 'The top keyword driving traffic to this page',
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
description: 'Estimated traffic value in USD',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
// Common types for Ahrefs API tools
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// Common parameters for all Ahrefs tools
|
||||
interface AhrefsBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
// Target mode for analysis
|
||||
export type AhrefsTargetMode = 'domain' | 'prefix' | 'subdomains' | 'exact'
|
||||
|
||||
// Historical scope for backlink-profile endpoints (no `date` param on these endpoints)
|
||||
export type AhrefsHistory = 'live' | 'all_time' | string // `since:YYYY-MM-DD` is also valid
|
||||
|
||||
// Domain Rating tool types
|
||||
export interface AhrefsDomainRatingParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
}
|
||||
|
||||
export interface AhrefsDomainRatingResponse extends ToolResponse {
|
||||
output: {
|
||||
domainRating: number
|
||||
ahrefsRank: number | null
|
||||
}
|
||||
}
|
||||
|
||||
// Backlinks tool types
|
||||
export interface AhrefsBacklinksParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
mode?: AhrefsTargetMode
|
||||
history?: AhrefsHistory
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsBacklink {
|
||||
urlFrom: string
|
||||
urlTo: string
|
||||
anchor: string
|
||||
domainRatingSource: number
|
||||
isDofollow: boolean
|
||||
firstSeen: string
|
||||
lastVisited: string
|
||||
}
|
||||
|
||||
export interface AhrefsBacklinksResponse extends ToolResponse {
|
||||
output: {
|
||||
backlinks: AhrefsBacklink[]
|
||||
}
|
||||
}
|
||||
|
||||
// Backlinks Stats tool types
|
||||
export interface AhrefsBacklinksStatsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
}
|
||||
|
||||
interface AhrefsBacklinksStatsResult {
|
||||
liveBacklinks: number
|
||||
liveReferringDomains: number
|
||||
allTimeBacklinks: number
|
||||
allTimeReferringDomains: number
|
||||
}
|
||||
|
||||
export interface AhrefsBacklinksStatsResponse extends ToolResponse {
|
||||
output: {
|
||||
stats: AhrefsBacklinksStatsResult
|
||||
}
|
||||
}
|
||||
|
||||
// Referring Domains tool types
|
||||
export interface AhrefsReferringDomainsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
mode?: AhrefsTargetMode
|
||||
history?: AhrefsHistory
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsReferringDomain {
|
||||
domain: string
|
||||
domainRating: number
|
||||
backlinks: number
|
||||
dofollowBacklinks: number
|
||||
firstSeen: string
|
||||
lastVisited: string | null
|
||||
}
|
||||
|
||||
export interface AhrefsReferringDomainsResponse extends ToolResponse {
|
||||
output: {
|
||||
referringDomains: AhrefsReferringDomain[]
|
||||
}
|
||||
}
|
||||
|
||||
// Organic Keywords tool types
|
||||
export interface AhrefsOrganicKeywordsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsOrganicKeyword {
|
||||
keyword: string
|
||||
volume: number
|
||||
position: number | null
|
||||
url: string | null
|
||||
traffic: number
|
||||
keywordDifficulty: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsOrganicKeywordsResponse extends ToolResponse {
|
||||
output: {
|
||||
keywords: AhrefsOrganicKeyword[]
|
||||
}
|
||||
}
|
||||
|
||||
// Top Pages tool types
|
||||
export interface AhrefsTopPagesParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsTopPage {
|
||||
url: string | null
|
||||
traffic: number
|
||||
keywords: number | null
|
||||
topKeyword: string | null
|
||||
value: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsTopPagesResponse extends ToolResponse {
|
||||
output: {
|
||||
pages: AhrefsTopPage[]
|
||||
}
|
||||
}
|
||||
|
||||
// Keyword Overview tool types
|
||||
export interface AhrefsKeywordOverviewParams extends AhrefsBaseParams {
|
||||
keyword: string
|
||||
country?: string
|
||||
}
|
||||
|
||||
interface AhrefsKeywordIntents {
|
||||
informational: boolean
|
||||
navigational: boolean
|
||||
commercial: boolean
|
||||
transactional: boolean
|
||||
branded: boolean
|
||||
local: boolean
|
||||
}
|
||||
|
||||
interface AhrefsKeywordOverviewResult {
|
||||
keyword: string
|
||||
searchVolume: number
|
||||
keywordDifficulty: number | null
|
||||
cpc: number | null
|
||||
clicks: number | null
|
||||
clicksPercentage: number | null
|
||||
parentTopic: string | null
|
||||
trafficPotential: number | null
|
||||
intents: AhrefsKeywordIntents | null
|
||||
}
|
||||
|
||||
export interface AhrefsKeywordOverviewResponse extends ToolResponse {
|
||||
output: {
|
||||
overview: AhrefsKeywordOverviewResult
|
||||
}
|
||||
}
|
||||
|
||||
// Broken Backlinks tool types
|
||||
export interface AhrefsBrokenBacklinksParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
mode?: AhrefsTargetMode
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsBrokenBacklink {
|
||||
urlFrom: string
|
||||
urlTo: string
|
||||
httpCode: number | null
|
||||
anchor: string
|
||||
domainRatingSource: number
|
||||
}
|
||||
|
||||
export interface AhrefsBrokenBacklinksResponse extends ToolResponse {
|
||||
output: {
|
||||
brokenBacklinks: AhrefsBrokenBacklink[]
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics tool types (single-call organic + paid search overview)
|
||||
export interface AhrefsMetricsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
}
|
||||
|
||||
interface AhrefsMetricsResult {
|
||||
organicTraffic: number
|
||||
organicKeywords: number
|
||||
organicKeywordsTop3: number
|
||||
organicCost: number | null
|
||||
paidTraffic: number
|
||||
paidKeywords: number
|
||||
paidPages: number
|
||||
paidCost: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsMetricsResponse extends ToolResponse {
|
||||
output: {
|
||||
metrics: AhrefsMetricsResult
|
||||
}
|
||||
}
|
||||
|
||||
// Organic Competitors tool types
|
||||
export interface AhrefsOrganicCompetitorsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsOrganicCompetitor {
|
||||
domain: string | null
|
||||
domainRating: number
|
||||
commonKeywords: number
|
||||
targetKeywords: number
|
||||
competitorKeywords: number
|
||||
traffic: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsOrganicCompetitorsResponse extends ToolResponse {
|
||||
output: {
|
||||
competitors: AhrefsOrganicCompetitor[]
|
||||
}
|
||||
}
|
||||
|
||||
// Rank Tracker device type
|
||||
export type AhrefsRankTrackerDevice = 'desktop' | 'mobile'
|
||||
|
||||
// Rank Tracker search volume calculation mode
|
||||
export type AhrefsVolumeMode = 'monthly' | 'average'
|
||||
|
||||
// Rank Tracker Overview tool types
|
||||
export interface AhrefsRankTrackerOverviewParams extends AhrefsBaseParams {
|
||||
projectId: number
|
||||
date: string // Date in YYYY-MM-DD format (required by the API)
|
||||
device: AhrefsRankTrackerDevice
|
||||
dateCompared?: string
|
||||
volumeMode?: AhrefsVolumeMode
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsRankTrackerOverviewItem {
|
||||
keyword: string
|
||||
position: number | null
|
||||
volume: number | null
|
||||
keywordDifficulty: number | null
|
||||
url: string | null
|
||||
traffic: number | null
|
||||
serpFeatures: string[]
|
||||
bestPositionKind: string | null
|
||||
}
|
||||
|
||||
export interface AhrefsRankTrackerOverviewResponse extends ToolResponse {
|
||||
output: {
|
||||
overviews: AhrefsRankTrackerOverviewItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Rank Tracker SERP Overview tool types
|
||||
export interface AhrefsRankTrackerSerpOverviewParams extends AhrefsBaseParams {
|
||||
projectId: number
|
||||
keyword: string
|
||||
country: string
|
||||
device: AhrefsRankTrackerDevice
|
||||
topPositions?: number
|
||||
date?: string // ISO date-time (YYYY-MM-DDThh:mm:ss)
|
||||
locationId?: number
|
||||
languageCode?: string
|
||||
}
|
||||
|
||||
interface AhrefsSerpPosition {
|
||||
position: number
|
||||
url: string
|
||||
title: string
|
||||
type: string[]
|
||||
domainRating: number
|
||||
urlRating: number
|
||||
backlinks: number
|
||||
refdomains: number
|
||||
traffic: number
|
||||
value: number | null
|
||||
topKeyword: string | null
|
||||
topKeywordVolume: number | null
|
||||
updateDate: string
|
||||
}
|
||||
|
||||
export interface AhrefsRankTrackerSerpOverviewResponse extends ToolResponse {
|
||||
output: {
|
||||
positions: AhrefsSerpPosition[]
|
||||
}
|
||||
}
|
||||
|
||||
// Rank Tracker Competitors Overview tool types
|
||||
export interface AhrefsRankTrackerCompetitorsOverviewParams extends AhrefsBaseParams {
|
||||
projectId: number
|
||||
date: string
|
||||
device: AhrefsRankTrackerDevice
|
||||
dateCompared?: string
|
||||
volumeMode?: AhrefsVolumeMode
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsCompetitorListItem {
|
||||
url: string
|
||||
position: number | null
|
||||
bestPositionKind: string | null
|
||||
traffic: number | null
|
||||
value: number | null
|
||||
}
|
||||
|
||||
interface AhrefsRankTrackerCompetitorsOverviewItem {
|
||||
keyword: string
|
||||
volume: number | null
|
||||
keywordDifficulty: number | null
|
||||
serpFeatures: string[]
|
||||
competitorsList: AhrefsCompetitorListItem[]
|
||||
}
|
||||
|
||||
export interface AhrefsRankTrackerCompetitorsOverviewResponse extends ToolResponse {
|
||||
output: {
|
||||
competitorKeywords: AhrefsRankTrackerCompetitorsOverviewItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Rank Tracker Competitors Stats tool types
|
||||
export interface AhrefsRankTrackerCompetitorsStatsParams extends AhrefsBaseParams {
|
||||
projectId: number
|
||||
date: string
|
||||
device: AhrefsRankTrackerDevice
|
||||
volumeMode?: AhrefsVolumeMode
|
||||
}
|
||||
|
||||
interface AhrefsCompetitorStat {
|
||||
competitor: string
|
||||
traffic: number | null
|
||||
trafficValue: number | null
|
||||
averagePosition: number | null
|
||||
pos1To3: number
|
||||
pos4To10: number
|
||||
shareOfVoice: number
|
||||
shareOfTrafficValue: number
|
||||
}
|
||||
|
||||
export interface AhrefsRankTrackerCompetitorsStatsResponse extends ToolResponse {
|
||||
output: {
|
||||
competitorsStats: AhrefsCompetitorStat[]
|
||||
}
|
||||
}
|
||||
|
||||
// Batch Analysis tool types
|
||||
export interface AhrefsBatchAnalysisParams extends AhrefsBaseParams {
|
||||
targets: string // Comma-separated list of domains/URLs
|
||||
mode?: AhrefsTargetMode
|
||||
protocol?: 'both' | 'http' | 'https'
|
||||
country?: string
|
||||
volumeMode?: AhrefsVolumeMode
|
||||
}
|
||||
|
||||
interface AhrefsBatchAnalysisResult {
|
||||
url: string
|
||||
index: number
|
||||
domainRating: number | null
|
||||
ahrefsRank: number | null
|
||||
backlinks: number | null
|
||||
referringDomains: number | null
|
||||
organicTraffic: number | null
|
||||
organicKeywords: number | null
|
||||
paidTraffic: number | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface AhrefsBatchAnalysisResponse extends ToolResponse {
|
||||
output: {
|
||||
results: AhrefsBatchAnalysisResult[]
|
||||
}
|
||||
}
|
||||
|
||||
// Site Audit Page Explorer tool types
|
||||
export interface AhrefsSiteAuditPageExplorerParams extends AhrefsBaseParams {
|
||||
projectId: number
|
||||
date?: string // ISO date-time (YYYY-MM-DDThh:mm:ss), defaults to most recent crawl
|
||||
limit?: number
|
||||
offset?: number
|
||||
issueId?: string
|
||||
}
|
||||
|
||||
interface AhrefsPageExplorerResult {
|
||||
url: string
|
||||
httpCode: number | null
|
||||
title: string[]
|
||||
internalLinks: number
|
||||
externalLinks: number
|
||||
backlinks: number | null
|
||||
compliant: boolean | null
|
||||
traffic: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsSiteAuditPageExplorerResponse extends ToolResponse {
|
||||
output: {
|
||||
auditPages: AhrefsPageExplorerResult[]
|
||||
}
|
||||
}
|
||||
|
||||
// Domain Rating History tool types
|
||||
export interface AhrefsDomainRatingHistoryParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
dateFrom: string
|
||||
dateTo?: string
|
||||
historyGrouping?: 'daily' | 'weekly' | 'monthly'
|
||||
}
|
||||
|
||||
interface AhrefsDomainRatingHistoryItem {
|
||||
date: string
|
||||
domainRating: number
|
||||
}
|
||||
|
||||
export interface AhrefsDomainRatingHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
domainRatings: AhrefsDomainRatingHistoryItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics History tool types
|
||||
export interface AhrefsMetricsHistoryParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
dateFrom: string
|
||||
dateTo?: string
|
||||
volumeMode?: AhrefsVolumeMode
|
||||
historyGrouping?: 'daily' | 'weekly' | 'monthly'
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
}
|
||||
|
||||
interface AhrefsMetricsHistoryItem {
|
||||
date: string
|
||||
organicTraffic: number
|
||||
organicCost: number | null
|
||||
paidTraffic: number
|
||||
paidCost: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsMetricsHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
metricsHistory: AhrefsMetricsHistoryItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Referring Domains History tool types
|
||||
export interface AhrefsRefdomainsHistoryParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
dateFrom: string
|
||||
dateTo?: string
|
||||
historyGrouping?: 'daily' | 'weekly' | 'monthly'
|
||||
mode?: AhrefsTargetMode
|
||||
}
|
||||
|
||||
interface AhrefsRefdomainsHistoryItem {
|
||||
date: string
|
||||
referringDomains: number
|
||||
}
|
||||
|
||||
export interface AhrefsRefdomainsHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
referringDomainsHistory: AhrefsRefdomainsHistoryItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Keywords History tool types
|
||||
export interface AhrefsKeywordsHistoryParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
dateFrom: string
|
||||
dateTo?: string
|
||||
historyGrouping?: 'daily' | 'weekly' | 'monthly'
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
}
|
||||
|
||||
interface AhrefsKeywordsHistoryItem {
|
||||
date: string
|
||||
top3: number
|
||||
top4To10: number
|
||||
top11To20: number
|
||||
top21To50: number
|
||||
top51Plus: number
|
||||
}
|
||||
|
||||
export interface AhrefsKeywordsHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
keywordsHistory: AhrefsKeywordsHistoryItem[]
|
||||
}
|
||||
}
|
||||
|
||||
// Related Terms tool types
|
||||
export interface AhrefsRelatedTermsParams extends AhrefsBaseParams {
|
||||
keyword: string
|
||||
country?: string
|
||||
terms?: 'also_rank_for' | 'also_talk_about' | 'all'
|
||||
viewFor?: 'top_10' | 'top_100'
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsRelatedTerm {
|
||||
keyword: string
|
||||
volume: number | null
|
||||
keywordDifficulty: number | null
|
||||
cpc: number | null
|
||||
parentTopic: string | null
|
||||
trafficPotential: number | null
|
||||
intents: Record<string, boolean> | null
|
||||
serpFeatures: string[]
|
||||
}
|
||||
|
||||
export interface AhrefsRelatedTermsResponse extends ToolResponse {
|
||||
output: {
|
||||
relatedTerms: AhrefsRelatedTerm[]
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors tool types
|
||||
export interface AhrefsAnchorsParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
mode?: AhrefsTargetMode
|
||||
history?: AhrefsHistory
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsAnchor {
|
||||
anchor: string
|
||||
backlinks: number
|
||||
dofollowBacklinks: number
|
||||
referringDomains: number
|
||||
firstSeen: string
|
||||
lastSeen: string | null
|
||||
}
|
||||
|
||||
export interface AhrefsAnchorsResponse extends ToolResponse {
|
||||
output: {
|
||||
anchors: AhrefsAnchor[]
|
||||
}
|
||||
}
|
||||
|
||||
// Paid Pages tool types
|
||||
export interface AhrefsPaidPagesParams extends AhrefsBaseParams {
|
||||
target: string
|
||||
country?: string
|
||||
mode?: AhrefsTargetMode
|
||||
date?: string // Date in YYYY-MM-DD format, defaults to today
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface AhrefsPaidPage {
|
||||
url: string | null
|
||||
traffic: number | null
|
||||
keywords: number | null
|
||||
topKeyword: string | null
|
||||
value: number | null
|
||||
adsCount: number | null
|
||||
}
|
||||
|
||||
export interface AhrefsPaidPagesResponse extends ToolResponse {
|
||||
output: {
|
||||
paidPages: AhrefsPaidPage[]
|
||||
}
|
||||
}
|
||||
|
||||
// Union type for all possible responses
|
||||
export type AhrefsResponse =
|
||||
| AhrefsDomainRatingResponse
|
||||
| AhrefsBacklinksResponse
|
||||
| AhrefsBacklinksStatsResponse
|
||||
| AhrefsReferringDomainsResponse
|
||||
| AhrefsOrganicKeywordsResponse
|
||||
| AhrefsTopPagesResponse
|
||||
| AhrefsKeywordOverviewResponse
|
||||
| AhrefsBrokenBacklinksResponse
|
||||
| AhrefsMetricsResponse
|
||||
| AhrefsOrganicCompetitorsResponse
|
||||
| AhrefsRankTrackerOverviewResponse
|
||||
| AhrefsRankTrackerSerpOverviewResponse
|
||||
| AhrefsRankTrackerCompetitorsOverviewResponse
|
||||
| AhrefsRankTrackerCompetitorsStatsResponse
|
||||
| AhrefsBatchAnalysisResponse
|
||||
| AhrefsSiteAuditPageExplorerResponse
|
||||
| AhrefsDomainRatingHistoryResponse
|
||||
| AhrefsMetricsHistoryResponse
|
||||
| AhrefsRefdomainsHistoryResponse
|
||||
| AhrefsKeywordsHistoryResponse
|
||||
| AhrefsRelatedTermsResponse
|
||||
| AhrefsAnchorsResponse
|
||||
| AhrefsPaidPagesResponse
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { AirtableCreateParams, AirtableCreateResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, AirtableCreateResponse> = {
|
||||
id: 'airtable_create_records',
|
||||
name: 'Airtable Create Records',
|
||||
description: 'Write new records to an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
records: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of records to create, each with a `fields` object',
|
||||
// Example: [{ fields: { "Field 1": "Value1", "Field 2": "Value2" } }]
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ records: params.records }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records: data.records ?? [],
|
||||
metadata: {
|
||||
recordCount: (data.records ?? []).length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of created Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records created' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { AirtableDeleteParams, AirtableDeleteResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableDeleteRecordsTool: ToolConfig<AirtableDeleteParams, AirtableDeleteResponse> = {
|
||||
id: 'airtable_delete_records',
|
||||
name: 'Airtable Delete Records',
|
||||
description: 'Delete one or more records from an Airtable table by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
recordIds: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record IDs to delete (each starts with "rec", e.g., ["recXXXXXXXXXXXXXX"]). Pass a single-element array to delete one record.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const base = `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`
|
||||
const ids = (params.recordIds ?? [])
|
||||
.map((id) => (id == null ? '' : String(id).trim()))
|
||||
.filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
throw new Error('At least one record ID is required to delete')
|
||||
}
|
||||
if (ids.length > 10) {
|
||||
throw new Error(
|
||||
`Airtable deletes at most 10 records per request (received ${ids.length}). Split the delete into batches of 10 or fewer.`
|
||||
)
|
||||
}
|
||||
const queryParams = new URLSearchParams()
|
||||
for (const id of ids) {
|
||||
queryParams.append('records[]', id as string)
|
||||
}
|
||||
return `${base}?${queryParams.toString()}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const records = data.records ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
metadata: {
|
||||
recordCount: records.length,
|
||||
deletedRecordIds: records.map((r: { id: string }) => r.id),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of deleted Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records deleted' },
|
||||
deletedRecordIds: { type: 'array', description: 'List of deleted record IDs' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface AirtableGetBaseSchemaParams {
|
||||
accessToken: string
|
||||
baseId: string
|
||||
}
|
||||
|
||||
interface AirtableFieldSchema {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
options?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface AirtableViewSchema {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface AirtableTableSchema {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
fields: AirtableFieldSchema[]
|
||||
views: AirtableViewSchema[]
|
||||
}
|
||||
|
||||
export interface AirtableGetBaseSchemaResponse extends ToolResponse {
|
||||
output: {
|
||||
tables: AirtableTableSchema[]
|
||||
metadata: {
|
||||
totalTables: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const airtableGetBaseSchemaTool: ToolConfig<
|
||||
AirtableGetBaseSchemaParams,
|
||||
AirtableGetBaseSchemaResponse
|
||||
> = {
|
||||
id: 'airtable_get_base_schema',
|
||||
name: 'Airtable Get Base Schema',
|
||||
description: 'Get the schema of all tables, fields, and views in an Airtable base',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const tables = (data.tables || []).map((table: Record<string, unknown>) => ({
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
description: table.description,
|
||||
fields: ((table.fields as Record<string, unknown>[]) || []).map((field) => ({
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
description: field.description,
|
||||
options: field.options,
|
||||
})),
|
||||
views: ((table.views as Record<string, unknown>[]) || []).map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
type: view.type,
|
||||
})),
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tables,
|
||||
metadata: {
|
||||
totalTables: tables.length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tables: {
|
||||
type: 'json',
|
||||
description: 'Array of table schemas with fields and views',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
fields: { type: 'json' },
|
||||
views: { type: 'json' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including total tables count',
|
||||
},
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user