diff --git a/README.md b/README.md index 194bfa0..eec5e8e 100644 --- a/README.md +++ b/README.md @@ -280,13 +280,13 @@ to your agent. ### Use as a Claude Code skill -Drop [`skills/watch-cli/`](skills/watch-cli/) into your -`~/.claude/skills/` folder and the agent will pick up `/watch ` -as a first-class command, including the prompt library above. +Copy the portable [`SKILL.md`](SKILL.md) into a `watch-cli` skill directory +and the agent will pick up `/watch ` as a first-class command, including +the prompt library above. ```bash -mkdir -p ~/.claude/skills -cp -r skills/watch-cli ~/.claude/skills/ +mkdir -p ~/.claude/skills/watch-cli +cp SKILL.md ~/.claude/skills/watch-cli/SKILL.md ``` --- diff --git a/SKILL.md b/SKILL.md index 83a9165..5680332 100644 --- a/SKILL.md +++ b/SKILL.md @@ -32,6 +32,8 @@ Reach for `watch` whenever the user gives you a video URL and wants you to do so Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook. Login-walled sources fall back to the user's signed-in browser cookies automatically. +If the user also provides [Xquik REST API](https://docs.xquik.com/api-reference/overview) or [MCP](https://docs.xquik.com/mcp/overview) output for an X post, treat it as read-only metadata context: post text, author, timestamp, source URL, media notes, and public metrics. It does not replace `watch`; still run `watch` when the user asks to inspect video frames, audio, or transcript, leave missing metadata unknown, and never place credentials in prompts or output. + ## What you get back The `watch` output gives you the raw materials to map to five concrete artifacts. Match the user's intent to one of them. @@ -72,3 +74,6 @@ Default frame count is 8. For a fast-cut or dense UI demo, double it. For a mult - Do not hard-fail on every non-zero exit. `exit 4` is recoverable partial success — frames populated, transcript `null`. Branch on `exit_code` before parsing. - Do not surface API keys or environment variable names in chat. `KYMA_API_KEY` setup lives in the README. - Do not embed the locked pitch into a longer marketing paragraph. The description line above is the source of truth; reuse it verbatim where the host shows skill metadata. + +Xquik is an independent third-party service. Not affiliated with X Corp. +"Twitter" and "X" are trademarks of X Corp. diff --git a/bin/watch b/bin/watch index ac57cfb..e33a493 100755 --- a/bin/watch +++ b/bin/watch @@ -117,13 +117,23 @@ if [[ "$FORMAT" != "text" && "$FORMAT" != "json" ]]; then exit 64 fi -# Dependency check up-front so we exit 2, not 1, when a binary is missing. -for dep in yt-dlp ffmpeg ffprobe jq curl python3; do - if ! command -v "$dep" >/dev/null 2>&1; then - echo "[watch] error: $dep not found on PATH tag=missing-dep:$dep" >&2 - exit 2 - fi -done +MISSING_DEP="" +check_dependencies() { + local dep + for dep in yt-dlp ffmpeg ffprobe jq curl python3; do + if ! command -v "$dep" >/dev/null 2>&1; then + MISSING_DEP="$dep" + echo "[watch] error: $dep not found on PATH tag=missing-dep:$dep" >&2 + return 2 + fi + done +} + +if [[ $PIPE -eq 0 ]]; then + check_dependencies + DEPENDENCY_RC=$? + [[ $DEPENDENCY_RC -ne 0 ]] && exit "$DEPENDENCY_RC" +fi # ── Per-URL pipeline ────────────────────────────────────────────── # Runs the download → frames → transcribe sequence for one URL. @@ -245,6 +255,8 @@ if [[ $PIPE -eq 1 ]]; then # Track the worst-case exit code so the pipeline as a whole reports # a non-zero exit when any URL failed. Successful runs return 0. PIPE_WORST=0 + PIPE_DEPENDENCIES_CHECKED=0 + PIPE_DEPENDENCIES_RC=0 while IFS= read -r LINE || [[ -n "$LINE" ]]; do # Skip blank lines / comments. LINE="${LINE%$'\r'}" @@ -260,8 +272,19 @@ if [[ $PIPE -eq 1 ]]; then (( PIPE_WORST < 64 )) && PIPE_WORST=64 continue fi - if ! process_url "$LINE" "$COUNT" "$FORMAT" "$COOKIES_ARG" 1; then - RC=$? + if [[ $PIPE_DEPENDENCIES_CHECKED -eq 0 ]]; then + check_dependencies + PIPE_DEPENDENCIES_RC=$? + PIPE_DEPENDENCIES_CHECKED=1 + fi + if [[ $PIPE_DEPENDENCIES_RC -ne 0 ]]; then + _emit_pipe_error "$LINE" "$PIPE_DEPENDENCIES_RC" "missing-dep:$MISSING_DEP" + (( PIPE_WORST < PIPE_DEPENDENCIES_RC )) && PIPE_WORST=$PIPE_DEPENDENCIES_RC + continue + fi + process_url "$LINE" "$COUNT" "$FORMAT" "$COOKIES_ARG" 1 + RC=$? + if [[ $RC -ne 0 ]]; then (( PIPE_WORST < RC )) && PIPE_WORST=$RC fi done diff --git a/tests/test-output-schema.sh b/tests/test-output-schema.sh index 4efbf03..74e7ec3 100755 --- a/tests/test-output-schema.sh +++ b/tests/test-output-schema.sh @@ -111,7 +111,25 @@ else fail "watch --pipe with invalid URL expected 1-line JSON error + non-zero; got rc=$PIPE_BAD_RC lines=$PIPE_BAD_LINES out='$PIPE_BAD_OUT'" fi -# 10. Forced local mode with no whisper-cli on PATH → exit 2, tag=missing-dep. +# 10. A valid pipe item with a missing dependency emits one JSON error and exits 2. +MISSING_DEP_PATH="$(mktemp -d)" +for dep in bash ffmpeg ffprobe jq curl python3 dirname readlink sed; do + dep_path="$(command -v "$dep")" + ln -s "$dep_path" "$MISSING_DEP_PATH/$dep" +done +PIPE_DEP_OUT="$(printf 'https://example.invalid/video\n' | PATH="$MISSING_DEP_PATH" "$WATCH" --pipe 2>/dev/null)" +PIPE_DEP_RC=$? +PIPE_DEP_LINES="$(printf '%s' "$PIPE_DEP_OUT" | grep -c .)" +rm -rf "$MISSING_DEP_PATH" +if [[ $PIPE_DEP_RC -eq 2 ]] \ + && [[ "$PIPE_DEP_LINES" == "1" ]] \ + && echo "$PIPE_DEP_OUT" | jq -e '.version == 1 and .exit_code == 2 and .error == "missing-dep:yt-dlp"' >/dev/null 2>&1; then + pass "watch --pipe with missing yt-dlp emits one v1 error object and exits 2" +else + fail "watch --pipe missing-dep expected 1-line JSON error + exit 2; got rc=$PIPE_DEP_RC lines=$PIPE_DEP_LINES out='$PIPE_DEP_OUT'" +fi + +# 11. Forced local mode with no whisper-cli on PATH → exit 2, tag=missing-dep. # Use a subshell with a PATH that excludes whisper-cli, set WATCH_AUDIO_MODE=local, # and verify the contract from docs/offline-mode.md. TR_BIN="$REPO_ROOT/bin/transcribe"