adbertram

cli-tool

MANDATORY: CLI tool skill router for ALL cli-tools requests. Use this skill first for any available CLI tool, service CLI operation, CLI command execution, or CLI lifecycle work. DO NOT run cli-tools commands, guess command syntax, or load individual [tool]-cli skills directly before this router selects them. For service operations, route through [cli-tools-root]/_repo/skills/cli-tool/workflows/skill-router.md to [cli-tools-root]/_repo/skills/[tool]-cli/SKILL.md and the adjacent usage.json. For lifecycle work, delegate to cli-tool-expert and use this skill's lifecycle workflows. Triggers: any cli tool, cli command, service cli, [tool] cli, run cli, execute cli, create cli, update cli, test cli, fix cli, add command, list cli tools, cli standards.

adbertram 6 Actualitzat fa 4 setm.

Recursos

8
GitHub

Instal·la

npx skillscat add adbertram/cli-tools/cli-tool

Instal·la des del registre de SkillsCat.

SKILL.md
Route every cli-tools request to the correct repo-owned CLI skill or lifecycle workflow, then execute against the real command contract. Service-operation routing stays in the current session: use `/_repo/skills/cli-tool/workflows/skill-router.md`, then load the selected service skill and its adjacent `usage.json`.

When this skill is invoked for CLI lifecycle work by a parent agent session and the current agent is not cli-tool-expert, delegate the lifecycle work to cli-tool-expert instead of performing CLI implementation work inline. Pass the complete user request, relevant file paths, constraints, and required validation.

When the current agent is cli-tool-expert, follow this skill normally.
</agent_routing>

The repo-owned source of truth for CLI service skills is `/_repo/skills`.

Classify skills by ownership and domain, not by whether they call a CLI as an
implementation detail. Service CLI skills and CLI lifecycle workflows live under
<cli-tools-root>/_repo/skills; project-specific workflows that merely call a
CLI belong to the target project's skill scope.

For any request that uses an existing CLI tool, read <cli-tools-root>/_repo/skills/cli-tool/workflows/skill-router.md before selecting a service skill. Do not look for the workflow at <cli-tools-root>/_repo/skills/skill-router.md. The selected service skill's SKILL.md and adjacent usage.json are mandatory before running any command.
</skill_router>

Use `/_repo/scripts/find-cli-tools.sh` to enumerate the available CLI tools from the source tree. The script prints one JSON array of records with `name`, `readme`, and `description`, where `description` is extracted from each tool README's `## DESCRIPTION` block. The `readme` value is relative to `` unless it is already absolute; resolve it against that root before deriving the tool directory or checking files. The default mode is JSON, and `--json` is accepted as an explicit alias for that default. To inspect one CLI, pass the exact tool name as a positional filter, for example `/_repo/scripts/find-cli-tools.sh --json upwork`; the output is still a JSON array. When filtering the saved output with `jq`, iterate the array explicitly, for example `jq -e '.[] | select(.name == "google")' `. Do not treat the output as JSONL records. When parsing the saved JSON with Python, do not wrap a quoted heredoc parser inside a single-quoted `bash -lc '...'` command; that quote layer can strip Python string literals such as `Path('/tmp/cli-tools.json')` before Python starts. Instead invoke the parser as a standalone multiline command and pass the JSON file path as argv, for example `python3 - "$json_file" <<'PY'`, then read `sys.argv[1]` inside Python. Pass `--markdown` for a compact `- name: ` list (~2K tokens) suitable for context injection; the Claude (`SessionStart` in `~/.claude/settings.json`) and Codex (`SessionStart` in `~/.codex/hooks.json`) session-start hooks call it this way to preload the CLI-tool roster every session.

Prefer this script over ad hoc find/ls scans when a task asks which CLI
tools exist, what they do, or which README describes them.
</tool_discovery>

Route to appropriate workflow based on user intent: - Use an existing CLI / service operation → workflows/skill-router.md - Create new CLI → workflows/create-cli.md - Test CLI → workflows/test-cli.md - Modify CLI → workflows/update-cli.md - Remove CLI → workflows/remove-cli.md - List CLIs → workflows/list-cli.md </quick_start> These principles apply to ALL CLI tool operations. They cannot be skipped. **Never create CLI files manually.** Use the scaffolding script: ```bash /_repo/skills/cli-tool/scripts/new-cli-tool --name --type [--personal] [options] ``` This handles: directory structure, uv tool installation (isolated venv + symlink), and placement inside the parent cli-tools monorepo. New browser CLIs inherit the shared Chromium user-data-dir from `cli_tools_shared.config.BaseConfig`; never hard-code a per-tool `--user-data-dir`. The `default` auth profile shares `~/.local/share/cli-tools/_shared/chromium-profile`; named auth profiles stay isolated. Chrome processes using the shared profile must run sequentially. See `_repo/skills/cli-tool-browser-expert/SKILL.md` and `_repo/cli-tools-shared/docs/shared-chromium-profile.md`. **stdout = DATA ONLY. stderr = MESSAGES ONLY.** See `references/output-standards.md` for details. Before passing optional repo paths to `rg`, `grep`, `find`, `ls`, `stat`, `cat`, `sed`, `nl`, `wc`, `head`, `tail`, or similar commands, prove each path exists or build the operand list from discovered existing paths. This applies equally to relative operands and absolute operands under ``, including optional root children such as `/scripts`; do not pass an absolute path just because the repo root exists. For file-reading commands such as `cat`, `sed`, `nl`, `wc`, `head`, and `tail`, filter glob and optional operands to regular files, not just existing paths; directories such as `__pycache__` are command errors. This includes personal CLI tool paths such as `_personal//install.sh`, `_personal//README.md`, and any other optional per-tool file. Missing optional paths are command errors, not no-match results, and wrong-kind operands are command errors too; report skipped optional paths separately instead of passing them as operands.

Shell globs used as search operands are optional paths too. Do not pass operands
such as */*_cli, tests, or docs directly to rg; with Bash's default
glob behavior, an unmatched glob stays literal and ripgrep exits with status
2 even if other operands produced matches. Expand the glob into an array,
keep only existing entries, and print a skipped marker for any optional root or
glob that produced no existing paths.

Do not run direct reads like sed -n '1,260p' _personal/<tool>/install.sh
unless that exact file has already been proven present in the same command or
by a prerequisite command whose result is in scope.
Do not run direct listings like ls -la <tool>/.venv unless that exact
directory has already been proven present in the same command or by a
prerequisite command whose result is in scope.
This also applies to guessed Python module paths after package layout
uncertainty, including shared package paths such as
_repo/cli-tools-shared/cli_tools_shared/...; discover the real file first
with an existing root and then read only the proven path.

Do not rely on a downstream pipeline stage to hide an upstream missing-operand
error. This is unsafe:

rg --files notion _repo/skills/notion-cli tests _repo | rg -F 'notion'

Build the operand list from paths proven to exist, print skipped optional paths,
then search only those operands:

paths=()
for path in notion _repo/skills/notion-cli tests _repo; do
  if [ -e "$path" ]; then
    paths+=("$path")
  else
    printf 'SKIPPED_MISSING_PATH: %s\n' "$path" >&2
  fi
done
if [ "${#paths[@]}" -eq 0 ]; then
  printf '%s\n' 'NO_EXISTING_PATHS'
  exit 0
fi
if rg -n -F -- 'notion' "${paths[@]}"; then
  exit 0
else
  rc=$?
  if [ "$rc" -eq 1 ]; then
    printf '%s\n' 'NO_MATCH:notion'
    exit 0
  fi
  exit "$rc"
fi

A no-match wrapper does not make missing operands safe. Filter optional paths
before the rg call, then handle rg status 1 only after the existing-path
operand list has been built.

When exploratory `rg` or `grep` searches may legitimately find no match, wrap each search so status `1` prints an explicit no-match marker and exits `0`. An unguarded no-match status is a Tool Failure Protocol violation even when the missing text was expected. Do not use `|| true` unless the command immediately interprets and reports the expected no-match.

Do not chain exploratory searches to dependent file reads with &&, such as
rg -n -F -- 'literal' file dir && sed -n '1,260p' file, unless the search is
first wrapped to consume expected status 1. A no-match result would skip the
dependent read and surface as a parent command failure instead of evidence.

When composing `rg` or `grep` verification wrappers, keep every flag, pattern, and path operand on the same physical command line unless you use explicit backslash continuations or a shell array. Do not insert a bare newline before a path operand; Bash treats the path as a separate command, which can produce `Permission denied` or execute the wrong file even after the search printed a match. Scope CLI-tool investigation searches to bounded source roots such as the target tool directory, `_repo/`, repo-owned skills, docs, tests, or exact files proven relevant. Do not run recursive `rg`, `grep`, or `find` over user cache/profile roots such as `/Users/adam/.npm`, `/Users/adam/.local/share`, `/Users/adam/Library`, or a whole home directory. When runtime profile or cache evidence is needed, inspect the exact known file or tool-owned profile path after proving it exists; do not discover it with a broad recursive search. For browser-backed service CLIs, the CLI-owned persistent browser profile is the only source of truth after `auth login` succeeds. Do not switch to Codex's in-app browser, Computer Use, a visible local browser, or another browser surface to reuse or repair that saved CLI auth context. Use the service CLI and its saved browser-session profile directly. If a post-login service command still opens a headed/manual browser flow, cannot run against the saved profile headlessly, or only hands the user off to finish the requested operation, treat that as a CLI implementation/documentation defect and fix the service CLI before claiming the operation is done. When composing CLI verification or diagnostic Bash commands, do not put leading hyphens in the `printf` format operand unless options are terminated. Use `printf -- '--- label ---\n'` for a literal format, or prefer `printf '%s\n' '--- label ---'` so hyphens are data. When a live smoke test intentionally uses fake or missing remote data to verify API wiring, wrap the command and explicitly validate the expected exit status plus the expected error marker. The wrapper must print an explicit expected-failure marker and exit `0` only for the intended response. A bare CLI command that exits non-zero is a Tool Failure Protocol violation even when the remote error was expected.
if output="$(airtable fields delete tblyZrpsEQCw20i20 fld00000000000000 --base app9uzzru5KZOImYQ --yes 2>&1)"; then
  status=0
else
  status=$?
fi
printf '%s\n' "$output"
if [ "$status" -eq 1 ] && printf '%s\n' "$output" | rg -q -F -- 'API request failed (404)'; then
  printf '%s\n' 'EXPECTED_FAILURE: fake field ID returned Airtable 404; API wiring reached the service.'
  exit 0
fi
exit "$status"
When a negative smoke test intentionally verifies that a mutating command refuses to run without explicit confirmation, wrap the command and explicitly validate the expected exit status plus the exact refusal message. The wrapper must print an explicit expected-failure marker and exit `0` only for that guard response. A bare command such as ` refunds create CAPTURE-123 --amount 4.50` that exits non-zero is a Tool Failure Protocol violation even when the refusal is the correct behavior.
if output="$(paypal refunds create CAPTURE-123 --amount 4.50 2>&1)"; then
  status=0
else
  status=$?
fi
printf '%s\n' "$output"
if [ "$status" -eq 1 ] && printf '%s\n' "$output" | rg -q -F -- 'Refusing to issue refund without --yes or --dry-run'; then
  printf '%s\n' 'EXPECTED_FAILURE: refund command refused to mutate without --yes or --dry-run.'
  exit 0
fi
exit "$status"
When an auth/status probe intentionally checks for an unauthenticated profile or missing credential state, wrap the command and explicitly validate the expected exit status plus the status evidence. The wrapper must print an explicit expected-status marker and exit `0` only for the intended unauthenticated result. Do not run bare commands such as `paypal auth status -t` when `authenticated: false` or missing credentials are an expected diagnostic outcome. Accept both YAML/text `authenticated: false` and JSON `"authenticated": false` status evidence. This includes `copilot auth status --profile default`, which can exit `2` while returning structured JSON with `"authenticated": false`; in that case the wrapper should treat the result as unauthenticated status data only after validating both the exit status and the JSON/text evidence.
if output="$(paypal auth status -t 2>&1)"; then
  status=0
else
  status=$?
fi
printf '%s\n' "$output"
if [ "$status" -eq 2 ] && printf '%s\n' "$output" | rg -q -e 'authenticated: false' -e '"authenticated"[[:space:]]*:[[:space:]]*false'; then
  printf '%s\n' 'EXPECTED_STATUS: paypal profile is unauthenticated.'
  exit 0
fi
exit "$status"
When a CLI command emits JSON that another process will inspect, save stdout to a task-workspace/temp file first, verify the command status and non-empty file, then parse that file. Do not pipe JSON into a heredoc-backed parser that expects stdin. In shapes like `az account list --all --output json | python3 - <<'PY'` or `az account show --output json | python3 - <<'PY'`, the heredoc occupies Python's stdin, so `json.load(sys.stdin)` reads empty input, Python reports `JSONDecodeError: Expecting value: line 1 column 1 (char 0)`, and the producer can report `Broken pipe`.

Before redirecting CLI JSON into a task-workspace artifact, create or prove the
artifact's parent directory. For explicit agent workspaces, run
mkdir -p "$workspace" before >"$workspace/<name>.json"; a missing parent
directory is a command-composition failure, not CLI output evidence.

Use this shape instead:

json_file=$(mktemp -t cli-json)
if az account show --output json >"$json_file"; then
  python3 - "$json_file" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as fh:
    data = json.load(fh)
print(data["name"])
PY
else
  rc=$?
  printf 'PRODUCER_FAILED: az account show rc=%s\n' "$rc" >&2
  exit "$rc"
fi

For API or CLI response verification, inspect or validate the JSON contract
before indexing, slicing, or selecting nested keys. Do not assume shapes such as
every GET /api/providers item having a top-level config key, or a CLI
list command's parsed root being sliceable, until the saved JSON proves the
container type. Check the container type and required paths explicitly; if the
contract is missing, fail with MISSING_JSON_PATH: providers[0].config or
JSON_CONTRACT_MISMATCH: expected list root, got object keys=[...] instead of
a raw KeyError, TypeError, or rows[:10] failure.

Treat the parser as its own batch stage. Capture and return the parser status
before running later update, cleanup, summary, or verification commands, or
carry it into a batch_rc that becomes the final exit status. Do not let a
later successful CLI command mask a failed JSON parser.

When searching for CLI template tokens or copied text containing braces, backticks, dollar signs, parentheses, or other regex metacharacters, use literal matching: `rg -n -F -- '{{description}}' `. Do not pass tokens such as `{{name}}`, `{{description}}`, or `{{AUTH_IMPORT}}` as regex patterns unless every regex metacharacter is intentionally escaped.

Keep CLI skill and usage.json inspection searches line-local unless a real
cross-line match is deliberately required. Do not use Hermes search_files or
plain rg with a pattern containing a literal \n escape such as
"list": \{\n\s+"help"; ripgrep rejects that without multiline mode, and
search_files has no multiline flag. Prefer separate fixed-string probes for
single-line anchors, or read the bounded JSON/Markdown section and inspect the
adjacent lines directly. Use rg -U only in a shell probe where multiline
matching is intentional and explicitly stated.

CLI tool project configs live under each tool directory. Do not assume `/pyproject.toml` exists. Do not assume `//pyproject.toml` or `/_personal//pyproject.toml` exists either. When inspecting dependencies, pytest configuration, package metadata, or a test runner for a named CLI, discover `//pyproject.toml` from the target tool directory, keep only proven regular files, and read it only after that exact file path exists. If discovery shows no project config exists, report `CONFIG_ABSENT: ` and do not issue `sed`, `cat`, `nl`, `wc`, `head`, or `tail` against the absent path. **Every `list` command MUST support `--filter`/`-f`. No dedicated filter commands.** See `references/filtering.md` for architecture. **Investigate public API, then internal APIs.** If neither provides a usable path and the requested command would require browser automation, stop before scaffolding or implementation and ask Adam: "No usable public or internal API path is available for this action. Should I make this command browser-driven?" Continue only after explicit approval. See `references/templates.md` for type details. When a CLI operation requires browser automation and `auth status` shows the needed `browser_session` profile is unauthenticated, use the browser surface authorized for the current host. Local interactive login uses Computer Use to open a visible terminal and run the CLI's documented Bash login command. An explicitly authorized non-interactive demo host or automation runner may invoke the same command without stdin or `/dev/tty`; `BrowserAutomation` then uses the live authenticated browser state as the completion signal and fails clearly if that state is not authenticated. Use `--credential-type browser_session` for multi-credential CLIs and `--force` for stale or expired sessions. Follow `references/secrets.md` for credentials and MFA. Do not substitute raw Playwright, session-file edits, or another auth path. When a browser-backed CLI hits Cloudflare, Akamai, DataDome, PerimeterX, or a similar bot wall, load and follow the `browser-automation` skill's `cdp-and-stealth.md`, `authentication/cloudflare.md`, and `captcha.md` references before changing code or retrying live commands.

The default cli_tools_shared.BrowserAutomation backend already uses the shared
CDP/browser-harness path, stealth launch flags, a persistent profile, and
navigator.webdriver masking. Do not add per-tool subprocess launches of
Chrome, raw Playwright auth flows, session-file edits, or one-off browser
workarounds. If the shared backend is insufficient, fix or extend the shared
engine instead.

Real system Chrome over CDP is a legitimate avoidance surface for managed
Cloudflare-style challenges because it presents a normal browser fingerprint,
but launching local real Chrome is a local GUI action. If Adam says not to use a
headed browser, not to interrupt him, or the host policy blocks local GUI
control, do not set HEADLESS=false, do not invoke Computer Use, and do not
start or foreground a local Chrome/CDP window. Use only non-interrupting paths:
an already-authenticated saved browser profile, an internal/API or in-page fetch
path using the saved session, an already-running approved CDP endpoint, or a
remote/non-watched browser host. If those paths cannot clear the work, report
the Cloudflare blocker and the exact browser surface that would need approval.

Never solve, click through, refresh around, or pay a service to solve an
interactive CAPTCHA, Turnstile, or human-verification challenge. A transient
managed/JS interstitial may be checked once after a short wait; a persistent
human challenge is a hard stop.

**Any reusable human-supplied secret for a CLI tool belongs in the CLI-tools secret manager.** Follow `references/secrets.md` before asking Adam for CLI credentials or storing new CLI credentials. Do not instruct users or agents to place reusable credentials in any `.env` file. `.env` files are limited to non-secret config and CLI-managed runtime auth state under `~/.local/share/cli-tools/...`. The tool user profile folder is `~/.local/share/cli-tools/`. Non-authentication configuration lives in `~/.local/share/cli-tools//.env`, never in the source tree.

Authentication-related runtime state lives under ~/.local/share/cli-tools/<tool>/authentication_profiles/<profile>/, including the profile .env, tokens, browser session data, auth markers, and auth-tied cache/state; see references/config-standards.md.

Agents must not tell users to manually place reusable credentials in those .env files. Use the CLI-tools secret manager for raw credentials and let the CLI persist only its own runtime state.

Do not clear stale browser profile processes with ad hoc shell filters such as `ps | awk` plus `kill`. Browser profile cleanup must go through the shared `cli_tools_shared.browser.processes.profile_process_pids` helper or an existing CLI/shared cleanup path that uses it. That helper matches the explicit Chrome `--user-data-dir` value, skips zombies, and excludes the current process ancestry so a wrapper command that mentions the profile path cannot kill itself.

If a CLI needs a new cleanup command or repair path, add it at the shared helper
or CLI command layer and cover it with tests. Do not hand-run profile-path PID
filters against ~/.local/share/cli-tools/<tool>/authentication_profiles/....

After any scaffolding script, installer, validation script, formatter, cleanup command, or subagent may have changed a file, reread that exact file before preparing an `apply_patch` hunk for it. Patch against the current on-disk content, not a remembered template or earlier read. Build each hunk from a current on-disk line plus verified surrounding context. If copied failure text, plan prose, or expected converted text is not present as its own current line, do not use it as a standalone patch anchor; choose a verified heading or current line instead. **NEVER write browser parsers from guesswork.** Validate every parser against real DOM snapshots captured via the BrowserAutomation page. See `references/templates.md` for browser CLI workflow. **The documented Typer commands and stdout shape are the contract.** Internal models, helper modules, and dependencies are optional implementation details. Use the smallest clear code that preserves command names, options, exit behavior, auth behavior, and JSON/table output. Plain dict records are acceptable when they directly represent the external output. See `references/model-standards.md` for details. When a deterministic command reaches a boundary that requires AI judgment, return the shared `AIInstruction` model as JSON on stdout using `print_ai_instruction()`. Do not call an LLM from inside the CLI, do not emit plain-text instructions, and do not include required pre-action commands. Optional verification or follow-up commands are allowed only after the AI agent completes the instruction. See `references/output-standards.md`, `references/model-standards.md`, and `references/templates.md`. **Every uv-installed CLI has its own isolated interpreter at `~/.local/share/uv/tools//bin/python3`.** The launcher at `~/.local/bin/` has a shebang that points to it. Running `python3 -c "import _cli.main"` or `python3 - <<'PY'` with ANY other interpreter (system python, Homebrew python, the test venv) will fail with `ModuleNotFoundError` because the CLI's dependencies are installed ONLY in that uv tool venv. Those failures are NOT CLI bugs — they are wrong-interpreter diagnoses.

Rule: For any ad-hoc import/smoke test of a CLI's modules, inspect the live launcher and use the interpreter from its shebang. Do not derive the uv tool path from the command name. This also applies to task-workspace scripts that import CLI packages or internals such as <pkg>_cli.commands; run those scripts with the launcher shebang interpreter instead of ambient python3.

When writing a task-workspace Python script that intentionally imports installed
CLI internals, make the runtime contract visible in the source. Prefer a shebang
that points at /usr/bin/env <cli> only when the CLI itself executes Python
files; otherwise include a header comment naming the launcher-derived interpreter
that must run the script. If Hermes/Pyright reports reportMissingImports for
that intentional CLI-internal import because the editor interpreter is not the
launcher interpreter, suppress only that import line (for example
from <pkg>_cli.client import get_client # pyright: ignore[reportMissingImports]).
Do not add brittle sys.path bootstraps into ad-hoc scripts just to satisfy the
editor; they can make the script run against a different source checkout than the
installed CLI launcher uses.

launcher="$(command -v <cli>)"
if [ ! -f "$launcher" ] || [ ! -x "$launcher" ]; then
  printf 'CLI_LAUNCHER_INVALID: %s\n' "$launcher" >&2
  exit 1
fi
interpreter="$(head -1 "$launcher" | sed 's/^#!//')"
"$interpreter" -c "import <pkg>_cli.main"
"$interpreter" path/to/task_script_that_imports_cli.py

An executable command path is valid only after proving it is both a regular file
and executable (-f plus -x). Directories can satisfy -x and must never be
treated as command launchers.

For heredoc probes, the heredoc target must be the same shebang interpreter;
never write the probe as a bare python3 - <<'PY' from inside a CLI source
directory:

launcher="$(command -v <cli>)"
if [ ! -f "$launcher" ] || [ ! -x "$launcher" ]; then
  printf 'CLI_LAUNCHER_INVALID: %s\n' "$launcher" >&2
  exit 1
fi
interpreter="$(head -1 "$launcher" | sed 's/^#!//')"
"$interpreter" - <<'PY'
import <pkg>_cli.main
PY

The shebang interpreter only proves you are in the installed CLI environment;
it does not prove an internal module path exists. Before importing non-entrypoint
internals such as auth helpers or client factories, verify the source layout or
use importlib.util.find_spec() with the shebang interpreter. Do not assume a
module named <pkg>_cli.auth exists. In scaffolded CLIs, command auth is often
mounted from cli_tools_shared.auth_commands, and client factories commonly
live in <pkg>_cli.client.

When inspecting source-defined mappings, registries, or constants from an
installed CLI module, enumerate the live keys first and index only those exact
keys. Do not normalize table, command, or resource names to guessed lowercase,
snake_case, or singular forms before looking them up.

This interpreter rule is only for ad-hoc imports and direct config probes. Do
not use the installed CLI interpreter to run a tool's pytest suite. The uv tool
venv (from uv tool install) contains runtime dependencies, not test-only
dependencies such as pytest.

Every CLI must declare pytest as a dev dependency so uv sync installs it
into the project .venv. Use the scaffold convention:

[dependency-groups]
dev = [
    "pytest>=7.0.0",
]

Once that group is declared and synced, run the suite through the project venv
interpreter — uv run pytest (from the tool dir) or .venv/bin/python -m pytest. Both resolve the project .venv, where editable cli-tools-shared
imports cleanly. Never run a bare global pytest: when pytest is absent from
the project .venv, uv run pytest silently falls back to the global pipx
pytest, whose interpreter cannot import editable cli-tools-shared, so test
modules fail collection with ModuleNotFoundError: No module named 'cli_tools_shared'.

For focused per-tool tests (and as a fallback for any tool whose dev group is
not yet synced), use the direct pytest command in workflows/test-cli.md, which
injects pytest into the run with --with pytest:

uv run --project <cli-tools-root>/$TOOL_NAME --with pytest python -m pytest <cli-tools-root>/$TOOL_NAME/tests

Do not use a source-checkout uv run --project ... python - heredoc for
Playwright or dependency availability probes. Those probes are installed-runtime
checks, so use the live launcher shebang interpreter and redirect stdout/stderr
to bounded task-workspace files before printing a short summary:

workspace=/path/to/task-workspace
mkdir -p "$workspace"
launcher="$(command -v paypal)"
interpreter="$(head -1 "$launcher" | sed 's/^#!//')"
stdout="$workspace/paypal_playwright_probe.stdout"
stderr="$workspace/paypal_playwright_probe.stderr"
if "$interpreter" - >"$stdout" 2>"$stderr" <<'PY'
import playwright
print("PLAYWRIGHT_AVAILABLE")
PY
then
  status=0
else
  status=$?
fi
printf 'STATUS:%s STDOUT_BYTES:%s STDERR_BYTES:%s\n' "$status" "$(wc -c <"$stdout")" "$(wc -c <"$stderr")"
head -c 500 "$stdout"
printf '\n'
exit "$status"

When probing a CLI source checkout rather than the installed launcher, use that
tool's pyproject.toml environment so editable sources such as
cli-tools-shared resolve, but still keep parent output bounded.

For any tool-scoped Python introspection against source files, run
uv run --project <tool-dir> python ... from the target tool environment. Do
not run system python3 or bare python from inside _personal/<tool> or
another tool source directory. For PayPal source probes and tests, do not print
unbounded uv run output directly to the parent tool result:

workspace=/path/to/task-workspace
mkdir -p "$workspace"
stdout="$workspace/paypal_source_probe.stdout"
stderr="$workspace/paypal_source_probe.stderr"
if uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/paypal python - >"$stdout" 2>"$stderr" <<'PY'
from paypal_cli.config import get_config
print(get_config().env_file_path)
PY
then
  status=0
else
  status=$?
fi
printf 'STATUS:%s STDOUT_BYTES:%s STDERR_BYTES:%s\n' "$status" "$(wc -c <"$stdout")" "$(wc -c <"$stderr")"
head -c 500 "$stdout"
printf '\n'
exit "$status"

Do not run "$interpreter" -m pip ... inside a uv tool venv for package
metadata diagnostics. uv tool environments may not include pip. Use
importlib.metadata from the launcher shebang interpreter instead, and read
editable source data from direct_url.json through the returned package file:

"$interpreter" - <<'PY'
import json
from importlib import metadata

dist = metadata.distribution("<distribution-name>")
print(dist.version)
direct_url = next((f for f in dist.files or [] if str(f).endswith("direct_url.json")), None)
if direct_url is not None:
    print(json.loads(direct_url.read_text())["url"])
PY

For multi-profile CLIs, direct config probes must also pass an explicit profile:

"$interpreter" -c "from <pkg>_cli.config import get_config; print(get_config(profile='<profile>').env_file_path)"

Do not expect per-tool shell variables such as JIRA_PROFILE or a generic CLI_TOOLS_PROFILE environment variable to select the profile for an ad-hoc Python probe. The CLI runtime resolves profiles through explicit --profile handling and internal runtime overrides, not ambient shell env vars.

validate-cli-tool.sh and the pytest test_launcher_shebang_points_to_uv_tool_python check enforce this shebang is correct. If they fail, reinstall with install-cli-tool.sh --force-refresh <name> (a bare uv tool install ... --force --refresh can pick a uv-managed Python instead of the system one, which then fails test_cli_uses_system_python). See references/common-issues.md for details (including the CWD / sys.path wrinkle when editing source).

After creating a CLI tool, you MUST update `/_repo/docs/cli_tools.md`: - Find the CLI tools table - Add the new tool row in alphabetical order - Document: command name and description **The canonical `usage.json` for a service CLI lives in its repo-owned skill folder:** `/_repo/skills/-cli/usage.json`.

Do not look for the command map in the CLI source folder, package folder,
runtime profile folder, or installed uv tool directory. Service-operation agents
must load <cli-tools-root>/_repo/skills/<tool>-cli/SKILL.md and consult the
adjacent usage.json before running that CLI. CLI lifecycle workflows that
change commands or parameters must refresh that same skill-folder usage.json.
Use <cli-tools-root>/_repo/skills/cli-tool/scripts/regenerate-usage-json <tool> to refresh it; do not import cli_test_utils from ad-hoc Python.
When an existing usage.json declares binary, the generator uses that exact
absolute executable path. This is the launcher contract for non-uv tools whose
command differs from the skill name. Without binary, the generator requires
the canonical uv launcher at ~/.local/bin/<tool>. Use --cli-executable only
for a new non-uv map or an isolated test fixture.

When programmatically inspecting one or more `usage.json` files, identify the tool from the parent skill directory, not from the filename; every command map file is named `usage.json`. First run a bounded schema probe that prints the root type, root keys, and `commands` keys; for deeper command paths, print only the current node type and keys before indexing. When a `jq` key is not a valid identifier, including keys with hyphens such as `replace-section`, use bracket notation such as `.commands.pages.commands.content.commands["replace-section"]` instead of dot notation. When filtering `to_entries` rows against a side array or lookup map, bind the current row before piping into the side data; jq changes `.` to the side value inside that pipeline. Use `map(. as $entry | select($actions | index($entry.key)))`, not `map(select($actions | index(.key)))`, because the latter evaluates `.key` against `$actions`. Avoid full-map dumps, recursive walks, interactive extractors, or probes that can block or emit excessive output. Before dereferencing a nested command path, inspect the available keys at the current level and fail clearly with `MISSING_JSON_PATH: commands..` when the path is absent. Generated command groups nest children under a `commands` object at each level, so a subcommand path alternates group name and `commands`: use `.commands.gmail.commands.labels.commands.add`, not `.commands.gmail.labels.add` or `.commands.gmail.labels.commands.add`, after the parent keys have been inspected. Do not assume command groups, subcommands, or fields such as `name` exist from memory or from another tool's map. In Python helpers, validate the container before membership checks: do not run `if key in node` until `node` has been proven to be a dict. A missing child is a contract error, not `None`; fail with the full alternating command path and available keys, for example `MISSING_JSON_PATH: commands.items.commands.search available=[create,get,list,password,username]`.

Usage nodes can omit documentation-only fields such as examples even when
they include options. Before reading node["examples"], inspect the node's
keys and treat examples as optional with node.get("examples", []); reserve
MISSING_JSON_PATH failures for fields required by the usage contract.
Usage node metadata fields can also use different containers across commands:
options may be a list of option records, not an object. Before calling object
methods such as .keys() or iterating entries, inspect the field type and
handle the actual container; fail clearly with JSON_CONTRACT_MISMATCH: commands.<path>.options expected object/list got <type> for unsupported
shapes.

**ALL test-cli-tool.sh failures MUST be fixed. No exceptions.**
  • Do not skip, ignore, or rationalize any test failure
  • Fix every [FAIL] in the test output before marking work complete
  • Re-run tests until output shows all tests passed
  • Warnings are acceptable - only failures block completion
  • No special cases - wrapper CLIs, passthrough patterns, and all other CLI types must pass all tests

Required command options for compliance:

  • list commands: --table/-t, --limit/-l, --filter/-f, --properties/-p
  • get commands: --table/-t
  • auth login: --force/-F (when auth commands are present)
  • auth status: Must output JSON in the per-profile, per-credential-type shape emitted by create_auth_app:
    {"profiles":[{"name","auth_type","active","authenticated","credential_types":{"<type>":{"credentials_saved","authenticated",...}}}]}.
    Use the shared cli_tools_shared.auth_commands.create_auth_app — no custom auth_status function unless the CLI is genuinely exceptional (see references/auth-standards.md).
  • README: Must document ALL commands with examples

Note: Auth commands are optional for some CLI types (wrapper, browser, --auth-type none). If a CLI has no auth subcommand, auth tests are automatically skipped.

</essential_principles>

Route to appropriate workflow based on user intent. If intent is clear from the user's message (e.g., "create a CLI for Stripe", "test the notion CLI"), route directly without asking.

If ambiguous, ask:

  1. Create a new CLI tool
  2. Test an existing CLI tool
  3. Update an existing CLI tool
  4. Remove a CLI tool
  5. List all CLI tools
| Response | Workflow | Description | |----------|----------|-------------| | service operation, "run", "execute", " cli", existing CLI command | workflows/skill-router.md | Select and load the repo-owned service skill | | 1, "create", "new", "build", "make" | workflows/create-cli.md | Full creation workflow | | 2, "test", "validate", "check" | workflows/test-cli.md | Run test suite and AI review | | 3, "update", "modify", "change", "add command", "fix" | workflows/update-cli.md | Modify existing CLI | | 4, "remove", "delete", "uninstall" | workflows/remove-cli.md | Remove CLI tool | | 5, "list", "show all", "which CLIs" | workflows/list-cli.md | List all CLI tools | | other | Clarify intent, then route |

Intent-based routing (if user provides clear context):

  • "create a CLI for Stripe" → workflows/create-cli.md
  • "test the notion CLI" → workflows/test-cli.md
  • "add a new command to airtable CLI" → workflows/update-cli.md
  • "remove the podio CLI" → workflows/remove-cli.md
  • "list all my CLIs" → workflows/list-cli.md

After reading the workflow, follow it exactly.

All domain knowledge in `references/`:

Output: output-standards.md (streams, format rules, truncation, field selection, list requirements)
Secrets: secrets.md (CLI-tools secret manager, naming, storage/retrieval rules, prohibited stores)
Auth: auth-standards.md (credential types, multi-auth, OAuth, force flag, wrapper auth, browser-session Computer Use login)
Config: config-standards.md (.env, path resolution, token refresh)
Commands: command-standards.md (naming, options, CRUD patterns, list/get, search, exit codes)
Data Shapes: model-standards.md (when to use local models, plain records, AIInstruction, SerializeAsAny)
Infrastructure: infra-standards.md (AI review, logging, repos, docs, symlinks)
Templates: templates.md (API, browser, wrapper structures, lean architecture, file responsibilities)
Filtering: filtering.md (filters.py, filter_map.py architecture)
Caching: caching.md (@cached decorator, cache commands, env config, serialization, integration steps)
Troubleshooting: common-issues.md (quick fixes for common problems)
Wrapper Collisions: wrapper-name-collisions.md (diagnosing a cli-tools wrapper that shares an executable name with an upstream/vendor CLI; gemini/kick/n8n worked examples)
Hermes CLI Provider: hermes-local-cli-provider.md (running a local coding CLI as a Hermes provider without leaking OAuth credentials into Hermes; safe configuration sequence; deferred gateway restart)
Agent Output Contract: output_contract.md (final-response contract for agents whose primary domain skill is cli-tool)
</reference_index>

| Workflow | Purpose | |----------|---------| | skill-router.md | Route an existing CLI/service operation to its repo-owned service skill | | create-cli.md | Create a new CLI tool from scratch | | test-cli.md | Run compliance tests and AI review | | update-cli.md | Modify an existing CLI tool | | remove-cli.md | Remove a CLI tool and all artifacts | | list-cli.md | List all CLI tools with status | </workflows_index> **CLI Types:** - `api` - REST API clients - `browser` - Web automation using `BrowserAutomation`, named sessions, and `BaseConfig` - `wrapper` - Wraps existing CLI tools through subprocess calls

Auth Types (for --auth-type flag on new-cli-tool, repeatable for multiple types):

  • none - No auth (skips auth command scaffolding entirely)
  • api_key - API key auth (env var: API_KEY) [default for api type]
  • personal_access_token - PAT auth (env var: PERSONAL_ACCESS_TOKEN)
  • oauth - OAuth 2.0 (env vars: CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN)
  • oauth_authorization_code - OAuth auth code flow (env vars: CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, REDIRECT_URI)
  • username_password - Basic auth (env vars: USERNAME, PASSWORD)
  • browser_session - Browser session (no env vars; stores session in profile) [default for browser type]

Data Shape Structure (Optional):

models.py or models/
└── only when validation, polymorphism, or serialization logic earns the code

Omit local models when parsed/API records can flow straight to the documented output. If a command group only needs credential metadata because the command logic is centralized in main.py, keep commands/<group>.py to a module docstring plus one COMMAND_CREDENTIALS assignment.

Script Locations:

  • Scaffolding: <cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool
  • CLI Tools: <cli-tools-root>/
  • Validate: <cli-tools-root>/_repo/skills/cli-tool/scripts/validate-cli-tool.sh
  • Install: <cli-tools-root>/_repo/skills/cli-tool/scripts/install-cli-tool.sh
  • Test: <cli-tools-root>/_repo/skills/cli-tool/scripts/test-cli-tool.sh
  • Remove: <cli-tools-root>/_repo/skills/cli-tool/scripts/remove-cli-tool.sh
  • List: <cli-tools-root>/_repo/skills/cli-tool/scripts/list-cli-tool.sh

Template Locations:

  • Templates: <cli-tools-root>/_repo/skills/cli-tool/templates/ (api/, browser/, wrapper/)
  • README Template: <cli-tools-root>/_repo/skills/cli-tool/templates/README_TEMPLATE.md

Common Commands:

<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name myservice --type api --base-url https://api.example.com
<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name internal-tool --type api --base-url https://api.example.com --personal
<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name myservice --type api --base-url https://api.example.com --auth-type personal_access_token
<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name mysite --type browser --base-url https://mysite.com
<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name mysite --type browser --base-url https://mysite.com --auth-type oauth --auth-type browser_session
<cli-tools-root>/_repo/skills/cli-tool/scripts/new-cli-tool --name myservice --type wrapper --cli-command original-cmd
<cli-tools-root>/_repo/skills/cli-tool/scripts/test-cli-tool.sh --cli-name myservice --verbose
</quick_reference> **Context:** Use this when creating or updating Python CLI tool `pyproject.toml` dependencies. **Key Facts:** Current Typer releases no longer expose the legacy `all` extra, so CLI tools should depend on `typer>=0.9.0` instead of `typer>=0.9.0`. The stale extra produces `uv` warnings during `uv run` and `uv tool install`, even though installation continues. **Gotchas:** If the warning appears while installing one CLI, inspect both that CLI and `cli-tools-shared`; transitive package metadata can be the source. After changing shared dependency metadata, refresh the dependent CLI install with `install-cli-tool.sh --force-refresh ` so it resolves the new `cli-tools-shared` commit. **Context:** Use when assessing or reporting disk usage of `uv tool` venvs (e.g. "N venvs, duplicated deps, X GB"). **Key Facts:** On macOS, uv installs tools into `~/.local/share/uv/tools/` using APFS copy-on-write clones (`--link-mode clone` is the default), not hardlinks or copies. Clone copies share physical blocks with the uv cache and with each other; `du` and per-tool sums count every clone at full size and overstate physical usage (measured ~2.6x on this fleet). `uv cache clean` frees only cache bytes not referenced by surviving venvs; deleting one of several same-content venvs frees ~nothing, deleting the last frees the content. Clones inherit identical mtime/birthtime from their cache source, so grouping duplicate files by timestamp pairs yields lineage counts = physical copy counts (calibrated forensics method). **Gotchas:** Never report a `du` total as physical disk usage for uv tool venvs; do not expect `uv cache clean` to reclaim space while venvs hold references; uninstalling a tool reclaims only bytes exclusive to it. </domain_knowledge> A successful skill invocation: - Routes to the correct workflow based on user intent - Loads appropriate references when needed - Follows workflow steps exactly - Preserves the documented command/output contract with the smallest clear implementation - Removes unused local helpers, local models, and direct dependencies - Meets workflow-specific success criteria - Updates cli_tools.md for new CLI tools - Runs `_repo/skills/cli-tool/scripts/create-cli-tool-skill ` after successful CLI creation - **⛔ Passes test-cli-tool.sh with ZERO FAILURES** (warnings acceptable)

BLOCKING: Do NOT mark any CLI work as complete if test-cli-tool.sh shows failures.
</success_criteria>