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,68 @@
|
||||
import type { ConvexFunctionCallParams, ConvexFunctionCallResponse } from '@/tools/convex/types'
|
||||
import {
|
||||
convexApiUrl,
|
||||
convexAuthHeaders,
|
||||
parseFunctionArgs,
|
||||
transformFunctionCallResponse,
|
||||
} from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const actionTool: ToolConfig<ConvexFunctionCallParams, ConvexFunctionCallResponse> = {
|
||||
id: 'convex_action',
|
||||
name: 'Convex Run Action',
|
||||
description: 'Run a Convex action function and return its result',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
functionPath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the action function (e.g., emails:send or folder/file:myAction)',
|
||||
},
|
||||
args: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Named arguments to pass to the function as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => convexApiUrl(params.deploymentUrl, '/api/action'),
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
...convexAuthHeaders(params.deployKey),
|
||||
}),
|
||||
body: (params) => ({
|
||||
path: params.functionPath.trim(),
|
||||
args: parseFunctionArgs(params.args),
|
||||
format: 'json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) =>
|
||||
transformFunctionCallResponse(response, 'action'),
|
||||
|
||||
outputs: {
|
||||
value: { type: 'json', description: 'Result returned by the action function' },
|
||||
logLines: {
|
||||
type: 'array',
|
||||
description: 'Log lines printed during the function execution',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
ConvexDocumentDeltasApiResponse,
|
||||
ConvexDocumentDeltasParams,
|
||||
ConvexDocumentDeltasResponse,
|
||||
} from '@/tools/convex/types'
|
||||
import { convexApiUrl, convexAuthHeaders, parseConvexResponse } from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const documentDeltasTool: ToolConfig<
|
||||
ConvexDocumentDeltasParams,
|
||||
ConvexDocumentDeltasResponse
|
||||
> = {
|
||||
id: 'convex_document_deltas',
|
||||
name: 'Convex Document Deltas',
|
||||
description:
|
||||
'List documents that changed after a snapshot or previous delta cursor. Deleted documents are returned with a _deleted flag. Requires streaming export, available on Convex paid plans.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Timestamp cursor to read deltas after. Use the snapshot value from List Documents or the cursor from a previous Document Deltas page.',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table to read deltas from. Omit to read deltas from all tables.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const cursor = String(params.cursor ?? '').trim()
|
||||
if (!cursor) {
|
||||
throw new Error(
|
||||
'Cursor is required: pass the snapshot value from List Documents or the cursor from a previous Document Deltas page'
|
||||
)
|
||||
}
|
||||
const query = new URLSearchParams({ format: 'json', cursor })
|
||||
if (params.tableName?.trim()) query.set('tableName', params.tableName.trim())
|
||||
return convexApiUrl(params.deploymentUrl, `/api/document_deltas?${query.toString()}`)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => convexAuthHeaders(params.deployKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await parseConvexResponse(response)) as ConvexDocumentDeltasApiResponse
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
documents: data.values ?? [],
|
||||
hasMore: data.hasMore ?? false,
|
||||
cursor: data.cursor !== undefined && data.cursor !== null ? String(data.cursor) : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
documents: {
|
||||
type: 'array',
|
||||
description: 'Changed documents, each including _table and _ts fields',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
hasMore: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more delta pages remain',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor to pass back in when fetching the next page of deltas',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { actionTool } from '@/tools/convex/action'
|
||||
import { documentDeltasTool } from '@/tools/convex/document_deltas'
|
||||
import { listDocumentsTool } from '@/tools/convex/list_documents'
|
||||
import { listTablesTool } from '@/tools/convex/list_tables'
|
||||
import { mutationTool } from '@/tools/convex/mutation'
|
||||
import { queryTool } from '@/tools/convex/query'
|
||||
import { runFunctionTool } from '@/tools/convex/run_function'
|
||||
|
||||
export const convexQueryTool = queryTool
|
||||
export const convexMutationTool = mutationTool
|
||||
export const convexActionTool = actionTool
|
||||
export const convexRunFunctionTool = runFunctionTool
|
||||
export const convexListTablesTool = listTablesTool
|
||||
export const convexListDocumentsTool = listDocumentsTool
|
||||
export const convexDocumentDeltasTool = documentDeltasTool
|
||||
@@ -0,0 +1,103 @@
|
||||
import type {
|
||||
ConvexListDocumentsParams,
|
||||
ConvexListDocumentsResponse,
|
||||
ConvexListSnapshotApiResponse,
|
||||
} from '@/tools/convex/types'
|
||||
import { convexApiUrl, convexAuthHeaders, parseConvexResponse } from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listDocumentsTool: ToolConfig<ConvexListDocumentsParams, ConvexListDocumentsResponse> =
|
||||
{
|
||||
id: 'convex_list_documents',
|
||||
name: 'Convex List Documents',
|
||||
description:
|
||||
'List documents from a Convex table via a paginated snapshot. Pass the returned snapshot and page cursor back in to fetch the next page. Requires streaming export, available on Convex paid plans.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table to list documents from. Omit to list documents from all tables.',
|
||||
},
|
||||
snapshot: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Snapshot timestamp from a previous page. Omit on the first request to start a new snapshot.',
|
||||
},
|
||||
pageCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Page cursor from a previous page of the same snapshot. Omit on the first request.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const query = new URLSearchParams({ format: 'json' })
|
||||
if (params.tableName?.trim()) query.set('tableName', params.tableName.trim())
|
||||
const snapshot = String(params.snapshot ?? '').trim()
|
||||
if (snapshot) query.set('snapshot', snapshot)
|
||||
const pageCursor = String(params.pageCursor ?? '').trim()
|
||||
if (pageCursor) query.set('cursor', pageCursor)
|
||||
return convexApiUrl(params.deploymentUrl, `/api/list_snapshot?${query.toString()}`)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => convexAuthHeaders(params.deployKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await parseConvexResponse(response)) as ConvexListSnapshotApiResponse
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
documents: data.values ?? [],
|
||||
hasMore: data.hasMore ?? false,
|
||||
snapshot:
|
||||
data.snapshot !== undefined && data.snapshot !== null ? String(data.snapshot) : null,
|
||||
pageCursor:
|
||||
data.cursor !== undefined && data.cursor !== null ? String(data.cursor) : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
documents: {
|
||||
type: 'array',
|
||||
description: 'Documents in this page of the snapshot',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
hasMore: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages remain in the snapshot',
|
||||
},
|
||||
snapshot: {
|
||||
type: 'string',
|
||||
description: 'Snapshot timestamp to pass back in when fetching the next page',
|
||||
optional: true,
|
||||
},
|
||||
pageCursor: {
|
||||
type: 'string',
|
||||
description: 'Page cursor to pass back in when fetching the next page',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ConvexListTablesParams, ConvexListTablesResponse } from '@/tools/convex/types'
|
||||
import { convexApiUrl, convexAuthHeaders, parseConvexResponse } from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listTablesTool: ToolConfig<ConvexListTablesParams, ConvexListTablesResponse> = {
|
||||
id: 'convex_list_tables',
|
||||
name: 'Convex List Tables',
|
||||
description:
|
||||
'List all tables in a Convex deployment along with their JSON schemas. Requires streaming export, available on Convex paid plans.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => convexApiUrl(params.deploymentUrl, '/api/json_schemas?format=json'),
|
||||
method: 'GET',
|
||||
headers: (params) => convexAuthHeaders(params.deployKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await parseConvexResponse(response)
|
||||
const schemas =
|
||||
data !== null && typeof data === 'object' && !Array.isArray(data)
|
||||
? (data as Record<string, unknown>)
|
||||
: {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tables: Object.keys(schemas).sort(),
|
||||
schemas,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tables: {
|
||||
type: 'array',
|
||||
description: 'Names of the tables in the deployment',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
schemas: {
|
||||
type: 'json',
|
||||
description: 'Map of table name to the JSON schema of its documents',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ConvexFunctionCallParams, ConvexFunctionCallResponse } from '@/tools/convex/types'
|
||||
import {
|
||||
convexApiUrl,
|
||||
convexAuthHeaders,
|
||||
parseFunctionArgs,
|
||||
transformFunctionCallResponse,
|
||||
} from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const mutationTool: ToolConfig<ConvexFunctionCallParams, ConvexFunctionCallResponse> = {
|
||||
id: 'convex_mutation',
|
||||
name: 'Convex Run Mutation',
|
||||
description: 'Run a Convex mutation function to write data and return its result',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
functionPath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the mutation function (e.g., messages:send or folder/file:myMutation)',
|
||||
},
|
||||
args: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Named arguments to pass to the function as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => convexApiUrl(params.deploymentUrl, '/api/mutation'),
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
...convexAuthHeaders(params.deployKey),
|
||||
}),
|
||||
body: (params) => ({
|
||||
path: params.functionPath.trim(),
|
||||
args: parseFunctionArgs(params.args),
|
||||
format: 'json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) =>
|
||||
transformFunctionCallResponse(response, 'mutation'),
|
||||
|
||||
outputs: {
|
||||
value: { type: 'json', description: 'Result returned by the mutation function' },
|
||||
logLines: {
|
||||
type: 'array',
|
||||
description: 'Log lines printed during the function execution',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ConvexFunctionCallParams, ConvexFunctionCallResponse } from '@/tools/convex/types'
|
||||
import {
|
||||
convexApiUrl,
|
||||
convexAuthHeaders,
|
||||
parseFunctionArgs,
|
||||
transformFunctionCallResponse,
|
||||
} from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryTool: ToolConfig<ConvexFunctionCallParams, ConvexFunctionCallResponse> = {
|
||||
id: 'convex_query',
|
||||
name: 'Convex Run Query',
|
||||
description: 'Run a Convex query function and return its result',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
functionPath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the query function (e.g., messages:list or folder/file:myQuery)',
|
||||
},
|
||||
args: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Named arguments to pass to the function as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => convexApiUrl(params.deploymentUrl, '/api/query'),
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
...convexAuthHeaders(params.deployKey),
|
||||
}),
|
||||
body: (params) => ({
|
||||
path: params.functionPath.trim(),
|
||||
args: parseFunctionArgs(params.args),
|
||||
format: 'json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => transformFunctionCallResponse(response, 'query'),
|
||||
|
||||
outputs: {
|
||||
value: { type: 'json', description: 'Result returned by the query function' },
|
||||
logLines: {
|
||||
type: 'array',
|
||||
description: 'Log lines printed during the function execution',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ConvexFunctionCallParams, ConvexFunctionCallResponse } from '@/tools/convex/types'
|
||||
import {
|
||||
convexApiUrl,
|
||||
convexAuthHeaders,
|
||||
parseFunctionArgs,
|
||||
transformFunctionCallResponse,
|
||||
} from '@/tools/convex/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const runFunctionTool: ToolConfig<ConvexFunctionCallParams, ConvexFunctionCallResponse> = {
|
||||
id: 'convex_run_function',
|
||||
name: 'Convex Run Function',
|
||||
description:
|
||||
'Run any Convex function (query, mutation, or action) by path without specifying its type',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deployment URL (e.g., https://your-deployment.convex.cloud)',
|
||||
},
|
||||
deployKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Convex deploy key from the dashboard Settings page',
|
||||
},
|
||||
functionPath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the function (e.g., messages:list or folder/file:myFunction)',
|
||||
},
|
||||
args: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Named arguments to pass to the function as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const identifier = params.functionPath
|
||||
.trim()
|
||||
.replace(':', '/')
|
||||
.split('/')
|
||||
.map(encodeURIComponent)
|
||||
.join('/')
|
||||
return convexApiUrl(params.deploymentUrl, `/api/run/${identifier}`)
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
...convexAuthHeaders(params.deployKey),
|
||||
}),
|
||||
body: (params) => ({
|
||||
args: parseFunctionArgs(params.args),
|
||||
format: 'json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) =>
|
||||
transformFunctionCallResponse(response, 'function'),
|
||||
|
||||
outputs: {
|
||||
value: { type: 'json', description: 'Result returned by the function' },
|
||||
logLines: {
|
||||
type: 'array',
|
||||
description: 'Log lines printed during the function execution',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Shared parameter and response definitions for Convex tools.
|
||||
* Based on the official Convex HTTP API documentation.
|
||||
* @see https://docs.convex.dev/http-api/
|
||||
*/
|
||||
|
||||
export interface ConvexBaseParams {
|
||||
deploymentUrl: string
|
||||
deployKey: string
|
||||
}
|
||||
|
||||
export interface ConvexFunctionCallParams extends ConvexBaseParams {
|
||||
functionPath: string
|
||||
args?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface ConvexFunctionCallResponse extends ToolResponse {
|
||||
output: {
|
||||
value: unknown
|
||||
logLines: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConvexListTablesParams extends ConvexBaseParams {}
|
||||
|
||||
export interface ConvexListTablesResponse extends ToolResponse {
|
||||
output: {
|
||||
tables: string[]
|
||||
schemas: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConvexListDocumentsParams extends ConvexBaseParams {
|
||||
tableName?: string
|
||||
snapshot?: string
|
||||
pageCursor?: string
|
||||
}
|
||||
|
||||
export interface ConvexListDocumentsResponse extends ToolResponse {
|
||||
output: {
|
||||
documents: unknown[]
|
||||
hasMore: boolean
|
||||
snapshot: string | null
|
||||
pageCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConvexDocumentDeltasParams extends ConvexBaseParams {
|
||||
cursor: string
|
||||
tableName?: string
|
||||
}
|
||||
|
||||
export interface ConvexDocumentDeltasResponse extends ToolResponse {
|
||||
output: {
|
||||
documents: unknown[]
|
||||
hasMore: boolean
|
||||
cursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw wire shape of Convex function-call responses (`/api/query`, `/api/mutation`,
|
||||
* `/api/action`, `/api/run/{identifier}`).
|
||||
* @see https://docs.convex.dev/http-api/#post-apiquery-apimutation-apiaction
|
||||
*/
|
||||
export interface ConvexFunctionCallApiResponse {
|
||||
status?: 'success' | 'error'
|
||||
value?: unknown
|
||||
logLines?: string[]
|
||||
errorMessage?: string
|
||||
errorData?: unknown
|
||||
}
|
||||
|
||||
/** Raw wire shape of `/api/list_snapshot` responses. */
|
||||
export interface ConvexListSnapshotApiResponse {
|
||||
values?: unknown[]
|
||||
hasMore?: boolean
|
||||
snapshot?: number | string | null
|
||||
cursor?: number | string | null
|
||||
}
|
||||
|
||||
/** Raw wire shape of `/api/document_deltas` responses. */
|
||||
export interface ConvexDocumentDeltasApiResponse {
|
||||
values?: unknown[]
|
||||
hasMore?: boolean
|
||||
cursor?: number | string | null
|
||||
}
|
||||
|
||||
export interface ConvexResponse extends ToolResponse {
|
||||
output: {
|
||||
value?: unknown
|
||||
logLines?: string[]
|
||||
tables?: string[]
|
||||
schemas?: Record<string, unknown>
|
||||
documents?: unknown[]
|
||||
hasMore?: boolean
|
||||
snapshot?: string | null
|
||||
pageCursor?: string | null
|
||||
cursor?: string | null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { truncate } from '@sim/utils/string'
|
||||
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||
import type {
|
||||
ConvexFunctionCallApiResponse,
|
||||
ConvexFunctionCallResponse,
|
||||
} from '@/tools/convex/types'
|
||||
|
||||
/**
|
||||
* Builds a Convex deployment API URL from the user-provided deployment URL.
|
||||
* Accepts URLs with or without a trailing slash.
|
||||
*
|
||||
* The deployment URL is validated with the shared SSRF guard so invalid hosts
|
||||
* fail fast with a clear message; the tool executor additionally re-validates
|
||||
* with DNS resolution and pins the resolved IP for the actual request.
|
||||
*/
|
||||
export function convexApiUrl(deploymentUrl: string, path: string): string {
|
||||
const trimmed = deploymentUrl.trim().replace(/\/+$/, '')
|
||||
const validation = validateExternalUrl(trimmed, 'Deployment URL')
|
||||
if (!validation.isValid) {
|
||||
throw new Error(`${validation.error} (e.g., https://your-deployment.convex.cloud)`)
|
||||
}
|
||||
const parsed = new URL(trimmed)
|
||||
if (parsed.search || parsed.hash) {
|
||||
throw new Error(
|
||||
'Deployment URL must not include a query string or fragment (e.g., https://your-deployment.convex.cloud)'
|
||||
)
|
||||
}
|
||||
return `${trimmed}${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the deployment admin authorization header for Convex HTTP API requests.
|
||||
* @see https://docs.convex.dev/http-api/#api-authentication
|
||||
*/
|
||||
export function convexAuthHeaders(deployKey: string): Record<string, string> {
|
||||
return { Authorization: `Convex ${deployKey.trim()}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses function arguments that may arrive as a JSON string or an object.
|
||||
* Convex function endpoints require a named-argument object.
|
||||
*/
|
||||
export function parseFunctionArgs(
|
||||
args: Record<string, unknown> | string | undefined
|
||||
): Record<string, unknown> {
|
||||
if (args === undefined || args === null) return {}
|
||||
if (typeof args === 'string') {
|
||||
const trimmed = args.trim()
|
||||
if (!trimmed) return {}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(trimmed)
|
||||
} catch {
|
||||
throw new Error('Invalid function arguments: expected a JSON object like {"key": "value"}')
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('Invalid function arguments: expected a JSON object, not an array or scalar')
|
||||
}
|
||||
return parsed as Record<string, unknown>
|
||||
}
|
||||
if (typeof args !== 'object' || Array.isArray(args)) {
|
||||
throw new Error('Invalid function arguments: expected a JSON object, not an array or scalar')
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Convex API response body, surfacing non-OK HTTP statuses (e.g. 401
|
||||
* from an invalid deploy key) as descriptive errors instead of empty results.
|
||||
*/
|
||||
export async function parseConvexResponse(response: Response): Promise<unknown> {
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
throw new Error(
|
||||
`Convex request failed (HTTP ${response.status})${text ? `: ${truncate(text.trim(), 300)}` : ''}`
|
||||
)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a Convex function-call response. Convex returns HTTP 200 with an
|
||||
* in-band `status: "error"` payload when the function itself fails, so errors
|
||||
* must be surfaced here rather than relying on the HTTP status code.
|
||||
* @see https://docs.convex.dev/http-api/#post-apiquery-apimutation-apiaction
|
||||
*/
|
||||
export async function transformFunctionCallResponse(
|
||||
response: Response,
|
||||
functionType: 'query' | 'mutation' | 'action' | 'function'
|
||||
): Promise<ConvexFunctionCallResponse> {
|
||||
const data = (await parseConvexResponse(response)) as ConvexFunctionCallApiResponse
|
||||
|
||||
if (data.status === 'error') {
|
||||
const details =
|
||||
data.errorData !== undefined && data.errorData !== null
|
||||
? ` (${JSON.stringify(data.errorData)})`
|
||||
: ''
|
||||
throw new Error(
|
||||
`Convex ${functionType} failed: ${data.errorMessage || 'Unknown error'}${details}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
value: data.value ?? null,
|
||||
logLines: data.logLines ?? [],
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user