Products Block

Product catalog and inventory management API
The Products Block provides complete e-commerce product management. Create product catalogs, manage inventory, handle variants, organize into categories, and deliver product data to any frontend.
Features
- Product Catalog - Rich product data with unlimited attributes
- Inventory Management - Real-time stock tracking with alerts
- Product Variants - Size, color, and custom variant support
- Categories & Collections - Hierarchical organization
- Dynamic Pricing - Multi-currency and tiered pricing
- Product Search - Full-text search with filtering
Quick Start
npm install @23blocks/sdk
API Endpoint
| Service | URL |
|---|---|
| Products | https://products.api.us.23blocks.com |
Environment Routing: One URL, one environment. All shared-platform API keys are live keys:
pk_live_*/sk_live_*→ your production tenant. Need a separate staging environment? It is available as a paid add-on on deployments in your own cloud account — see 23blocks in a Box.
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { products: 'https://products.api.us.23blocks.com' },
apiKey: 'your-api-key', // Your live API key (pk_live_*)
});
// Create a product
const product = await client.products.create({
name: 'Premium Wireless Headphones',
sku: 'WH-PRO-2024',
price: { amount: 29900, currency: 'USD' },
inventory: { quantity: 100, track_stock: true }
});
// List products with filters
const products = await client.products.list({
category: 'electronics',
in_stock: true,
sort: '-created_at'
});
Authentication
All API requests require authentication headers:
curl -X GET "https://api.23blocks.com/v1/products" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-App-Id: YOUR_APP_ID" \
-H "Content-Type: application/json"
Products
Create Product
Create a new product in your catalog.
POST /v1/products
Request Body:
{
"data": {
"type": "products",
"attributes": {
"name": "Premium Wireless Headphones",
"sku": "WH-PRO-2024",
"description": "High-fidelity audio with active noise cancellation and 30-hour battery life.",
"short_description": "Premium wireless headphones with ANC",
"status": "active",
"type": "physical",
"price": {
"amount": 29900,
"currency": "USD",
"compare_at": 34900
},
"cost": {
"amount": 12000,
"currency": "USD"
},
"inventory": {
"quantity": 247,
"track_stock": true,
"allow_backorder": false,
"low_stock_threshold": 25
},
"weight": {
"value": 250,
"unit": "g"
},
"dimensions": {
"length": 20,
"width": 18,
"height": 8,
"unit": "cm"
},
"attributes": {
"brand": "AudioPro",
"battery_life": "30 hours",
"connectivity": "Bluetooth 5.3",
"driver_size": "40mm"
},
"seo": {
"title": "Premium Wireless Headphones | AudioPro",
"description": "Experience studio-quality sound with our premium wireless headphones.",
"slug": "premium-wireless-headphones"
},
"metadata": {
"warehouse_location": "A1-23"
}
},
"relationships": {
"categories": {
"data": [
{ "type": "categories", "id": "cat_electronics" },
{ "type": "categories", "id": "cat_audio" }
]
}
}
}
}
Response:
{
"data": {
"type": "products",
"id": "prod_abc123",
"attributes": {
"name": "Premium Wireless Headphones",
"sku": "WH-PRO-2024",
"description": "High-fidelity audio with active noise cancellation...",
"status": "active",
"type": "physical",
"price": {
"amount": 29900,
"currency": "USD",
"compare_at": 34900,
"formatted": "$299.00",
"compare_at_formatted": "$349.00"
},
"inventory": {
"quantity": 247,
"available": 247,
"reserved": 0,
"in_stock": true,
"track_stock": true,
"low_stock": false
},
"created_at": "2026-01-02T10:30:00Z",
"updated_at": "2026-01-02T10:30:00Z"
}
}
}
List Products
Retrieve a paginated list of products with filtering and sorting.
GET /v1/products
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
page[number] | integer | Page number (default: 1) |
page[size] | integer | Items per page (default: 20, max: 100) |
filter[status] | string | Filter by status: active, draft, archived |
filter[category] | string | Filter by category ID or slug |
filter[in_stock] | boolean | Filter by stock availability |
filter[type] | string | Filter by type: physical, digital, service |
filter[price_min] | integer | Minimum price in cents |
filter[price_max] | integer | Maximum price in cents |
filter[created_after] | datetime | Products created after date |
search | string | Full-text search query |
sort | string | Sort field: name, price, created_at, -created_at |
include | string | Include related resources: variants, categories, images |
Example:
GET /v1/products?filter[category]=electronics&filter[in_stock]=true&sort=-created_at&include=variants,images
Get Product
Retrieve a single product by ID or SKU.
GET /v1/products/:id
GET /v1/products/sku/:sku
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
include | string | Include: variants, categories, images, related |
Update Product
Update an existing product.
PATCH /v1/products/:id
Request Body:
{
"data": {
"type": "products",
"id": "prod_abc123",
"attributes": {
"price": {
"amount": 27900,
"compare_at": 29900
},
"status": "active"
}
}
}
Delete Product
Delete a product (soft delete by default).
DELETE /v1/products/:id
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
permanent | boolean | Permanently delete (default: false) |
Product Variants
Variants represent different versions of a product (size, color, material).
Create Variant
POST /v1/products/:product_id/variants
Request Body:
{
"data": {
"type": "variants",
"attributes": {
"name": "Black",
"sku": "WH-PRO-2024-BLK",
"options": {
"color": "black"
},
"price_modifier": {
"type": "fixed",
"amount": 0
},
"inventory": {
"quantity": 120,
"track_stock": true
},
"weight": {
"value": 250,
"unit": "g"
},
"position": 1
}
}
}
Response:
{
"data": {
"type": "variants",
"id": "var_xyz789",
"attributes": {
"name": "Black",
"sku": "WH-PRO-2024-BLK",
"options": {
"color": "black"
},
"price": {
"amount": 29900,
"currency": "USD",
"formatted": "$299.00"
},
"inventory": {
"quantity": 120,
"in_stock": true
},
"position": 1,
"created_at": "2026-01-02T10:35:00Z"
}
}
}
List Variants
GET /v1/products/:product_id/variants
Update Variant
PATCH /v1/products/:product_id/variants/:variant_id
Delete Variant
DELETE /v1/products/:product_id/variants/:variant_id
Variant Options
Define available options for variants:
PUT /v1/products/:product_id/options
Request Body:
{
"data": {
"options": [
{
"name": "Color",
"values": ["Black", "White", "Navy Blue"]
},
{
"name": "Size",
"values": ["S", "M", "L", "XL"]
}
]
}
}
Categories
Organize products into hierarchical categories.
Create Category
POST /v1/categories
Request Body:
{
"data": {
"type": "categories",
"attributes": {
"name": "Electronics",
"slug": "electronics",
"description": "Electronic devices and accessories",
"parent_id": null,
"position": 1,
"seo": {
"title": "Electronics - Shop the Latest Tech",
"description": "Browse our selection of electronics and gadgets."
},
"metadata": {
"icon": "electronics-icon"
}
}
}
}
List Categories
GET /v1/categories
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
filter[parent_id] | string | Filter by parent (null for root) |
include | string | Include: children, products, product_count |
tree | boolean | Return full category tree |
Get Category
GET /v1/categories/:id
GET /v1/categories/slug/:slug
Update Category
PATCH /v1/categories/:id
Delete Category
DELETE /v1/categories/:id
Assign Products to Category
POST /v1/categories/:id/products
Request Body:
{
"data": {
"product_ids": ["prod_abc123", "prod_def456"]
}
}
Collections
Curated product groupings for merchandising.
Create Collection
POST /v1/collections
Request Body:
{
"data": {
"type": "collections",
"attributes": {
"name": "Summer Sale 2026",
"slug": "summer-sale-2026",
"description": "Hot deals for summer",
"type": "manual",
"status": "active",
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-08-31T23:59:59Z",
"rules": null,
"seo": {
"title": "Summer Sale - Up to 50% Off",
"description": "Shop our biggest summer sale with discounts up to 50%."
}
}
}
}
Automated Collections
Create collections with automatic product matching:
{
"data": {
"type": "collections",
"attributes": {
"name": "New Arrivals",
"type": "automated",
"rules": {
"match": "all",
"conditions": [
{
"field": "created_at",
"operator": "greater_than",
"value": "30_days_ago"
},
{
"field": "status",
"operator": "equals",
"value": "active"
}
]
}
}
}
}
List Collections
GET /v1/collections
Get Collection Products
GET /v1/collections/:id/products
Add Products to Collection
POST /v1/collections/:id/products
Remove Products from Collection
DELETE /v1/collections/:id/products/:product_id
Inventory
Get Inventory Levels
GET /v1/products/:id/inventory
Response:
{
"data": {
"type": "inventory",
"id": "inv_abc123",
"attributes": {
"product_id": "prod_abc123",
"total_quantity": 247,
"available": 235,
"reserved": 12,
"committed": 0,
"in_stock": true,
"low_stock": false,
"locations": [
{
"location_id": "loc_warehouse1",
"name": "Main Warehouse",
"quantity": 200,
"available": 188
},
{
"location_id": "loc_store1",
"name": "Downtown Store",
"quantity": 47,
"available": 47
}
],
"updated_at": "2026-01-02T10:30:00Z"
}
}
}
Adjust Inventory
POST /v1/products/:id/inventory/adjust
Request Body:
{
"data": {
"adjustments": [
{
"location_id": "loc_warehouse1",
"quantity": 50,
"reason": "restock",
"reference": "PO-2026-001"
}
]
}
}
Set Inventory
Set absolute inventory levels:
PUT /v1/products/:id/inventory
Request Body:
{
"data": {
"locations": [
{
"location_id": "loc_warehouse1",
"quantity": 250
}
]
}
}
Reserve Inventory
Reserve stock for pending orders:
POST /v1/products/:id/inventory/reserve
Request Body:
{
"data": {
"quantity": 2,
"order_id": "ord_abc123",
"expires_at": "2026-01-02T11:30:00Z"
}
}
Response:
{
"data": {
"type": "reservation",
"id": "res_xyz789",
"attributes": {
"product_id": "prod_abc123",
"quantity": 2,
"order_id": "ord_abc123",
"status": "active",
"expires_at": "2026-01-02T11:30:00Z"
}
}
}
Release Reservation
DELETE /v1/inventory/reservations/:reservation_id
Inventory Locations
Manage warehouse and store locations:
POST /v1/inventory/locations
{
"data": {
"type": "locations",
"attributes": {
"name": "Main Warehouse",
"code": "WH-001",
"type": "warehouse",
"address": {
"line1": "123 Industrial Way",
"city": "Austin",
"state": "TX",
"postal_code": "78701",
"country": "US"
},
"is_default": true
}
}
}
Pricing
Price Tiers
Set up volume-based pricing:
PUT /v1/products/:id/price-tiers
Request Body:
{
"data": {
"tiers": [
{
"min_quantity": 1,
"max_quantity": 9,
"price": { "amount": 29900, "currency": "USD" }
},
{
"min_quantity": 10,
"max_quantity": 49,
"price": { "amount": 27900, "currency": "USD" }
},
{
"min_quantity": 50,
"max_quantity": null,
"price": { "amount": 24900, "currency": "USD" }
}
]
}
}
Customer-Specific Pricing
Set prices for specific customers or groups:
PUT /v1/products/:id/customer-prices
Request Body:
{
"data": {
"prices": [
{
"customer_group_id": "grp_wholesale",
"price": { "amount": 19900, "currency": "USD" }
},
{
"customer_id": "cust_vip123",
"price": { "amount": 22900, "currency": "USD" }
}
]
}
}
Multi-Currency Prices
PUT /v1/products/:id/prices
Request Body:
{
"data": {
"prices": [
{ "amount": 29900, "currency": "USD" },
{ "amount": 27900, "currency": "EUR" },
{ "amount": 3990000, "currency": "JPY" }
]
}
}
Scheduled Pricing
Schedule price changes:
POST /v1/products/:id/scheduled-prices
Request Body:
{
"data": {
"type": "scheduled_price",
"attributes": {
"price": { "amount": 24900, "currency": "USD" },
"compare_at": { "amount": 29900, "currency": "USD" },
"starts_at": "2026-06-01T00:00:00Z",
"ends_at": "2026-06-30T23:59:59Z",
"name": "June Sale"
}
}
}
Product Images
Upload Image
POST /v1/products/:id/images
Request Body (multipart/form-data):
file: [binary image data]
alt_text: "Premium Wireless Headphones - Black"
position: 1
is_primary: true
Response:
{
"data": {
"type": "images",
"id": "img_abc123",
"attributes": {
"url": "https://cdn.23blocks.com/products/prod_abc123/img_abc123.jpg",
"alt_text": "Premium Wireless Headphones - Black",
"position": 1,
"is_primary": true,
"width": 1200,
"height": 1200,
"thumbnails": {
"small": "https://cdn.23blocks.com/products/prod_abc123/img_abc123_100x100.jpg",
"medium": "https://cdn.23blocks.com/products/prod_abc123/img_abc123_400x400.jpg",
"large": "https://cdn.23blocks.com/products/prod_abc123/img_abc123_800x800.jpg"
}
}
}
}
List Images
GET /v1/products/:id/images
Reorder Images
PUT /v1/products/:id/images/reorder
Request Body:
{
"data": {
"image_ids": ["img_xyz789", "img_abc123", "img_def456"]
}
}
Delete Image
DELETE /v1/products/:id/images/:image_id
Object Detection for Multi-Product Images
NEW: Automatically detect multiple products in shelf photos and batch create product listings.
POST /v1/products/:id/files/:file_id/process
Request Body:
{
"processing_mode": "object_detection"
}
Response:
{
"data": {
"type": "product_file_processing",
"id": "proc_abc123",
"attributes": {
"file_id": "file_xyz789",
"objects_detected": 5,
"detection_metadata": [
{
"class": "product",
"confidence": 0.95,
"bbox": {
"x": 120,
"y": 45,
"width": 180,
"height": 240
},
"suggested_crop": "https://cdn.23blocks.com/crops/crop_001.jpg"
},
{
"class": "product",
"confidence": 0.92,
"bbox": {
"x": 320,
"y": 50,
"width": 175,
"height": 235
},
"suggested_crop": "https://cdn.23blocks.com/crops/crop_002.jpg"
}
// ... 3 more detected products
],
"processing_status": "completed",
"processed_at": "2026-06-19T10:30:00Z"
}
}
}
Use Cases:
- Shelf Photo Upload - User uploads photo containing 5 products → system detects all 5
- Bulk Product Creation - Create 5 individual product listings from one shelf photo
- Visual Search Improvement - Search per-product instead of per-scene
- Automated Cropping - Get pre-cropped product images from detection bounding boxes
Example Workflow:
// 1. Upload shelf photo
const file = await client.products.uploadFile(productId, shelfPhoto);
// 2. Process with object detection
const detection = await client.products.processFile(productId, file.id, {
processing_mode: 'object_detection'
});
// 3. Show preview to user
console.log(`Detected ${detection.objects_detected} products`);
detection.detection_metadata.forEach((obj, i) => {
console.log(`Product ${i+1}: ${obj.confidence * 100}% confidence`);
console.log(`Crop preview: ${obj.suggested_crop}`);
});
// 4. Bulk create products from detections
for (const detected of detection.detection_metadata) {
await client.products.create({
name: `Product from Shelf ${i+1}`,
image_url: detected.suggested_crop
});
}
UI Components Needed:
- Detection preview with bounding box overlays
- "Create X products" bulk action button
- Individual product selection interface
- Cropped image previews
Product Search
Search Products
GET /v1/products/search
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
q | string | Search query |
filters | object | Facet filters |
sort | string | Sort order |
page | integer | Page number |
per_page | integer | Results per page |
use_object_detection | boolean | NEW: Enable object detection for multi-object images (default: true) |
Example - Text Search:
GET /v1/products/search?q=wireless+headphones&filters[category]=electronics&filters[price][min]=10000&filters[price][max]=50000&sort=relevance
Example - Visual Search with Object Detection (Multi-Object Image):
POST /v1/products/search
Content-Type: multipart/form-data
image: [binary image data - shelf photo with 5 wine bottles]
use_object_detection: true # Default behavior
Multi-Object Search Response:
{
"data": [
{
"type": "products",
"id": "prod_wine_123",
"attributes": {
"name": "Château Margaux 2015",
"similarity_score": 0.94,
"detection_metadata": {
"object_index": 0,
"total_objects": 5,
"class": "wine_bottle",
"confidence": 0.96,
"bbox": { "x": 120, "y": 45, "width": 180, "height": 240 }
}
}
},
{
"type": "products",
"id": "prod_wine_456",
"attributes": {
"name": "Penfolds Grange 2016",
"similarity_score": 0.91,
"detection_metadata": {
"object_index": 1,
"total_objects": 5,
"class": "wine_bottle",
"confidence": 0.94,
"bbox": { "x": 320, "y": 50, "width": 175, "height": 235 }
}
}
}
// ... matches for remaining 3 detected bottles
],
"meta": {
"total": 15,
"objects_detected": 5,
"average_detection_time": "1.8s",
"page": 1,
"per_page": 20
}
}
Behavior:
- 0-1 objects detected: Standard single-image search (automatic fallback)
- 2+ objects detected: Multi-object search with grouped results
- Performance: ~500ms overhead for detection, 1.5-2.5s total for multi-object
- Disable detection: Set
use_object_detection=falseto skip detection entirely
Text Search Response:
{
"data": [...],
"meta": {
"total": 45,
"page": 1,
"per_page": 20,
"facets": {
"category": [
{ "value": "electronics", "count": 32 },
{ "value": "audio", "count": 28 }
],
"brand": [
{ "value": "AudioPro", "count": 15 },
{ "value": "SoundMax", "count": 12 }
],
"price_range": [
{ "min": 10000, "max": 20000, "count": 12 },
{ "min": 20000, "max": 30000, "count": 18 },
{ "min": 30000, "max": 50000, "count": 15 }
]
},
"suggestions": ["wireless earbuds", "bluetooth headphones"]
}
}
Search Suggestions
GET /v1/products/search/suggest?q=wire
Response:
{
"data": {
"suggestions": [
"wireless headphones",
"wireless earbuds",
"wireless speakers"
],
"products": [
{
"id": "prod_abc123",
"name": "Premium Wireless Headphones",
"image": "https://cdn.23blocks.com/..."
}
]
}
}
Related Products
Set Related Products
PUT /v1/products/:id/related
Request Body:
{
"data": {
"cross_sells": ["prod_def456", "prod_ghi789"],
"up_sells": ["prod_jkl012"],
"accessories": ["prod_mno345", "prod_pqr678"]
}
}
Get Related Products
GET /v1/products/:id/related
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
type | string | Type: cross_sells, up_sells, accessories |
limit | integer | Maximum products to return |
Product Bundles
Create Bundle
POST /v1/products
Request Body:
{
"data": {
"type": "products",
"attributes": {
"name": "Complete Audio Setup",
"sku": "BUNDLE-AUDIO-001",
"type": "bundle",
"price": {
"amount": 49900,
"currency": "USD"
},
"bundle_items": [
{
"product_id": "prod_headphones",
"quantity": 1,
"required": true
},
{
"product_id": "prod_case",
"quantity": 1,
"required": true
},
{
"product_id": "prod_cable",
"quantity": 2,
"required": false
}
],
"bundle_pricing": "fixed"
}
}
}
Webhooks
Subscribe to product events:
Available Events
| Event | Description |
|---|---|
product.created | New product created |
product.updated | Product updated |
product.deleted | Product deleted |
product.published | Product status changed to active |
inventory.low_stock | Inventory below threshold |
inventory.out_of_stock | Inventory depleted |
inventory.adjusted | Inventory level changed |
variant.created | New variant created |
variant.updated | Variant updated |
category.updated | Category modified |
Webhook Payload
{
"event": "inventory.low_stock",
"timestamp": "2026-01-02T10:30:00Z",
"data": {
"product_id": "prod_abc123",
"sku": "WH-PRO-2024",
"name": "Premium Wireless Headphones",
"current_quantity": 20,
"threshold": 25,
"location_id": "loc_warehouse1"
}
}
SDK Examples
JavaScript/TypeScript
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { products: 'https://products.api.us.23blocks.com' },
appId: 'your-app-id',
apiKey: 'your-api-key',
});
// Create product with variants
const product = await client.products.create({
name: 'Cotton T-Shirt',
sku: 'TSHIRT-001',
price: { amount: 2500, currency: 'USD' },
options: [
{ name: 'Size', values: ['S', 'M', 'L', 'XL'] },
{ name: 'Color', values: ['White', 'Black', 'Navy'] }
]
});
// Generate variants from options
await client.products.generateVariants(product.id);
// Search products with facets
const results = await client.products.search({
query: 't-shirt',
filters: {
category: 'clothing',
size: ['M', 'L'],
price: { min: 1000, max: 5000 }
},
sort: 'price_asc'
});
// Check inventory across locations
const inventory = await client.inventory.get(product.id, {
include_locations: true
});
// Reserve inventory for cart
const reservation = await client.inventory.reserve({
product_id: product.id,
quantity: 2,
cart_id: 'cart_abc123',
expires_in: 900 // 15 minutes
});
React Example
import { useProducts, useProduct } from '@23blocks/react';
function ProductGrid() {
const { products, loading, facets, loadMore } = useProducts({
category: 'electronics',
in_stock: true,
per_page: 20
});
if (loading) return <Spinner />;
return (
<div>
<Facets data={facets} />
<div className="grid grid-cols-4 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
<button onClick={loadMore}>Load More</button>
</div>
);
}
function ProductDetail({ id }: { id: string }) {
const { product, variants, selectVariant, selectedVariant } = useProduct(id);
return (
<div>
<h1>{product.name}</h1>
<Price amount={selectedVariant?.price || product.price} />
<VariantSelector
variants={variants}
selected={selectedVariant}
onSelect={selectVariant}
/>
<InventoryStatus stock={selectedVariant?.inventory || product.inventory} />
</div>
);
}
Error Codes
| Code | Description |
|---|---|
PRODUCT_NOT_FOUND | Product does not exist |
SKU_ALREADY_EXISTS | SKU is already in use |
INVALID_PRICE | Price format is invalid |
INSUFFICIENT_INVENTORY | Not enough stock available |
INVENTORY_RESERVED | Stock is reserved for other orders |
VARIANT_NOT_FOUND | Variant does not exist |
CATEGORY_NOT_FOUND | Category does not exist |
INVALID_OPTION_VALUES | Variant options don't match product options |
BUNDLE_INVALID | Bundle configuration is invalid |
IMAGE_UPLOAD_FAILED | Failed to upload product image |
Rate Limits
| Endpoint | Limit |
|---|---|
GET /products | 1000/minute |
GET /products/:id | 2000/minute |
POST /products | 100/minute |
PATCH /products/:id | 200/minute |
GET /products/search | 500/minute |
POST /inventory/adjust | 500/minute |
POST /inventory/reserve | 1000/minute |
Best Practices
- SKU Management - Use consistent SKU formats across variants
- Inventory Sync - Set up webhooks for real-time stock updates
- Image Optimization - Use provided thumbnail URLs for performance
- Search Indexing - Include relevant attributes for better search results
- Category Structure - Keep category depth to 3-4 levels maximum
- Price Precision - Always use cents/smallest currency unit
- Reservation Expiry - Set appropriate expiration for cart reservations
Related Resources
- Sales Block - Shopping carts and orders
- Files Block - Product image storage
- Search Block - Advanced search capabilities
- Frontend SDK - Client libraries
Use This Block with AI Agents
This block ships an official Claude Code plugin from the public 23blocks marketplace (19 plugins, 205 skills). Your agent gets full working knowledge of this API — endpoints, identity registration, and conventions.
# one-time: add the 23blocks marketplace
claude plugin marketplace add https://github.com/23blocks-OS/ai-agents
# install this block's plugin
claude plugin install products-block
Configure BLOCKS_API_URL, BLOCKS_API_KEY, and BLOCKS_AUTH_TOKEN. Register identity first: each block is autonomous — register the user with this block's identity endpoint before private calls (only user_unique_id is required; email/phone are optional notification-routing fields).
Full reference: AI Agents & Claude Code Plugins · github.com/23blocks-OS/ai-agents