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,165 @@
|
||||
import type {
|
||||
ShopifyAdjustInventoryParams,
|
||||
ShopifyInventoryAdjustmentResponse,
|
||||
} from '@/tools/shopify/types'
|
||||
import { INVENTORY_ADJUSTMENT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyAdjustInventoryTool: ToolConfig<
|
||||
ShopifyAdjustInventoryParams,
|
||||
ShopifyInventoryAdjustmentResponse
|
||||
> = {
|
||||
id: 'shopify_adjust_inventory',
|
||||
name: 'Shopify Adjust Inventory',
|
||||
description: 'Adjust inventory quantity for a product variant at a specific location',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
inventoryItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Inventory item ID (gid://shopify/InventoryItem/123456789)',
|
||||
},
|
||||
locationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Location ID (gid://shopify/Location/123456789)',
|
||||
},
|
||||
delta: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Amount to adjust (positive to increase, negative to decrease)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.inventoryItemId) {
|
||||
throw new Error('Inventory item ID is required')
|
||||
}
|
||||
if (!params.locationId) {
|
||||
throw new Error('Location ID is required')
|
||||
}
|
||||
if (params.delta === undefined || params.delta === null) {
|
||||
throw new Error('Delta is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) {
|
||||
inventoryAdjustQuantities(input: $input) {
|
||||
inventoryAdjustmentGroup {
|
||||
createdAt
|
||||
reason
|
||||
changes {
|
||||
name
|
||||
delta
|
||||
quantityAfterChange
|
||||
item {
|
||||
id
|
||||
sku
|
||||
}
|
||||
location {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
reason: 'correction',
|
||||
name: 'available',
|
||||
changes: [
|
||||
{
|
||||
inventoryItemId: params.inventoryItemId.trim(),
|
||||
locationId: params.locationId.trim(),
|
||||
delta: params.delta,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to adjust inventory',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.inventoryAdjustQuantities
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const adjustmentGroup = result?.inventoryAdjustmentGroup
|
||||
if (!adjustmentGroup) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Inventory adjustment was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inventoryLevel: {
|
||||
adjustmentGroup,
|
||||
changes: adjustmentGroup.changes,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inventoryLevel: {
|
||||
type: 'object',
|
||||
description: 'The inventory adjustment result',
|
||||
properties: INVENTORY_ADJUSTMENT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { ShopifyCancelOrderParams, ShopifyCancelOrderResponse } from '@/tools/shopify/types'
|
||||
import { CANCEL_ORDER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyCancelOrderTool: ToolConfig<
|
||||
ShopifyCancelOrderParams,
|
||||
ShopifyCancelOrderResponse
|
||||
> = {
|
||||
id: 'shopify_cancel_order',
|
||||
name: 'Shopify Cancel Order',
|
||||
description: 'Cancel an order in your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order ID to cancel (gid://shopify/Order/123456789)',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cancellation reason (CUSTOMER, DECLINED, FRAUD, INVENTORY, STAFF, OTHER)',
|
||||
},
|
||||
notifyCustomer: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to notify the customer about the cancellation',
|
||||
},
|
||||
restock: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to restock the inventory committed to the order',
|
||||
},
|
||||
refundMethod: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional refund method object, for example {"originalPaymentMethodsRefund": true}',
|
||||
},
|
||||
staffNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'A note about the cancellation for staff reference',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.orderId) {
|
||||
throw new Error('Order ID is required to cancel an order')
|
||||
}
|
||||
if (!params.reason) {
|
||||
throw new Error('Cancellation reason is required')
|
||||
}
|
||||
if (typeof params.restock !== 'boolean') {
|
||||
throw new Error('Restock is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation orderCancel($orderId: ID!, $reason: OrderCancelReason!, $notifyCustomer: Boolean, $refundMethod: OrderCancelRefundMethodInput, $restock: Boolean!, $staffNote: String) {
|
||||
orderCancel(orderId: $orderId, reason: $reason, notifyCustomer: $notifyCustomer, refundMethod: $refundMethod, restock: $restock, staffNote: $staffNote) {
|
||||
job {
|
||||
id
|
||||
done
|
||||
}
|
||||
orderCancelUserErrors {
|
||||
field
|
||||
message
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
orderId: params.orderId.trim(),
|
||||
reason: params.reason,
|
||||
notifyCustomer: params.notifyCustomer ?? false,
|
||||
refundMethod: params.refundMethod ?? null,
|
||||
restock: params.restock,
|
||||
staffNote: params.staffNote?.trim() || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to cancel order',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.orderCancel
|
||||
if (result?.orderCancelUserErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.orderCancelUserErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
order: {
|
||||
id: result?.job?.id,
|
||||
cancelled: result?.job?.done ?? true,
|
||||
message: 'Order cancellation initiated',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
order: {
|
||||
type: 'object',
|
||||
description: 'The cancellation result',
|
||||
properties: CANCEL_ORDER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { ShopifyCreateCustomerParams, ShopifyCustomerResponse } from '@/tools/shopify/types'
|
||||
import { CUSTOMER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyCreateCustomerTool: ToolConfig<
|
||||
ShopifyCreateCustomerParams,
|
||||
ShopifyCustomerResponse
|
||||
> = {
|
||||
id: 'shopify_create_customer',
|
||||
name: 'Shopify Create Customer',
|
||||
description: 'Create a new customer in your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer email address',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer last name',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer phone number',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Note about the customer',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer tags',
|
||||
},
|
||||
addresses: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer addresses',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
// Shopify requires at least one of: email, phone, firstName, or lastName
|
||||
const hasEmail = params.email?.trim()
|
||||
const hasPhone = params.phone?.trim()
|
||||
const hasFirstName = params.firstName?.trim()
|
||||
const hasLastName = params.lastName?.trim()
|
||||
|
||||
if (!hasEmail && !hasPhone && !hasFirstName && !hasLastName) {
|
||||
throw new Error('Customer must have at least one of: email, phone, firstName, or lastName')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = {}
|
||||
|
||||
if (hasEmail) {
|
||||
input.email = params.email
|
||||
}
|
||||
if (hasFirstName) {
|
||||
input.firstName = params.firstName
|
||||
}
|
||||
if (hasLastName) {
|
||||
input.lastName = params.lastName
|
||||
}
|
||||
if (hasPhone) {
|
||||
input.phone = params.phone
|
||||
}
|
||||
if (params.note) {
|
||||
input.note = params.note
|
||||
}
|
||||
if (params.tags && Array.isArray(params.tags)) {
|
||||
input.tags = params.tags
|
||||
}
|
||||
if (params.addresses && Array.isArray(params.addresses)) {
|
||||
input.addresses = params.addresses
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation customerCreate($input: CustomerInput!) {
|
||||
customerCreate(input: $input) {
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
note
|
||||
tags
|
||||
amountSpent {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
addresses {
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
phone
|
||||
}
|
||||
defaultAddress {
|
||||
address1
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to create customer',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.customerCreate
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const customer = result?.customer
|
||||
if (!customer) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Customer creation was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customer,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customer: {
|
||||
type: 'object',
|
||||
description: 'The created customer',
|
||||
properties: CUSTOMER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import type {
|
||||
ShopifyCreateFulfillmentParams,
|
||||
ShopifyFulfillmentResponse,
|
||||
} from '@/tools/shopify/types'
|
||||
import { FULFILLMENT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyCreateFulfillmentTool: ToolConfig<
|
||||
ShopifyCreateFulfillmentParams,
|
||||
ShopifyFulfillmentResponse
|
||||
> = {
|
||||
id: 'shopify_create_fulfillment',
|
||||
name: 'Shopify Create Fulfillment',
|
||||
description:
|
||||
'Create a fulfillment to mark order items as shipped. Requires a fulfillment order ID (get this from the order details).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
fulfillmentOrderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The fulfillment order ID (e.g., gid://shopify/FulfillmentOrder/123456789)',
|
||||
},
|
||||
trackingNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tracking number for the shipment',
|
||||
},
|
||||
trackingCompany: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Shipping carrier name (e.g., UPS, FedEx, USPS, DHL)',
|
||||
},
|
||||
trackingUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL to track the shipment',
|
||||
},
|
||||
notifyCustomer: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to send a shipping confirmation email to the customer (default: true)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
// Build tracking info if any tracking details provided
|
||||
const trackingInfo: {
|
||||
number?: string
|
||||
company?: string
|
||||
url?: string
|
||||
} = {}
|
||||
|
||||
if (params.trackingNumber) {
|
||||
trackingInfo.number = params.trackingNumber
|
||||
}
|
||||
if (params.trackingCompany) {
|
||||
trackingInfo.company = params.trackingCompany
|
||||
}
|
||||
if (params.trackingUrl) {
|
||||
trackingInfo.url = params.trackingUrl
|
||||
}
|
||||
|
||||
const fulfillmentInput: {
|
||||
lineItemsByFulfillmentOrder: Array<{ fulfillmentOrderId: string }>
|
||||
notifyCustomer?: boolean
|
||||
trackingInfo?: typeof trackingInfo
|
||||
} = {
|
||||
lineItemsByFulfillmentOrder: [
|
||||
{
|
||||
fulfillmentOrderId: params.fulfillmentOrderId.trim(),
|
||||
},
|
||||
],
|
||||
notifyCustomer: params.notifyCustomer !== false, // Default to true
|
||||
}
|
||||
|
||||
// Only include trackingInfo if we have at least one tracking field
|
||||
if (Object.keys(trackingInfo).length > 0) {
|
||||
fulfillmentInput.trackingInfo = trackingInfo
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation fulfillmentCreate($fulfillment: FulfillmentInput!) {
|
||||
fulfillmentCreate(fulfillment: $fulfillment) {
|
||||
fulfillment {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
trackingInfo {
|
||||
company
|
||||
number
|
||||
url
|
||||
}
|
||||
fulfillmentLineItems(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
lineItem {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fulfillment: fulfillmentInput,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to create fulfillment',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.fulfillmentCreate
|
||||
if (!result) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to create fulfillment',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (result.userErrors && result.userErrors.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const fulfillment = result.fulfillment
|
||||
if (!fulfillment) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No fulfillment returned',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
// Transform fulfillment line items from edges format
|
||||
const fulfillmentLineItems =
|
||||
fulfillment.fulfillmentLineItems?.edges?.map((edge: { node: unknown }) => edge.node) || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
fulfillment: {
|
||||
id: fulfillment.id,
|
||||
status: fulfillment.status,
|
||||
createdAt: fulfillment.createdAt,
|
||||
updatedAt: fulfillment.updatedAt,
|
||||
trackingInfo: fulfillment.trackingInfo || [],
|
||||
fulfillmentLineItems,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
fulfillment: {
|
||||
type: 'object',
|
||||
description: 'The created fulfillment with tracking info and fulfilled items',
|
||||
properties: FULFILLMENT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import type { ShopifyCreateProductParams, ShopifyProductResponse } from '@/tools/shopify/types'
|
||||
import { PRODUCT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyCreateProductTool: ToolConfig<
|
||||
ShopifyCreateProductParams,
|
||||
ShopifyProductResponse
|
||||
> = {
|
||||
id: 'shopify_create_product',
|
||||
name: 'Shopify Create Product',
|
||||
description: 'Create a new product in your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product title',
|
||||
},
|
||||
descriptionHtml: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product description (HTML)',
|
||||
},
|
||||
vendor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product vendor/brand',
|
||||
},
|
||||
productType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product type/category',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product tags',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product status (ACTIVE, DRAFT, ARCHIVED)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.title || !params.title.trim()) {
|
||||
throw new Error('Title is required to create a Shopify product')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
}
|
||||
|
||||
if (params.descriptionHtml) {
|
||||
input.descriptionHtml = params.descriptionHtml
|
||||
}
|
||||
if (params.vendor) {
|
||||
input.vendor = params.vendor
|
||||
}
|
||||
if (params.productType) {
|
||||
input.productType = params.productType
|
||||
}
|
||||
if (params.tags && Array.isArray(params.tags)) {
|
||||
input.tags = params.tags
|
||||
}
|
||||
if (params.status) {
|
||||
input.status = params.status
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation productCreate($product: ProductCreateInput!) {
|
||||
productCreate(product: $product) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
handle
|
||||
descriptionHtml
|
||||
vendor
|
||||
productType
|
||||
tags
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price
|
||||
compareAtPrice
|
||||
sku
|
||||
inventoryQuantity
|
||||
}
|
||||
}
|
||||
}
|
||||
images(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
product: input,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to create product',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.productCreate
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const product = result?.product
|
||||
if (!product) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Product creation was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
product: {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
handle: product.handle,
|
||||
descriptionHtml: product.descriptionHtml,
|
||||
vendor: product.vendor,
|
||||
productType: product.productType,
|
||||
tags: product.tags,
|
||||
status: product.status,
|
||||
createdAt: product.createdAt,
|
||||
updatedAt: product.updatedAt,
|
||||
variants: product.variants,
|
||||
images: product.images,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
product: {
|
||||
type: 'object',
|
||||
description: 'The created product',
|
||||
properties: PRODUCT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { ShopifyDeleteCustomerParams, ShopifyDeleteResponse } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyDeleteCustomerTool: ToolConfig<
|
||||
ShopifyDeleteCustomerParams,
|
||||
ShopifyDeleteResponse
|
||||
> = {
|
||||
id: 'shopify_delete_customer',
|
||||
name: 'Shopify Delete Customer',
|
||||
description: 'Delete a customer from your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
customerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer ID to delete (gid://shopify/Customer/123456789)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.customerId) {
|
||||
throw new Error('Customer ID is required to delete a customer')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation customerDelete($input: CustomerDeleteInput!) {
|
||||
customerDelete(input: $input) {
|
||||
deletedCustomerId
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: params.customerId,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to delete customer',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.customerDelete
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!result?.deletedCustomerId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Customer deletion was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deletedId: result.deletedCustomerId,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deletedId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the deleted customer',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { ShopifyDeleteProductParams, ShopifyDeleteResponse } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyDeleteProductTool: ToolConfig<
|
||||
ShopifyDeleteProductParams,
|
||||
ShopifyDeleteResponse
|
||||
> = {
|
||||
id: 'shopify_delete_product',
|
||||
name: 'Shopify Delete Product',
|
||||
description: 'Delete a product from your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product ID to delete (gid://shopify/Product/123456789)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.productId) {
|
||||
throw new Error('Product ID is required to delete a product')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation productDelete($input: ProductDeleteInput!) {
|
||||
productDelete(input: $input) {
|
||||
deletedProductId
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: params.productId,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to delete product',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.productDelete
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!result?.deletedProductId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Product deletion was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deletedId: result.deletedProductId,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deletedId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the deleted product',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { ShopifyCollectionResponse, ShopifyGetCollectionParams } from '@/tools/shopify/types'
|
||||
import { COLLECTION_WITH_PRODUCTS_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyGetCollectionTool: ToolConfig<
|
||||
ShopifyGetCollectionParams,
|
||||
ShopifyCollectionResponse
|
||||
> = {
|
||||
id: 'shopify_get_collection',
|
||||
name: 'Shopify Get Collection',
|
||||
description:
|
||||
'Get a specific collection by ID, including its products. Use this to retrieve products within a collection.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The collection ID (e.g., gid://shopify/Collection/123456789)',
|
||||
},
|
||||
productsFirst: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of products to return from this collection (default: 50, max: 250)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const productsFirst = Math.min(params.productsFirst || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query getCollection($id: ID!, $productsFirst: Int!) {
|
||||
collection(id: $id) {
|
||||
id
|
||||
title
|
||||
handle
|
||||
description
|
||||
descriptionHtml
|
||||
productsCount {
|
||||
count
|
||||
}
|
||||
sortOrder
|
||||
updatedAt
|
||||
image {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
products(first: $productsFirst) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
handle
|
||||
status
|
||||
vendor
|
||||
productType
|
||||
totalInventory
|
||||
featuredMedia {
|
||||
preview {
|
||||
image {
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.collectionId.trim(),
|
||||
productsFirst,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to get collection',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const collection = data.data?.collection
|
||||
if (!collection) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Collection not found',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
// Transform products from edges format and map featuredMedia to featuredImage
|
||||
const products =
|
||||
collection.products?.edges?.map(
|
||||
(edge: {
|
||||
node: {
|
||||
id: string
|
||||
title: string
|
||||
handle: string
|
||||
status: string
|
||||
vendor: string
|
||||
productType: string
|
||||
totalInventory: number
|
||||
featuredMedia?: {
|
||||
preview?: {
|
||||
image?: {
|
||||
url: string
|
||||
altText: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) => {
|
||||
const product = edge.node
|
||||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
handle: product.handle,
|
||||
status: product.status,
|
||||
vendor: product.vendor,
|
||||
productType: product.productType,
|
||||
totalInventory: product.totalInventory,
|
||||
featuredImage: product.featuredMedia?.preview?.image || null,
|
||||
}
|
||||
}
|
||||
) || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
collection: {
|
||||
id: collection.id,
|
||||
title: collection.title,
|
||||
handle: collection.handle,
|
||||
description: collection.description,
|
||||
descriptionHtml: collection.descriptionHtml,
|
||||
productsCount: collection.productsCount?.count ?? 0,
|
||||
sortOrder: collection.sortOrder,
|
||||
updatedAt: collection.updatedAt,
|
||||
image: collection.image,
|
||||
products,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'object',
|
||||
description: 'The collection details including its products',
|
||||
properties: COLLECTION_WITH_PRODUCTS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ShopifyCustomerResponse, ShopifyGetCustomerParams } from '@/tools/shopify/types'
|
||||
import { CUSTOMER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyGetCustomerTool: ToolConfig<ShopifyGetCustomerParams, ShopifyCustomerResponse> =
|
||||
{
|
||||
id: 'shopify_get_customer',
|
||||
name: 'Shopify Get Customer',
|
||||
description: 'Get a single customer by ID from your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
customerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer ID (gid://shopify/Customer/123456789)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.customerId) {
|
||||
throw new Error('Customer ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
query getCustomer($id: ID!) {
|
||||
customer(id: $id) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
note
|
||||
tags
|
||||
amountSpent {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
addresses {
|
||||
firstName
|
||||
lastName
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
provinceCode
|
||||
country
|
||||
countryCode
|
||||
zip
|
||||
phone
|
||||
}
|
||||
defaultAddress {
|
||||
firstName
|
||||
lastName
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.customerId,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to get customer',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const customer = data.data?.customer
|
||||
if (!customer) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Customer not found',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customer,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customer: {
|
||||
type: 'object',
|
||||
description: 'The customer details',
|
||||
properties: CUSTOMER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import type {
|
||||
ShopifyGetInventoryLevelParams,
|
||||
ShopifyInventoryResponse,
|
||||
} from '@/tools/shopify/types'
|
||||
import { INVENTORY_LEVEL_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyGetInventoryLevelTool: ToolConfig<
|
||||
ShopifyGetInventoryLevelParams,
|
||||
ShopifyInventoryResponse
|
||||
> = {
|
||||
id: 'shopify_get_inventory_level',
|
||||
name: 'Shopify Get Inventory Level',
|
||||
description: 'Get inventory level for a product variant at a specific location',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
inventoryItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Inventory item ID (gid://shopify/InventoryItem/123456789)',
|
||||
},
|
||||
locationId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Location ID to filter by (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.inventoryItemId) {
|
||||
throw new Error('Inventory item ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
query getInventoryItem($id: ID!) {
|
||||
inventoryItem(id: $id) {
|
||||
id
|
||||
sku
|
||||
tracked
|
||||
inventoryLevels(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantities(names: ["available", "on_hand", "committed", "incoming", "reserved"]) {
|
||||
name
|
||||
quantity
|
||||
}
|
||||
location {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.inventoryItemId.trim(),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to get inventory level',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const inventoryItem = data.data?.inventoryItem
|
||||
if (!inventoryItem) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Inventory item not found',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const requestedLocationId = params?.locationId?.trim()
|
||||
const inventoryLevels = inventoryItem.inventoryLevels.edges
|
||||
.map(
|
||||
(edge: {
|
||||
node: {
|
||||
id: string
|
||||
quantities: Array<{ name: string; quantity: number }>
|
||||
location: { id: string; name: string }
|
||||
}
|
||||
}) => {
|
||||
const node = edge.node
|
||||
// Extract quantities into a more usable format
|
||||
const quantitiesMap: Record<string, number> = {}
|
||||
node.quantities.forEach((q) => {
|
||||
quantitiesMap[q.name] = q.quantity
|
||||
})
|
||||
return {
|
||||
id: node.id,
|
||||
available: quantitiesMap.available ?? 0,
|
||||
onHand: quantitiesMap.on_hand ?? 0,
|
||||
committed: quantitiesMap.committed ?? 0,
|
||||
incoming: quantitiesMap.incoming ?? 0,
|
||||
reserved: quantitiesMap.reserved ?? 0,
|
||||
location: node.location,
|
||||
}
|
||||
}
|
||||
)
|
||||
.filter(
|
||||
(level: { location: { id: string } }) =>
|
||||
!requestedLocationId || level.location.id === requestedLocationId
|
||||
)
|
||||
|
||||
if (requestedLocationId && inventoryLevels.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No inventory level found for the provided location',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inventoryLevel: {
|
||||
id: inventoryItem.id,
|
||||
sku: inventoryItem.sku,
|
||||
tracked: inventoryItem.tracked,
|
||||
levels: inventoryLevels,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inventoryLevel: {
|
||||
type: 'object',
|
||||
description: 'The inventory level details',
|
||||
properties: INVENTORY_LEVEL_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import type { ShopifyGetOrderParams, ShopifyOrderResponse } from '@/tools/shopify/types'
|
||||
import { ORDER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyGetOrderTool: ToolConfig<ShopifyGetOrderParams, ShopifyOrderResponse> = {
|
||||
id: 'shopify_get_order',
|
||||
name: 'Shopify Get Order',
|
||||
description: 'Get a single order by ID from your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order ID (gid://shopify/Order/123456789)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.orderId) {
|
||||
throw new Error('Order ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
query getOrder($id: ID!) {
|
||||
order(id: $id) {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
cancelledAt
|
||||
closedAt
|
||||
displayFinancialStatus
|
||||
displayFulfillmentStatus
|
||||
totalPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
subtotalPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
totalTaxSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
totalShippingPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
note
|
||||
tags
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
}
|
||||
lineItems(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
quantity
|
||||
variant {
|
||||
id
|
||||
title
|
||||
price
|
||||
sku
|
||||
}
|
||||
originalTotalSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
discountedTotalSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shippingAddress {
|
||||
firstName
|
||||
lastName
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
provinceCode
|
||||
country
|
||||
countryCode
|
||||
zip
|
||||
phone
|
||||
}
|
||||
billingAddress {
|
||||
firstName
|
||||
lastName
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
provinceCode
|
||||
country
|
||||
countryCode
|
||||
zip
|
||||
phone
|
||||
}
|
||||
fulfillments {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
trackingInfo {
|
||||
company
|
||||
number
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.orderId,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to get order',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const order = data.data?.order
|
||||
if (!order) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Order not found',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
order,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
order: {
|
||||
type: 'object',
|
||||
description: 'The order details',
|
||||
properties: ORDER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { ShopifyGetProductParams, ShopifyProductResponse } from '@/tools/shopify/types'
|
||||
import { PRODUCT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyGetProductTool: ToolConfig<ShopifyGetProductParams, ShopifyProductResponse> = {
|
||||
id: 'shopify_get_product',
|
||||
name: 'Shopify Get Product',
|
||||
description: 'Get a single product by ID from your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product ID (gid://shopify/Product/123456789)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.productId) {
|
||||
throw new Error('Product ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
query getProduct($id: ID!) {
|
||||
product(id: $id) {
|
||||
id
|
||||
title
|
||||
handle
|
||||
descriptionHtml
|
||||
vendor
|
||||
productType
|
||||
tags
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
variants(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price
|
||||
compareAtPrice
|
||||
sku
|
||||
inventoryQuantity
|
||||
}
|
||||
}
|
||||
}
|
||||
images(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.productId,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to get product',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const product = data.data?.product
|
||||
if (!product) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
product,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
product: {
|
||||
type: 'object',
|
||||
description: 'The product details',
|
||||
properties: PRODUCT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Product Tools
|
||||
|
||||
export { shopifyAdjustInventoryTool } from './adjust_inventory'
|
||||
export { shopifyCancelOrderTool } from './cancel_order'
|
||||
// Customer Tools
|
||||
export { shopifyCreateCustomerTool } from './create_customer'
|
||||
// Fulfillment Tools
|
||||
export { shopifyCreateFulfillmentTool } from './create_fulfillment'
|
||||
export { shopifyCreateProductTool } from './create_product'
|
||||
export { shopifyDeleteCustomerTool } from './delete_customer'
|
||||
export { shopifyDeleteProductTool } from './delete_product'
|
||||
export { shopifyGetCollectionTool } from './get_collection'
|
||||
export { shopifyGetCustomerTool } from './get_customer'
|
||||
export { shopifyGetInventoryLevelTool } from './get_inventory_level'
|
||||
// Order Tools
|
||||
export { shopifyGetOrderTool } from './get_order'
|
||||
export { shopifyGetProductTool } from './get_product'
|
||||
// Collection Tools
|
||||
export { shopifyListCollectionsTool } from './list_collections'
|
||||
export { shopifyListCustomersTool } from './list_customers'
|
||||
// Inventory Tools
|
||||
export { shopifyListInventoryItemsTool } from './list_inventory_items'
|
||||
// Location Tools
|
||||
export { shopifyListLocationsTool } from './list_locations'
|
||||
export { shopifyListOrdersTool } from './list_orders'
|
||||
export { shopifyListProductsTool } from './list_products'
|
||||
export * from './types'
|
||||
export { shopifyUpdateCustomerTool } from './update_customer'
|
||||
export { shopifyUpdateOrderTool } from './update_order'
|
||||
export { shopifyUpdateProductTool } from './update_product'
|
||||
@@ -0,0 +1,169 @@
|
||||
import type {
|
||||
ShopifyCollectionsResponse,
|
||||
ShopifyListCollectionsParams,
|
||||
} from '@/tools/shopify/types'
|
||||
import { COLLECTION_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListCollectionsTool: ToolConfig<
|
||||
ShopifyListCollectionsParams,
|
||||
ShopifyCollectionsResponse
|
||||
> = {
|
||||
id: 'shopify_list_collections',
|
||||
name: 'Shopify List Collections',
|
||||
description:
|
||||
'List product collections from your Shopify store. Filter by title, type (custom/smart), or handle.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of collections to return (default: 50, max: 250)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query to filter collections (e.g., "title:Summer" or "collection_type:smart")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listCollections($first: Int!, $query: String) {
|
||||
collections(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
handle
|
||||
description
|
||||
descriptionHtml
|
||||
productsCount {
|
||||
count
|
||||
}
|
||||
sortOrder
|
||||
updatedAt
|
||||
image {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
query: params.query || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list collections',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const collectionsData = data.data?.collections
|
||||
if (!collectionsData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve collections',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const collections = collectionsData.edges.map(
|
||||
(edge: {
|
||||
node: {
|
||||
id: string
|
||||
title: string
|
||||
handle: string
|
||||
description: string | null
|
||||
descriptionHtml: string | null
|
||||
productsCount: { count: number }
|
||||
sortOrder: string
|
||||
updatedAt: string
|
||||
image: { id: string; url: string; altText: string | null } | null
|
||||
}
|
||||
}) => ({
|
||||
id: edge.node.id,
|
||||
title: edge.node.title,
|
||||
handle: edge.node.handle,
|
||||
description: edge.node.description,
|
||||
descriptionHtml: edge.node.descriptionHtml,
|
||||
productsCount: edge.node.productsCount?.count ?? 0,
|
||||
sortOrder: edge.node.sortOrder,
|
||||
updatedAt: edge.node.updatedAt,
|
||||
image: edge.node.image,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
collections,
|
||||
pageInfo: collectionsData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collections: {
|
||||
type: 'array',
|
||||
description: 'List of collections with their IDs, titles, and product counts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: COLLECTION_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { ShopifyCustomersResponse, ShopifyListCustomersParams } from '@/tools/shopify/types'
|
||||
import { CUSTOMER_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListCustomersTool: ToolConfig<
|
||||
ShopifyListCustomersParams,
|
||||
ShopifyCustomersResponse
|
||||
> = {
|
||||
id: 'shopify_list_customers',
|
||||
name: 'Shopify List Customers',
|
||||
description: 'List customers from your Shopify store with optional filtering',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of customers to return (default: 50, max: 250)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query to filter customers (e.g., "first_name:John" or "last_name:Smith" or "email:*@gmail.com" or "tag:vip")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listCustomers($first: Int!, $query: String) {
|
||||
customers(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
note
|
||||
tags
|
||||
amountSpent {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
defaultAddress {
|
||||
address1
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
query: params.query || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list customers',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const customersData = data.data?.customers
|
||||
if (!customersData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve customers',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const customers = customersData.edges.map((edge: { node: unknown }) => edge.node)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customers,
|
||||
pageInfo: customersData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customers: {
|
||||
type: 'array',
|
||||
description: 'List of customers',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: CUSTOMER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
INVENTORY_ITEM_OUTPUT_PROPERTIES,
|
||||
PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
type ShopifyInventoryItemsResponse,
|
||||
type ShopifyListInventoryItemsParams,
|
||||
} from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListInventoryItemsTool: ToolConfig<
|
||||
ShopifyListInventoryItemsParams,
|
||||
ShopifyInventoryItemsResponse
|
||||
> = {
|
||||
id: 'shopify_list_inventory_items',
|
||||
name: 'Shopify List Inventory Items',
|
||||
description:
|
||||
'List inventory items from your Shopify store. Use this to find inventory item IDs by SKU.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of inventory items to return (default: 50, max: 250)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query to filter inventory items (e.g., "sku:ABC123")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listInventoryItems($first: Int!, $query: String) {
|
||||
inventoryItems(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
sku
|
||||
tracked
|
||||
createdAt
|
||||
updatedAt
|
||||
variant {
|
||||
id
|
||||
title
|
||||
product {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
inventoryLevels(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantities(names: ["available", "on_hand"]) {
|
||||
name
|
||||
quantity
|
||||
}
|
||||
location {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
query: params.query || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list inventory items',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const inventoryItemsData = data.data?.inventoryItems
|
||||
if (!inventoryItemsData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve inventory items',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const inventoryItems = inventoryItemsData.edges.map(
|
||||
(edge: {
|
||||
node: {
|
||||
id: string
|
||||
sku: string | null
|
||||
tracked: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
variant?: {
|
||||
id: string
|
||||
title: string
|
||||
product?: {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
inventoryLevels: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string
|
||||
quantities: Array<{ name: string; quantity: number }>
|
||||
location: { id: string; name: string }
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
}) => {
|
||||
const node = edge.node
|
||||
// Transform inventory levels to include available quantity
|
||||
const inventoryLevels = node.inventoryLevels.edges.map((levelEdge) => {
|
||||
const levelNode = levelEdge.node
|
||||
const availableQty =
|
||||
levelNode.quantities.find((q) => q.name === 'available')?.quantity ?? 0
|
||||
return {
|
||||
id: levelNode.id,
|
||||
available: availableQty,
|
||||
location: levelNode.location,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
sku: node.sku,
|
||||
tracked: node.tracked,
|
||||
createdAt: node.createdAt,
|
||||
updatedAt: node.updatedAt,
|
||||
variant: node.variant,
|
||||
inventoryLevels,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inventoryItems,
|
||||
pageInfo: inventoryItemsData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inventoryItems: {
|
||||
type: 'array',
|
||||
description: 'List of inventory items with their IDs, SKUs, and stock levels',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: INVENTORY_ITEM_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { ShopifyListLocationsParams, ShopifyLocationsResponse } from '@/tools/shopify/types'
|
||||
import { LOCATION_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListLocationsTool: ToolConfig<
|
||||
ShopifyListLocationsParams,
|
||||
ShopifyLocationsResponse
|
||||
> = {
|
||||
id: 'shopify_list_locations',
|
||||
name: 'Shopify List Locations',
|
||||
description:
|
||||
'List inventory locations from your Shopify store. Use this to find location IDs needed for inventory operations.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of locations to return (default: 50, max: 250)',
|
||||
},
|
||||
includeInactive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include deactivated locations (default: false)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listLocations($first: Int!, $includeInactive: Boolean) {
|
||||
locations(first: $first, includeInactive: $includeInactive) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
fulfillsOnlineOrders
|
||||
address {
|
||||
address1
|
||||
address2
|
||||
city
|
||||
province
|
||||
provinceCode
|
||||
country
|
||||
countryCode
|
||||
zip
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
includeInactive: params.includeInactive || false,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list locations',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const locationsData = data.data?.locations
|
||||
if (!locationsData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve locations',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const locations = locationsData.edges.map((edge: { node: unknown }) => edge.node)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
locations,
|
||||
pageInfo: locationsData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
locations: {
|
||||
type: 'array',
|
||||
description: 'List of locations with their IDs, names, and addresses',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: LOCATION_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { ShopifyListOrdersParams, ShopifyOrdersResponse } from '@/tools/shopify/types'
|
||||
import { ORDER_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListOrdersTool: ToolConfig<ShopifyListOrdersParams, ShopifyOrdersResponse> = {
|
||||
id: 'shopify_list_orders',
|
||||
name: 'Shopify List Orders',
|
||||
description: 'List orders from your Shopify store with optional filtering',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of orders to return (default: 50, max: 250)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by order status (open, closed, cancelled, any)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query to filter orders (e.g., "financial_status:paid" or "fulfillment_status:unfulfilled" or "email:customer@example.com")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
// Build query string with status filter if provided
|
||||
const queryParts: string[] = []
|
||||
if (params.status && params.status !== 'any') {
|
||||
queryParts.push(`status:${params.status}`)
|
||||
}
|
||||
if (params.query) {
|
||||
queryParts.push(params.query)
|
||||
}
|
||||
const queryString = queryParts.length > 0 ? queryParts.join(' ') : null
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listOrders($first: Int!, $query: String) {
|
||||
orders(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
cancelledAt
|
||||
closedAt
|
||||
displayFinancialStatus
|
||||
displayFulfillmentStatus
|
||||
totalPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
subtotalPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
note
|
||||
tags
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
lineItems(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
quantity
|
||||
variant {
|
||||
id
|
||||
title
|
||||
price
|
||||
sku
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shippingAddress {
|
||||
firstName
|
||||
lastName
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
query: queryString,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list orders',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const ordersData = data.data?.orders
|
||||
if (!ordersData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve orders',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const orders = ordersData.edges.map((edge: { node: unknown }) => edge.node)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
orders,
|
||||
pageInfo: ordersData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
orders: {
|
||||
type: 'array',
|
||||
description: 'List of orders',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: ORDER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { ShopifyListProductsParams, ShopifyProductsResponse } from '@/tools/shopify/types'
|
||||
import { PAGE_INFO_OUTPUT_PROPERTIES, PRODUCT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyListProductsTool: ToolConfig<
|
||||
ShopifyListProductsParams,
|
||||
ShopifyProductsResponse
|
||||
> = {
|
||||
id: 'shopify_list_products',
|
||||
name: 'Shopify List Products',
|
||||
description: 'List products from your Shopify store with optional filtering',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of products to return (default: 50, max: 250)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query to filter products (e.g., "title:shirt" or "vendor:Nike" or "status:active")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const first = Math.min(params.first || 50, 250)
|
||||
|
||||
return {
|
||||
query: `
|
||||
query listProducts($first: Int!, $query: String) {
|
||||
products(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
handle
|
||||
descriptionHtml
|
||||
vendor
|
||||
productType
|
||||
tags
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price
|
||||
compareAtPrice
|
||||
sku
|
||||
inventoryQuantity
|
||||
}
|
||||
}
|
||||
}
|
||||
images(first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first,
|
||||
query: params.query || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to list products',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const productsData = data.data?.products
|
||||
if (!productsData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve products',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const products = productsData.edges.map((edge: { node: unknown }) => edge.node)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
products,
|
||||
pageInfo: productsData.pageInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
products: {
|
||||
type: 'array',
|
||||
description: 'List of products',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: PRODUCT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: PAGE_INFO_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
import type { ShopifyCustomerResponse, ShopifyUpdateCustomerParams } from '@/tools/shopify/types'
|
||||
import { CUSTOMER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyUpdateCustomerTool: ToolConfig<
|
||||
ShopifyUpdateCustomerParams,
|
||||
ShopifyCustomerResponse
|
||||
> = {
|
||||
id: 'shopify_update_customer',
|
||||
name: 'Shopify Update Customer',
|
||||
description: 'Update an existing customer in your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
customerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Customer ID to update (gid://shopify/Customer/123456789)',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer email address',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer last name',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer phone number',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New note about the customer',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer tags',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.customerId) {
|
||||
throw new Error('Customer ID is required to update a customer')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = {
|
||||
id: params.customerId,
|
||||
}
|
||||
|
||||
if (params.email !== undefined) {
|
||||
input.email = params.email
|
||||
}
|
||||
if (params.firstName !== undefined) {
|
||||
input.firstName = params.firstName
|
||||
}
|
||||
if (params.lastName !== undefined) {
|
||||
input.lastName = params.lastName
|
||||
}
|
||||
if (params.phone !== undefined) {
|
||||
input.phone = params.phone
|
||||
}
|
||||
if (params.note !== undefined) {
|
||||
input.note = params.note
|
||||
}
|
||||
if (params.tags !== undefined) {
|
||||
input.tags = params.tags
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation customerUpdate($input: CustomerInput!) {
|
||||
customerUpdate(input: $input) {
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
note
|
||||
tags
|
||||
amountSpent {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
addresses {
|
||||
address1
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
defaultAddress {
|
||||
address1
|
||||
city
|
||||
province
|
||||
country
|
||||
zip
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to update customer',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.customerUpdate
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const customer = result?.customer
|
||||
if (!customer) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Customer update was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customer,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customer: {
|
||||
type: 'object',
|
||||
description: 'The updated customer',
|
||||
properties: CUSTOMER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { ShopifyOrderResponse, ShopifyUpdateOrderParams } from '@/tools/shopify/types'
|
||||
import { ORDER_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyUpdateOrderTool: ToolConfig<ShopifyUpdateOrderParams, ShopifyOrderResponse> = {
|
||||
id: 'shopify_update_order',
|
||||
name: 'Shopify Update Order',
|
||||
description: 'Update an existing order in your Shopify store (note, tags, email)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order ID to update (gid://shopify/Order/123456789)',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New order note',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New order tags',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New customer email for the order',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.orderId) {
|
||||
throw new Error('Order ID is required to update an order')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = {
|
||||
id: params.orderId,
|
||||
}
|
||||
|
||||
if (params.note !== undefined) {
|
||||
input.note = params.note
|
||||
}
|
||||
if (params.tags !== undefined) {
|
||||
input.tags = params.tags
|
||||
}
|
||||
if (params.email !== undefined) {
|
||||
input.email = params.email
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation orderUpdate($input: OrderInput!) {
|
||||
orderUpdate(input: $input) {
|
||||
order {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
createdAt
|
||||
updatedAt
|
||||
note
|
||||
tags
|
||||
displayFinancialStatus
|
||||
displayFulfillmentStatus
|
||||
totalPriceSet {
|
||||
shopMoney {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to update order',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.orderUpdate
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const order = result?.order
|
||||
if (!order) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Order update was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
order,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
order: {
|
||||
type: 'object',
|
||||
description: 'The updated order',
|
||||
properties: ORDER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import type { ShopifyProductResponse, ShopifyUpdateProductParams } from '@/tools/shopify/types'
|
||||
import { PRODUCT_OUTPUT_PROPERTIES } from '@/tools/shopify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const shopifyUpdateProductTool: ToolConfig<
|
||||
ShopifyUpdateProductParams,
|
||||
ShopifyProductResponse
|
||||
> = {
|
||||
id: 'shopify_update_product',
|
||||
name: 'Shopify Update Product',
|
||||
description: 'Update an existing product in your Shopify store',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'shopify',
|
||||
},
|
||||
|
||||
params: {
|
||||
shopDomain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Shopify store domain (e.g., mystore.myshopify.com)',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Product ID to update (gid://shopify/Product/123456789)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product title',
|
||||
},
|
||||
descriptionHtml: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product description (HTML)',
|
||||
},
|
||||
vendor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product vendor/brand',
|
||||
},
|
||||
productType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product type/category',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product tags',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New product status (ACTIVE, DRAFT, ARCHIVED)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Missing access token for Shopify API request')
|
||||
}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Access-Token': params.accessToken,
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.productId) {
|
||||
throw new Error('Product ID is required to update a product')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = {
|
||||
id: params.productId,
|
||||
}
|
||||
|
||||
if (params.title !== undefined) {
|
||||
input.title = params.title
|
||||
}
|
||||
if (params.descriptionHtml !== undefined) {
|
||||
input.descriptionHtml = params.descriptionHtml
|
||||
}
|
||||
if (params.vendor !== undefined) {
|
||||
input.vendor = params.vendor
|
||||
}
|
||||
if (params.productType !== undefined) {
|
||||
input.productType = params.productType
|
||||
}
|
||||
if (params.tags !== undefined) {
|
||||
input.tags = params.tags
|
||||
}
|
||||
if (params.status !== undefined) {
|
||||
input.status = params.status
|
||||
}
|
||||
|
||||
return {
|
||||
query: `
|
||||
mutation productUpdate($product: ProductUpdateInput!) {
|
||||
productUpdate(product: $product) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
handle
|
||||
descriptionHtml
|
||||
vendor
|
||||
productType
|
||||
tags
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price
|
||||
compareAtPrice
|
||||
sku
|
||||
inventoryQuantity
|
||||
}
|
||||
}
|
||||
}
|
||||
images(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
product: input,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors[0]?.message || 'Failed to update product',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data?.productUpdate
|
||||
if (result?.userErrors?.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.userErrors.map((e: { message: string }) => e.message).join(', '),
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const product = result?.product
|
||||
if (!product) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Product update was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
product,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
product: {
|
||||
type: 'object',
|
||||
description: 'The updated product',
|
||||
properties: PRODUCT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user