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.
Resources
8Install
npx skillscat add adbertram/cli-tools/cli-tool Install via the SkillsCat registry.
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>
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>
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>
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 status2 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"
fiA 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.
Do not chain exploratory searches to dependent file reads with &&, such asrg -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.
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, runmkdir -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"
fiFor 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 CLIlist 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 orJSON_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.
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, andsearch_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.
The default cli_tools_shared.BrowserAutomation backend already uses the shared
CDP/browser-harness path, stealth launch flags, a persistent profile, andnavigator.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.
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.
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/....
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 examplefrom <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.pyAn 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
PYThe 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/testsDo 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 ascli-tools-shared resolve, but still keep parent output bounded.
For any tool-scoped Python introspection against source files, runuv 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. Useimportlib.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"])
PYFor 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).
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.
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", []); reserveMISSING_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.
- 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:
listcommands:--table/-t,--limit/-l,--filter/-f,--properties/-pgetcommands:--table/-tauth login:--force/-F(when auth commands are present)auth status: Must output JSON in the per-profile, per-credential-type shape emitted bycreate_auth_app:{"profiles":[{"name","auth_type","active","authenticated","credential_types":{"<type>":{"credentials_saved","authenticated",...}}}]}.
Use the sharedcli_tools_shared.auth_commands.create_auth_app— no customauth_statusfunction unless the CLI is genuinely exceptional (seereferences/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>
If ambiguous, ask:
- Create a new CLI tool
- Test an existing CLI tool
- Update an existing CLI tool
- Remove a CLI tool
- List all CLI tools
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.
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>
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 codeOmit 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>