Skill: killgfat/teable-api (version 1.0.6)

# Teable API Complete Teable API management skill for OpenClaw. Provides full CRUD operations for records, bases, spaces, tables, dashboards, and trash. ## Metadata ```yaml name: teable-api description: Complete Teable API management - manage records, bases, spaces, tables, dashboards, and trash author: Cortana version: 1.0.6 created: 2026-02-27 requires: env: [TEABLE_API_KEY] ``` **Note**: `TEABLE_URL` is optional for self-hosted instances (defaults to https://app.teable.ai) ## Installation ### Prerequisites Install the required Python package: ```bash pipx install requests # or globally pip3 install requests ``` ### Environment Variables **Required**: Set the `TEABLE_API_KEY` environment variable before using any scripts: ```bash # Temporary (current shell session) export TEABLE_API_KEY="your_personal_access_token_here" # Permanent (add to ~/.bashrc or ~/.zshrc) echo 'export TEABLE_API_KEY="your_personal_access_token_here"' >> ~/.bashrc source ~/.bashrc ``` **Getting Your Teable API Token**: 1. Log in to your Teable instance 2. Go to Settings → Access Token 3. Create a new Personal Access Token 4. Copy and save the token (shown only once) **Optional**: For self-hosted Teable instances, set `TEABLE_URL`: ```bash export TEABLE_URL="https://your-teable-instance.com" # Default: https://app.teable.ai ``` ## Usage ### Command-Line Scripts All scripts are located in the `scripts/` directory: ```bash # Record operations python3 scripts/teable_record.py list --table-id <tableId> python3 scripts/teable_record.py create --table-id <tableId> --fields '{"Name":"Test","Age":30}' python3 scripts/teable_record.py update --table-id <tableId> --record-id <recordId> --fields '{"Name":"Updated"}' python3 scripts/teable_record.py delete --table-id <tableId> --record-ids '["rec1","rec2"]' # Base operations python3 scripts/teable_base.py list --space-id <spaceId> python3 scripts/teable_base.py get --base-id <baseId> python3 scripts/teable_base.py create --space-id <spaceId> --name "My Base" python3 scripts/teable_base.py delete --base-id <baseId> # Space operations python3 scripts/teable_space.py list python3 scripts/teable_space.py get --space-id <spaceId> python3 scripts/teable_space.py create --name "My Space" python3 scripts/teable_space.py delete --space-id <spaceId> # Table operations python3 scripts/teable_table.py list --base-id <baseId> python3 scripts/teable_table.py get --table-id <tableId> python3 scripts/teable_table.py create --base-id <baseId> --name "My Table" python3 scripts/teable_table.py delete --table-id <tableId> # Dashboard operations python3 scripts/teable_dashboard.py list --base-id <baseId> python3 scripts/teable_dashboard.py create --base-id <baseId> --name "My Dashboard" python3 scripts/teable_dashboard.py delete --dashboard-id <dashboardId> # Trash operations python3 scripts/teable_trash.py list --base-id <baseId> python3 scripts/teable_trash.py reset --resource-id <baseId> --resource-type base ``` ### Python API ```python from teable_record import TeableRecordClient # Initialize client client = TeableRecordClient() # List records records = client.list_records(table_id="tblXXXX", take=100) # Create a record new_record = client.create_record( table_id="tblXXXX", fields={"Name": "Test", "Age": 30} ) # Update a record updated = client.update_record( table_id="tblXXXX", record_id="recXXXX", fields={"Status": "Completed"} ) # Delete records client.delete_records(table_id="tblXXXX", record_ids=["rec1", "rec2"]) ``` ## Available Scripts ### teable_record.py - Record Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List records | `--table-id` | | `get` | Get single record | `--table-id`, `--record-id` | | `create` | Create record | `--table-id`, `--fields` | | `update` | Update record | `--table-id`, `--record-id`, `--fields` | | `delete` | Delete records | `--table-id`, `--record-ids` | | `duplicate` | Duplicate record | `--table-id`, `--record-id` | | `history` | Get record history | `--table-id`, `--record-id` | **Optional Parameters**: - `--view-id`: Specify view - `--take`: Number of records (max 1000) - `--skip`: Skip count (pagination) - `--field-key-type`: Field key type (name/id/dbFieldName) - `--cell-format`: Cell format (json/text) - `--projection`: Return only specified fields - `--order-by`: Sorting - `--filter`: Filter conditions - `--typecast`: Enable automatic type conversion ### teable_base.py - Base Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List bases | `--space-id` (optional) | | `get` | Get base details | `--base-id` | | `create` | Create base | `--space-id`, `--name` | | `update` | Update base | `--base-id` | | `delete` | Delete base | `--base-id` | | `duplicate` | Duplicate base | `--base-id` | | `export` | Export base | `--base-id`, `--output` | | `collaborators` | List collaborators | `--base-id` | ### teable_space.py - Space Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List spaces | None | | `get` | Get space details | `--space-id` | | `create` | Create space | `--name` | | `update` | Update space | `--space-id` | | `delete` | Delete space | `--space-id` | | `bases` | List bases in space | `--space-id` | ### teable_table.py - Table Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List tables | `--base-id` | | `get` | Get table details | `--table-id` | | `create` | Create table | `--base-id`, `--name` | | `update` | Update table | `--table-id` | | `delete` | Delete table | `--table-id` | | `fields` | List fields | `--table-id` | | `views` | List views | `--table-id` | ### teable_dashboard.py - Dashboard Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List dashboards | `--base-id` | | `get` | Get dashboard details | `--dashboard-id` | | `create` | Create dashboard | `--base-id`, `--name` | | `update` | Update dashboard | `--dashboard-id` | | `delete` | Delete dashboard | `--dashboard-id` | | `install-plugin` | Install plugin | `--dashboard-id`, `--plugin-id` | ### teable_trash.py - Trash Management | Command | Description | Required Parameters | |---------|-------------|---------------------| | `list` | List trash items | `--base-id` | | `reset` | Empty trash | `--resource-id`, `--resource-type` | | `restore` | Restore item | `--item-id` | | `delete` | Permanently delete | `--item-id` | ## Examples ### Create a Record with Typecast ```bash # Enable typecast for automatic type conversion python3 scripts/teable_record.py create \ --table-id "tblABC123" \ --fields '{"Name":"John","Age":"30","Completed":"yes","DueDate":"2024-01-15"}' \ --typecast ``` ### Batch Create Records ```bash python3 scripts/teable_record.py create \ --table-id "tblABC123" \ --fields '[{"Name":"Alice"},{"Name":"Bob"},{"Name":"Charlie"}]' ``` ### List Records with Pagination ```bash # Get first 100 records python3 scripts/teable_record.py list --table-id "tblABC123" --take 100 # Get next 100 (skip first 100) python3 scripts/teable_record.py list --table-id "tblABC123" --take 100 --skip 100 ``` ### Filter and Sort Records ```bash python3 scripts/teable_record.py list \ --table-id "tblABC123" \ --filter '{"operator":"and","filterSet":[{"fieldId":"fld1","operator":"is","value":"Active"}]}' \ --order-by '[{"fieldId":"fld2","order":"asc"}]' ``` ### Complete Workflow Example ```bash # 1. Create a space python3 scripts/teable_space.py create --name "Project Alpha" --icon "🚀" # 2. Create a base in the space python3 scripts/teable_base.py create --space-id "spcXXX" --name "Task Manager" # 3. Create a table in the base python3 scripts/teable_table.py create --base-id "bseXXX" --name "Tasks" # 4. Add records python3 scripts/teable_record.py create \ --table-id "tblXXX" \ --fields '{"Task":"Design","Status":"In Progress","Priority":"High"}' ``` ## API Reference ### Base URL - **Teable Cloud**: `https://app.teable.ai` - **Self-hosted**: Your instance URL (set via `TEABLE_URL`) All API endpoints are relative to the base URL and start with `/api`. ### Authentication All requests require Bearer token authentication: ```bash curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://app.teable.ai/api/table/tblXXX/record' ``` ### ID Formats | Resource | ID Format | Example | |----------|-----------|---------| | Space | `spc...` | `spcABC123` | | Base | `bse...` | `bseDEF456` | | Table | `tbl...` | `tblGHI789` | | Record | `rec...` | `recJKL012` | | Field | `fld...` | `fldMNO345` | | View | `viw...` | `viwPQR678` | | Dashboard | `dbd...` | `dbdSTU901` | ## Troubleshooting ### 401 Unauthorized Check that `TEABLE_API_KEY` is correctly set: ```bash echo $TEABLE_API_KEY ``` ### 403 Forbidden Your token lacks the required permissions. Create a new token with appropriate scopes in Teable settings. ### 404 Not Found Verify that IDs are correct (spaceId, baseId, tableId, recordId, etc.) and match the expected format. ### Connection Timeout - Self-hosted users: Check that `TEABLE_URL` is accessible - Users in mainland China accessing official API: Use `proxychains` ```bash proxychains python3 scripts/teable_record.py list --table-id "tblXXX" ``` ### Typecast Not Working Ensure the `--typecast` flag is included in your command: ```bash python3 scripts/teable_record.py create \ --table-id "tblXXX" \ --fields '{"Age":"30"}' \ --typecast # Required for string-to-number conversion ``` ## Data Types Reference For detailed information about Teable field types and typecast functionality, see: - **Data Types Guide**: `references/data-types-and-typecast.md` This guide covers: - All 20+ field types with examples - Typecast automatic conversion rules - Best practices for data import - Common issues and solutions ## Additional Resources - [Teable Official Documentation](https://help.teable.ai/en/api-doc/overview) - [Getting IDs Guide](https://help.teable.ai/en/api-doc/get-id) - [Record Field Interface](https://help.teable.ai/en/api-doc/record/interface) - [API Error Codes](https://help.teable.ai/en/api-doc/error-code) ## License MIT License ## Support For issues or questions, please open an issue on the GitHub repository or contact the skill author.

path: README.md
content: # Teable API Skill

Complete Teable API management skill for OpenClaw. Provides full CRUD operations for records, bases, spaces, tables, dashboards, and trash.

## Quick Start

### 1. Install Dependencies

```bash
pipx install requests
# or
pip3 install requests
```

### 2. Configure Environment Variables

```bash
# Required: API Token
export TEABLE_API_KEY="your_personal_access_token_here"

# Optional: Self-hosted Teable URL (defaults to https://app.teable.ai)
export TEABLE_URL="https://your-teable-instance.com"
```

### 3. Usage Examples

```bash
# List spaces
python3 scripts/teable_space.py list

# Create a base
python3 scripts/teable_base.py create --space-id "spcXXX" --name "My Base"

# List records
python3 scripts/teable_record.py list --table-id "tblXXX" --take 50

# Create a record
python3 scripts/teable_record.py create --table-id "tblXXX" --fields '{"Name":"Test"}'
```

## Available Scripts

| Script | Functionality |
|--------|---------------|
| `teable_space.py` | Space management (create/read/update/delete) |
| `teable_base.py` | Base management + collaborators + invitation links |
| `teable_table.py` | Table management + fields + views |
| `teable_record.py` | Full CRUD + history + duplicate |
| `teable_dashboard.py` | Dashboard management + plugins |
| `teable_trash.py` | Trash management + restore + permanent delete |

## Documentation

- [SKILL.md](SKILL.md) - Complete API documentation
- [examples/usage_examples.md](examples/usage_examples.md) - Usage examples
- [references/data-types-and-typecast.md](references/data-types-and-typecast.md) - Data types guide

## Security Features

- **URL Validation**: Only HTTP/HTTPS schemes allowed, warns on HTTP
- **Path Traversal Protection**: Output files restricted to current directory
- **Credential Protection**: API key only sent to validated Teable endpoints

## Notes

- Users in mainland China accessing the official API may need `proxychains`
- Self-hosted users should set `TEABLE_URL` to point to their instance
- API Tokens can be generated in Teable Settings → Access Token
- Resource ID formats: spcXXX (Space), bseXXX (Base), tblXXX (Table), recXXX (Record)

path: clawhub.json
content: {
  "slug": "teable-api",
  "name": "Teable API",
  "version": "1.0.6",
  "description": "Complete Teable API management - manage records, bases, spaces, tables, dashboards, and trash",
  "author": "Cortana",
  "changelog": "Fixed metadata and documentation inconsistencies: corrected env var names in examples (TEABLE_URL), translated all docs to English, removed unsupported optionalEnv field",
  "tags": ["teable", "database", "api", "crud", "no-code", "latest"],
  "requires": {
    "env": ["TEABLE_API_KEY"],
    "primaryEnv": "TEABLE_API_KEY"
  },
  "emoji": "📊",
  "category": "database",
  "homepage": "https://teable.io",
  "notes": "TEABLE_URL is optional for self-hosted instances (defaults to https://app.teable.ai)"
}

path: examples/usage_examples.md
content: # Teable Skill Usage Examples

## Environment Setup

```bash
# Set API Key (required)
export TEABLE_API_KEY="your_personal_access_token_here"

# For self-hosted Teable (optional)
export TEABLE_URL="https://your-teable-instance.com"
```

## Example 1: Manage Spaces

```bash
# List all spaces
python3 scripts/teable_space.py list

# Create a new space
python3 scripts/teable_space.py create --name "My Project" --icon "🚀"

# Get space details
python3 scripts/teable_space.py get --space-id "spcXXXXXX"

# Get all bases in a space
python3 scripts/teable_space.py bases --space-id "spcXXXXXX"
```

## Example 2: Manage Bases

```bash
# Create a base in a space
python3 scripts/teable_base.py create --space-id "spcXXXXXX" --name "CRM System" --icon "👥"

# Get base details
python3 scripts/teable_base.py get --base-id "bseXXXXXX"

# List all bases in a space
python3 scripts/teable_base.py list --space-id "spcXXXXXX"

# Update base name
python3 scripts/teable_base.py update --base-id "bseXXXXXX" --name "CRM System v2"

# Duplicate a base
python3 scripts/teable_base.py duplicate --base-id "bseXXXXXX" --name "CRM System - Backup"

# Export a base
python3 scripts/teable_base.py export --base-id "bseXXXXXX" --output backup.zip

# Get collaborators
python3 scripts/teable_base.py collaborators --base-id "bseXXXXXX"

# Delete a base (move to trash)
python3 scripts/teable_base.py delete --base-id "bseXXXXXX"
```

## Example 3: Manage Tables

```bash
# Create a table in a base
python3 scripts/teable_table.py create --base-id "bseXXXXXX" --name "Customers"

# List all tables in a base
python3 scripts/teable_table.py list --base-id "bseXXXXXX"

# Get table details
python3 scripts/teable_table.py get --table-id "tblXXXXXX"

# Get field list
python3 scripts/teable_table.py fields --table-id "tblXXXXXX"

# Get view list
python3 scripts/teable_table.py views --table-id "tblXXXXXX"

# Update a table
python3 scripts/teable_table.py update --table-id "tblXXXXXX" --name "Customers 2024" --description "All customer information"

# Delete a table
python3 scripts/teable_table.py delete --table-id "tblXXXXXX"
```

## Example 4: Manage Records

```bash
# List records
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --take 50

# List records by view
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --view-id "viwXXXXXX" --take 20

# Get a single record
python3 scripts/teable_record.py get --table-id "tblXXXXXX" --record-id "recXXXXXX"

# Create a record
python3 scripts/teable_record.py create --table-id "tblXXXXXX" --fields '{"Name":"John Doe","Phone":"13800138000","City":"Beijing"}'

# Batch create records
python3 scripts/teable_record.py create --table-id "tblXXXXXX" --fields '[{"Name":"Jane Smith"},{"Name":"Bob Johnson"}]'

# Update a record
python3 scripts/teable_record.py update --table-id "tblXXXXXX" --record-id "recXXXXXX" --fields '{"City":"Shanghai","Status":"Active"}'

# Duplicate a record
python3 scripts/teable_record.py duplicate --table-id "tblXXXXXX" --record-id "recXXXXXX"

# Get record history
python3 scripts/teable_record.py history --table-id "tblXXXXXX" --record-id "recXXXXXX" --take 10

# Delete records
python3 scripts/teable_record.py delete --table-id "tblXXXXXX" --record-ids '["rec1","rec2","rec3"]'
```

## Example 5: Manage Dashboards

```bash
# List dashboards
python3 scripts/teable_dashboard.py list --base-id "bseXXXXXX"

# Create a dashboard
python3 scripts/teable_dashboard.py create --base-id "bseXXXXXX" --name "Sales Dashboard"

# Get dashboard details
python3 scripts/teable_dashboard.py get --dashboard-id "dbdXXXXXX"

# Update dashboard layout
python3 scripts/teable_dashboard.py update --dashboard-id "dbdXXXXXX" --layout '[{"x":0,"y":0,"w":6,"h":4}]'

# Install a plugin
python3 scripts/teable_dashboard.py install-plugin --dashboard-id "dbdXXXXXX" --plugin-id "plugin_chart" --name "Sales Chart"

# List installed plugins
python3 scripts/teable_dashboard.py list-plugins --dashboard-id "dbdXXXXXX"

# Delete a dashboard
python3 scripts/teable_dashboard.py delete --dashboard-id "dbdXXXXXX"
```

## Example 6: Manage Trash

```bash
# List trash items
python3 scripts/teable_trash.py list --base-id "bseXXXXXX"

# Empty trash
python3 scripts/teable_trash.py reset --resource-id "bseXXXXXX" --resource-type base

# Get trash item details
python3 scripts/teable_trash.py get --item-id "trashXXXXXX"

# Restore a trash item
python3 scripts/teable_trash.py restore --item-id "trashXXXXXX"

# Batch restore
python3 scripts/teable_trash.py restore --item-ids '["trash1","trash2"]'

# Permanently delete
python3 scripts/teable_trash.py delete --item-id "trashXXXXXX"
```

## Python Code Examples

```python
from teable_record import TeableRecordClient
from teable_base import TeableBaseClient

# Create clients
record_client = TeableRecordClient()
base_client = TeableBaseClient()

# Get records
records = record_client.list_records(
    table_id="tblXXXXXX",
    take=100,
    filter_cond={
        "operator": "and",
        "filterSet": [
            {"fieldId": "fld1", "operator": "is", "value": "Beijing"}
        ]
    }
)

# Create a record
new_record = record_client.create_record(
    table_id="tblXXXXXX",
    fields={"Name": "Jane Doe", "Phone": "13900139000"}
)

# Update a record
updated = record_client.update_record(
    table_id="tblXXXXXX",
    record_id="recXXXXXX",
    fields={"Status": "Completed"}
)

# Get base information
base_info = base_client.get_base("bseXXXXXX")
print(f"Base name: {base_info['name']}")
```

## Advanced Usage

### Paginate Large Datasets

```bash
# Get 1000 records at a time (max limit)
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --take 1000 --skip 0
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --take 1000 --skip 1000
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --take 1000 --skip 2000
```

### Use Field IDs Instead of Names

```bash
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --field-key-type id
python3 scripts/teable_record.py create --table-id "tblXXXXXX" --fields '{"fldABC123":"value"}' --field-key-type id
```

### Return Specific Fields Only

```bash
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --projection "Name,Phone,Email"
```

### Sort Records

```bash
python3 scripts/teable_record.py list --table-id "tblXXXXXX" --order-by '[{"fieldId":"fld1","order":"asc"}]'
```

## Troubleshooting

### 401 Unauthorized
```bash
# Check if API Key is correctly set
echo $TEABLE_API_KEY
```

### 403 Forbidden
Your token lacks the required permissions. Create a new token with appropriate scopes in Teable settings.

### 404 Not Found
Check if IDs are correct and match the expected format (spcXXX, bseXXX, tblXXX, recXXX, etc.).

### Using proxychains (for users in mainland China)
```bash
proxychains python3 scripts/teable_record.py list --table-id "tblXXXXXX"
```

path: references/data-types-and-typecast.md
content: # Teable Data Types and Typecast Guide

This document provides a comprehensive guide to all Teable field types, data formats, and the typecast automatic conversion feature.

## Table of Contents

1. [Teable Field Types Overview](#teable-field-types-overview)
2. [Detailed Type Specifications](#detailed-type-specifications)
3. [Typecast Automatic Conversion](#typecast-automatic-conversion)
4. [Best Practices](#best-practices)
5. [FAQ](#faq)

---

## Teable Field Types Overview

| Type ID | Type Name | Description | Typecast Support |
|---------|-----------|-------------|------------------|
| `singleLineText` | Single Line Text | Short text strings | ✅ |
| `longText` | Long Text | Multi-line text content | ✅ |
| `number` | Number | Integer or decimal | ✅ |
| `singleSelect` | Single Select | Choose one from predefined options | ✅ |
| `multipleSelects` | Multiple Selects | Choose multiple from options | ✅ |
| `date` | Date | Date values | ✅ |
| `dateTime` | Date Time | Date + Time | ✅ |
| `checkbox` | Checkbox | Boolean (true/false) | ✅ |
| `attachment` | Attachment | File uploads | ⚠️ Partial |
| `link` | Link | Link to records in other tables | ✅ |
| `user` | User | Select users | ✅ |
| `rating` | Rating | 1-5 star rating | ✅ |
| `formula` | Formula | Auto-calculated (read-only) | ❌ |
| `rollup` | Rollup | Aggregation from linked records (read-only) | ❌ |
| `count` | Count | Count of linked records (read-only) | ❌ |
| `createdTime` | Created Time | Auto-recorded (read-only) | ❌ |
| `lastModifiedTime` | Last Modified Time | Auto-recorded (read-only) | ❌ |
| `createdBy` | Created By | Auto-recorded (read-only) | ❌ |
| `lastModifiedBy` | Last Modified By | Auto-recorded (read-only) | ❌ |
| `autoNumber` | Auto Number | Auto-incrementing (read-only) | ❌ |
| `button` | Button | Trigger actions | ❌ |

**Legend**: 
- ✅ = Full typecast support
- ⚠️ = Partial support
- ❌ = Not supported (read-only or special types)

---

## Detailed Type Specifications

### 1. Single Line Text (singleLineText)

```json
{
  "fieldName": "Text value"
}
```

**Example**:
```json
{
  "Name": "John Doe",
  "Email": "[REDACTED_SECRET]"
}
```

### 2. Long Text (longText)

```json
{
  "fieldName": "Multi-line text\nwith line breaks"
}
```

**Example**:
```json
{
  "Description": "First line\nSecond line\nThird line"
}
```

### 3. Number (number)

```json
{
  "fieldName": 123.45
}
```

**Example**:
```json
{
  "Age": 30,
  "Price": 99.99,
  "Quantity": 100
}
```

**Typecast Example**:
```json
// Strings are automatically converted to numbers
{
  "Price": "99.99",  // Auto-converted when typecast enabled
  "Quantity": "50"
}
```

### 4. Single Select (singleSelect)

```json
{
  "fieldName": "Option Name"
}
```

**Example**:
```json
{
  "Status": "In Progress",
  "Priority": "High"
}
```

**Typecast Advantage**: If the option doesn't exist, typecast will create it automatically (requires permissions).

### 5. Multiple Selects (multipleSelects)

```json
{
  "fieldName": ["Option 1", "Option 2", "Option 3"]
}
```

**Example**:
```json
{
  "Skills": ["Python", "JavaScript", "SQL"],
  "Tags": ["Important", "Urgent", "Pending"]
}
```

**Typecast Example**:
```json
// Strings are also accepted and converted to arrays
{
  "Tags": "Important, Urgent"  // Split when typecast enabled
}
```

### 6. Date (date)

```json
{
  "fieldName": "YYYY-MM-DD"
}
```

**Example**:
```json
{
  "BirthDate": "1990-01-15",
  "StartDate": "2024-01-01",
  "EndDate": "2024-12-31"
}
```

**Typecast Supported Formats**:
```json
{
  "Date": "2024/01/15",      // ✅ Slash separator
  "Date": "01/15/2024",      // ✅ MM/DD/YYYY
  "Date": "Jan 15, 2024",    // ✅ English format
  "Date": "2024-01-15"       // ✅ ISO format
}
```

### 7. Date Time (dateTime)

```json
{
  "fieldName": "YYYY-MM-DDTHH:mm:ss.sssZ"
}
```

**Example**:
```json
{
  "MeetingTime": "2024-01-15T14:30:00.000Z",
  "Deadline": "2024-12-31T23:59:59.999+08:00"
}
```

**Typecast Supported Formats**:
```json
{
  "Time": "2024-01-15 14:30:00",  // ✅ Space separator
  "Time": "2024/01/15 2:30 PM",   // ✅ 12-hour format
  "Time": "2024-01-15T14:30:00Z"  // ✅ ISO 8601
}
```

### 8. Checkbox (checkbox)

```json
{
  "fieldName": true
}
```

**Example**:
```json
{
  "Completed": true,
  "Approved": false
}
```

**Typecast Example**:
```json
{
  "Completed": "yes",        // ✅ Converted to true
  "Completed": "true",       // ✅ Converted to true
  "Completed": 1,            // ✅ Converted to true
  "NotDone": "no",           // ✅ Converted to false
  "NotDone": "false"         // ✅ Converted to false
}
```

### 9. Attachment (attachment)

```json
{
  "fieldName": [
    {
      "name": "filename.pdf",
      "url": "https://example.com/file.pdf"
    }
  ]
}
```

**Example**:
```json
{
  "Resume": [
    {
      "name": "John_Resume.pdf",
      "url": "https://example.com/resume.pdf"
    }
  ],
  "Photos": [
    {
      "name": "avatar.jpg",
      "url": "https://example.com/avatar.jpg"
    },
    {
      "name": "work_photo.png",
      "url": "https://example.com/work.png"
    }
  ]
}
```

**Upload via Script**:
```bash
# Use upload-attachment API
python3 scripts/teable_record.py update \
  --table-id "tblXXX" \
  --record-id "recXXX" \
  --fields '{"Attachments": [{"name": "file.pdf", "url": "https://..."}]}'
```

### 10. Link (link)

```json
{
  "fieldName": ["recId1", "recId2"]
}
```

**Example**:
```json
{
  "LinkedCustomers": ["recABC123", "recDEF456"],
  "ProjectLead": ["rec789XYZ"]
}
```

**Typecast Example** (using primary key text):
```json
{
  // With typecast enabled, you can use primary key values directly
  "LinkedCustomers": ["Customer A", "Customer B"],  // ✅ Auto-lookup and link
  "ProjectLead": "John Doe"              // ✅ Single value converted to array
}
```

### 11. User (user)

```json
{
  "fieldName": [
    {
      "userId": "usrXXX",
      "name": "Username"
    }
  ]
}
```

**Example**:
```json
{
  "Owner": [
    {
      "userId": "usr123",
      "name": "John Doe"
    }
  ],
  "Participants": [
    {"userId": "usr123", "name": "John Doe"},
    {"userId": "usr456", "name": "Jane Smith"}
  ]
}
```

**Typecast Example**:
```json
{
  // With typecast enabled, you can use email or username
  "Owner": "[REDACTED_SECRET]",  // ✅ Auto-lookup user
  "Participants": "John, Jane"  // ✅ Auto-split and lookup
}
```

### 12. Rating (rating)

```json
{
  "fieldName": 4
}
```

**Example**:
```json
{
  "Satisfaction": 5,
  "Recommendation": 4,
  "Quality": 3
}
```

**Typecast Example**:
```json
{
  "Satisfaction": "5",    // ✅ String to number
  "Satisfaction": 5       // ✅ Direct number
}
```

---

## Typecast Automatic Conversion

### What is Typecast?

Typecast is a smart Teable feature that allows you to input data in formats that don't exactly match the field type. The system automatically attempts to convert the data to the correct format.

### When to Use Typecast?

✅ **Recommended**:
- Importing data from CSV/Excel (all data is strings)
- Quick data entry without strict formatting
- Link fields using text instead of IDs
- User fields using email/username instead of userId

❌ **Not Recommended**:
- Scenarios requiring strict data validation
- High-performance batch processing
- Data that might be ambiguous

### Enable Typecast

#### Method 1: Command Line

```bash
# Create record with typecast
python3 scripts/teable_record.py create \
  --table-id "tblXXX" \
  --fields '{"Age": "30", "Date": "2024-01-15"}' \
  --typecast

# Update record with typecast
python3 scripts/teable_record.py update \
  --table-id "tblXXX" \
  --record-id "recXXX" \
  --fields '{"Done": "yes"}' \
  --typecast
```

#### Method 2: Python API

```python
from teable_record import TeableRecordClient

client = TeableRecordClient()

# Create record with typecast enabled
record = client.create_record(
    table_id="tblXXX",
    fields={
        "Age": "30",           # String → Number
        "Date": "2024/01/15",  # Non-standard → Date
        "Done": "yes"          # Text → Boolean
    },
    typecast=True  # Enable automatic conversion
)

# Batch create
records = client.create_records(
    table_id="tblXXX",
    records=[
        {"fields": {"Name": "John", "Age": "25"}},
        {"fields": {"Name": "Jane", "Age": "30"}}
    ],
    typecast=True
)

# Update record
updated = client.update_record(
    table_id="tblXXX",
    record_id="recXXX",
    fields={"Status": "In Progress"},  # Text → Select option
    typecast=True
)
```

#### Method 3: Direct API Call

```bash
curl -X POST 'https://app.teable.ai/api/table/tblXXX/record' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "records": [
      {
        "fields": {
          "Name": "John",
          "Age": "30",
          "JoinDate": "2024/01/15"
        }
      }
    ],
    "typecast": true
  }'
```

### Typecast Conversion Rules

| Target Type | Accepted Input | Conversion Result | Example |
|------------|---------------|-------------------|---------|
| **number** | Numeric string | Number | `"123"` → `123` |
| | Comma-separated | Number | `"1,234.56"` → `1234.56` |
| | Invalid string | ❌ Error | `"abc"` → Error |
| **checkbox** | "yes"/"true"/1 | `true` | `"yes"` → `true` |
| | "no"/"false"/0 | `false` | `"no"` → `false` |
| **date** | ISO format | Date | `"2024-01-15"` → Date |
| | Slash separator | Date | `"2024/01/15"` → Date |
| | Various formats | Date | `"Jan 15, 2024"` → Date |
| **dateTime** | ISO 8601 | DateTime | `"2024-01-15T14:30:00Z"` |
| | Space separator | DateTime | `"2024-01-15 14:30:00"` |
| **singleSelect** | Text | Option | `"In Progress"` → Option |
| | Non-existent option | Create new* | `"New Status"` → New Option |
| **multipleSelects** | Array | Multi-select | `["A", "B"]` |
| | Comma-separated | Split array | `"A, B, C"` → `["A","B","C"]` |
| **link** | Record ID array | Link | `["rec1", "rec2"]` |
| | Primary key values | Lookup & link* | `["Customer A", "Customer B"]` |
| **user** | userId array | User | `[{"userId":"usr1"}]` |
| | Email/username | Lookup user* | `"[REDACTED_SECRET]"` |

\* Requires appropriate permissions

### Typecast Considerations

⚠️ **Important Notes**:

1. **Performance Impact**: Typecast increases server processing time; large batch operations may be slower
2. **Permission Requirements**: Auto-creating options requires edit permissions on the table
3. **Data Quality**: May hide data formatting issues; recommended to enable during development, use strict validation in production
4. **Error Handling**: Conversion failures return HTTP 400 errors; implement proper error handling

### Typecast Error Handling Example

```python
from teable_record import TeableRecordClient
import requests

client = TeableRecordClient()

try:
    record = client.create_record(
        table_id="tblXXX",
        fields={"Age": "not-a-number"},  # Cannot convert
        typecast=True
    )
except requests.exceptions.HTTPError as e:
    if e.response.status_code == 400:
        error_detail = e.response.json()
        print(f"Type conversion failed: {error_detail}")
        # Handle error: log, notify user, etc.
```

---

## Best Practices

### 1. Data Import Scenarios

```python
import csv
from teable_record import TeableRecordClient

client = TeableRecordClient()

# Import from CSV (all CSV fields are strings)
with open('customers.csv', 'r', encoding='utf-8') as f:
    reader = csv.DictReader(f)
    for row in reader:
        client.create_record(
            table_id="tblXXX",
            fields={
                "Name": row["name"],
                "Age": row["age"],        # String → Number
                "Email": row["email"],
                "JoinDate": row["date"],  # String → Date
                "Level": row["level"],    # String → Single select
                "Verified": row["verified"]  # String → Boolean
            },
            typecast=True  # Key: enable automatic conversion
        )
```

### 2. Rapid Prototyping

```python
# Development: use typecast for quick testing
client.create_record(
    table_id="tblXXX",
    fields={
        "Done": "yes",
        "Date": "next week",
        "Owner": "John"
    },
    typecast=True
)

# Production: strict validation
client.create_record(
    table_id="tblXXX",
    fields={
        "Done": True,
        "Date": "2024-01-15",
        "Owner": [{"userId": "usr123", "name": "John Doe"}]
    },
    typecast=False
)
```

### 3. Batch Operations with Linked Records

```python
# Simplify linking with typecast
client.update_record(
    table_id="tblOrders",
    record_id="rec123",
    fields={
        # Traditional: need to query customer table for recId
        # "Customer": ["recCustomer1", "recCustomer2"]
        
        # Typecast: use customer names directly
        "Customer": ["Customer A", "Customer B"]
    },
    typecast=True
)
```

### 4. Mixed Type Handling

```python
# Handle data from different sources
records = [
    {"Name": "John", "Age": 30, "Birthday": "1990-01-15"},    # Correct format
    {"Name": "Jane", "Age": "25", "Birthday": "1995/05/20"},  # Strings
    {"Name": "Bob", "Age": "twenty", "Birthday": "unknown"},  # Invalid
]

for data in records:
    try:
        client.create_record(
            table_id="tblXXX",
            fields=data,
            typecast=True
        )
        print(f"✓ {data['Name']} imported successfully")
    except Exception as e:
        print(f"✗ {data['Name']} import failed: {e}")
        # Log failed data for manual processing
```

---

## FAQ

### Q1: Does Typecast Affect Performance?

**A**: Yes, there is a slight performance impact. Recommendations:
- Small batches (<100 records): Use freely
- Large imports (>1000 records): Pre-format data, disable typecast
- Real-time API calls:权衡 based on your scenario

### Q2: Why Isn't Typecast Working?

**Checklist**:
- [ ] Is `typecast: true` set in your request?
- [ ] Does the field type support typecast?
- [ ] Is the data format within convertible range?
- [ ] Do you have sufficient permissions (e.g., to create new options)?

### Q3: What Happens When Typecast Conversion Fails?

**A**: Returns HTTP 400 error with detailed information:
```json
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Cannot convert 'abc' to number type",
    "fieldId": "fldXXX",
    "fieldName": "Age"
  }
}
```

### Q4: Can I Use Typecast for Only Some Fields?

**A**: No. Typecast is a global setting for the entire request. Recommendations:
- Pre-process fields that need conversion
- Or use typecast for everything and accept the performance overhead

### Q5: How Do I Check the Exact Field Type?

**A**: Use the table script to get field information:
```bash
python3 scripts/teable_table.py fields --table-id "tblXXX"
```

Or via API:
```python
from teable_table import TeableTableClient
client = TeableTableClient()
fields = client.get_fields(table_id="tblXXX")
for field in fields:
    print(f"{field['name']}: {field['type']}")
```

---

## Resources

- [Teable Official API Documentation](https://help.teable.ai/en/api-doc/overview)
- [Record Field Interface](https://help.teable.ai/en/api-doc/record/interface)
- [Create Records API](https://help.teable.ai/en/api-doc/record/create)
- [Update Record API](https://help.teable.ai/en/api-doc/record/update)

---

**Document Version**: 1.0.0  
**Last Updated**: 2026-02-27  
**Maintainer**: Cortana


path: requirements.txt
content: requests>=2.28.0


path: scripts/teable_base.py
content: #!/usr/bin/env python3
"""
Teable Base API Client
Complete management for Teable databases (Bases) with security validations
"""

import os
import sys
import json
import argparse
import requests
from typing import Optional, List, Dict, Any
from urllib.parse import urljoin, urlparse

# Configuration
DEFAULT_BASE_URL = "https://app.teable.ai"
API_PREFIX = "/api"
ALLOWED_SCHEMES = ["https", "http"]


def validate_url(url: str) -> str:
    """
    Validate URL to prevent SSRF and credential exfiltration.
    
    Args:
        url: URL to validate
        
    Returns:
        Validated URL
        
    Raises:
        ValueError: If URL is invalid or potentially malicious
    """
    if not url:
        return url
    
    try:
        parsed = urlparse(url)
        
        if parsed.scheme.lower() not in ALLOWED_SCHEMES:
            raise ValueError(
                f"Invalid URL scheme: '{parsed.scheme}'. "
                f"Only {ALLOWED_SCHEMES} are allowed."
            )
        
        if not parsed.netloc:
            raise ValueError(f"Invalid URL: missing domain in '{url}'")
        
        if parsed.scheme.lower() == "http":
            print(
                "WARNING: Using HTTP instead of HTTPS. "
                "Your API key will be transmitted in plaintext!",
                file=sys.stderr
            )
        
        return url.rstrip("/")
        
    except Exception as e:
        raise ValueError(f"Invalid TEABLE_URL '{url}': {e}")


def validate_output_path(path: str, base_dir: Optional[str] = None) -> str:
    """
    Validate output file path to prevent path traversal attacks.
    
    Args:
        path: Output file path
        base_dir: Base directory (defaults to current working directory)
        
    Returns:
        Absolute path if valid
        
    Raises:
        ValueError: If path attempts directory traversal
    """
    if not path:
        raise ValueError("Output path cannot be empty")
    
    if base_dir is None:
        base_dir = os.getcwd()
    
    abs_base = os.path.abspath(base_dir)
    abs_path = os.path.abspath(path)
    
    if not abs_path.startswith(abs_base):
        raise ValueError(
            f"Security error: Output path '{path}' is outside allowed directory. "
            f"Files can only be written to '{abs_base}' or its subdirectories."
        )
    
    return abs_path


class TeableBaseClient:
    """Teable Base API Client with security validations"""

    def __init__(self, base_url: Optional[str] = None, [REDACTED_SECRET] = None):
        """
        Initialize client with security validations.
        
        Args:
            base_url: Teable instance URL (validated for security)
            [REDACTED_SECRET] API Token
        """
        raw_url = base_url or os.getenv("TEABLE_URL") or DEFAULT_BASE_URL
        self.base_url = validate_url(raw_url)
        self.[REDACTED_SECRET] or os.getenv("TEABLE_API_KEY")

        if not self.api_key:
            raise ValueError(
                "Teable API Key is not set! Please set TEABLE_API_KEY environment variable."
            )

        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })

    def _request(self, method: str, path: str, **kwargs) -> requests.Response:
        """Send HTTP request"""
        url = urljoin(self.base_url + API_PREFIX, path.lstrip("/"))
        response = self.session.request(method, url, **kwargs)
        return response

    # ==================== Read Base ====================

    def get_base(self, base_id: str) -> Dict[str, Any]:
        """Get Base details"""
        response = self._request("GET", f"/base/{base_id}")
        response.raise_for_status()
        return response.json()

    def list_bases(
        self,
        space_id: Optional[str] = None,
        take: Optional[int] = None,
        skip: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """Get Base list"""
        params = {}
        if space_id:
            params["spaceId"] = space_id
        if take:
            params["take"] = take
        if skip:
            params["skip"] = skip

        if space_id:
            response = self._request("GET", f"/space/{space_id}/base", params=params)
        else:
            response = self._request("GET", "/base", params=params)

        response.raise_for_status()
        return response.json()

    # ==================== Create Base ====================

    def create_base(
        self,
        space_id: str,
        name: str,
        icon: Optional[str] = None
    ) -> Dict[str, Any]:
        """Create Base"""
        payload = {
            "spaceId": space_id,
            "name": name
        }
        if icon:
            payload["icon"] = icon

        response = self._request("POST", "/base", json=payload)
        response.raise_for_status()
        return response.json()

    # ==================== Update Base ====================

    def update_base(
        self,
        base_id: str,
        name: Optional[str] = None,
        icon: Optional[str] = None
    ) -> Dict[str, Any]:
        """Update Base"""
        payload = {}
        if name is not None:
            payload["name"] = name
        if icon is not None:
            payload["icon"] = icon

        response = self._request("PATCH", f"/base/{base_id}", json=payload)
        response.raise_for_status()
        return response.json()

    # ==================== Delete Base ====================

    def delete_base(self, base_id: str, permanent: bool = False) -> bool:
        """Delete Base"""
        endpoint = f"/base/{base_id}/permanent" if permanent else f"/base/{base_id}"
        response = self._request("DELETE", endpoint)
        response.raise_for_status()
        return response.status_code == 200

    # ==================== Duplicate Base ====================

    def duplicate_base(
        self,
        base_id: str,
        space_id: Optional[str] = None,
        name: Optional[str] = None
    ) -> Dict[str, Any]:
        """Duplicate Base"""
        payload = {}
        if space_id:
            payload["spaceId"] = space_id
        if name:
            payload["name"] = name

        response = self._request("POST", f"/base/{base_id}/duplicate", json=payload)
        response.raise_for_status()
        return response.json()

    # ==================== Export Base ====================

    def export_base(self, base_id: str, include_data: bool = True) -> bytes:
        """
        Export Base
        
        Note: Export data is returned as bytes for caller to handle safely.
        """
        params = {"includeData": str(include_data).lower()}
        response = self._request("GET", f"/base/{base_id}/export", params=params)
        response.raise_for_status()
        return response.content

    # ==================== Collaborators ====================

    def get_collaborators(self, base_id: str) -> List[Dict[str, Any]]:
        """Get Base collaborators list"""
        response = self._request("GET", f"/base/{base_id}/collaborators")
        response.raise_for_status()
        return response.json()

    def add_collaborator(
        self,
        base_id: str,
        user_id: str,
        role: str
    ) -> Dict[str, Any]:
        """Add collaborator"""
        payload = {
            "userId": user_id,
            "role": role
        }
        response = self._request("POST", f"/base/{base_id}/collaborator", json=payload)
        response.raise_for_status()
        return response.json()

    def update_collaborator(
        self,
        base_id: str,
        user_id: str,
        role: str
    ) -> Dict[str, Any]:
        """Update collaborator permissions"""
        payload = {"role": role}
        response = self._request("PATCH", f"/base/{base_id}/collaborators/{user_id}", json=payload)
        response.raise_for_status()
        return response.json()

    def remove_collaborator(self, base_id: str, user_id: str) -> bool:
        """Remove collaborator"""
        response = self._request("DELETE", f"/base/{base_id}/collaborators/{user_id}")
        response.raise_for_status()
        return response.status_code == 200

    # ==================== Invitation Links ====================

    def create_invitation_link(
        self,
        base_id: str,
        role: str,
        expires_time: Optional[int] = None
    ) -> Dict[str, Any]:
        """Create invitation link"""
        payload = {"role": role}
        if expires_time:
            payload["expiresTime"] = expires_time

        response = self._request("POST", f"/base/{base_id}/invitationLink", json=payload)
        response.raise_for_status()
        return response.json()

    def get_invitation_links(self, base_id: str) -> List[Dict[str, Any]]:
        """Get invitation link list"""
        response = self._request("GET", f"/base/{base_id}/invitationLink")
        response.raise_for_status()
        return response.json()

    def delete_invitation_link(self, base_id: str, link_id: str) -> bool:
        """Delete invitation link"""
        response = self._request("DELETE", f"/base/{base_id}/invitationLink/{link_id}")
        response.raise_for_status()
        return response.status_code == 200

    # ==================== Permissions ====================

    def get_permission(self, base_id: str) -> Dict[str, Any]:
        """Get Base permissions"""
        response = self._request("GET", f"/base/{base_id}/permission")
        response.raise_for_status()
        return response.json()

    # ==================== ERD ====================

    def get_erd(self, base_id: str) -> Dict[str, Any]:
        """Get Base ERD (Entity Relationship Diagram)"""
        response = self._request("GET", f"/base/{base_id}/erd")
        response.raise_for_status()
        return response.json()


# ==================== CLI Interface ====================

def main():
    parser = argparse.ArgumentParser(description="Teable Base API Client")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # list command
    list_parser = subparsers.add_parser("list", help="List bases")
    list_parser.add_argument("--space-id", help="Space ID")
    list_parser.add_argument("--take", type=int, help="Number of results")
    list_parser.add_argument("--skip", type=int, help="Skip count")

    # get command
    get_parser = subparsers.add_parser("get", help="Get Base details")
    get_parser.add_argument("--base-id", required=True, help="Base ID")

    # create command
    create_parser = subparsers.add_parser("create", help="Create Base")
    create_parser.add_argument("--space-id", required=True, help="Space ID")
    create_parser.add_argument("--name", required=True, help="Base name")
    create_parser.add_argument("--icon", help="Base icon")

    # update command
    update_parser = subparsers.add_parser("update", help="Update Base")
    update_parser.add_argument("--base-id", required=True, help="Base ID")
    update_parser.add_argument("--name", help="New name")
    update_parser.add_argument("--icon", help="New icon")

    # delete command
    delete_parser = subparsers.add_parser("delete", help="Delete Base")
    delete_parser.add_argument("--base-id", required=True, help="Base ID")
    delete_parser.add_argument("--permanent", action="store_true", help="Permanent delete")

    # duplicate command
    dup_parser = subparsers.add_parser("duplicate", help="Duplicate Base")
    dup_parser.add_argument("--base-id", required=True, help="Base ID to duplicate")
    dup_parser.add_argument("--space-id", help="Target Space ID")
    dup_parser.add_argument("--name", help="New Base name")

    # export command
    export_parser = subparsers.add_parser("export", help="Export Base")
    export_parser.add_argument("--base-id", required=True, help="Base ID")
    export_parser.add_argument("--output", "-o", required=True, 
                               help="Output file path (must be in current directory or subdirectory)")
    export_parser.add_argument("--no-data", action="store_true", help="Exclude data")

    # collaborators command
    collab_parser = subparsers.add_parser("collaborators", help="List collaborators")
    collab_parser.add_argument("--base-id", required=True, help="Base ID")

    # add-collaborator command
    add_collab_parser = subparsers.add_parser("add-collaborator", help="Add collaborator")
    add_collab_parser.add_argument("--base-id", required=True, help="Base ID")
    add_collab_parser.add_argument("--user-id", required=True, help="User ID")
    add_collab_parser.add_argument("--role", required=True,
                                   choices=["owner", "creator", "editor", "commenter", "viewer"])

    # remove-collaborator command
    remove_collab_parser = subparsers.add_parser("remove-collaborator", help="Remove collaborator")
    remove_collab_parser.add_argument("--base-id", required=True, help="Base ID")
    remove_collab_parser.add_argument("--user-id", required=True, help="User ID")

    # invitation-link command
    invite_parser = subparsers.add_parser("invitation-link", help="Create invitation link")
    invite_parser.add_argument("--base-id", required=True, help="Base ID")
    invite_parser.add_argument("--role", required=True,
                               choices=["owner", "creator", "editor", "commenter", "viewer"])

    # erd command
    erd_parser = subparsers.add_parser("erd", help="Get ERD")
    erd_parser.add_argument("--base-id", required=True, help="Base ID")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    try:
        client = TeableBaseClient()

        if args.command == "list":
            result = client.list_bases(
                space_id=args.space_id,
                take=args.take,
                skip=args.skip
            )
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "get":
            result = client.get_base(args.base_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "create":
            result = client.create_base(
                space_id=args.space_id,
                name=args.name,
                icon=args.icon
            )
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "update":
            result = client.update_base(
                base_id=args.base_id,
                name=args.name,
                icon=args.icon
            )
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "delete":
            success = client.delete_base(args.base_id, permanent=args.permanent)
            print(f"Deletion {'successful' if success else 'failed'}")

        elif args.command == "duplicate":
            result = client.duplicate_base(
                base_id=args.base_id,
                space_id=args.space_id,
                name=args.name
            )
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "export":
            data = client.export_base(args.base_id, include_data=not args.no_data)
            # Validate output path for security
            safe_path = validate_output_path(args.output)
            with open(safe_path, "wb") as f:
                f.write(data)
            print(f"Exported to {safe_path}")

        elif args.command == "collaborators":
            result = client.get_collaborators(args.base_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "add-collaborator":
            result = client.add_collaborator(args.base_id, args.user_id, args.role)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "remove-collaborator":
            success = client.remove_collaborator(args.base_id, args.user_id)
            print(f"Removal {'successful' if success else 'failed'}")

        elif args.command == "invitation-link":
            result = client.create_invitation_link(args.base_id, args.role)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "erd":
            result = client.get_erd(args.base_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

    except ValueError as e:
        print(f"Validation error: {e}", file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error: {e.response.status_code}", file=sys.stderr)
        print(e.response.text, file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/teable_dashboard.py
content: #!/usr/bin/env python3
"""
Teable Dashboard API Client
Complete management for Teable dashboards with security validations
"""

import os
import sys
import json
import argparse
import requests
from typing import Optional, List, Dict, Any
from urllib.parse import urljoin, urlparse

DEFAULT_BASE_URL = "https://app.teable.ai"
API_PREFIX = "/api"
ALLOWED_SCHEMES = ["https", "http"]


def validate_url(url: str) -> str:
    """Validate URL to prevent SSRF and credential exfiltration."""
    if not url:
        return url
    
    try:
        parsed = urlparse(url)
        
        if parsed.scheme.lower() not in ALLOWED_SCHEMES:
            raise ValueError(f"Invalid URL scheme: '{parsed.scheme}'. Only {ALLOWED_SCHEMES} are allowed.")
        
        if not parsed.netloc:
            raise ValueError(f"Invalid URL: missing domain in '{url}'")
        
        if parsed.scheme.lower() == "http":
            print("WARNING: Using HTTP instead of HTTPS. Your API key will be transmitted in plaintext!", file=sys.stderr)
        
        return url.rstrip("/")
        
    except Exception as e:
        raise ValueError(f"Invalid TEABLE_URL '{url}': {e}")


class TeableDashboardClient:
    """Teable Dashboard API Client with security validations"""

    def __init__(self, base_url: Optional[str] = None, [REDACTED_SECRET] = None):
        raw_url = base_url or os.getenv("TEABLE_URL") or DEFAULT_BASE_URL
        self.base_url = validate_url(raw_url)
        self.[REDACTED_SECRET] or os.getenv("TEABLE_API_KEY")

        if not self.api_key:
            raise ValueError("Teable API Key is not set! Please set TEABLE_API_KEY environment variable.")

        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })

    def _request(self, method: str, path: str, **kwargs) -> requests.Response:
        url = urljoin(self.base_url + API_PREFIX, path.lstrip("/"))
        response = self.session.request(method, url, **kwargs)
        return response

    def list_dashboards(self, base_id: str) -> List[Dict[str, Any]]:
        """Get Dashboard list"""
        response = self._request("GET", f"/base/{base_id}/dashboard")
        response.raise_for_status()
        return response.json()

    def get_dashboard(self, dashboard_id: str) -> Dict[str, Any]:
        """Get Dashboard details"""
        response = self._request("GET", f"/dashboard/{dashboard_id}")
        response.raise_for_status()
        return response.json()

    def create_dashboard(self, base_id: str, name: str, layout: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
        """Create Dashboard"""
        payload = {"baseId": base_id, "name": name}
        if layout:
            payload["layout"] = layout

        response = self._request("POST", f"/base/{base_id}/dashboard", json=payload)
        response.raise_for_status()
        return response.json()

    def update_dashboard(self, dashboard_id: str, name: Optional[str] = None,
                         layout: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
        """Update Dashboard"""
        payload = {}
        if name is not None:
            payload["name"] = name
        if layout is not None:
            payload["layout"] = layout

        response = self._request("PATCH", f"/dashboard/{dashboard_id}", json=payload)
        response.raise_for_status()
        return response.json()

    def rename_dashboard(self, dashboard_id: str, name: str) -> Dict[str, Any]:
        """Rename Dashboard"""
        return self.update_dashboard(dashboard_id, name=name)

    def update_dashboard_layout(self, dashboard_id: str, layout: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Update Dashboard layout"""
        return self.update_dashboard(dashboard_id, layout=layout)

    def delete_dashboard(self, dashboard_id: str) -> bool:
        """Delete Dashboard"""
        response = self._request("DELETE", f"/dashboard/{dashboard_id}")
        response.raise_for_status()
        return response.status_code == 200

    def duplicate_dashboard(self, dashboard_id: str, base_id: Optional[str] = None,
                           name: Optional[str] = None) -> Dict[str, Any]:
        """Duplicate Dashboard"""
        payload = {}
        if base_id:
            payload["baseId"] = base_id
        if name:
            payload["name"] = name

        response = self._request("POST", f"/dashboard/{dashboard_id}/duplicate", json=payload)
        response.raise_for_status()
        return response.json()

    # ==================== Plugin Management ====================

    def install_plugin(self, dashboard_id: str, plugin_id: str, name: Optional[str] = None,
                      storage: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Install plugin to Dashboard"""
        payload = {"pluginId": plugin_id}
        if name:
            payload["name"] = name
        if storage:
            payload["storage"] = storage

        response = self._request("POST", f"/dashboard/{dashboard_id}/plugin", json=payload)
        response.raise_for_status()
        return response.json()

    def get_plugin(self, dashboard_id: str, plugin_install_id: str) -> Dict[str, Any]:
        """Get installed plugin details"""
        response = self._request("GET", f"/dashboard/{dashboard_id}/plugin/{plugin_install_id}")
        response.raise_for_status()
        return response.json()

    def list_plugins(self, dashboard_id: str) -> List[Dict[str, Any]]:
        """Get installed plugin list"""
        response = self._request("GET", f"/dashboard/{dashboard_id}/plugin")
        response.raise_for_status()
        return response.json()

    def rename_plugin(self, dashboard_id: str, plugin_install_id: str, name: str) -> Dict[str, Any]:
        """Rename installed plugin"""
        payload = {"name": name}
        response = self._request("PATCH", f"/dashboard/{dashboard_id}/plugin/{plugin_install_id}/rename", json=payload)
        response.raise_for_status()
        return response.json()

    def update_plugin_storage(self, dashboard_id: str, plugin_install_id: str,
                             storage: Dict[str, Any]) -> Dict[str, Any]:
        """Update plugin storage config"""
        payload = {"storage": storage}
        response = self._request("PATCH", f"/dashboard/{dashboard_id}/plugin/{plugin_install_id}/update-storage", json=payload)
        response.raise_for_status()
        return response.json()

    def remove_plugin(self, dashboard_id: str, plugin_install_id: str) -> bool:
        """Remove plugin from Dashboard"""
        response = self._request("DELETE", f"/dashboard/{dashboard_id}/plugin/{plugin_install_id}")
        response.raise_for_status()
        return response.status_code == 200

    def duplicate_plugin(self, dashboard_id: str, plugin_install_id: str) -> Dict[str, Any]:
        """Duplicate installed plugin"""
        response = self._request("POST", f"/dashboard/{dashboard_id}/plugin/{plugin_install_id}/duplicate")
        response.raise_for_status()
        return response.json()


def main():
    parser = argparse.ArgumentParser(description="Teable Dashboard API Client")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    list_parser = subparsers.add_parser("list", help="List dashboards")
    list_parser.add_argument("--base-id", required=True, help="Base ID")

    get_parser = subparsers.add_parser("get", help="Get Dashboard details")
    get_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")

    create_parser = subparsers.add_parser("create", help="Create Dashboard")
    create_parser.add_argument("--base-id", required=True, help="Base ID")
    create_parser.add_argument("--name", required=True, help="Dashboard name")
    create_parser.add_argument("--layout", help="Layout config (JSON)")

    update_parser = subparsers.add_parser("update", help="Update Dashboard")
    update_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")
    update_parser.add_argument("--name", help="New name")
    update_parser.add_argument("--layout", help="New layout (JSON)")

    delete_parser = subparsers.add_parser("delete", help="Delete Dashboard")
    delete_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")

    dup_parser = subparsers.add_parser("duplicate", help="Duplicate Dashboard")
    dup_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")
    dup_parser.add_argument("--base-id", help="Target Base ID")
    dup_parser.add_argument("--name", help="New Dashboard name")

    install_plugin_parser = subparsers.add_parser("install-plugin", help="Install plugin")
    install_plugin_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")
    install_plugin_parser.add_argument("--plugin-id", required=True, help="Plugin ID")
    install_plugin_parser.add_argument("--name", help="Plugin name")
    install_plugin_parser.add_argument("--storage", help="Storage config (JSON)")

    list_plugins_parser = subparsers.add_parser("list-plugins", help="List installed plugins")
    list_plugins_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")

    remove_plugin_parser = subparsers.add_parser("remove-plugin", help="Remove plugin")
    remove_plugin_parser.add_argument("--dashboard-id", required=True, help="Dashboard ID")
    remove_plugin_parser.add_argument("--plugin-install-id", required=True, help="Plugin install ID")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    try:
        client = TeableDashboardClient()

        if args.command == "list":
            result = client.list_dashboards(args.base_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "get":
            result = client.get_dashboard(args.dashboard_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "create":
            layout = json.loads(args.layout) if args.layout else None
            result = client.create_dashboard(args.base_id, args.name, layout)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "update":
            layout = json.loads(args.layout) if args.layout else None
            result = client.update_dashboard(args.dashboard_id, args.name, layout)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "delete":
            success = client.delete_dashboard(args.dashboard_id)
            print(f"Deletion {'successful' if success else 'failed'}")

        elif args.command == "duplicate":
            result = client.duplicate_dashboard(args.dashboard_id, args.base_id, args.name)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "install-plugin":
            storage = json.loads(args.storage) if args.storage else None
            result = client.install_plugin(args.dashboard_id, args.plugin_id, args.name, storage)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "list-plugins":
            result = client.list_plugins(args.dashboard_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))

        elif args.command == "remove-plugin":
            success = client.remove_plugin(args.dashboard_id, args.plugin_install_id)
            print(f"Removal {'successful' if success else 'failed'}")

    except ValueError as e:
        print(f"Validation error: {e}", file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error: {e.response.status_code}", file=sys.stderr)
        print(e.response.text, file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)