API CLI Mastery
Six levels from curl | jq '.' to concurrent httpx audits and cross-endpoint pipeline analysis. Every example runs against domus-api (localhost:8080).
Level 1: curl + jq Foundations
HTTP Verbs and Response Inspection
# GET with status code — the -w flag writes metadata after the response
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/
# Pretty print any JSON endpoint
curl -s localhost:8080/stats | jq '.'
# Extract a single scalar
curl -s localhost:8080/stats | jq '.total_pages'
# Extract nested fields
curl -s localhost:8080/ | jq '.counts.standards'
# Array length — how many standards exist
curl -s localhost:8080/standards | jq '.standards | length'
# Select specific fields from array objects
curl -s localhost:8080/standards | jq '.standards[] | {id, title, status}'
# Raw output for piping — -r strips the quotes
curl -s localhost:8080/standards | jq -r '.standards[].id'
Why -s and -r Matter
-s (silent): suppresses curl’s progress meter. Without it, stderr pollutes your pipes.
-r (raw): strips JSON string quotes. "STD-001" becomes STD-001. Essential when feeding jq output into awk/sort/uniq.
Level 2: Filtering and Transformation
select(), map(), sort_by(), group_by()
# Filter: only Active standards
curl -s localhost:8080/standards | jq '[.standards[] | select(.status == "Active")]'
# Invert filter: standards NOT Active (needs review)
curl -s localhost:8080/standards | jq '[.standards[] | select(.status != "Active")]'
# Tab-separated output for terminal consumption
curl -s localhost:8080/standards | jq -r '.standards[] | "\(.id)\t\(.status)\t\(.title)"'
# Sort by field
curl -s localhost:8080/standards | jq '.standards | sort_by(.id)'
# Reverse sort
curl -s localhost:8080/standards | jq '.standards | sort_by(.id) | reverse'
# Count by status using group_by
curl -s localhost:8080/standards | \
jq '[.standards[].status] | group_by(.) | map({status: .[0], count: length})'
# @tsv for clean tab-separated output (use with column -t)
curl -s localhost:8080/standards | \
jq -r '.standards[] | [.id, .status, .title] | @tsv' | column -t
# @csv for spreadsheet export
curl -s localhost:8080/standards | \
jq -r '.standards[] | [.id, .status, .domain, .title] | @csv'
Level 3: Pipeline Composition — curl | jq | awk | sort | uniq
The Unix philosophy: each tool does one thing. jq extracts, awk reshapes, sort orders, uniq counts.
Cross-Endpoint Analysis
# Category distribution — which categories have the most pages
curl -s localhost:8080/stats | \
jq -r '.categories | to_entries[] | "\(.value)\t\(.key)"' | sort -rn
# Top 5 education subcategories
curl -s localhost:8080/education | \
jq -r '.subcategories | to_entries[] | "\(.value)\t\(.key)"' | sort -rn | head -5
# Search results by category — extract paths, awk splits on /, count
curl -s 'localhost:8080/search?q=security&limit=50' | \
jq -r '.results[].path' | awk -F/ '{print $1}' | sort | uniq -c | sort -rn
# Codex categories with fewer than 5 entries (knowledge gaps)
curl -s localhost:8080/codex | \
jq -r '.categories | to_entries[] | select(.value < 5) | "\(.key): \(.value)"'
Process Substitution — diff Two API Responses
# Compare standard IDs from /standards vs /pages?category=standards
diff <(curl -s localhost:8080/standards | jq -r '.standards[].id' | sort) \
<(curl -s 'localhost:8080/pages?category=standards' | \
jq -r '.pages[].path' | awk -F/ '{print $NF}' | sort)
# Before/after comparison — save state, make a change, compare
curl -s localhost:8080/projects | jq '.projects[].path' | sort > /tmp/before.json
# ... create a project ...
curl -s localhost:8080/projects | jq '.projects[].path' | sort > /tmp/after.json
diff /tmp/before.json /tmp/after.json
The awk Advantage
# Why awk over cut: awk handles variable whitespace and complex extraction
curl -s localhost:8080/standards | \
jq -r '.standards[] | "\(.id) \(.status) \(.title)"' | \
awk '$2 != "Active" {printf "%-10s %-12s %s\n", $1, $2, substr($0, index($0,$3))}'
# Backlink analysis with awk aggregation
curl -s localhost:8080/graph/references/standards/operations/change-control | \
jq -r '.referenced_by[] | "\(.category)\t\(.title)"' | \
awk -F'\t' '{cats[$1]++; titles[$1] = titles[$1] ? titles[$1] ", " $2 : $2} END {for (c in cats) printf "%s (%d): %s\n", c, cats[c], titles[c]}'
Level 4: Advanced jq — reduce, env, functions, recursive descent
reduce for Aggregation
# Total files across all example categories
curl -s localhost:8080/examples | \
jq '.categories | to_entries | reduce .[] as $item (0; . + $item.value)'
# Running total with intermediate output
curl -s localhost:8080/stats | \
jq '.categories | to_entries | reduce .[] as $item (
{total: 0, breakdown: []};
.total += $item.value |
.breakdown += [{category: $item.key, count: $item.value, running: .total}]
)'
Custom Functions with def
# Define a formatter and reuse it
curl -s localhost:8080/codex | \
jq 'def fmt: "\(.value) entries"; .categories | to_entries[] | "\(.key): \(.value | fmt)"'
# Conditional formatting with if-then-else
curl -s localhost:8080/standards | \
jq -r '.standards[] | "\(.id) [\(if .status == "Active" then "OK" else "REVIEW" end)] \(.title)"'
Environment Variables with --arg and $ENV
# Parameterize queries — pass shell variables into jq
QUERY="mandiant"
curl -s "localhost:8080/search?q=${QUERY}" | \
jq --arg q "$QUERY" '{query: $q, hits: .total, top_3: [.results[:3][] | .title]}'
# Pass numeric arguments
curl -s localhost:8080/codex | \
jq --argjson min 5 '.categories | to_entries[] | select(.value >= $min)'
# Access environment directly (jq 1.7+)
export API="localhost:8080"
jq -n 'env.API'
Recursive Descent (..)
# Find every "title" field anywhere in a nested response
curl -s localhost:8080/case-studies | jq '[.. | .title? // empty] | unique'
# Find all string values matching a pattern
curl -s localhost:8080/ | jq '[.. | strings | select(test("domus"))]'
# Get all paths to numeric values
curl -s localhost:8080/stats | jq '[path(.. | numbers)] | map(join("."))'
Level 5: httpx — Python Async HTTP Client
Basic Patterns
import asyncio
import httpx
BASE = "http://localhost:8080"
async def main():
async with httpx.AsyncClient(base_url=BASE) as client:
# Single request
r = await client.get("/stats")
print(f"Total pages: {r.json()['total_pages']}")
# Concurrent requests — asyncio.gather fires all at once
endpoints = ["/standards", "/projects", "/patterns", "/codex"]
tasks = [client.get(ep) for ep in endpoints]
responses = await asyncio.gather(*tasks)
for ep, resp in zip(endpoints, responses):
data = resp.json()
count = data.get("count", data.get("total", data.get("total_entries", "?")))
print(f"{ep}: {count}")
# POST with JSON body
incident = await client.post("/case-studies/incidents", json={
"severity": "P4",
"description": "httpx test incident",
})
print(f"Created: {incident.json()['id']}")
asyncio.run(main())
Concurrent Audit — Hit Every Endpoint
import asyncio
import httpx
ENDPOINTS = [
"/", "/stats", "/attributes", "/standards",
"/pages", "/search?q=security", "/projects",
"/case-studies", "/skills", "/worklogs",
"/patterns", "/codex", "/sessions",
"/education", "/portfolio", "/reference",
"/templates", "/api-hub", "/objectives",
"/discoveries", "/runbooks", "/meta",
"/drafts", "/trackers", "/partials", "/examples",
]
async def audit():
async with httpx.AsyncClient(base_url="http://localhost:8080", timeout=10.0) as client:
tasks = [client.get(ep) for ep in ENDPOINTS]
results = await asyncio.gather(*tasks, return_exceptions=True)
for ep, result in zip(ENDPOINTS, results):
if isinstance(result, Exception):
print(f"FAIL {ep}: {result}")
else:
status = "OK" if result.status_code == 200 else f"HTTP {result.status_code}"
print(f"{status:>8} {ep}")
asyncio.run(audit())
Level 6: Real-World Scenarios
Documentation Audit
# Find standards needing review
curl -s localhost:8080/standards | \
jq -r '.standards[] | select(.status != "Active") | "\(.id): \(.status) — \(.title)"'
# Knowledge gap analysis — codex categories with sparse coverage
curl -s localhost:8080/codex | \
jq -r '.categories | to_entries[] | select(.value < 5) | "\(.key): \(.value) entries"'
# Orphan detection — pages with no backlinks
# (requires checking each page against /graph/references)
Create-Then-Query Chain
# Create incident, then find related documentation
curl -s -X POST localhost:8080/case-studies/incidents \
-H 'Content-Type: application/json' \
-d '{"severity":"P3","description":"DNS resolution timeout on internal resolvers"}' | \
jq -r '.id' | xargs -I{} sh -c \
'echo "Created: {}"; curl -s "localhost:8080/search?q=dns&limit=5" | jq -r ".results[] | .title"'
Generate a Status Report from API Data
{
echo "# domus-api Status Report — $(date +%Y-%m-%d)"
echo ""
echo "## Counts"
curl -s localhost:8080/stats | \
jq -r '"- Pages: \(.total_pages)\n- Partials: \(.total_partials)\n- Standards: \(.standards_count)\n- Projects: \(.projects_count)"'
echo ""
echo "## Standards Needing Review"
curl -s localhost:8080/standards | \
jq -r '.standards[] | select(.status != "Active") | "- \(.id): \(.title) (\(.status))"'
echo ""
echo "## Top 5 Content Categories"
curl -s localhost:8080/stats | \
jq -r '.categories | to_entries | sort_by(-.value)[:5][] | "- \(.key): \(.value) pages"'
} > /tmp/status-report.md
API Diff Over Time
# Snapshot current state
curl -s localhost:8080/stats | jq -S '.' > /tmp/stats-$(date +%Y%m%d).json
# Compare with previous snapshot
diff <(jq -S '.' /tmp/stats-20260406.json) <(jq -S '.' /tmp/stats-20260407.json)
# Structural diff — what keys changed
diff <(jq 'paths | join(".")' /tmp/stats-20260406.json | sort) \
<(jq 'paths | join(".")' /tmp/stats-20260407.json | sort)
Batch Operations with xargs
# Get all pattern domains, then fetch each one
curl -s localhost:8080/patterns | \
jq -r '.taxonomy | to_entries[].value.domains | keys[]' | sort -u | \
xargs -I{} sh -c 'echo "=== {}"; curl -s "localhost:8080/patterns/{}" | jq ".count"'
# Parallel: fetch all codex categories concurrently (-P4 = 4 at a time)
curl -s localhost:8080/codex | \
jq -r '.categories | keys[]' | \
xargs -P4 -I{} sh -c 'curl -s "localhost:8080/codex/{}" | jq -r "\"{}: \" + (.count | tostring) + \" entries\""'