Skill: techstylex/supabase-skills (version 1.0.0)

--- name: volcengine-supabase description: Manage Volcengine Supabase workspaces, branches, SQL queries, migrations, Edge Functions, Storage, and TypeScript type generation via a local CLI. Run uv run ./scripts/call_volcengine_supabase.py to get real-time results. metadata: {"clawdbot":{"emoji":"🧩","homepage":"https://www.volcengine.com/","requires":{"bins":["uv"]},"os":["darwin","linux"]},"openclaw":{"emoji":"🧩","homepage":"https://www.volcengine.com/","requires":{"bins":["uv"]},"os":["darwin","linux"]},"moltbot":{"emoji":"🧩","homepage":"https://www.volcengine.com/","requires":{"bins":["uv"]},"os":["darwin","linux"]}} --- # 火山引擎 Supabase 本 Skill 用于在对话中充当**火山引擎 Supabase 的智能运维与开发代理**。 它会： - 识别用户的 Supabase 自然语言需求 - 直接调用 `scripts/call_volcengine_supabase.py` 获取实时结果 - 基于返回结果做解释、排障和下一步建议 ## 运行方式 ```bash # 方式 1：使用 uv（推荐） uv run ./scripts/call_volcengine_supabase.py <action> [options] # 方式 2：使用 python（需预装依赖） python ./scripts/call_volcengine_supabase.py <action> [options] ``` ## 前置条件 - 必需环境变量：`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`（如果在沙箱环境/vefaas IAM 环境下运行，将自动获取临时凭证，可不配置环境变量） - 可选环境变量：`VOLCENGINE_REGION`、`DEFAULT_WORKSPACE_ID`、`READ_ONLY`、`SUPABASE_WORKSPACE_SLUG`、`SUPABASE_ENDPOINT_SCHEME` - 若未配置依赖，可先执行：`uv pip install -r requirements.txt` 或 `pip install -r requirements.txt` ## 标准使用流程 1. 先确认目标资源：`workspace_id` 或 `branch_id` 2. 优先执行只读查询，确认现状 3. 需要变更时，再执行写操作 4. 变更后再次查询，确认结果已生效 ## 常用命令示例 ```bash # 查看可访问的 workspace uv run ./scripts/call_volcengine_supabase.py list-workspaces # 查看 workspace 详情 uv run ./scripts/call_volcengine_supabase.py describe-workspace --workspace-id ws-xxxx # 获取 workspace URL uv run ./scripts/call_volcengine_supabase.py get-workspace-url --workspace-id ws-xxxx # 查看分支 uv run ./scripts/call_volcengine_supabase.py list-branches --workspace-id ws-xxxx # 执行 SQL uv run ./scripts/call_volcengine_supabase.py execute-sql --workspace-id ws-xxxx --query "SELECT * FROM pg_tables LIMIT 5" # 从文件执行 migration uv run ./scripts/call_volcengine_supabase.py apply-migration --workspace-id ws-xxxx --name create_todos_table --query-file ./migration.sql # 部署 Edge Function uv run ./scripts/call_volcengine_supabase.py deploy-edge-function --workspace-id ws-xxxx --function-name hello --source-file ./index.ts # 创建 Storage bucket uv run ./scripts/call_volcengine_supabase.py create-storage-bucket --workspace-id ws-xxxx --bucket-name uploads --public ``` ## 能力范围 ### 工作区与分支 - `list-workspaces` - `describe-workspace` - `create-workspace` - `pause-workspace` - `restore-workspace` - `get-workspace-url` - `get-keys` - `list-branches` - `create-branch` - `delete-branch` - `reset-branch` ### 数据库 - `execute-sql` - `list-tables` - `list-migrations` - `list-extensions` - `apply-migration` - `generate-typescript-types` ### Edge Functions / Storage - `list-edge-functions` - `get-edge-function` - `deploy-edge-function` - `delete-edge-function` - `list-storage-buckets` - `create-storage-bucket` - `delete-storage-bucket` - `get-storage-config` ## 应用开发参考 在使用本 Skill 管理 Supabase 资源的同时，以下文档提供应用开发场景的指导： | 需求 | 文档 | |------|------| | 将 Supabase 接入 TS/Python 应用（SDK 初始化 + CRUD） | `references/app-integration-guide.md` | | 数据库表结构设计与迁移规范 | `references/schema-guide.md` | | 行级安全策略（RLS）配置 | `references/rls-guide.md` | | Edge Function 编写与部署 | `references/edge-function-dev-guide.md` | > 💡 **典型工作流**：先用 CLI 创建 workspace / 建表 / 配置 RLS，再参考应用开发文档在业务代码中集成 Supabase SDK。 ## 注意事项 - 默认遵循“先查后改” - `get-keys` 默认脱敏，只有明确需要时才加 `--reveal` - `reset-branch` 会丢失未追踪变更，且后端当前会忽略 `migration_version` - `READ_ONLY=true` 时，所有写操作会被拒绝 ## 参考资料 - 工具说明：`references/tool-reference.md` - 操作流程：`references/workflows.md` - SQL 示例：`references/sql-playbook.md` - 应用集成：`references/app-integration-guide.md` - Schema 设计：`references/schema-guide.md` - RLS 策略：`references/rls-guide.md` - Edge Function 开发：`references/edge-function-dev-guide.md`

path: _meta.json
content: slug: volcengine-supabase
version: 1.0.0

path: requirements.txt
content: httpx>=0.27.0
pydantic>=2.0.0
git+https://github.com/sjcsjcsjc/volcengine-python-sdk.git@9905a8853a0e5fd26fdae93eefb4f201e8bef539


path: scripts/call_volcengine_supabase.py
content: #!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import asyncio
import json
import logging
import os
import sys
from pathlib import Path
from typing import Optional


def _bootstrap_env(argv: list[str]) -> None:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--region")
    parser.add_argument("--default-workspace-id")
    parser.add_argument("--read-only", choices=["true", "false"])
    parser.add_argument("--workspace-slug")
    parser.add_argument("--endpoint-scheme")
    known, _ = parser.parse_known_args(argv)
    if known.region:
        os.environ["VOLCENGINE_REGION"] = known.region
    if known.default_workspace_id:
        os.environ["DEFAULT_WORKSPACE_ID"] = known.default_workspace_id
    if known.read_only:
        os.environ["READ_ONLY"] = known.read_only
    if known.workspace_slug:
        os.environ["SUPABASE_WORKSPACE_SLUG"] = known.workspace_slug
    if known.endpoint_scheme:
        os.environ["SUPABASE_ENDPOINT_SCHEME"] = known.endpoint_scheme


_BOOTSTRAP_ARGS = sys.argv[1:]
_bootstrap_env(_BOOTSTRAP_ARGS)

WORKSPACE_ACTIONS = {
    "list-workspaces",
    "describe-workspace",
    "create-workspace",
    "pause-workspace",
    "restore-workspace",
    "get-workspace-url",
    "get-keys",
    "list-branches",
    "create-branch",
    "delete-branch",
    "reset-branch",
}
DATABASE_ACTIONS = {
    "execute-sql",
    "list-tables",
    "list-migrations",
    "list-extensions",
    "apply-migration",
    "generate-typescript-types",
}
EDGE_ACTIONS = {
    "list-edge-functions",
    "get-edge-function",
    "deploy-edge-function",
    "delete-edge-function",
}
STORAGE_ACTIONS = {
    "list-storage-buckets",
    "create-storage-bucket",
    "delete-storage-bucket",
    "get-storage-config",
}


def _read_text(value: Optional[str], file_path: Optional[str], label: str) -> str:
    if value and file_path:
        raise ValueError(f"{label} and {label}-file cannot be used together")
    if file_path:
        return Path(file_path).read_text(encoding="utf-8")
    if value:
        return value
    raise ValueError(f"{label} or {label}-file is required")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="火山引擎 Supabase 本地 CLI（无 MCP 依赖）",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("action", help="执行动作")
    parser.add_argument("--region", default=os.getenv("VOLCENGINE_REGION", "cn-beijing"), help="火山引擎地域")
    parser.add_argument("--default-workspace-id", default=os.getenv("DEFAULT_WORKSPACE_ID"), help="默认 workspace ID")
    parser.add_argument("--read-only", choices=["true", "false"], default=os.getenv("READ_ONLY", "false"), help="是否只读")
    parser.add_argument("--workspace-slug", default=os.getenv("SUPABASE_WORKSPACE_SLUG", "default"), help="Edge Functions 项目 slug")
    parser.add_argument("--endpoint-scheme", default=os.getenv("SUPABASE_ENDPOINT_SCHEME", "http"), help="workspace URL 协议")
    parser.add_argument("--workspace-id", help="workspace ID 或 branch ID")
    parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="日志级别")

    parser.add_argument("--workspace-name")
    parser.add_argument("--engine-version", default="Supabase_1_24")
    parser.add_argument("--engine-type", default="Supabase")
    parser.add_argument("--name")
    parser.add_argument("--branch-id")
    parser.add_argument("--migration-version")
    parser.add_argument("--reveal", action="store_true")

    parser.add_argument("--query")
    parser.add_argument("--query-file")
    parser.add_argument("--schemas", default="public")

    parser.add_argument("--function-name")
    parser.add_argument("--source-code")
    parser.add_argument("--source-file")
    parser.add_argument("--verify-jwt", dest="verify_jwt", action="store_true", default=True)
    parser.add_argument("--no-verify-jwt", dest="verify_jwt", action="store_false")
    parser.add_argument("--runtime", default="native-node20/v1")
    parser.add_argument("--import-map")
    parser.add_argument("--import-map-file")

    parser.add_argument("--bucket-name")
    parser.add_argument("--public", action="store_true")
    parser.add_argument("--file-size-limit", type=int)
    parser.add_argument("--allowed-mime-types")
    return parser


async def run_action(args: argparse.Namespace) -> str:
    from volcengine_supabase.platform import AidapClient
    from volcengine_supabase.tools.database_tools import DatabaseTools
    from volcengine_supabase.tools.edge_function_tools import EdgeFunctionTools
    from volcengine_supabase.tools.storage_tools import StorageTools
    from volcengine_supabase.tools.workspace_tools import WorkspaceTools

    aidap = AidapClient()
    workspace_tools = WorkspaceTools(aidap, args.default_workspace_id)
    database_tools = DatabaseTools(aidap, args.default_workspace_id)
    edge_tools = EdgeFunctionTools(aidap, args.default_workspace_id)
    storage_tools = StorageTools(aidap, args.default_workspace_id)

    action = args.action
    if action == "list-workspaces":
        return await workspace_tools.list_workspaces()
    if action == "describe-workspace":
        if not args.workspace_id:
            raise ValueError("--workspace-id is required")
        return await workspace_tools.get_workspace(args.workspace_id)
    if action == "create-workspace":
        if not args.workspace_name:
            raise ValueError("--workspace-name is required")
        return await workspace_tools.create_workspace(args.workspace_name, args.engine_version, args.engine_type)
    if action == "pause-workspace":
        return await workspace_tools.pause_workspace(args.workspace_id)
    if action == "restore-workspace":
        return await workspace_tools.restore_workspace(args.workspace_id)
    if action == "get-workspace-url":
        return await workspace_tools.get_workspace_url(args.workspace_id)
    if action == "get-keys":
        return await workspace_tools.get_publishable_keys(args.workspace_id, args.reveal)
    if action == "list-branches":
        return await workspace_tools.list_branches(args.workspace_id)
    if action == "create-branch":
        return await workspace_tools.create_branch(args.name or "develop", args.workspace_id)
    if action == "delete-branch":
        if not args.branch_id:
            raise ValueError("--branch-id is required")
        return await workspace_tools.delete_branch(args.branch_id, args.workspace_id)
    if action == "reset-branch":
        if not args.branch_id:
            raise ValueError("--branch-id is required")
        return await workspace_tools.reset_branch(args.branch_id, args.migration_version, args.workspace_id)

    if action == "execute-sql":
        query = _read_text(args.query, args.query_file, "--query")
        return await database_tools.execute_sql(query, args.workspace_id)
    if action == "list-tables":
        schemas = [schema.strip() for schema in args.schemas.split(",") if schema.strip()]
        return await database_tools.list_tables(schemas, args.workspace_id)
    if action == "list-migrations":
        return await database_tools.list_migrations(args.workspace_id)
    if action == "list-extensions":
        return await database_tools.list_extensions(args.workspace_id)
    if action == "apply-migration":
        if not args.name:
            raise ValueError("--name is required")
        query = _read_text(args.query, args.query_file, "--query")
        return await database_tools.apply_migration(args.name, query, args.workspace_id)
    if action == "generate-typescript-types":
        schemas = [schema.strip() for schema in args.schemas.split(",") if schema.strip()]
        return await database_tools.generate_typescript_types(schemas, args.workspace_id)

    if action == "list-edge-functions":
        return await edge_tools.list_edge_functions(args.workspace_id)
    if action == "get-edge-function":
        if not args.function_name:
            raise ValueError("--function-name is required")
        return await edge_tools.get_edge_function(args.function_name, args.workspace_id)
    if action == "deploy-edge-function":
        if not args.function_name:
            raise ValueError("--function-name is required")
        source_code = _read_text(args.source_code, args.source_file, "--source-code")
        import_map = None
        if args.import_map or args.import_map_file:
            import_map = _read_text(args.import_map, args.import_map_file, "--import-map")
        return await edge_tools.deploy_edge_function(
            args.function_name,
            source_code,
            args.verify_jwt,
            args.runtime,
            import_map,
            args.workspace_id,
        )
    if action == "delete-edge-function":
        if not args.function_name:
            raise ValueError("--function-name is required")
        return await edge_tools.delete_edge_function(args.function_name, args.workspace_id)

    if action == "list-storage-buckets":
        return await storage_tools.list_storage_buckets(args.workspace_id)
    if action == "create-storage-bucket":
        if not args.bucket_name:
            raise ValueError("--bucket-name is required")
        return await storage_tools.create_storage_bucket(
            args.bucket_name,
            args.public,
            args.file_size_limit,
            args.allowed_mime_types,
            args.workspace_id,
        )
    if action == "delete-storage-bucket":
        if not args.bucket_name:
            raise ValueError("--bucket-name is required")
        return await storage_tools.delete_storage_bucket(args.bucket_name, args.workspace_id)
    if action == "get-storage-config":
        return await storage_tools.get_storage_config(args.workspace_id)

    supported = sorted(WORKSPACE_ACTIONS | DATABASE_ACTIONS | EDGE_ACTIONS | STORAGE_ACTIONS)
    raise ValueError(f"Unsupported action: {action}. Available actions: {', '.join(supported)}")


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()

    logging.basicConfig(
        level=getattr(logging, args.log_level),
        format="[%(levelname)s] %(message)s",
    )

    try:
        result = asyncio.run(run_action(args))
        print(result)
        return 0
    except Exception as exc:
        print(json.dumps({"success": False, "error": str(exc)}, ensure_ascii=False))
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/volcengine_supabase/config.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import logging

logger = logging.getLogger(__name__)

READ_ONLY = os.getenv("READ_ONLY", "false").lower() == "true"

VOLCENGINE_ACCESS_KEY = os.getenv("VOLCENGINE_ACCESS_KEY")
VOLCENGINE_SECRET_KEY = os.getenv("VOLCENGINE_SECRET_KEY")
VOLCENGINE_SESSION_TOKEN = ""
VOLCENGINE_REGION = os.getenv("VOLCENGINE_REGION", "cn-beijing")

# 当环境变量未设置时，尝试从 vefaas IAM 获取凭证（沙箱环境）
if not (VOLCENGINE_ACCESS_KEY and VOLCENGINE_SECRET_KEY):
    try:
        from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
    except ImportError:
        get_credential_from_vefaas_iam = None

    if get_credential_from_vefaas_iam:
        try:
            cred = get_credential_from_vefaas_iam()
            VOLCENGINE_ACCESS_KEY = cred.access_key_id
            VOLCENGINE_SECRET_KEY = cred.secret_access_key
            VOLCENGINE_SESSION_TOKEN = cred.session_token
        except Exception as e:
            logger.warning(f"Failed to get credential from vefaas IAM: {e}")

# 验证必需的环境变量
if not VOLCENGINE_ACCESS_KEY:
    logger.warning("VOLCENGINE_ACCESS_KEY not set")
if not VOLCENGINE_SECRET_KEY:
    logger.warning("VOLCENGINE_SECRET_KEY not set")

_default_branch_cache = {}
_endpoint_cache = {}
_api_key_cache = {}
_branch_workspace_cache = {}


def get_branch_cache():
    return _default_branch_cache


def get_endpoint_cache():
    return _endpoint_cache


def get_api_key_cache():
    return _api_key_cache


def get_branch_workspace_cache():
    return _branch_workspace_cache


def clear_branch_cache(workspace_id: str = None):
    if workspace_id:
        _default_branch_cache.pop(workspace_id, None)
    else:
        _default_branch_cache.clear()


def clear_endpoint_cache(workspace_id: str = None, branch_id: str = None):
    if workspace_id and branch_id:
        _endpoint_cache.pop(f"{workspace_id}:{branch_id}", None)
    elif workspace_id:
        _endpoint_cache.pop(workspace_id, None)
        keys_to_delete = [key for key in _endpoint_cache if key.startswith(f"{workspace_id}:")]
        for key in keys_to_delete:
            _endpoint_cache.pop(key, None)
    else:
        _endpoint_cache.clear()


def clear_api_key_cache(workspace_id: str = None, branch_id: str = None):
    if workspace_id and branch_id:
        keys_to_delete = [key for key in _api_key_cache if key.startswith(f"{workspace_id}:") and key.endswith(f":{branch_id}")]
        for key in keys_to_delete:
            _api_key_cache.pop(key, None)
    elif workspace_id:
        keys_to_delete = [key for key in _api_key_cache if key == workspace_id or key.startswith(f"{workspace_id}:")]
        for key in keys_to_delete:
            _api_key_cache.pop(key, None)
    else:
        _api_key_cache.clear()


def clear_branch_workspace_cache(workspace_id: str = None, branch_id: str = None):
    if branch_id:
        _branch_workspace_cache.pop(branch_id, None)
    elif workspace_id:
        branch_ids = [key for key, value in _branch_workspace_cache.items() if value == workspace_id]
        for key in branch_ids:
            _branch_workspace_cache.pop(key, None)
    else:
        _branch_workspace_cache.clear()


def clear_all_caches(workspace_id: str = None, branch_id: str = None):
    clear_branch_cache(workspace_id)
    clear_endpoint_cache(workspace_id, branch_id)
    clear_api_key_cache(workspace_id, branch_id)
    clear_branch_workspace_cache(workspace_id, branch_id)


path: scripts/volcengine_supabase/__init__.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Supabase MCP Server package."""

__version__ = "0.1.0"


path: scripts/volcengine_supabase/models/database.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional, List
from pydantic import BaseModel, Field


class Column(BaseModel):
    name: str
    format: str
    is_nullable: Optional[bool] = None
    is_unique: Optional[bool] = None
    default_value: Optional[str] = None


class Table(BaseModel):
    schema_name: str = Field(alias="schema")
    name: str
    columns: List[Column] = []
    
    class Config:
        populate_by_name = True


class Migration(BaseModel):
    version: str
    name: str


path: scripts/volcengine_supabase/models/__init__.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .workspace import Workspace, Branch, ApiKey
from .edge_function import EdgeFunction, EdgeFunctionDeployment
from .storage import StorageBucket, StorageConfig
from .database import Table, Column, Migration

__all__ = [
    'Workspace',
    'Branch',
    'ApiKey',
    'EdgeFunction',
    'EdgeFunctionDeployment',
    'StorageBucket',
    'StorageConfig',
    'Table',
    'Column',
    'Migration',
]


path: scripts/volcengine_supabase/models/edge_function.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional, List, Dict, Any
from pydantic import BaseModel


class EdgeFunction(BaseModel):
    id: str
    slug: str
    name: str
    status: str
    version: int
    created_at: str
    updated_at: str
    verify_jwt: bool
    entrypoint_path: str
    runtime_config: Optional[str] = None
    runtime: Optional[str] = None


class EdgeFunctionFile(BaseModel):
    name: str
    content: str


class EdgeFunctionDeployment(BaseModel):
    name: str
    entrypoint_path: str = "index.ts"
    verify_jwt: bool = True
    import_map_path: Optional[str] = None
    files: List[EdgeFunctionFile]


path: scripts/volcengine_supabase/models/storage.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from pydantic import BaseModel


class StorageBucket(BaseModel):
    id: str
    name: str
    owner: Optional[str] = None
    public: bool = False
    created_at: Optional[str] = None
    updated_at: Optional[str] = None


class StorageConfig(BaseModel):
    fileSizeLimit: int
    totalFileSizeLimit: Optional[int] = None


path: scripts/volcengine_supabase/models/workspace.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field


class Workspace(BaseModel):
    workspace_id: str
    workspace_name: str
    status: str
    region: str
    create_time: str
    engine_type: str
    engine_version: str


class Branch(BaseModel):
    branch_id: str
    branch_name: Optional[str] = None
    default: bool = False
    workspace_id: Optional[str] = None


class ApiKey(BaseModel):
    key: str
    name: str
    type: str
    create_time: Optional[str] = None


path: scripts/volcengine_supabase/utils/targets.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional


def select_target_id(target_id: Optional[str], default_target_id: Optional[str]) -> Optional[str]:
    return target_id or default_target_id


async def resolve_target(aidap_client, target_id: Optional[str], default_target_id: Optional[str]) -> tuple[Optional[str], Optional[str]]:
    resolved_id = select_target_id(target_id, default_target_id)
    if not resolved_id:
        return None, None
    return await aidap_client.resolve_workspace_and_branch(resolved_id)


path: scripts/volcengine_supabase/utils/__init__.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .common import compact_dict, pick_value, to_json
from .decorators import format_error, handle_errors, read_only_check
from .targets import resolve_target, select_target_id

__all__ = [
    'compact_dict',
    'format_error',
    'handle_errors',
    'pick_value',
    'read_only_check',
    'resolve_target',
    'select_target_id',
    'to_json',
]


path: scripts/volcengine_supabase/utils/common.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from typing import Any


def to_json(payload: Any) -> str:
    return json.dumps(payload, indent=2, ensure_ascii=False)


def compact_dict(payload: dict) -> dict:
    return {key: value for key, value in payload.items() if value is not None}


def pick_value(source: Any, *field_names: str) -> Any:
    source_dict = source.to_dict() if hasattr(source, "to_dict") else source if isinstance(source, dict) else {}
    for field_name in field_names:
        value = None
        if isinstance(source, dict):
            value = source.get(field_name)
        else:
            value = getattr(source, field_name, None)
        if value is None and isinstance(source_dict, dict):
            value = source_dict.get(field_name)
        if isinstance(value, str):
            value = value.strip()
            if not value:
                value = None
        if value is not None:
            return value
    return None


path: scripts/volcengine_supabase/utils/decorators.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
from functools import wraps
from typing import Any, Callable

from .common import to_json

logger = logging.getLogger(__name__)


def format_error(e: Exception) -> str:
    error_msg = str(e) if str(e) else f"{type(e).__name__}"
    return error_msg


def handle_errors(func: Callable) -> Callable:
    @wraps(func)
    async def wrapper(*args, **kwargs) -> str:
        try:
            result = await func(*args, **kwargs)
            if isinstance(result, str):
                return result
            if isinstance(result, list):
                if result and hasattr(result[0], 'model_dump'):
                    result = [item.model_dump() for item in result]
            elif hasattr(result, 'model_dump'):
                result = result.model_dump()
            return to_json(result)
        except Exception as e:
            error_msg = format_error(e)
            logger.error(f"Error in {func.__name__}: {error_msg}")
            return to_json({"error": error_msg})
    return wrapper


def read_only_check(func: Callable) -> Callable:
    @wraps(func)
    async def wrapper(*args, **kwargs) -> Any:
        from ..config import READ_ONLY
        if READ_ONLY:
            return to_json({"error": f"Cannot execute {func.__name__} in read-only mode"})
        return await func(*args, **kwargs)
    return wrapper


path: scripts/volcengine_supabase/platform/__init__.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .aidap_client import AidapClient
from .supabase_client import SupabaseClient

__all__ = ['AidapClient', 'SupabaseClient']


path: scripts/volcengine_supabase/platform/supabase_client.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import httpx
import logging
import json
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)


class SupabaseApiError(Exception):
    def __init__(self, status_code: int, path: str, endpoint: str, payload: Any):
        self.status_code = status_code
        self.path = path
        self.endpoint = endpoint
        self.payload = payload
        super().__init__(
            json.dumps(
                {
                    "status_code": status_code,
                    "path": path,
                    "endpoint": endpoint,
                    "error": payload,
                },
                ensure_ascii=False,
            )
        )


class SupabaseClient:
    def __init__(self, endpoint: str, api_key: str):
        self.endpoint = endpoint
        self.[REDACTED_SECRET]
        self._client: Optional[httpx.AsyncClient] = None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create HTTP client with connection pooling"""
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                timeout=30.0,
                limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
            )
        return self._client

    async def close(self):
        """Close HTTP client"""
        if self._client and not self._client.is_closed:
            await self._client.aclose()

    async def call_api(
        self,
        path: str,
        method: str = "GET",
        json_data: Optional[Dict] = None,
        headers: Optional[Dict] = None,
        params: Optional[Dict] = None,
        content: Optional[bytes] = None,
        timeout: float = 30.0
    ) -> Any:
        url = f"{self.endpoint}{path}"
        logger.info(f"[DEBUG] Calling API: method={method}, url={url}, path={path}")

        default_headers = {
            "apikey": self.api_key,
            "Authorization": f"Bearer {self.api_key}",
        }
        if headers:
            default_headers.update(headers)

        client = await self._get_client()
        for attempt in range(3):
            try:
                if content:
                    response = await client.request(
                        method, url, content=content, headers=default_headers,
                        params=params, timeout=timeout
                    )
                else:
                    response = await client.request(
                        method, url, json=json_data, headers=default_headers,
                        params=params, timeout=timeout
                    )
                response.raise_for_status()

                if response.status_code == 204 or not response.content:
                    return {"success": True}

                content_type = response.headers.get("content-type", "")
                if "application/json" in content_type:
                    return response.json()
                return {"raw": response.text}
            except httpx.HTTPStatusError as e:
                response = e.response
                if response.status_code in {502, 503, 504} and attempt < 2:
                    await asyncio.sleep(0.5 * (attempt + 1))
                    continue
                payload: Any
                try:
                    payload = response.json()
                except Exception:
                    payload = response.text
                raise SupabaseApiError(
                    status_code=response.status_code,
                    path=path,
                    endpoint=self.endpoint,
                    payload=payload,
                ) from e
            except httpx.TransportError as e:
                if attempt < 2:
                    await asyncio.sleep(0.5 * (attempt + 1))
                    continue
                detail = str(e) or type(e).__name__
                raise Exception(f"{detail} [endpoint: {self.endpoint}, path: {path}]") from e
            except Exception as e:
                if isinstance(e, SupabaseApiError):
                    raise
                detail = str(e) or type(e).__name__
                if hasattr(e, "__cause__") and e.__cause__:
                    cause_detail = str(e.__cause__) or type(e.__cause__).__name__
                    detail += f" | Cause: {cause_detail}"
                raise Exception(f"{detail} [endpoint: {self.endpoint}, path: {path}]") from e


path: scripts/volcengine_supabase/platform/aidap_client.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import asyncio
import os
import random
from typing import Any, Optional
from ..config import (
    VOLCENGINE_ACCESS_KEY,
    VOLCENGINE_SECRET_KEY,
    VOLCENGINE_SESSION_TOKEN,
    VOLCENGINE_REGION,
    get_branch_cache,
    get_branch_workspace_cache,
    get_endpoint_cache,
    get_api_key_cache,
    clear_all_caches,
)
from ..utils import pick_value

logger = logging.getLogger(__name__)
ENDPOINT_SCHEME = os.getenv("SUPABASE_ENDPOINT_SCHEME", "http").strip().lower() or "http"

try:
    import volcenginesdkcore
    from volcenginesdkaidap import AIDAPApi
    from volcenginesdkaidap.models import (
        DescribeBranchesRequest,
        DescribeWorkspacesRequest,
        DescribeWorkspaceEndpointRequest,
        DescribeAPIKeysRequest,
        ResetBranchRequest,
        CreateBranchRequest,
        DeleteBranchRequest,
        BranchSettingsForCreateBranchInput,
        CreateWorkspaceRequest,
        WorkspaceSettingsForCreateWorkspaceInput,
        BranchSettingsForCreateWorkspaceInput,
        ComputeSettingsForCreateWorkspaceInput,
        StartWorkspaceRequest,
        StopWorkspaceRequest,
    )
except ImportError:
    logger.error("volcengine-python-sdk not installed")
    raise


class AidapClient:
    def __init__(self) -> None:
        configuration = volcenginesdkcore.Configuration()
        configuration.ak = VOLCENGINE_ACCESS_KEY
        configuration.sk = VOLCENGINE_SECRET_KEY
        configuration.region = VOLCENGINE_REGION
        if VOLCENGINE_SESSION_TOKEN:
            configuration.session_token = VOLCENGINE_SESSION_TOKEN

        api_client = volcenginesdkcore.ApiClient(configuration)
        self.client = AIDAPApi(api_client)

    def _branch_error_code(self, error_text: str) -> str:
        if "OperationDenied_BranchNotReady" in error_text:
            return "OperationDenied_BranchNotReady"
        if "BranchStatusNotMatch" in error_text:
            return "BranchStatusNotMatch"
        if "BranchNotFound" in error_text:
            return "BranchNotFound"
        return "AIDAPError"

    def _pick_value(self, source: Any, *field_names: str) -> Any:
        return pick_value(source, *field_names)

    def _looks_like_branch_id(self, value: Optional[str]) -> bool:
        return bool(value and value.strip().startswith("br-"))

    def _cache_branch_workspace(self, workspace_id: Optional[str], branch_id: Optional[str]) -> None:
        if workspace_id and branch_id:
            get_branch_workspace_cache()[branch_id] = workspace_id

    def _workspace_ids_from_response(self, response: Any) -> list[str]:
        workspace_ids = []
        for workspace in list(getattr(response, "workspaces", []) or []):
            workspace_id = self._pick_value(workspace, "workspace_id")
            if workspace_id:
                workspace_ids.append(workspace_id)
        return workspace_ids

    def _branch_payload(self, branch: Any, fallback_name: Optional[str] = None) -> dict:
        parent_branch = self._pick_value(branch, "parent_branch")
        parent_id = self._pick_value(parent_branch, "branch_id", "parent_id")
        payload = {
            "branch_id": self._pick_value(branch, "branch_id"),
            "name": self._pick_value(branch, "name", "branch_name") or fallback_name,
            "status": self._pick_value(branch, "status", "branch_status"),
            "default": bool(self._pick_value(branch, "default", "is_default") or False),
            "parent_id": parent_id or self._pick_value(branch, "parent_id", "parent_branch_id"),
            "workspace_id": self._pick_value(branch, "workspace_id"),
            "archived": self._pick_value(branch, "archived"),
            "protected": self._pick_value(branch, "protected"),
            "created_at": self._pick_value(branch, "create_time", "created_at"),
            "updated_at": self._pick_value(branch, "update_time", "updated_at"),
        }
        result = {key: value for key, value in payload.items() if value is not None}
        self._cache_branch_workspace(result.get("workspace_id"), result.get("branch_id"))
        return result

    def _describe_supabase_workspaces_response(self):
        request = DescribeWorkspacesRequest()
        return self.client.describe_workspaces(request)

    async def _find_branch(
        self,
        workspace_id: str,
        branch_id: Optional[str] = None,
        name: Optional[str] = None,
        max_attempts: int = 6,
        ) -> Optional[dict]:
        for attempt in range(1, max_attempts + 1):
            branches = await self.list_branches(workspace_id)
            for branch in branches:
                if branch_id and branch.get("branch_id") == branch_id:
                    return branch
                if name and branch.get("name") == name:
                    return branch
            if attempt < max_attempts:
                await self._sleep_backoff(attempt, base_seconds=0.5, max_seconds=3.0)
        return None

    async def _find_workspace_id_for_branch(self, branch_id: str) -> Optional[str]:
        cached_workspace_id = get_branch_workspace_cache().get(branch_id)
        if cached_workspace_id:
            return cached_workspace_id
        response = self._describe_supabase_workspaces_response()
        for workspace_id in self._workspace_ids_from_response(response):
            branch = await self._find_branch(workspace_id, branch_id=branch_id, max_attempts=1)
            if branch:
                self._cache_branch_workspace(workspace_id, branch_id)
                return workspace_id
        return None

    async def resolve_workspace_and_branch(self, workspace_or_branch_id: str) -> tuple[str, Optional[str]]:
        normalized_id = workspace_or_branch_id.strip()
        if not self._looks_like_branch_id(normalized_id):
            return normalized_id, None
        workspace_id = await self._find_workspace_id_for_branch(normalized_id)
        if not workspace_id:
            raise ValueError(f"Could not resolve workspace for branch {normalized_id}")
        return workspace_id, normalized_id

    async def get_branch(self, workspace_id: str, branch_id: str) -> Optional[dict]:
        return await self._find_branch(workspace_id, branch_id=branch_id, max_attempts=1)

    async def _sleep_backoff(
        self,
        attempt: int,
        base_seconds: float = 1.0,
        max_seconds: float = 10.0,
    ) -> None:
        delay = min(max_seconds, base_seconds * (2 ** max(attempt - 1, 0)))
        jitter = random.uniform(0.0, delay * 0.2)
        await asyncio.sleep(delay + jitter)
    
    async def get_default_branch_id(self, workspace_id: str, use_cache: bool = True) -> Optional[str]:
        cache = get_branch_cache()
        if use_cache and workspace_id in cache:
            return cache[workspace_id]
        
        try:
            request = DescribeBranchesRequest(workspace_id=workspace_id)
            response = self.client.describe_branches(request)
            
            if hasattr(response, 'branches') and response.branches:
                for branch in response.branches:
                    if getattr(branch, 'default', False):
                        branch_id = branch.branch_id
                        cache[workspace_id] = branch_id
                        self._cache_branch_workspace(workspace_id, branch_id)
                        return branch_id
                
                first_branch = response.branches[0]
                branch_id = first_branch.branch_id
                cache[workspace_id] = branch_id
                self._cache_branch_workspace(workspace_id, branch_id)
                return branch_id
            
            return None
        except Exception as e:
            logger.error(f"Error getting default branch: {e}")
            return None
    
    async def list_branches(self, workspace_id: str) -> list[dict]:
        try:
            request = DescribeBranchesRequest(workspace_id=workspace_id)
            response = self.client.describe_branches(request)

            branches = []
            if hasattr(response, 'branches') and response.branches:
                for branch in response.branches:
                    branches.append(self._branch_payload(branch))
            return branches
        except Exception as e:
            logger.error(f"Error listing branches: {e}")
            raise RuntimeError(str(e))

    async def create_branch(self, workspace_id: str, name: str = "develop") -> dict:
        try:
            request = CreateBranchRequest(
                workspace_id=workspace_id,
                branch_settings=BranchSettingsForCreateBranchInput(name=name),
            )
            response = self.client.create_branch(request)

            branch_id = getattr(response, 'branch_id', None)
            if not branch_id and hasattr(response, 'branch'):
                branch_id = getattr(response.branch, 'branch_id', None)

            branch_payload = None
            if branch_id or name:
                try:
                    branch_payload = await self._find_branch(workspace_id, branch_id, name)
                except Exception as lookup_error:
                    logger.warning(f"Error loading created branch details: {lookup_error}")

            result = {
                "success": True,
                "branch_id": branch_id,
                "workspace_id": workspace_id,
                "name": name,
            }
            if branch_payload:
                result.update(branch_payload)
            return result
        except Exception as e:
            logger.error(f"Error creating branch: {e}")
            return {
                "success": False,
                "error": str(e),
            }

    async def create_workspace(
        self,
        workspace_name: str,
        engine_type: str = "Supabase",
        engine_version: str = "Supabase_1_24",
    ) -> dict:
        try:
            request = CreateWorkspaceRequest(
                workspace_name=workspace_name,
                engine_type=engine_type,
                engine_version=engine_version,
                branch_settings=BranchSettingsForCreateWorkspaceInput(branch_name="main"),
                compute_settings=ComputeSettingsForCreateWorkspaceInput(
                    auto_scaling_limit_min_cu=0.25,
                    auto_scaling_limit_max_cu=1,
                    suspend_timeout_seconds=300
                ),
                workspace_settings=WorkspaceSettingsForCreateWorkspaceInput(
                    public_connection="Disabled",
                    deletion_protection="Disabled"
                ),
            )
            response = self.client.create_workspace(request)

            workspace_id = getattr(response, 'workspace_id', None)
            if not workspace_id and hasattr(response, 'workspace'):
                workspace_id = getattr(response.workspace, 'workspace_id', None)

            return {
                "success": True,
                "workspace_id": workspace_id,
                "workspace_name": workspace_name,
                "engine_type": engine_type,
                "engine_version": engine_version,
            }
        except Exception as e:
            logger.error(f"Error creating workspace: {e}")
            return {
                "success": False,
                "error": str(e),
            }

    async def start_workspace(self, workspace_id: str) -> dict:
        try:
            request = StartWorkspaceRequest(workspace_id=workspace_id)
            self.client.start_workspace(request)
            return {"success": True, "workspace_id": workspace_id, "status": "starting"}
        except Exception as e:
            logger.error(f"Error starting workspace: {e}")
            return {"success": False, "error": str(e)}

    async def stop_workspace(self, workspace_id: str) -> dict:
        try:
            request = StopWorkspaceRequest(workspace_id=workspace_id)
            self.client.stop_workspace(request)
            return {"success": True, "workspace_id": workspace_id, "status": "stopping"}
        except Exception as e:
            logger.error(f"Error stopping workspace: {e}")
            return {"success": False, "error": str(e)}

    async def delete_branch(self, workspace_id: str, branch_id: str) -> dict:
        max_attempts = 8
        for attempt in range(1, max_attempts + 1):
            try:
                request = DeleteBranchRequest(
                    workspace_id=workspace_id,
                    branch_id=branch_id,
                )
                self.client.delete_branch(request)
                clear_all_caches(workspace_id, branch_id)
                return {"success": True}
            except Exception as e:
                error_text = str(e)
                code = self._branch_error_code(error_text)
                retriable = code in {"OperationDenied_BranchNotReady", "BranchStatusNotMatch"}
                if retriable and attempt < max_attempts:
                    await self._sleep_backoff(attempt)
                    continue
                logger.error(f"Error deleting branch: {e}")
                return {
                    "success": False,
                    "error": error_text,
                    "code": code,
                    "retriable": retriable,
                }
        return {
            "success": False,
            "error": "delete_branch failed after retries",
            "code": "OperationDenied_BranchNotReady",
            "retriable": True,
        }

    async def get_endpoint(self, workspace_id: str, branch_id: Optional[str] = None, use_cache: bool = True) -> Optional[str]:
        # 检查缓存
        cache_key = f"{workspace_id}:{branch_id}" if branch_id else workspace_id
        endpoint_cache = get_endpoint_cache()

        if use_cache and cache_key in endpoint_cache:
            return endpoint_cache[cache_key]

        if not branch_id:
            branch_id = await self.get_default_branch_id(workspace_id)
            if not branch_id:
                return None

        try:
            request = DescribeWorkspaceEndpointRequest(
                workspace_id=workspace_id,
                branch_id=branch_id
            )
            response = self.client.describe_workspace_endpoint(request)

            if hasattr(response, 'endpoints') and response.endpoints:
                domains = []
                for endpoint in response.endpoints:
                    if hasattr(endpoint, 'addresses') and endpoint.addresses:
                        for addr in endpoint.addresses:
                            if hasattr(addr, 'address_domain'):
                                domains.append(addr.address_domain)

                for domain in domains:
                    if 'volces.com' in domain and 'ivolces.com' not in domain:
                        if ENDPOINT_SCHEME == "https":
                            result = f"https://{domain}"
                        else:
                            result = f"http://{domain}:80"
                        endpoint_cache[cache_key] = result
                        return result

                if domains:
                    if ENDPOINT_SCHEME == "https":
                        result = f"https://{domains[0]}"
                    else:
                        result = f"http://{domains[0]}:80"
                    endpoint_cache[cache_key] = result
                    return result

            return None
        except Exception as e:
            logger.error(f"Error getting endpoint: {e}")
            return None
    
    async def reset_branch(self, workspace_id: str, branch_id: str) -> dict:
        max_attempts = 8
        for attempt in range(1, max_attempts + 1):
            try:
                request = ResetBranchRequest(
                    workspace_id=workspace_id,
                    branch_id=branch_id,
                )
                self.client.reset_branch(request)
                return {"success": True}
            except Exception as e:
                error_text = str(e)
                code = self._branch_error_code(error_text)
                retriable = code in {"OperationDenied_BranchNotReady", "BranchStatusNotMatch"}
                if retriable and attempt < max_attempts:
                    await self._sleep_backoff(attempt)
                    continue
                logger.error(f"Error resetting branch: {e}")
                return {
                    "success": False,
                    "error": error_text,
                    "code": code,
                    "retriable": retriable,
                }
        return {
            "success": False,
            "error": "reset_branch failed after retries",
            "code": "OperationDenied_BranchNotReady",
            "retriable": True,
        }

    async def get_api_key(self, workspace_id: str, key_type: str = "service_role",
                         branch_id: Optional[str] = None, use_cache: bool = True) -> Optional[str]:
        # 检查缓存
        cache_key = f"{workspace_id}:{key_type}:{branch_id}" if branch_id else f"{workspace_id}:{key_type}"
        api_key_cache = get_api_key_cache()

        if use_cache and cache_key in api_key_cache:
            return api_key_cache[cache_key]

        if not branch_id:
            branch_id = await self.get_default_branch_id(workspace_id)
            if not branch_id:
                return None

        try:
            request = DescribeAPIKeysRequest(
                workspace_id=workspace_id,
                branch_id=branch_id
            )
            response = self.client.describe_api_keys(request)

            if hasattr(response, 'api_keys') and response.api_keys:
                type_mapping = {
                    "service_role": "Service",
                    "anon": "Public"
                }
                target_type = type_mapping.get(key_type, "Service")

                for key in response.api_keys:
                    if hasattr(key, 'type') and key.type == target_type:
                        result = key.key if hasattr(key, 'key') else None
                        if result:
                            api_key_cache[cache_key] = result
                        return result

            return None
        except Exception as e:
            logger.error(f"Error getting API key: {e}")
            return None

    async def get_api_keys(
        self,
        workspace_id: str,
        branch_id: Optional[str] = None,
        use_default_branch: bool = False,
    ) -> list[dict]:
        if use_default_branch and not branch_id:
            branch_id = await self.get_default_branch_id(workspace_id)
            if not branch_id:
                raise RuntimeError(f"Could not get default branch for workspace {workspace_id}")

        request_kwargs = {"workspace_id": workspace_id}
        if branch_id:
            request_kwargs["branch_id"] = branch_id

        request = DescribeAPIKeysRequest(**request_kwargs)
        response = self.client.describe_api_keys(request)

        keys = []
        if hasattr(response, 'api_keys') and response.api_keys:
            for key in response.api_keys:
                keys.append({
                    "type": getattr(key, "type", None),
                    "key": getattr(key, "key", None),
                    "description": getattr(key, "description", None),
                })
        return keys


path: scripts/volcengine_supabase/tools/__init__.py
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .base import BaseTools
from .edge_function_tools import EdgeFunctionTools
from .storage_tools import StorageTools
from .database_tools import DatabaseTools
from .workspace_tools import WorkspaceTools

__all__ = ['BaseTools', 'EdgeFunctionTools', 'StorageTools', 'DatabaseTools', 'WorkspaceTools']


path: agents/openai.yaml
content: # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
interface:
  display_name: "Volcengine Supabase"
  short_description: "Manage Volcengine Supabase workspaces, branches, databases, Edge Functions, and Storage"
  default_prompt: "Use $volcengine-supabase to complete this Supabase task on Volcengine."


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