From ee451dd96b075462a8948eea2ce38004c30cd8b9 Mon Sep 17 00:00:00 2001 From: wy Date: Sun, 22 Mar 2026 02:10:15 +0800 Subject: [PATCH 1/3] Add JSON CLI output and publishable ClawHub skill bundle --- AGENTS.md | 25 ++++ README.md | 16 ++ README.zh-CN.md | 16 ++ cli/args_parser.cpp | 8 + cli/args_parser.h | 2 + cli/ccap_cli.cpp | 2 +- cli/ccap_cli_utils.cpp | 230 ++++++++++++++++++++++++++++- cli/ccap_cli_utils.h | 2 +- docs/content/cli.md | 19 +++ docs/content/cli.zh.md | 19 +++ skills/README.md | 15 ++ skills/ccap/PUBLISHING.md | 52 +++++++ skills/ccap/SKILL.md | 114 ++++++++++++++ skills/ccap/references/commands.md | 59 ++++++++ skills/ccap/references/install.md | 71 +++++++++ tests/test_ccap_cli.cpp | 41 +++++ tests/test_cli_args_parser.cpp | 13 ++ 17 files changed, 695 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md create mode 100644 skills/README.md create mode 100644 skills/ccap/PUBLISHING.md create mode 100644 skills/ccap/SKILL.md create mode 100644 skills/ccap/references/commands.md create mode 100644 skills/ccap/references/install.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..81039d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# CameraCapture Agent Entry + +This repository includes a publish-ready ClawHub/OpenClaw skill for `ccap`. + +If you need to install or use `ccap` as a tool for camera capture, device inspection, image capture, or video metadata inspection, read the published skill folder first: + +- [skills/ccap/SKILL.md](skills/ccap/SKILL.md) + +Use that skill when the task involves any of the following: + +- install ccap on the current machine +- build CameraCapture from source +- use Homebrew to install ccap on macOS +- download and run a release binary when no build toolchain is available +- list camera devices +- inspect device capabilities +- capture frames with the CLI +- inspect video metadata + +The `.github/skills/` directory is for repository-local development workflows. +The `skills/ccap/` directory is the standalone skill bundle intended for ClawHub/OpenClaw distribution. + +Prefer the `ccap` CLI over calling internal source files directly. +Prefer structured JSON output when the command supports `--json`. +Do not assume camera permission, GUI availability, or video file playback support on every platform. \ No newline at end of file diff --git a/README.md b/README.md index e4c3b42..3c7c1f2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,22 @@ A high-performance, lightweight cross-platform camera capture library with hardw ## Quick Start +### AI Agent Skill + +This repository also includes a publish-ready skill bundle for ClawHub/OpenClaw. + +- Agent entry: [AGENTS.md](./AGENTS.md) +- Publishable skill folder: [skills/ccap](./skills/ccap) +- Skill definition: [skills/ccap/SKILL.md](./skills/ccap/SKILL.md) + +The skill is structured as a standalone skill folder with a top-level `SKILL.md`, optional supporting text files, and instructions that guide agents to choose among an existing installation, Homebrew on macOS, source builds, and release-binary fallback, then prefer the `ccap` CLI with `--json` where supported. + +If you want to publish the skill to ClawHub, publish the skill folder itself rather than the repository root: + +```bash +clawhub publish ./skills/ccap --slug ccap --name "ccap" --version 0.1.0 --tags latest --changelog "Initial ClawHub release" +``` + ### Installation 1. Build and install from source (on Windows, use git-bash): diff --git a/README.zh-CN.md b/README.zh-CN.md index d18988f..3b5fb56 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -44,6 +44,22 @@ ## 快速开始 +### AI Agent 技能入口 + +本仓库现在也包含了一个可发布到 ClawHub/OpenClaw 的独立技能包,可将 `ccap` 当作实际可用的相机与视频输入工具来使用。 + +- Agent 入口: [AGENTS.md](./AGENTS.md) +- 可发布技能目录: [skills/ccap](./skills/ccap) +- 技能定义: [skills/ccap/SKILL.md](./skills/ccap/SKILL.md) + +该技能按照可发布的独立 skill folder 组织:顶层 `SKILL.md` 加可选辅助文本文件。它会指导 Agent 在已有安装、macOS Homebrew、源码构建和 release 二进制回退之间做选择,并在支持时优先使用带 `--json` 的 `ccap` CLI。 + +如果要发布到 ClawHub,应发布技能目录本身,而不是整个仓库根目录: + +```bash +clawhub publish ./skills/ccap --slug ccap --name "ccap" --version 0.1.0 --tags latest --changelog "Initial ClawHub release" +``` + ### 安装 1. 从源码编译并安装 (在 Windows 下需要 git-bash 执行) diff --git a/cli/args_parser.cpp b/cli/args_parser.cpp index 16a7526..9676e53 100644 --- a/cli/args_parser.cpp +++ b/cli/args_parser.cpp @@ -135,6 +135,8 @@ void printUsage(const char* programName) { << "Global options:\n" << " --verbose enable verbose logging (shows all messages)\n" << " -q, --quiet quiet mode (only show errors, equivalent to log level Error)\n" + << " --json emit structured JSON output for supported commands\n" + << " --schema-version version schema version for JSON output (default: 1.0)\n" << " --timeout seconds program timeout (auto-exit after N seconds)\n" << " --timeout-exit-code code exit code when timeout occurs (default: 0)\n" << "\n" @@ -342,6 +344,12 @@ CLIOptions parseArgs(int argc, char* argv[]) { opts.showVersion = true; } else if (arg == "--verbose") { opts.verbose = true; + } else if (arg == "--json") { + opts.jsonOutput = true; + } else if (arg == "--schema-version") { + if (i + 1 < argc) { + opts.schemaVersion = argv[++i]; + } } else if (arg == "-l" || arg == "--list-devices") { opts.listDevices = true; } else if (arg == "-I" || arg == "--device-info") { diff --git a/cli/args_parser.h b/cli/args_parser.h index d544628..0ff986d 100644 --- a/cli/args_parser.h +++ b/cli/args_parser.h @@ -34,6 +34,8 @@ struct CLIOptions { bool listDevices = false; bool showDeviceInfo = false; bool verbose = false; + bool jsonOutput = false; + std::string schemaVersion = "1.0"; // Windows camera backend override std::string windowsCameraBackend; diff --git a/cli/ccap_cli.cpp b/cli/ccap_cli.cpp index d05609b..b8f81ec 100644 --- a/cli/ccap_cli.cpp +++ b/cli/ccap_cli.cpp @@ -147,7 +147,7 @@ int main(int argc, char* argv[]) { // If video file specified without action, print video info if (!opts.videoFilePath.empty() && !hasAction) { - return ccap_cli::printVideoInfo(opts.videoFilePath); + return ccap_cli::printVideoInfo(opts, opts.videoFilePath); } // If camera device specified without action, print camera info diff --git a/cli/ccap_cli_utils.cpp b/cli/ccap_cli_utils.cpp index 05a5181..72a6eaf 100644 --- a/cli/ccap_cli_utils.cpp +++ b/cli/ccap_cli_utils.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #if defined(_WIN32) || defined(_WIN64) @@ -52,6 +53,101 @@ namespace ccap_cli { namespace { +void writeJsonEscapedString(std::ostream& os, std::string_view value) { + os << '"'; + for (unsigned char ch : value) { + switch (ch) { + case '"': + os << "\\\""; + break; + case '\\': + os << "\\\\"; + break; + case '\b': + os << "\\b"; + break; + case '\f': + os << "\\f"; + break; + case '\n': + os << "\\n"; + break; + case '\r': + os << "\\r"; + break; + case '\t': + os << "\\t"; + break; + default: + if (ch < 0x20) { + constexpr char hexDigits[] = "0123456789abcdef"; + os << "\\u00" << hexDigits[ch >> 4] << hexDigits[ch & 0x0f]; + } else { + os << static_cast(ch); + } + break; + } + } + os << '"'; +} + +void writeJsonResolutions(std::ostream& os, const std::vector& resolutions) { + os << '['; + for (size_t index = 0; index < resolutions.size(); ++index) { + if (index > 0) { + os << ','; + } + os << "{\"width\":" << resolutions[index].width << ",\"height\":" << resolutions[index].height << '}'; + } + os << ']'; +} + +void writeJsonPixelFormats(std::ostream& os, const std::vector& pixelFormats) { + os << '['; + for (size_t index = 0; index < pixelFormats.size(); ++index) { + if (index > 0) { + os << ','; + } + writeJsonEscapedString(os, ccap::pixelFormatToString(pixelFormats[index])); + } + os << ']'; +} + +void writeJsonDevice(std::ostream& os, size_t deviceIndex, const std::string& deviceName, + const std::optional& info) { + os << "{\"index\":" << deviceIndex << ",\"name\":"; + writeJsonEscapedString(os, deviceName); + os << ",\"info_available\":" << (info.has_value() ? "true" : "false") + << ",\"supported_resolutions\":"; + if (info.has_value()) { + writeJsonResolutions(os, info->supportedResolutions); + } else { + os << "[]"; + } + os << ",\"supported_pixel_formats\":"; + if (info.has_value()) { + writeJsonPixelFormats(os, info->supportedPixelFormats); + } else { + os << "[]"; + } + os << '}'; +} + +void printJsonError(std::string_view schemaVersion, std::string_view command, std::string_view code, + std::string_view message, int exitCode) { + std::ostringstream os; + os << "{\"schema_version\":"; + writeJsonEscapedString(os, schemaVersion); + os << ",\"command\":"; + writeJsonEscapedString(os, command); + os << ",\"success\":false,\"exit_code\":" << exitCode << ",\"error\":{\"code\":"; + writeJsonEscapedString(os, code); + os << ",\"message\":"; + writeJsonEscapedString(os, message); + os << "}}"; + std::cout << os.str() << std::endl; +} + #if defined(_WIN32) || defined(_WIN64) constexpr const char* kWindowsBackendEnvVar = "CCAP_WINDOWS_BACKEND"; #endif @@ -137,6 +233,33 @@ int listDevices(const CLIOptions& opts) { ccap::Provider provider; auto deviceNames = provider.findDeviceNames(); + if (opts.jsonOutput) { + std::ostringstream os; + os << "{\"schema_version\":"; + writeJsonEscapedString(os, opts.schemaVersion); + os << ",\"command\":"; + writeJsonEscapedString(os, "list-devices"); + os << ",\"success\":true,\"data\":{\"device_count\":" << deviceNames.size() << ",\"devices\":["; + + for (size_t index = 0; index < deviceNames.size(); ++index) { + if (index > 0) { + os << ','; + } + + ccap::Provider devProvider(deviceNames[index]); + std::optional info; + if (devProvider.isOpened()) { + info = devProvider.getDeviceInfo(); + } + + writeJsonDevice(os, index, deviceNames[index], info); + } + + os << "]}}"; + std::cout << os.str() << std::endl; + return 0; + } + if (deviceNames.empty()) { std::cout << "No camera devices found." << std::endl; return 0; @@ -196,13 +319,22 @@ int showDeviceInfo(const CLIOptions& opts, int deviceIndex) { auto deviceNames = provider.findDeviceNames(); if (deviceNames.empty()) { + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "device-info", "no_devices_found", "No camera devices found.", 1); + return 1; + } std::cerr << "No camera devices found." << std::endl; return 1; } - auto showInfo = [&](size_t idx) { + auto showInfo = [&](size_t idx, std::ostream* jsonStream) { if (idx >= deviceNames.size()) { - std::cerr << "Device index " << idx << " out of range." << std::endl; + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "device-info", "device_index_out_of_range", + "Device index " + std::to_string(idx) + " out of range.", 1); + } else { + std::cerr << "Device index " << idx << " out of range." << std::endl; + } return false; } @@ -210,16 +342,30 @@ int showDeviceInfo(const CLIOptions& opts, int deviceIndex) { ccap::Provider devProvider(name); if (!devProvider.isOpened()) { - std::cerr << "Failed to open device: " << name << std::endl; + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "device-info", "device_open_failed", "Failed to open device: " + name, 1); + } else { + std::cerr << "Failed to open device: " << name << std::endl; + } return false; } auto info = devProvider.getDeviceInfo(); if (!info) { - std::cerr << "Failed to get info for device: " << name << std::endl; + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "device-info", "device_info_unavailable", + "Failed to get info for device: " + name, 1); + } else { + std::cerr << "Failed to get info for device: " << name << std::endl; + } return false; } + if (jsonStream != nullptr) { + writeJsonDevice(*jsonStream, idx, name, info); + return true; + } + std::cout << "\n===== Device [" << idx << "]: " << name << " =====" << std::endl; std::cout << " Supported resolutions:" << std::endl; @@ -237,12 +383,49 @@ int showDeviceInfo(const CLIOptions& opts, int deviceIndex) { }; if (deviceIndex < 0) { + if (opts.jsonOutput) { + std::ostringstream os; + os << "{\"schema_version\":"; + writeJsonEscapedString(os, opts.schemaVersion); + os << ",\"command\":"; + writeJsonEscapedString(os, "device-info"); + os << ",\"success\":true,\"data\":{\"device_count\":" << deviceNames.size() << ",\"devices\":["; + + for (size_t index = 0; index < deviceNames.size(); ++index) { + if (index > 0) { + os << ','; + } + if (!showInfo(index, &os)) { + return 1; + } + } + + os << "]}}"; + std::cout << os.str() << std::endl; + return 0; + } + // Show info for all devices for (size_t i = 0; i < deviceNames.size(); ++i) { - showInfo(i); + showInfo(i, nullptr); } } else { - if (!showInfo(static_cast(deviceIndex))) { + if (opts.jsonOutput) { + std::ostringstream os; + os << "{\"schema_version\":"; + writeJsonEscapedString(os, opts.schemaVersion); + os << ",\"command\":"; + writeJsonEscapedString(os, "device-info"); + os << ",\"success\":true,\"data\":{\"device_count\":1,\"devices\":["; + if (!showInfo(static_cast(deviceIndex), &os)) { + return 1; + } + os << "]}}"; + std::cout << os.str() << std::endl; + return 0; + } + + if (!showInfo(static_cast(deviceIndex), nullptr)) { return 1; } } @@ -261,14 +444,23 @@ int showDeviceInfo(const CLIOptions& opts, const std::string& deviceName) { } } + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "device-info", "device_not_found", "Device not found: " + deviceName, 1); + return 1; + } + std::cerr << "Device not found: " << deviceName << std::endl; return 1; } -int printVideoInfo(const std::string& videoPath) { +int printVideoInfo(const CLIOptions& opts, const std::string& videoPath) { #if defined(CCAP_ENABLE_FILE_PLAYBACK) ccap::Provider provider; if (!provider.open(videoPath, false)) { // Don't start capture, just get info + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "video-info", "video_open_failed", "Failed to open video file: " + videoPath, 1); + return 1; + } std::cerr << "Failed to open video file: " << videoPath << std::endl; return 1; } @@ -279,6 +471,24 @@ int printVideoInfo(const std::string& videoPath) { int width = static_cast(provider.get(ccap::PropertyName::Width)); int height = static_cast(provider.get(ccap::PropertyName::Height)); + if (opts.jsonOutput) { + std::ostringstream os; + os << "{\"schema_version\":"; + writeJsonEscapedString(os, opts.schemaVersion); + os << ",\"command\":"; + writeJsonEscapedString(os, "video-info"); + os << ",\"success\":true,\"data\":{\"video_path\":"; + writeJsonEscapedString(os, videoPath); + os << ",\"width\":" << width + << ",\"height\":" << height + << ",\"frame_rate\":" << frameRate + << ",\"duration_seconds\":" << duration + << ",\"total_frames\":" << static_cast(frameCount) + << "}}"; + std::cout << os.str() << std::endl; + return 0; + } + std::cout << "\n===== Video File Information =====" << std::endl; std::cout << " File: " << videoPath << std::endl; std::cout << " Resolution: " << width << "x" << height << std::endl; @@ -288,7 +498,13 @@ int printVideoInfo(const std::string& videoPath) { std::cout << "===================================" << std::endl; return 0; #else + if (opts.jsonOutput) { + printJsonError(opts.schemaVersion, "video-info", "file_playback_unsupported", + "Video file playback is not supported. Rebuild with CCAP_ENABLE_FILE_PLAYBACK=ON", 1); + return 1; + } std::cerr << "Video file playback is not supported. Rebuild with CCAP_ENABLE_FILE_PLAYBACK=ON" << std::endl; + (void)opts; (void)videoPath; return 1; #endif diff --git a/cli/ccap_cli_utils.h b/cli/ccap_cli_utils.h index e1995d1..487c964 100644 --- a/cli/ccap_cli_utils.h +++ b/cli/ccap_cli_utils.h @@ -42,7 +42,7 @@ int showDeviceInfo(const CLIOptions& opts, const std::string& deviceName); * @param videoPath Path to video file * @return Exit code (0 on success, 1 on error) */ -int printVideoInfo(const std::string& videoPath); +int printVideoInfo(const CLIOptions& opts, const std::string& videoPath); /** * @brief Capture frames from camera or video file diff --git a/docs/content/cli.md b/docs/content/cli.md index 5db687e..163220e 100644 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -85,6 +85,8 @@ The executable will be located in the `build/` directory (or `build/Debug`, `bui | `-h, --help` | Show help message and exit | | `-v, --version` | Show version information | | `--verbose` | Enable verbose logging output | +| `--json` | Emit structured JSON output for supported commands | +| `--schema-version VERSION` | Set the schema version field in JSON output (default: `1.0`) | | `--timeout SECONDS` | Program timeout: auto-exit after N seconds | | `--timeout-exit-code CODE` | Exit code when timeout occurs (default: 0) | @@ -213,6 +215,23 @@ Print video file information: ccap -i /path/to/video.mp4 ``` +List devices as JSON for automation: +```bash +ccap --list-devices --json +``` + +Inspect a single device as JSON: +```bash +ccap --device-info 0 --json +``` + +Print video metadata as JSON: +```bash +ccap -i /path/to/video.mp4 --json +``` + +JSON output currently covers device enumeration, device info, and video metadata. The payload uses a stable top-level envelope with `schema_version`, `command`, `success`, and either `data` or `error`. + ### Basic Frame Capture Capture 5 frames from the default camera: diff --git a/docs/content/cli.zh.md b/docs/content/cli.zh.md index 9c031bb..880f30b 100644 --- a/docs/content/cli.zh.md +++ b/docs/content/cli.zh.md @@ -85,6 +85,8 @@ cmake --build build | `-h, --help` | 显示帮助信息并退出 | | `-v, --version` | 显示版本信息 | | `--verbose` | 启用详细日志输出 | +| `--json` | 对已支持的命令输出结构化 JSON | +| `--schema-version VERSION` | 设置 JSON 输出中的 schema_version 字段(默认:`1.0`) | ### Windows 相机后端选项 @@ -189,6 +191,23 @@ ccap --device-info 0 ccap --device-info ``` +以 JSON 形式列出设备,便于脚本或 Agent 消费: +```bash +ccap --list-devices --json +``` + +以 JSON 形式查看单个设备的能力信息: +```bash +ccap --device-info 0 --json +``` + +以 JSON 形式输出视频元数据: +```bash +ccap -i /path/to/video.mp4 --json +``` + +当前 JSON 输出先覆盖设备枚举、设备信息和视频元数据。返回体采用稳定的顶层结构,包含 `schema_version`、`command`、`success`,以及 `data` 或 `error`。 + ### 基本帧捕获 从默认相机捕获单帧(默认保存为 JPG): diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..d961d97 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,15 @@ +# Publishable Skills + +This directory contains standalone skill folders intended for ClawHub/OpenClaw publication. + +Each publishable skill should follow these rules: + +- it lives in its own folder under `skills/` +- the folder has a top-level `SKILL.md` +- supporting files are text-based +- frontmatter stays concise and machine-parseable +- publish commands target the skill folder itself, not the repository root + +For this repository, the publishable skill is: + +- [ccap](./ccap) \ No newline at end of file diff --git a/skills/ccap/PUBLISHING.md b/skills/ccap/PUBLISHING.md new file mode 100644 index 0000000..3af5365 --- /dev/null +++ b/skills/ccap/PUBLISHING.md @@ -0,0 +1,52 @@ +# Publishing ccap To ClawHub + +This file is for maintainers of the `ccap` skill bundle. + +## Prerequisites + +- `clawhub` CLI installed +- authenticated with `clawhub login` +- ready to publish the folder `skills/ccap` + +Install the CLI if needed: + +```bash +npm i -g clawhub +``` + +Authenticate: + +```bash +clawhub login +clawhub whoami +``` + +## First Publish Example + +```bash +clawhub publish ./skills/ccap \ + --slug ccap \ + --name "ccap" \ + --version 0.1.0 \ + --tags latest \ + --changelog "Initial ClawHub release" +``` + +## Update Example + +```bash +clawhub publish ./skills/ccap \ + --slug ccap \ + --name "ccap" \ + --version 0.1.1 \ + --tags latest \ + --changelog "Refine installation guidance and command examples" +``` + +## Notes + +- Publish the skill folder, not the repository root. +- Keep `SKILL.md` as the main public entrypoint. +- Keep frontmatter stable and concise; ClawHub parses it during publish. +- If release-binary guidance changes, verify asset naming before updating the skill text. +- If platform support changes, update both `SKILL.md` and the referenced notes. \ No newline at end of file diff --git a/skills/ccap/SKILL.md b/skills/ccap/SKILL.md new file mode 100644 index 0000000..89e2fed --- /dev/null +++ b/skills/ccap/SKILL.md @@ -0,0 +1,114 @@ +--- +name: ccap +description: "Install or use the ccap CLI for camera capture, webcam inspection, device listing, frame capture, and video metadata. Use when you need to work with CameraCapture on macOS, Linux, or Windows, especially for listing devices, checking device capabilities, capturing frames, inspecting video files, or choosing between existing install, Homebrew, source build, and release-binary fallback." +argument-hint: "install | list-devices | device-info | capture | video-info" +metadata: { "openclaw": { "emoji": "📷", "homepage": "https://github.com/wysaid/CameraCapture", "install": [{ "id": "brew", "kind": "brew", "formula": "wysaid/ccap/ccap", "bins": ["ccap"], "os": ["macos"], "label": "Install ccap (Homebrew)" }] } } +--- + +# ccap + +Use this skill when an agent needs to install or operate `ccap` as a practical vision-input CLI. + +This skill is designed for ClawHub/OpenClaw publication as a standalone skill folder. + +## Typical Triggers + +Use this skill when the user asks for things like: + +- "list my cameras" +- "show device capabilities" +- "capture one frame from webcam 0" +- "inspect this mp4" +- "install ccap on this machine" +- "use CameraCapture from the CLI" + +## What This Skill Is For + +- Installing `ccap` on the current machine +- Reusing an existing `ccap` installation when already available +- Building `ccap` from source when local development tools exist +- Falling back to a release binary when no build toolchain is available but a matching release asset exists +- Listing camera devices +- Inspecting device capabilities +- Capturing one or more frames with the CLI +- Reading video metadata + +## What This Skill Is Not For + +- Editing CameraCapture library internals +- Explaining or modifying OpenClaw internals +- Assuming camera access permission is already granted +- Assuming preview is safe in CI, SSH, or headless environments +- Assuming all platforms support video-file playback equally + +## Default Behavior + +- Prefer the `ccap` CLI over internal source files. +- Prefer `--json` when the command supports it. +- Prefer non-preview workflows unless the task explicitly needs preview. +- Treat camera devices, permissions, GUI support, and backend support as environment-dependent. + +## Procedure + +1. Determine whether the task is installation, device inspection, capture, or video inspection. +2. Reuse an existing `ccap` binary if one is already available. +3. If installation is needed, follow the environment decision tree instead of inventing a platform-specific path. +4. Prefer a JSON-capable command when the task can be solved with structured output. +5. Verify the selected workflow with a minimal command before attempting larger capture or preview runs. +6. If the environment blocks the task, report the exact blocker: missing binary, missing permission, no device, unsupported platform path, or unavailable release asset. + +## Environment Decision Tree + +1. Check whether `ccap` is already installed and runnable. +2. If running on macOS and `brew` exists, prefer Homebrew installation. +3. Otherwise, if CMake and a compiler are available, build from source. +4. Otherwise, if network access is available and a matching GitHub Release asset exists, download and use that binary. +5. If none of the above is possible, stop and report the missing prerequisite instead of guessing. + +See [installation details](./references/install.md). + +## Stable Command Patterns + +Use these first before improvising: + +- List devices: `ccap --list-devices --json` +- Device info: `ccap --device-info 0 --json` +- Video info: `ccap -i /path/to/video.mp4 --json` +- Capture one frame: `ccap -d 0 -c 1 -o ./captures` +- Capture one frame from a named device: `ccap -d "OBS Virtual Camera" -c 1 -o ./captures` + +More examples and fallback notes are in [command reference](./references/commands.md). + +## Operational Rules + +- If a command supports `--json`, prefer parsing JSON over scraping text output. +- If device enumeration succeeds but opening the device fails, treat that as a permission or device-access issue. +- If camera permission is denied, report the authorization requirement instead of repeated retries. +- If there is no camera device, consider a video-file workflow if that satisfies the task. +- If the environment is headless or remote, do not enable preview by default. +- If video playback is unsupported on the current platform or build, report it explicitly. + +## Response Expectations + +- Prefer reporting the exact command used. +- Prefer reporting whether the output is structured JSON or plain text. +- If installation was performed, state which path was chosen: existing binary, Homebrew, source build, or release binary. +- If the task failed, report the smallest actionable next step instead of a vague failure summary. + +## Verification + +After installation or selection of an existing binary, verify with one or more of: + +```bash +ccap --version +ccap --list-devices +ccap --list-devices --json +ccap -i /path/to/video.mp4 --json +``` + +## Source Of Truth + +Before guessing flags, build steps, or platform behavior, consult: + +- [install notes](./references/install.md) +- [command reference](./references/commands.md) diff --git a/skills/ccap/references/commands.md b/skills/ccap/references/commands.md new file mode 100644 index 0000000..4c13280 --- /dev/null +++ b/skills/ccap/references/commands.md @@ -0,0 +1,59 @@ +# ccap Command Reference For Agents + +Use the CLI directly. Prefer JSON when available. + +## Device Enumeration + +Preferred: + +```bash +ccap --list-devices --json +``` + +Fallback text mode: + +```bash +ccap --list-devices +``` + +## Device Info + +Preferred: + +```bash +ccap --device-info 0 --json +``` + +If the device opens successfully, expect capability data. +If the device cannot be opened, expect a structured error or text error depending on mode. + +## Video Metadata + +Preferred: + +```bash +ccap -i /path/to/video.mp4 --json +``` + +Use this when no camera is required and the task is only about file inspection. + +## Frame Capture + +One frame: + +```bash +ccap -d 0 -c 1 -o ./captures +``` + +Batch capture: + +```bash +ccap -d 0 -c 10 -o ./captures +``` + +## Operational Cautions + +- Do not assume camera permission is granted. +- Do not default to preview in headless environments. +- Do not assume Linux supports the same video playback features as macOS or Windows. +- Prefer JSON parsing over text scraping when `--json` exists. \ No newline at end of file diff --git a/skills/ccap/references/install.md b/skills/ccap/references/install.md new file mode 100644 index 0000000..7e9fb06 --- /dev/null +++ b/skills/ccap/references/install.md @@ -0,0 +1,71 @@ +# ccap Installation Notes + +This file supports the publishable `ccap` skill. + +## Preferred Order + +1. Reuse an existing `ccap` installation if present. +2. On macOS with Homebrew available, prefer Homebrew. +3. If a local compiler and CMake toolchain are available, build from source. +4. Only if no suitable build toolchain exists, check whether a matching GitHub Release binary exists. + +## Reuse Existing Install + +```bash +command -v ccap +ccap --version +``` + +## Homebrew On macOS + +```bash +brew tap wysaid/ccap +brew install ccap +ccap --version +``` + +Use this when the host is macOS and the task only needs the CLI. + +## Build From Source + +Repository install shortcut: + +```bash +./scripts/build_and_install.sh +``` + +CLI-focused build: + +```bash +cmake -B build -DCCAP_BUILD_CLI=ON +cmake --build build +./build/ccap --version +``` + +Release-style CLI build: + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release -DCCAP_BUILD_CLI=ON +cmake --build build +./build/ccap --version +``` + +## GitHub Release Binary Fallback + +Only use this path when: + +- no suitable local build toolchain is available +- network access is available +- a matching asset exists for the current OS and architecture + +Rules: + +- Verify the asset name before download. +- Do not invent URLs or filenames. +- If no matching release asset exists, fall back to source build instead of guessing. + +## Notes + +- Homebrew installation is documented for macOS. +- Source build is the broadest fallback and should be preferred over speculative binary downloads. +- On Windows, repository documentation expects source build/install to run in Git Bash for the provided script path. \ No newline at end of file diff --git a/tests/test_ccap_cli.cpp b/tests/test_ccap_cli.cpp index dba3824..299f87c 100644 --- a/tests/test_ccap_cli.cpp +++ b/tests/test_ccap_cli.cpp @@ -385,6 +385,15 @@ TEST_F(CCAPCLIDeviceTest, ListDevices) { EXPECT_THAT(result.output, ::testing::HasSubstr("camera device")); } +TEST_F(CCAPCLIDeviceTest, ListDevicesJson) { + auto result = runCLI("--list-devices --json"); + EXPECT_EQ(result.exitCode, 0); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"schema_version\":\"1.0\"")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"command\":\"list-devices\"")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"success\":true")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"devices\":")); +} + TEST_F(CCAPCLIDeviceTest, ShowDeviceInfo) { auto result = runCLI("--device-info 0"); EXPECT_EQ(result.exitCode, 0); @@ -392,6 +401,21 @@ TEST_F(CCAPCLIDeviceTest, ShowDeviceInfo) { EXPECT_THAT(result.output, ::testing::HasSubstr("Device [")); } +TEST_F(CCAPCLIDeviceTest, ShowDeviceInfoJson) { + auto result = runCLI("--device-info 0 --json"); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"command\":\"device-info\"")); + if (result.exitCode == 0) { + EXPECT_THAT(result.output, ::testing::HasSubstr("\"success\":true")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"device_count\":1")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"supported_resolutions\":")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"supported_pixel_formats\":")); + } else { + EXPECT_THAT(result.output, ::testing::HasSubstr("\"success\":false")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"error\":")); + EXPECT_THAT(result.output, ::testing::HasSubstr("\"code\":")); + } +} + TEST_F(CCAPCLIDeviceTest, CaptureOneFrame) { std::string outputDir = testOutputDir.string(); auto result = runCLI("-d 0 -c 1 --save-bmp -o " + outputDir); @@ -726,6 +750,23 @@ TEST_F(CCAPCLITest, VideoPlayback_InvalidFile) { EXPECT_THAT(result.output, testing::HasSubstr("Failed to open video file")); } +TEST_F(CCAPCLITest, VideoInfoJson) { + std::string videoPath = getTestVideoPath(); + if (videoPath.empty()) { + GTEST_SKIP() << "Test video not available"; + } + + std::string cmd = "--video \"" + videoPath + "\" --json"; + auto result = runCLI(cmd); + + ASSERT_EQ(result.exitCode, 0) << "Video info JSON failed: " << result.output; + EXPECT_THAT(result.output, testing::HasSubstr("\"command\":\"video-info\"")); + EXPECT_THAT(result.output, testing::HasSubstr("\"success\":true")); + EXPECT_THAT(result.output, testing::HasSubstr("\"video_path\":")); + EXPECT_THAT(result.output, testing::HasSubstr("\"frame_rate\":")); + EXPECT_THAT(result.output, testing::HasSubstr("\"duration_seconds\":")); +} + TEST_F(CCAPCLITest, VideoPlayback_WithPixelFormat) { std::string videoPath = getTestVideoPath(); if (videoPath.empty()) { diff --git a/tests/test_cli_args_parser.cpp b/tests/test_cli_args_parser.cpp index adc3796..345b67b 100644 --- a/tests/test_cli_args_parser.cpp +++ b/tests/test_cli_args_parser.cpp @@ -45,6 +45,19 @@ TEST(CLIArgsParserTest, CaptureDefaultsStayUnchangedWithoutPreview) { EXPECT_EQ(opts.height, 720); } +TEST(CLIArgsParserTest, ParsesJsonOutputOptions) { + char arg0[] = "ccap"; + char arg1[] = "--json"; + char arg2[] = "--schema-version"; + char arg3[] = "1.2"; + char* argv[] = { arg0, arg1, arg2, arg3, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(4, argv); + + EXPECT_TRUE(opts.jsonOutput); + EXPECT_EQ(opts.schemaVersion, "1.2"); +} + #if defined(_WIN32) || defined(_WIN64) TEST(CLIArgsParserTest, ParsesWindowsCameraBackendOption) { char arg0[] = "ccap"; From 46451b22406ab5d523d32b7647721104c9a4a2e4 Mon Sep 17 00:00:00 2001 From: wy Date: Sun, 22 Mar 2026 03:12:17 +0800 Subject: [PATCH 2/3] Fix review issues in JSON CLI contract and skill docs --- cli/args_parser.cpp | 7 +++++-- cli/ccap_cli.cpp | 9 ++++++--- cli/ccap_cli_utils.cpp | 18 ++++++++++++++---- cli/ccap_cli_utils.h | 1 + docs/content/cli.md | 2 +- docs/content/cli.zh.md | 2 +- skills/ccap/references/install.md | 11 ++++++++++- tests/test_cli_args_parser.cpp | 14 ++++++++++++++ 8 files changed, 52 insertions(+), 12 deletions(-) diff --git a/cli/args_parser.cpp b/cli/args_parser.cpp index 9676e53..b138d14 100644 --- a/cli/args_parser.cpp +++ b/cli/args_parser.cpp @@ -347,9 +347,12 @@ CLIOptions parseArgs(int argc, char* argv[]) { } else if (arg == "--json") { opts.jsonOutput = true; } else if (arg == "--schema-version") { - if (i + 1 < argc) { - opts.schemaVersion = argv[++i]; + if (i + 1 >= argc || argv[i + 1][0] == '-') { + std::cerr << "Error: --schema-version requires a value.\n\n"; + printUsage(argv[0]); + std::exit(1); } + opts.schemaVersion = argv[++i]; } else if (arg == "-l" || arg == "--list-devices") { opts.listDevices = true; } else if (arg == "-I" || arg == "--device-info") { diff --git a/cli/ccap_cli.cpp b/cli/ccap_cli.cpp index b8f81ec..3eb2721 100644 --- a/cli/ccap_cli.cpp +++ b/cli/ccap_cli.cpp @@ -124,9 +124,15 @@ int main(int argc, char* argv[]) { return ccap_cli::convertYuvToImage(opts); } + // Check if we should just print info (no action specified) + bool hasAction = opts.enablePreview || opts.saveFrames || opts.captureCountSpecified || !opts.outputDir.empty(); + // Check if video file playback is requested but not supported on Linux #if defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__) if (!opts.videoFilePath.empty()) { + if (!hasAction) { + return ccap_cli::printVideoInfo(opts, opts.videoFilePath); + } std::cerr << "Error: Video file playback is not supported on Linux.\n" << "\n" << "Video file playback is currently only available on:\n" @@ -142,9 +148,6 @@ int main(int argc, char* argv[]) { } #endif - // Check if we should just print info (no action specified) - bool hasAction = opts.enablePreview || opts.saveFrames || opts.captureCountSpecified || !opts.outputDir.empty(); - // If video file specified without action, print video info if (!opts.videoFilePath.empty() && !hasAction) { return ccap_cli::printVideoInfo(opts, opts.videoFilePath); diff --git a/cli/ccap_cli_utils.cpp b/cli/ccap_cli_utils.cpp index 72a6eaf..8b6e279 100644 --- a/cli/ccap_cli_utils.cpp +++ b/cli/ccap_cli_utils.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,14 @@ void writeJsonEscapedString(std::ostream& os, std::string_view value) { os << '"'; } +void writeJsonFiniteNumberOrNull(std::ostream& os, double value) { + if (std::isfinite(value)) { + os << value; + } else { + os << "null"; + } +} + void writeJsonResolutions(std::ostream& os, const std::vector& resolutions) { os << '['; for (size_t index = 0; index < resolutions.size(); ++index) { @@ -481,9 +490,11 @@ int printVideoInfo(const CLIOptions& opts, const std::string& videoPath) { writeJsonEscapedString(os, videoPath); os << ",\"width\":" << width << ",\"height\":" << height - << ",\"frame_rate\":" << frameRate - << ",\"duration_seconds\":" << duration - << ",\"total_frames\":" << static_cast(frameCount) + << ",\"frame_rate\":"; + writeJsonFiniteNumberOrNull(os, frameRate); + os << ",\"duration_seconds\":"; + writeJsonFiniteNumberOrNull(os, duration); + os << ",\"total_frames\":" << static_cast(frameCount) << "}}"; std::cout << os.str() << std::endl; return 0; @@ -504,7 +515,6 @@ int printVideoInfo(const CLIOptions& opts, const std::string& videoPath) { return 1; } std::cerr << "Video file playback is not supported. Rebuild with CCAP_ENABLE_FILE_PLAYBACK=ON" << std::endl; - (void)opts; (void)videoPath; return 1; #endif diff --git a/cli/ccap_cli_utils.h b/cli/ccap_cli_utils.h index 487c964..23d7010 100644 --- a/cli/ccap_cli_utils.h +++ b/cli/ccap_cli_utils.h @@ -39,6 +39,7 @@ int showDeviceInfo(const CLIOptions& opts, const std::string& deviceName); /** * @brief Print video file information + * @param opts CLI options controlling output mode, including JSON formatting * @param videoPath Path to video file * @return Exit code (0 on success, 1 on error) */ diff --git a/docs/content/cli.md b/docs/content/cli.md index 163220e..9a5d85e 100644 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -230,7 +230,7 @@ Print video metadata as JSON: ccap -i /path/to/video.mp4 --json ``` -JSON output currently covers device enumeration, device info, and video metadata. The payload uses a stable top-level envelope with `schema_version`, `command`, `success`, and either `data` or `error`. +JSON output currently covers device enumeration, device info, and video metadata. The payload uses a stable top-level envelope with `schema_version`, `command`, and `success`; successful responses include `data`, while error responses include `exit_code` and `error`. ### Basic Frame Capture diff --git a/docs/content/cli.zh.md b/docs/content/cli.zh.md index 880f30b..0c30527 100644 --- a/docs/content/cli.zh.md +++ b/docs/content/cli.zh.md @@ -206,7 +206,7 @@ ccap --device-info 0 --json ccap -i /path/to/video.mp4 --json ``` -当前 JSON 输出先覆盖设备枚举、设备信息和视频元数据。返回体采用稳定的顶层结构,包含 `schema_version`、`command`、`success`,以及 `data` 或 `error`。 +当前 JSON 输出先覆盖设备枚举、设备信息和视频元数据。返回体采用稳定的顶层结构,包含 `schema_version`、`command` 与 `success`;成功时返回 `data`,错误时返回 `exit_code` 和 `error`。 ### 基本帧捕获 diff --git a/skills/ccap/references/install.md b/skills/ccap/references/install.md index 7e9fb06..78af387 100644 --- a/skills/ccap/references/install.md +++ b/skills/ccap/references/install.md @@ -28,12 +28,21 @@ Use this when the host is macOS and the task only needs the CLI. ## Build From Source -Repository install shortcut: +Repository install shortcut (library-focused): ```bash ./scripts/build_and_install.sh ``` +If the task specifically requires the `ccap` CLI binary, use an explicit CLI build/install flow instead: + +```bash +cmake -B build -DCCAP_BUILD_CLI=ON -DCCAP_INSTALL=ON +cmake --build build +cmake --install build +ccap --version +``` + CLI-focused build: ```bash diff --git a/tests/test_cli_args_parser.cpp b/tests/test_cli_args_parser.cpp index 345b67b..346db07 100644 --- a/tests/test_cli_args_parser.cpp +++ b/tests/test_cli_args_parser.cpp @@ -58,6 +58,20 @@ TEST(CLIArgsParserTest, ParsesJsonOutputOptions) { EXPECT_EQ(opts.schemaVersion, "1.2"); } +TEST(CLIArgsParserTest, RejectsMissingSchemaVersionValue) { + char arg0[] = "ccap"; + char arg1[] = "--schema-version"; + char* argv[] = { arg0, arg1, nullptr }; + + EXPECT_EXIT( + { + (void)ccap_cli::parseArgs(2, argv); + std::exit(0); + }, + ::testing::ExitedWithCode(1), + "--schema-version requires a value"); +} + #if defined(_WIN32) || defined(_WIN64) TEST(CLIArgsParserTest, ParsesWindowsCameraBackendOption) { char arg0[] = "ccap"; From 47736edd4858dd713c205a8206e22b7f6e6a2f7f Mon Sep 17 00:00:00 2001 From: wy Date: Sun, 22 Mar 2026 03:51:44 +0800 Subject: [PATCH 3/3] Fix: handle non-finite frameCount in video-info JSON; add test assertion --- cli/ccap_cli_utils.cpp | 31 +++++++++++++++++++++++++------ tests/test_ccap_cli.cpp | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cli/ccap_cli_utils.cpp b/cli/ccap_cli_utils.cpp index 8b6e279..6e39010 100644 --- a/cli/ccap_cli_utils.cpp +++ b/cli/ccap_cli_utils.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,15 @@ void writeJsonFiniteNumberOrNull(std::ostream& os, double value) { } } +void writeJsonFiniteIntOrNull(std::ostream& os, double value) { + if (std::isfinite(value) && value >= static_cast(std::numeric_limits::min()) + && value <= static_cast(std::numeric_limits::max())) { + os << static_cast(value); + } else { + os << "null"; + } +} + void writeJsonResolutions(std::ostream& os, const std::vector& resolutions) { os << '['; for (size_t index = 0; index < resolutions.size(); ++index) { @@ -490,11 +500,13 @@ int printVideoInfo(const CLIOptions& opts, const std::string& videoPath) { writeJsonEscapedString(os, videoPath); os << ",\"width\":" << width << ",\"height\":" << height - << ",\"frame_rate\":"; - writeJsonFiniteNumberOrNull(os, frameRate); - os << ",\"duration_seconds\":"; - writeJsonFiniteNumberOrNull(os, duration); - os << ",\"total_frames\":" << static_cast(frameCount) + << ",\"frame_rate\":"; + writeJsonFiniteNumberOrNull(os, frameRate); + os << ",\"duration_seconds\":"; + writeJsonFiniteNumberOrNull(os, duration); + os << ",\"total_frames\":"; + writeJsonFiniteIntOrNull(os, frameCount); + os << "}}"; std::cout << os.str() << std::endl; return 0; @@ -505,7 +517,14 @@ int printVideoInfo(const CLIOptions& opts, const std::string& videoPath) { std::cout << " Resolution: " << width << "x" << height << std::endl; std::cout << " Frame rate: " << frameRate << " fps" << std::endl; std::cout << " Duration: " << duration << " seconds" << std::endl; - std::cout << " Total frames: " << static_cast(frameCount) << std::endl; + std::cout << " Total frames: "; + if (std::isfinite(frameCount) && frameCount >= static_cast(std::numeric_limits::min()) + && frameCount <= static_cast(std::numeric_limits::max())) { + std::cout << static_cast(frameCount); + } else { + std::cout << "unknown"; + } + std::cout << std::endl; std::cout << "===================================" << std::endl; return 0; #else diff --git a/tests/test_ccap_cli.cpp b/tests/test_ccap_cli.cpp index 299f87c..ee373e2 100644 --- a/tests/test_ccap_cli.cpp +++ b/tests/test_ccap_cli.cpp @@ -765,6 +765,7 @@ TEST_F(CCAPCLITest, VideoInfoJson) { EXPECT_THAT(result.output, testing::HasSubstr("\"video_path\":")); EXPECT_THAT(result.output, testing::HasSubstr("\"frame_rate\":")); EXPECT_THAT(result.output, testing::HasSubstr("\"duration_seconds\":")); + EXPECT_THAT(result.output, testing::HasSubstr("\"total_frames\":")); } TEST_F(CCAPCLITest, VideoPlayback_WithPixelFormat) {