Skill: duncandobbins/med-info (version 0.2.3)

--- name: med-info description: Label-backed medication answers with citations and traceable IDs. RxCUI/NDC/set_id, key label sections, optional recalls/shortages/FAERS/interactions. metadata: {"clawdbot": {"emoji": "💊", "os": ["darwin", "linux"], "requires": {"bins": ["python3"]}}} --- # med-info **Medication answers you can cite.** `med-info` turns a drug name (or RxCUI, NDC, SPL set_id) into a **label-backed** summary with **traceable identifiers** and **source links**. Use it when you want “show your work” medication information for notes, training, QA, internal docs, or agent workflows. Not medical advice. ## What you get - **Authoritative sources first**: FDA labeling via openFDA + DailyMed, identifiers via RxNorm/RxClass. - **Citations + traceability**: RxCUI, NDC (product/package), SPL set_id, effective dates, and URLs. - **The sections you actually need**: boxed warning, indications, dosing, contraindications, warnings, interactions, adverse reactions. - **Optional safety context** (opt-in flags): recalls, shortages, FAERS aggregates, interactions, drug class, hazardous drug flag, REMS linkouts, Orange Book, Purple Book. - **Automation friendly**: `--json` output for pipelines. ## Privacy Do not include PHI. Query by drug name or identifiers only. ## Quickstart ```bash cd {baseDir} python3 scripts/med_info.py "Eliquis" --brief ``` Common workflows: ```bash # Only the sections you care about python3 scripts/med_info.py "Eliquis" --sections contraindications,drug_interactions --brief # Find keyword hits in label text (fast way to answer "does the label mention X?") python3 scripts/med_info.py "Eliquis" --find ritonavir --find CYP3A4 --find P-gp --find-max 8 # Deterministic lookups by identifier (best for reproducibility) python3 scripts/med_info.py "70518-4370-0" # NDC (package) python3 scripts/med_info.py "70518-4370" # NDC (product) python3 scripts/med_info.py [REDACTED_SECRET] # SPL set_id ``` ## Disambiguation (when there are multiple labels) ```bash python3 scripts/med_info.py "metformin" --candidates python3 scripts/med_info.py "metformin" --candidates --pick 2 --brief python3 scripts/med_info.py "metformin" --set-id [REDACTED_SECRET] ``` ## Optional add-ons ```bash # Pharmacist-friendly output bundle python3 scripts/med_info.py "Eliquis" --pharmacist --brief # Safety signals and operational context (opt-in) python3 scripts/med_info.py "metformin" --recalls --brief python3 scripts/med_info.py "amphetamine" --shortages --brief python3 scripts/med_info.py "Eliquis" --faers --faers-max 10 python3 scripts/med_info.py "Eliquis" --interactions --interactions-max 20 python3 scripts/med_info.py "Eliquis" --rxclass python3 scripts/med_info.py "cyclophosphamide" --hazardous python3 scripts/med_info.py "isotretinoin" --rems # Reference datasets python3 scripts/med_info.py "adalimumab" --purplebook python3 scripts/med_info.py "metformin" --orangebook # Chemistry (best-effort) python3 scripts/med_info.py "ibuprofen" --chem ``` ## Output shaping ```bash python3 scripts/med_info.py "ibuprofen" --json python3 scripts/med_info.py "Eliquis" --brief --sections all python3 scripts/med_info.py "Eliquis" --print-url --brief # prints queried URLs (api_key redacted) ``` ## Sources (high level) - openFDA: drug labels, NDC directory, recalls/enforcement, shortages, FAERS - RxNorm / RxClass (RxNav): normalization and drug classes - DailyMed: SPL label history and media - MedlinePlus Connect: patient-friendly summaries (links) - Orange Book and Purple Book: best-effort context ## Safety notes - For clinical decisions, verify against the **full official label**. - Input is treated as untrusted, openFDA `search` strings are escaped to prevent query injection. ## Keys and rate limits Works without any keys. Optionally: - `OPENFDA_API_KEY`: increases openFDA rate limits for heavy usage.

path: docs/PROBE_RESULTS.md
content: # Resource probe results

This file is generated from the automated probe harness:

```bash
python3 scripts/probe_resources.py --out artifacts/resource_probe.json
```

See `artifacts/resource_probe.json` for the latest full JSON output.


path: docs/README.md
content: # med-info docs

- `RESOURCES.md`: Catalog of public/open sources that can be integrated into med-info (and what each adds).
- `PROBE_RESULTS.md`: Summary of the latest automated probe run (see artifacts for JSON).


path: docs/RESOURCES.md
content: # Public and open resources for making `med-info` better

This catalog lists public APIs, open datasets, and authoritative documentation sources that can extend `med-info`.

- Goal: 100+ sources.
- Each entry should include: name, URL, what it adds, access method, license/terms note, and a suggested smoke-test URL.

(Generated/maintained by Jarvis. See also: `scripts/resources_manifest.json` and `scripts/probe_resources.py`.)


path: findings.md
content: # Findings: med-info growth pass

## Current published state (ClawHub)
- Skill page: https://clawhub.ai/DuncanDobbins/med-info
- VirusTotal flagged “Suspicious” previously due to openFDA query injection concern, now mitigated by escaping/validating openFDA search strings in v0.2.1.

## Known data model detail (openFDA drug/ndc)
- `product_ndc` is top-level.
- `package_ndc` is nested under `packaging[].package_ndc`.

## planning-with-files skill note
- The installed planning-with-files skill does not include `session-catchup.py` even though SKILL.md references it.

## Design intent for new features
- Disambiguation must provide `set_id` and DailyMed link so a user can force an exact label.
- Recalls and shortages should be opt-in and return a compact list with dates and reasons.


path: progress.md
content: # Progress log: med-info upgrades

## 2026-02-11/12
- Created planning files.
- Baseline: v0.1.1 already fixes NDC lookup correctness and prevents RxNorm fallback for NDC inputs.
- Implemented upgrades for v0.2.0:
  - Disambiguation: --candidates/--pick/--set-id
  - Recalls: --recalls (openFDA /drug/enforcement)
  - Shortages: --shortages (openFDA /drug/shortages)
  - Output shaping: --sections, --brief, --print-url
  - Debug URL log (api_key redacted)
- Tests:
  - metformin --candidates (prints 10 candidates, includes combo products)
  - metformin --pick 2 --brief --sections indications_and_usage,dosage_and_administration
  - set_id direct lookup: 05999192-ebc6-4198-bd1e-f46abbfb4f8a
  - metformin --recalls (returns multiple recalls)
  - amphetamine --shortages (returns current shortages)
  - Eliquis --sections contraindications,drug_interactions --brief --print-url


path: scripts/probe_resources.py
content: #!/usr/bin/env python3
"""Probe public/open resources listed in resources_manifest.json.

This is a lightweight smoke test that the URLs are reachable and return the expected content type.
It does NOT validate semantic correctness of the data.

Usage:
  python3 scripts/probe_resources.py --out artifacts/resource_probe.json

Exit codes:
  0: all ok (or only skipped)
  1: one or more hard failures
"""

from __future__ import annotations

import argparse
import json
import os
import time
import urllib.request
import urllib.error
from pathlib import Path
from typing import Any, Dict, List, Optional


def load_manifest(path: Path) -> List[Dict[str, Any]]:
    j = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(j, list):
        raise SystemExit("manifest must be a JSON list")
    return j


def fetch(url: str, timeout_s: int = 15) -> Dict[str, Any]:
    req = urllib.request.Request(url)
    req.add_header("User-Agent", "clawdbot-med-info-probe/0.1")
    t0 = time.time()
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            data = resp.read(4096)
            ct = resp.headers.get("content-type")
            return {
                "ok": True,
                "status": getattr(resp, "status", 200),
                "content_type": ct,
                "ms": int((time.time() - t0) * 1000),
                "sample": data.decode("utf-8", errors="replace")[:200],
            }
    except urllib.error.HTTPError as e:
        body = ""
        try:
            body = e.read(4096).decode("utf-8", errors="replace")
        except Exception:
            pass
        return {
            "ok": False,
            "status": e.code,
            "content_type": e.headers.get("content-type"),
            "ms": int((time.time() - t0) * 1000),
            "error": f"HTTP {e.code}",
            "sample": body[:200],
        }
    except Exception as e:
        return {
            "ok": False,
            "status": None,
            "content_type": None,
            "ms": int((time.time() - t0) * 1000),
            "error": str(e),
        }


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument(
        "--manifest",
        default=str(Path(__file__).resolve().parent / "resources_manifest.json"),
        help="Path to resources manifest JSON (default: scripts/resources_manifest.json)",
    )
    ap.add_argument("--out", default="", help="Write full JSON report to this file")
    ap.add_argument("--timeout", type=int, default=15, help="Per-request timeout seconds")
    ap.add_argument("--limit", type=int, default=0, help="Only probe first N entries")
    args = ap.parse_args()

    manifest_path = Path(args.manifest)
    items = load_manifest(manifest_path)
    if args.limit and args.limit > 0:
        items = items[: int(args.limit)]

    report: Dict[str, Any] = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "manifest": str(manifest_path),
        "count": len(items),
        "results": [],
    }

    hard_fail = 0
    for it in items:
        name = it.get("name")
        test_url = it.get("test_url") or it.get("url")
        requires_key = bool(it.get("requires_key"))

        if requires_key and not os.environ.get(it.get("env_key", "") or ""):
            report["results"].append({"name": name, "skipped": True, "reason": "requires_key"})
            continue

        r = fetch(str(test_url), timeout_s=int(args.timeout))
        r["name"] = name
        r["test_url"] = test_url
        report["results"].append(r)

        if not r.get("ok"):
            hard_fail += 1

    if args.out:
        outp = Path(args.out)
        outp.parent.mkdir(parents=True, exist_ok=True)
        outp.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    # Minimal console summary.
    ok = sum(1 for r in report["results"] if r.get("ok"))
    skipped = sum(1 for r in report["results"] if r.get("skipped"))
    fail = sum(1 for r in report["results"] if (not r.get("ok") and not r.get("skipped")))
    print(f"Probed: {len(report['results'])} | ok={ok} skipped={skipped} fail={fail}")

    return 0 if fail == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/resources_manifest.json
content: id: openfda_drug_label
category: US FDA
name: openFDA drug/label
url: https://open.fda.gov/apis/drug/label/
test_url: https://api.fda.gov/drug/label.json?search=openfda.brand_name:ELIQUIS&limit=1
license: US Government public data (see open.fda.gov terms)
adds: Authoritative label sections + identifiers (set_id, product_ndc, rxcui).

id: rxnav_rxnorm
category: NIH/NLM
name: RxNav RxNorm API
url: https://lhncbc.nlm.nih.gov/RxNav/APIs/
test_url: https://rxnav.nlm.nih.gov/REST/rxcui/198440/properties.json
license: Public domain (US Gov)
adds: Normalize names to RxCUI, get properties, spelling, interactions.

id: pubchem_pugrest
category: NIH/NLM
name: PubChem PUG REST
url: https://pubchem.ncbi.nlm.nih.gov/docs/pug-rest
test_url: https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/ibuprofen/property/MolecularFormula,MolecularWeight/JSON
license: Public domain (US Gov)
adds: Chemical identity and properties for ingredients.

path: task_plan.md
content: # Task Plan: med-info v0.3.0 growth pass (resources + features + marketing)

## Goal
1) Improve the ClawHub consumer-facing page (SKILL.md copy, examples).
2) Research 100+ public/open resources that can make `med-info` better, document them, and run a smoke-test probe.
3) Implement a small set of high-value new integrations, run checks, then publish a new version.

## Current Phase
Phase 4b (Pharmacist workflow upgrades)

## Phases

### Phase 1: Page review + requirements
- [ ] Review current ClawHub page (what is shown from SKILL.md)
- [ ] Decide what “enticing but accurate” means, tighten copy and examples
- **Status:** in_progress

### Phase 2: Research 100+ resources
- [ ] Build a catalog of 100+ open/public sources (APIs, datasets, docs) relevant to medication info
- [ ] For each: what it adds, access method, and a suggested smoke-test
- **Status:** not_started

### Phase 3: Smoke-test/probe harness
- [ ] Create a resources manifest (JSON/YAML)
- [ ] Implement a probe script that can run quickly and record results
- [ ] Save probe report artifact(s)
- **Status:** not_started

### Phase 4: Implement high-value features
- [x] RxNav interactions integration (`--interactions`)
- [x] PubChem chemical profile (`--chem`)
- [ ] (Optional if time) Drugs@FDA approvals via openFDA drugsfda (`--approvals`)
- **Status:** complete

### Phase 4b: Pharmacist workflow upgrades (2026-02-13)
- [ ] Add pharmacist output profile (curated SPL section bundle)
- [ ] Strengthen NDC mode (NDC-11 normalization, packaging details, marketing status)
- [ ] Add safety flags block (boxed warning present, schedule guess)
- [ ] Add NIOSH hazardous list flag (best-effort, cached; uses NIOSH 2024 list PDF)
- [ ] Add FDA REMS best-effort lookup/linking (graceful degrade if blocked)
- [ ] Update SKILL.md examples + docs + resources manifest
- [ ] Run smoke tests (py_compile + a few real queries)
- **Status:** in_progress

### Phase 5: Checks + publish
- [ ] Run lint/compile checks and a couple real-world queries
- [ ] Bump version and publish to ClawHub
- [ ] Verify listing, rerun VirusTotal scan later if needed
- **Status:** not_started

## Key Questions
1. What is the most useful candidate summary for `--candidates` (set_id, effective_time, product, ingredients, route, dosage_form)?
2. What search strategy for enforcement/shortages is reliable (by brand/generic, by product_ndc, by openfda fields)?

## Decisions Made
| Decision | Rationale |
|---|---|
| Add explicit CLI flags rather than changing default output | Avoid breaking existing workflows |
| Make recalls/shortages opt-in flags | Keeps default fast and avoids noisy results |

## Errors Encountered
| Error | Attempt | Resolution |
|---|---:|---|
| planning-with-files referenced session-catchup.py missing | 1 | Proceed without catchup, note in findings |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)