
Volcengine Cli
- 35 installs
- 411 repo stars
- Updated August 4, 2026
- bytedance/agentkit-samples
volcengine-cli is a Claude skill that creates and manages Volcengine cloud resources through the ve command-line tool.
About
volcengine-cli is a Claude skill that creates and manages Volcengine cloud resources by calling OpenAPIs through the ve command. It verifies credentials, locates the right service and action, retrieves parameters, and classifies operations as read-only, write, or destructive with confirmation and DryRun safeguards. A developer uses it for infrastructure tasks like creating an ECS instance, setting up a VPC, or listing security groups. It needs Volcengine access keys and region.
- Creates and manages Volcengine cloud resources via the ve CLI
- Read/write/destructive safety classification with confirmation and DryRun
- Covers ECS, VPC, CLB, RDS, Redis and more
Volcengine Cli by the numbers
- 35 all-time installs (skills.sh)
- Ranked #774 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
volcengine-cli capabilities & compatibility
Free ve CLI; needs Volcengine access keys and region; resources created are billed by Volcengine
- Capabilities
- cloud resource management · infrastructure provisioning
- Use cases
- devops
- Runs
- Runs locally
- Pricing
- Bring your own API key
What volcengine-cli says it does
Create and manage Volcengine cloud resources by calling Volcengine OpenAPIs through the `ve` command.
Supports all Volcengine services including ECS, VPC, CLB, RDS, Redis, and more.
npx skills add https://github.com/bytedance/agentkit-samples --skill volcengine-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 411 |
| Last updated | August 4, 2026 |
| Repository | bytedance/agentkit-samples ↗ |
What it does
Create, query, and manage Volcengine cloud resources through the ve CLI with read/write/destructive safeguards.
Who is it for?
Provisioning and managing Volcengine resources like ECS, VPC, and RDS from the ve CLI.
Skip if: Generating SDK code (use volcengine-sdk-generator) or only querying specs (use volcengine-api).
When should I use this skill?
You need to create, query, modify, or delete Volcengine cloud resources.
What you get
Resources are queried or changed through the ve CLI with confirmation and DryRun safety.
- created, queried, or modified Volcengine cloud resources
By the numbers
- three operation levels: read-only, write, and destructive, each with distinct confirmation behavior
Files
Volcengine CLI Skill
Create and manage Volcengine cloud resources by calling Volcengine OpenAPIs through the ve command.
---
0. Install the ve CLI
If the ve command is not available on the system:
Option 1: npm (recommended)
npm i -g @volcengine/cliOption 2: GitHub Releases Download: https://github.com/volcengine/volcengine-cli/releases
Verify the installation: ve --version
---
1. Initialization (run at the start of every session)
Run the identity verification command to confirm that credentials are usable:
ve sts GetCallerIdentitySuccess -> inform the user of the current account identity and region, then proceed with the task.
Switching regions: the--regionflag and theVOLCENGINE_REGIONenvironment variable do not override the region in the config file. Switch regions viave configure profile --profile <name>. Useve configure listto view available profiles.
Failure -> credentials are not configured or invalid. Guide the user through one of the following:
Option 1: Environment variables (recommended for temporary use)
export VOLCENGINE_ACCESS_KEY="<YOUR_AK>"
export VOLCENGINE_SECRET_KEY="<YOUR_SK>"
export VOLCENGINE_REGION="cn-beijing"Option 2: Config file (persistent)
If ~/.volcengine/config.json does not exist, create an empty template for the user to fill in:
mkdir -p ~/.volcengine
cat > ~/.volcengine/config.json << 'EOF'
{
"current": "default",
"profiles": {
"default": {
"name": "default",
"mode": "ak",
"access-key": "<YOUR_AK>",
"secret-key": "<YOUR_SK>",
"region": "cn-beijing",
"endpoint": "",
"session-token": "",
"disable-ssl": false
}
},
"enableColor": false
}
EOFNever read `~/.volcengine/config.json` — the file contains sensitive credentials. Only create an empty template; never read an existing config.
---
2. Safety Rules (mandatory)
Read/Write Classification
| Level | Operation Types | Behavior |
|---|---|---|
| Read-only | Describe\ / List\ / Get\ / Query\ | Execute directly, no confirmation needed |
| Write | Create\ / Run\ / Allocate\ / Attach\ / Associate\ / Authorize\ | Show the full command and wait for user confirmation |
| Destructive | Delete\ / Terminate\ / Release\ / Revoke\ / Modify\ / Stop\ / Detach\* | Show command + impact summary; require user confirmation |
Core Principles
1. Default to read-only — unless the user explicitly requests a change, execute in read-only mode 2. DryRun first — if a write/destructive operation supports --DryRun true, run a DryRun to preview the plan, then confirm before executing 3. Confirm before executing — show the full command for write operations and wait for approval 4. Protect credentials — never read ~/.volcengine/config.json; never expose access-key, secret-key, or session-token in output
DryRun Notes
A successful DryRun validation returns exit code 1 (non-zero) with DryRunOperation in stderr. This is expected behavior:
output=$(ve <svc> <action> --DryRun true ... 2>&1)
if echo "$output" | grep -q "DryRunOperation"; then
echo "Parameter validation passed"
fi---
3. Locate APIs and Retrieve Parameters
Locate the API (find the service name + Action name)
Step 1: Service name + Action known? -> Use them directly; skip to "Retrieve parameters"
Step 2: Service name known, Action unknown?
-> ve <service> 2>&1 | grep -i <keyword>
Step 3: Service name also unknown?
-> ve 2>&1 | grep -i <service keyword>
Step 4: None of the above work?
-> python3 scripts/find_api.py <keyword>Retrieve parameters (once the Action is known)
Choose a strategy based on operation type:
| Operation Type | Strategy | Rationale |
|---|---|---|
| Read-only (Describe/List/Get) | ve <svc> <action> --help | Few, simple parameters — names alone are sufficient |
| Write/destructive (Create/Run/Delete, etc.) | scripts/fetch_swagger.py for full docs | Many parameters, nested structures — need required fields, examples, and descriptions |
| Still unclear after `--help` | Supplement with scripts/fetch_swagger.py | Use whenever parameter meaning is uncertain |
*Errors like `Invalid / Missing`* | Recheck with scripts/fetch_swagger.py | On InvalidParameter, InvalidXxx.NotFound, or MissingParameter, verify parameter names, required fields, and value ranges |
# Read-only — --help is sufficient
ve ecs DescribeInstances --help
# Write — retrieve full documentation
python3 scripts/fetch_swagger.py --service ecs --action RunInstancesve command name and API version relationship
- Default version -> ve command = base service name (e.g.,
iam) - Non-default version -> ve command =
service name + version without hyphens(e.g.,iamv2021-08-01 ->iam20210801) - When in doubt:
ve 2>&1 | grep <service>to confirm
Python helper usage
# Search for an API (when the service name is unknown)
python3 scripts/find_api.py <keyword> [--limit N]
# Get full API parameter documentation (when descriptions/examples are needed)
python3 scripts/fetch_swagger.py --service <ServiceCode> --action <ActionName>
# List all APIs for a service
python3 scripts/fetch_swagger.py --service <ServiceCode> --listAlways pass the base service name to scripts/fetch_swagger.py (e.g.,--service iam, notiam20210801) — the script auto-detects the version.
---
4. Execute API Calls
Basic Format
ve <ServiceCode> <ActionName> --ParamName "value"Parameter Passing Rules
Determine the format from --help output:
- Flat parameter format:
--helplists individual--Key typeentries (e.g., ECS, VPC, IAM) -> pass with--Key "value" - JSON format:
--helponly shows--body '{...}'(e.g., Redis, CR, and other POST APIs) -> pass with--body '{...}'
# Flat parameters — nested fields use dot notation; arrays use .N index (starting from 1)
ve ecs RunInstances --Placement.ZoneId "cn-beijing-a"
ve ecs RunInstances --NetworkInterfaces.1.SubnetId "subnet-xxxx"
ve ecs RunInstances --Tags.1.Key "env" --Tags.2.Key "app"
# JSON format (when --help only shows --body)
ve redis CreateDBInstance --body '{"InstanceName":"demo", "RegionId":"cn-beijing", ...}'Response Format
// Success
{ "ResponseMetadata": { "RequestId": "..." }, "Result": { ... } }
// Failure
{ "ResponseMetadata": { "Error": { "Code": "...", "Message": "..." } } }Async Resource Creation Requires Polling
Some resources (VKE clusters, RDS instances, ECS instances, etc.) take several minutes to create. After creation, poll the Describe endpoint until the resource reaches the desired status before proceeding.
Creating sub-resources (e.g., security groups) immediately after VPC creation may fail with InvalidVpc.InvalidStatus. Create sub-resources sequentially (subnet first, then security group), or wait a few seconds and retry.# General polling pattern: check every 30 seconds until the target status is reached
while true; do
cur_status=$(ve <svc> Describe<Resource> --<IdParam> "xxx" 2>&1 | grep -o '"Status":"[^"]*"')
echo "$(date +%H:%M:%S) $cur_status"
echo "$cur_status" | grep -q '"Status":"Running"' && break
sleep 30
done---
5. End-to-End Execution Flow (Summary)
1. Initialize: verify credentials -> GetCallerIdentity -> confirm region
2. Understand the task: is the user querying or making changes?
3. Locate the API: ve --help first -> Python helpers as fallback
4. Query dependent resources: use Describe*/List* to obtain required IDs
5. Read operation -> execute directly and display results
Write operation -> show command -> DryRun (if supported) -> user confirmation -> execute
6. Parse the response and report results to the user---
6. Service-Specific Notes
Consult or update the corresponding notes file when encountering service-specific issues:
- ECS: notes/ecs.md
- IAM: notes/iam.md
- Redis: notes/redis.md
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
ECS Service Notes
DescribeInstanceTypes returns only 10 results by default
Calling DescribeInstanceTypes without filters returns at most 10 entries, making it impossible to find the smallest available instance type.
Correct approach (two steps):
# 1. Query available instance types in a specific zone
ve ecs DescribeAvailableResource \
--ZoneId "cn-beijing-a" \
--DestinationResource "InstanceType"
# 2. Filter by type name and query CPU/memory details
# Naming convention: .large < .xlarge < .2xlarge (higher = larger)
# The smallest general-purpose type typically ends with .large
ve ecs DescribeInstanceTypes --InstanceTypes.1 "ecs.c3i.large"---
veLinux image search requires an exact name
A fuzzy keyword like "velinux" matches GPU, Docker, ARM, and other variant images, producing too many results.
Correct approach: use an exact name prefix.
# Search for veLinux 2.0 64-bit
ve ecs DescribeImages \
--ImageName "veLinux 2.0 64" \
--ImageOwnerAlias "system"Common image names:
veLinux 2.0 64— standard x86_64veLinux 2.0 ARM 64— ARM
---
Full ECS instance creation workflow
# 1. List availability zones
ve ecs DescribeZones --Region cn-beijing
# 2. Query available instance types (do NOT use DescribeInstanceTypes directly)
ve ecs DescribeAvailableResource \
--ZoneId "cn-beijing-a" \
--DestinationResource InstanceType
# 3. Get the veLinux image ID
ve ecs DescribeImages \
--ImageName "veLinux 2.0 64" \
--ImageOwnerAlias "system"
# 4. Get VPC and subnet IDs
ve vpc DescribeVpcs
ve vpc DescribeSubnets --VpcId "vpc-xxxx"
# 5. Get or create a security group and open port 22
ve ecs DescribeSecurityGroups --VpcId "vpc-xxxx"
ve ecs AuthorizeSecurityGroupIngress \
--SecurityGroupId "sg-xxxx" \
--Protocol "tcp" \
--PortStart 22 \
--PortEnd 22 \
--CidrIp "0.0.0.0/0"
# 6. DryRun validation
output=$(ve ecs RunInstances \
--Placement.ZoneId "cn-beijing-a" \
--InstanceTypeId "ecs.c3i.large" \
--ImageId "image-xxxx" \
--NetworkInterfaces.1.SubnetId "subnet-xxxx" \
--NetworkInterfaces.1.SecurityGroupIds.1 "sg-xxxx" \
--SystemVolume.Size 40 \
--SystemVolume.VolumeType "ESSD_PL0" \
--InstanceName "my-instance" \
--DryRun true 2>&1)
echo "$output" | grep -q "DryRunOperation" && echo "DryRun passed"
# 7. Create the instance
ve ecs RunInstances \
--Placement.ZoneId "cn-beijing-a" \
--InstanceTypeId "ecs.c3i.large" \
--ImageId "image-xxxx" \
--NetworkInterfaces.1.SubnetId "subnet-xxxx" \
--NetworkInterfaces.1.SecurityGroupIds.1 "sg-xxxx" \
--SystemVolume.Size 40 \
--SystemVolume.VolumeType "ESSD_PL0" \
--InstanceName "my-instance" \
--Count 1---
ZoneId notes
- Beijing zones:
cn-beijing-a/cn-beijing-b/cn-beijing-c, etc. --Placement.ZoneIdis a nested parameter (dot-separated), not--ZoneId.- ZoneId is required when creating instances.
---
Instance type existence does not guarantee zone availability
DescribeInstanceTypes returns a global list of instance types regardless of actual zone inventory. Creating an instance may fail with InvalidInstanceType.NotFound.
Correct approach: use DescribeAvailableResource to query available types in the target zone. If creation fails, try an alternative type within the same family (e.g., ecs.hfc4i.large -> ecs.hfc3il.large).
---
Cloud Assistant Agent requires a reboot after installation
After calling InstallCloudAssistant, the agent status is ReadyReboot and RunCommand will keep timing out.
Correct approach: 1. Call InstallCloudAssistant. 2. Call RebootInstance to restart the instance. 3. Poll DescribeCloudAssistantStatus until status becomes Running. 4. Then execute RunCommand.
Tip: pass --InstallRunCommandAgent true during instance creation to avoid manual installation later.---
RunCommand Timeout minimum is 60
Setting --Timeout below 60 seconds triggers LimitExceeded.MaximumTimeout. Valid range: 60–86400 seconds.
IAM Service Notes
UpdateUser does not support Tags
UpdateUser can only modify basic attributes such as Description and DisplayName. It cannot manage tags.
Correct approach: use TagResources to tag users separately:
ve iam TagResources --ResourceType "User" --ResourceNames.1 "<UserName>" --Tags.1.Key "key" --Tags.1.Value "value"The same applies to roles — use TagResources with ResourceType set to Role.
Redis Service Notes
Allow-list deletion requires the instance to be fully released
Calling DeleteAllowList immediately after DeleteDBInstance fails with AllowListBindInstanceCannotDelete because the instance has not been fully removed yet.
Correct approach: wait for the Redis instance to be fully deleted (~30 seconds) before deleting the associated allow list.
# Poll until the instance no longer exists
ve redis DescribeDBInstanceDetail --body '{"InstanceId":"redis-xxx"}'
# Once the instance is gone, delete the allow list
ve redis DeleteAllowList --body '{"AllowListId":"acl-xxx"}'# 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.
#!/usr/bin/env python3
"""
Fetch Volcengine API Swagger and convert to Markdown documentation.
Usage: python3 fetch_swagger.py --service ecs --action RunInstances [--version 2020-04-01]
"""
import argparse
import json
import sys
import urllib.request
import urllib.error
BASE_URL = "https://api.volcengine.com/api/common/explorer"
def fetch_json(url, allow_404=False):
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
if allow_404 and e.code == 404:
return None
print(f"Error fetching {url}: {e}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"Error fetching {url}: {e}", file=sys.stderr)
sys.exit(1)
def get_all_versions(service_code):
"""Return all versions for a service, IsDefault=1 first."""
url = f"{BASE_URL}/versions?ServiceCode={service_code}"
data = fetch_json(url)
assert data is not None
versions = data.get("Result", {}).get("Versions", [])
if not versions:
print(f"No version found for service: {service_code}", file=sys.stderr)
sys.exit(1)
versions.sort(key=lambda v: 0 if v.get("IsDefault") == 1 else 1)
return [v["Version"] for v in versions]
def get_default_version(service_code):
return get_all_versions(service_code)[0]
def get_swagger(service_code, version, action_name, all_versions=None):
"""Fetch swagger for an action. If not found in given version, try all other versions."""
def _try(ver):
url = (
f"{BASE_URL}/api-swagger"
f"?ServiceCode={service_code}"
f"&Version={ver}"
f"&APIVersion={ver}"
f"&ActionName={action_name}"
)
data = fetch_json(url, allow_404=True)
if data is None:
return None, None
api = data.get("Result", {}).get("Api")
return api, ver
api, matched_ver = _try(version)
if api:
return api, matched_ver
# Fallback: try remaining versions
if all_versions is None:
all_versions = get_all_versions(service_code)
for ver in all_versions:
if ver == version:
continue
api, matched_ver = _try(ver)
if api:
print(f"Note: action '{action_name}' not found in v{version}, using v{matched_ver}", file=sys.stderr)
return api, matched_ver
print(f"No swagger found for {service_code}.{action_name} in any version: {all_versions}", file=sys.stderr)
sys.exit(1)
def resolve_ref(ref_str, schemas):
"""Resolve a $ref like '#/components/schemas/Foo' to its schema dict."""
if ref_str.startswith("#/components/schemas/"):
name = ref_str[len("#/components/schemas/"):]
return name, schemas.get(name, {})
return ref_str, {}
def get_type_str(schema, schemas):
"""Get a human-readable type string from a schema object."""
if "$ref" in schema:
name, _ = resolve_ref(schema["$ref"], schemas)
return f"object({name})"
t = schema.get("type", "string")
if t == "array":
items = schema.get("items", {})
if "$ref" in items:
name, _ = resolve_ref(items["$ref"], schemas)
return f"array[object({name})]"
return f"array[{items.get('type', 'string')}]"
if t == "integer":
fmt = schema.get("format", "")
return "integer" if not fmt else f"integer({fmt})"
if t == "number":
return "number"
return t
def escape_md(text):
"""Escape pipe characters and newlines for markdown table cells."""
if not text:
return ""
text = str(text)
# Collapse multi-line descriptions to single line, keep it readable
text = text.replace("\n", " ").replace("|", "|")
# Trim markdown tip/note blocks
import re
text = re.sub(r":::.*?:::", "", text, flags=re.DOTALL).strip()
# Limit length
if len(text) > 200:
text = text[:197] + "..."
return text
def format_example(example):
if example is None:
return ""
if isinstance(example, list):
return ", ".join(str(e) for e in example[:2])
return str(example)
def build_params_table(params_list):
"""Build a markdown table from a list of param dicts."""
lines = [
"| 参数名 | 类型 | 必填 | 说明 | 示例值 |",
"|--------|------|:----:|------|--------|",
]
for p in params_list:
required = "✓" if p.get("required") else ""
lines.append(
f"| `{p['name']}` | {p['type']} | {required} | {escape_md(p.get('description', ''))} | {escape_md(format_example(p.get('example', '')))} |"
)
return "\n".join(lines)
def parse_get_params(parameters, schemas):
"""Parse GET query parameters, separating flat and nested ($ref) params."""
flat = []
nested = [] # list of (param_name, is_array, schema_name, schema_dict)
for p in parameters:
schema = p.get("schema", {})
name = p.get("name", "")
required = p.get("required", False)
description = p.get("description", "")
example = p.get("example")
if "$ref" in schema:
ref_name, ref_schema = resolve_ref(schema["$ref"], schemas)
nested.append((name, False, ref_name, ref_schema, required, description))
elif schema.get("type") == "array" and "$ref" in schema.get("items", {}):
ref_name, ref_schema = resolve_ref(schema["items"]["$ref"], schemas)
nested.append((name, True, ref_name, ref_schema, required, description))
else:
flat.append({
"name": name,
"type": get_type_str(schema, schemas),
"required": required,
"description": description,
"example": example or schema.get("example"),
})
return flat, nested
def parse_nested_schema(schema_name, schema_dict, parent_prefix, is_array, schemas, visited=None):
"""Recursively parse a schema into a flat list of param dicts with full paths."""
if visited is None:
visited = set()
if schema_name in visited:
return []
visited.add(schema_name)
params = []
properties = schema_dict.get("properties", {})
required_list = schema_dict.get("required", [])
sort_order = schema_dict.get("x-sort-params", [])
# Use sort order if provided, otherwise alphabetical
sorted_keys = sort_order if sort_order else sorted(properties.keys())
# Make sure all keys are included (sort_order might be incomplete)
sorted_keys = sorted_keys + [k for k in properties if k not in sorted_keys]
for prop_name in sorted_keys:
if prop_name not in properties:
continue
prop = properties[prop_name]
required = prop_name in required_list
full_name = f"{parent_prefix}.N.{prop_name}" if is_array else f"{parent_prefix}.{prop_name}"
if "$ref" in prop:
sub_name, sub_schema = resolve_ref(prop["$ref"], schemas)
params.append({
"name": full_name,
"type": f"object",
"required": required,
"description": prop.get("description", ""),
"example": prop.get("example"),
})
params.extend(parse_nested_schema(sub_name, sub_schema, full_name, False, schemas, visited))
elif prop.get("type") == "array" and "$ref" in prop.get("items", {}):
sub_name, sub_schema = resolve_ref(prop["items"]["$ref"], schemas)
params.append({
"name": full_name,
"type": "array[object]",
"required": required,
"description": prop.get("description", ""),
"example": prop.get("example"),
})
params.extend(parse_nested_schema(sub_name, sub_schema, full_name, True, schemas, visited))
else:
params.append({
"name": full_name,
"type": get_type_str(prop, schemas),
"required": required,
"description": prop.get("description", ""),
"example": prop.get("example"),
})
visited.remove(schema_name)
return params
def parse_post_body(request_body, schemas):
"""Parse a POST requestBody schema into flat + nested params."""
content = request_body.get("content", {})
schema = content.get("application/json", {}).get("schema", {})
# Handle top-level $ref
if "$ref" in schema:
_, schema = resolve_ref(schema["$ref"], schemas)
flat = []
nested_sections = []
properties = schema.get("properties", {})
required_list = schema.get("required", [])
sort_order = schema.get("x-sort-params", [])
sorted_keys = sort_order + [k for k in properties if k not in sort_order]
for key in sorted_keys:
if key not in properties:
continue
prop = properties[key]
required = key in required_list
description = prop.get("description", "")
example = prop.get("example")
if "$ref" in prop:
ref_name, ref_schema = resolve_ref(prop["$ref"], schemas)
flat.append({
"name": key,
"type": "object",
"required": required,
"description": description,
"example": example,
})
nested_sections.append((key, False, ref_name, ref_schema))
elif prop.get("type") == "array" and "$ref" in prop.get("items", {}):
ref_name, ref_schema = resolve_ref(prop["items"]["$ref"], schemas)
flat.append({
"name": key,
"type": "array[object]",
"required": required,
"description": description,
"example": example,
})
nested_sections.append((key, True, ref_name, ref_schema))
else:
flat.append({
"name": key,
"type": get_type_str(prop, schemas),
"required": required,
"description": description,
"example": example or prop.get("example"),
})
return flat, nested_sections
def swagger_to_markdown(service_code, action_name, version, api_swagger):
schemas = api_swagger.get("components", {}).get("schemas", {})
paths = api_swagger.get("paths", {})
# Find the operation
method = "GET"
operation = {}
for _, methods in paths.items():
for m, op in methods.items():
method = m.upper()
operation = op
break
break
# API description (from operation summary or first param description)
summary = operation.get("summary", operation.get("description", ""))
lines = []
lines.append(f"# `ve {service_code} {action_name}`")
if summary:
lines.append(f"\n{summary.strip()}")
lines.append(f"\n**服务**: `{service_code}` | **版本**: `{version}` | **方法**: `{method}`")
lines.append(f"\n**CLI 格式**: `ve {service_code} {action_name} [参数...]`")
lines.append("")
if method == "GET":
parameters = operation.get("parameters", [])
flat_params, nested_list = parse_get_params(parameters, schemas)
lines.append("## 请求参数\n")
if flat_params:
lines.append(build_params_table(flat_params))
else:
lines.append("_无独立参数_")
for (param_name, is_array, schema_name, schema_dict, _, description) in nested_list:
type_label = "array[object]" if is_array else "object"
lines.append(f"\n### 嵌套参数:`{param_name}` ({type_label})\n")
if description and description != param_name:
lines.append(f"{description}\n")
nested_params = parse_nested_schema(
schema_name, schema_dict, param_name, is_array, schemas
)
if nested_params:
lines.append(build_params_table(nested_params))
else: # POST
request_body = operation.get("requestBody", {})
if request_body:
flat_params, nested_sections = parse_post_body(request_body, schemas)
lines.append("## 请求参数 (Request Body JSON)\n")
if flat_params:
lines.append(build_params_table(flat_params))
for (key, is_array, ref_name, ref_schema) in nested_sections:
type_label = "array[object]" if is_array else "object"
lines.append(f"\n### 嵌套参数:`{key}` ({type_label})\n")
nested_params = parse_nested_schema(
ref_name, ref_schema, key, is_array, schemas
)
if nested_params:
lines.append(build_params_table(nested_params))
else:
params = operation.get("parameters", [])
flat_params, nested_list = parse_get_params(params, schemas)
lines.append("## 请求参数\n")
if flat_params:
lines.append(build_params_table(flat_params))
for (param_name, is_array, schema_name, schema_dict, _req, _desc) in nested_list:
type_label = "array[object]" if is_array else "object"
lines.append(f"\n### 嵌套参数:`{param_name}` ({type_label})\n")
nested_params = parse_nested_schema(
schema_name, schema_dict, param_name, is_array, schemas
)
if nested_params:
lines.append(build_params_table(nested_params))
lines.append("\n---")
lines.append(f"_生成自 Volcengine OpenAPI Explorer: {service_code} {action_name} v{version}_")
return "\n".join(lines)
def list_apis(service_code, version):
"""List all available APIs for a service/version."""
url = (
f"{BASE_URL}/apis"
f"?ServiceCode={service_code}"
f"&Version={version}"
f"&APIVersion={version}"
)
data = fetch_json(url)
assert data is not None
groups = data.get("Result", {}).get("Groups", [])
lines = [f"# {service_code} v{version} API 列表\n"]
for group in groups:
group_name = group.get("Name", "")
apis = group.get("Apis", [])
if apis:
lines.append(f"## {group_name}\n")
for api in apis:
action = api.get("Action", "")
name_cn = api.get("NameCn", "")
desc = api.get("Description", "")
lines.append(f"- **{action}** - {name_cn}: {desc[:80]}")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Fetch Volcengine API Swagger and output as Markdown"
)
parser.add_argument("--service", "-s", required=True, help="Service code, e.g. ecs")
parser.add_argument("--action", "-a", help="API action name, e.g. RunInstances")
parser.add_argument("--version", "-v", help="API version, e.g. 2020-04-01 (auto-detected if omitted)")
parser.add_argument("--list", "-l", action="store_true", help="List all APIs for the service")
args = parser.parse_args()
service_code = args.service
all_versions = get_all_versions(service_code)
version = args.version or all_versions[0]
if args.list or not args.action:
print(list_apis(service_code, version))
return
api_swagger, matched_version = get_swagger(service_code, version, args.action, all_versions)
md = swagger_to_markdown(service_code, args.action, matched_version, api_swagger)
print(md)
if __name__ == "__main__":
main()
# 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.
#!/usr/bin/env python3
"""
火山引擎 API 搜索工具
用法:python3 find_api.py <搜索关键词> [--limit N]
示例:python3 find_api.py 获取项目列表
python3 find_api.py ListProjects --limit 5
"""
import sys
import re
import json
import argparse
import urllib.request
import urllib.parse
def strip_em(text):
return re.sub(r"</?em>", "", text or "")
def search(query, limit=10):
params = urllib.parse.urlencode({"Query": query, "Channel": "api", "Limit": limit})
url = f"https://api.volcengine.com/api/common/search/all?{params}"
req = urllib.request.Request(url, headers={"User-Agent": "volcengine-cli-skill/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
return data.get("Result", {}).get("List", [])
def main():
parser = argparse.ArgumentParser(description="Search Volcengine APIs")
parser.add_argument("query", nargs="+", help="搜索关键词")
parser.add_argument("--limit", type=int, default=10, help="返回条数(默认10)")
parser.add_argument("--json", action="store_true", help="输出原始 JSON")
args = parser.parse_args()
query = " ".join(args.query)
results = search(query, args.limit)
if not results:
print("No results found.")
sys.exit(0)
if args.json:
print(json.dumps(results, ensure_ascii=False, indent=2))
return
# 找出 content 字段的描述
def get_desc(item):
for h in item.get("Highlight", []):
if h.get("Field") == "content":
return strip_em(h.get("Summary", ""))
return ""
# 表格宽度
rows = []
for item in results:
biz = item.get("BizInfo", {})
rows.append({
"action": biz.get("Action", ""),
"service": biz.get("ServiceCode", ""),
"service_cn": biz.get("ServiceCn", ""),
"version": biz.get("Version", ""),
"desc": get_desc(item),
})
col_w = {k: len(k) for k in ("action", "service", "service_cn", "version", "desc")}
for r in rows:
for k in col_w:
col_w[k] = max(col_w[k], len(r[k]))
def fmt(r):
return (
f" {r['action']:<{col_w['action']}} "
f"{r['service']:<{col_w['service']}} "
f"{r['service_cn']:<{col_w['service_cn']}} "
f"{r['version']:<{col_w['version']}} "
f"{r['desc']}"
)
header = (
f" {'Action':<{col_w['action']}} "
f"{'Service':<{col_w['service']}} "
f"{'服务名':<{col_w['service_cn']}} "
f"{'Version':<{col_w['version']}} "
f"{'描述'}"
)
sep = " " + "-" * (sum(col_w.values()) + 8)
print(f"\nSearch: {query!r} ({len(rows)} results)\n")
print(header)
print(sep)
for r in rows:
print(fmt(r))
print()
if __name__ == "__main__":
main()
Related skills
FAQ
How does it avoid destructive mistakes?
It classifies operations as read-only, write, or destructive, requires confirmation for writes, and runs DryRun before applying where supported.
How do I install the ve CLI?
Install via npm i -g @volcengine/cli or download from the Volcengine CLI GitHub releases, then verify with ve --version.