CLI
  • Curio
  • CLI
  • Forums
  • Support
  • Examples
  • Docs
  • Release Notes
  • Examples
  • Docs
  • Release Notes
  • Download
Curio CLI Examples/Attorney Assistant

Integration

Attorney Assistant

How a litigation practice and Claude use the Curio CLI to build page-anchored evidence bundles, file correspondence into a documentary hierarchy, and link every claim in written work back to the exact page that supports it.

Status
Community
Updated
August 27, 2026
By
Claude, with counsel at a litigation practice
Live Curio editing Curio 34.0+ · CLI 1.0.0-beta.9+ · macOS 15.0+ · Python 3.12+
View complete source

Litigation runs on pages

A contested legal matter arrives as paper: pleadings, court orders, discovered documents, expert reports, and years of correspondence, often thousands of pages across hundreds of PDFs. Courts and opposing parties work by page reference, so the page, not the file, is the real unit of the work.

For many years this practice has organized every matter as a Curio project. Sections mirror the structure of the case: "Pleadings," "Plaintiff's discovery," "Expert reports," "Correspondence." Each document is imported as a Spread PDF, so every page is its own idea space that can be flagged, annotated, and linked. Curio is where the human reads, marks, and thinks.

Then an AI assistant joined the practice, working from the terminal. That created a gap. The assistant could read and analyze the documents, but everything it produced, including summaries, chronologies, and memoranda, floated free of the organized case file. Every reference it made had to be re-found by hand, and nothing it wrote landed where the human actually works.

The Curio CLI closed that gap. The same project the human sees on screen became something the agent can build, file into, cite from, and verify, using supported commands.

What we wanted to automate

Four repetitive jobs, and one discipline:

  1. Bundle construction at scale. A new matter can mean hundreds of PDFs. Creating sections, importing each document page by page, and titling every page by hand is days of work.
  2. Correspondence filing. Emails and their attachments should land in the project as a documentary hierarchy, not a flat pile.
  3. Page-anchored citation. Every factual claim in a chronology or memorandum should be one click away from the page that supports it.
  4. A shared margin. Notes the human and the agent both read and write, attached to the pages they concern.
  5. Verification. The agent must prove its work actually persisted, rather than assume it did.

Building a bundle the agent can cite

Everything below uses fictional examples. Start with a fresh project, created entirely from the shell:

curio project create "~/Matters/Example Matter/Example Matter (Working Bundle)"

The response returns a project_uuid and a default_section_id, and the project opens ready for work. Create the case structure:

curio create section --title "Pleadings" --project-id <project-uuid>

Then import each document as a Spread PDF with a page-title manifest:

curio import "notice-of-motion.pdf" --as spread-pdf \
  --section <section-uuid> --page-titles titles.json \
  --project-id <project-uuid>

where titles.json follows a simple convention:

[
  {"page": 1, "title": "14-03-2025 — Notice of Motion · p1/12"},
  {"page": 2, "title": "p2/12"},
  {"page": 3, "title": "p3/12"}
]

The first page of each document carries the date and a description; later pages carry only their page number. Two useful things fall out of this. Sections sorted by title read as a chronology of the case, and any page the agent cites can be identified at a glance.

Two habits matter throughout:

  • --project-id on every call. The CLI accepts a project selector on its live commands, so a scripted build addresses its target explicitly and never depends on which window happens to be frontmost. The build runs in the background without stealing focus from whatever the user is doing.
  • curio batch for whole-matter builds. One command per line, named results, and $name.id references let a single batch file create a folder and then import into it. Each line returns a JSON result, and the batch ends with a save.

The part that makes all of this worth doing: every item the CLI creates returns a stable link field, a curio:// URL addressed by UUID. The build harvests every link into a small JSON "link map." When the agent later writes a chronology or memorandum, each entry carries the link for the page it relies on. Clicking it opens the project at that exact page. Retitling pages or reorganizing the project does not break these links, because they address UUIDs rather than titles.

Warning
This workflow writes to Curio projects. Rehearse any new script against a scratch project first, keep backups of important projects, and have the agent propose a plan before it changes anything. The plan-refine-act pattern from the other examples applies with more force, not less, when the project holds months of work.

Filing correspondence as a documentary hierarchy

Email needs more structure than a flat import. The convention we settled on, after some trial and error: folders carry the structure, spread pages are the leaves.

Correspondence (section)
└─ 📁 12-03-2025 — Email — Opposing attorneys — R — extension request
     ├─ the email's own spread pages
     └─ 📁 Attachment 1 — Draft consent order (draft-order.pdf)
          └─ that attachment's spread pages

Each email becomes a folder titled with its date, sender, direction (received or sent), and a one-line gist. The email's pages sit directly inside; each attachment gets its own subfolder; attachments of attachments recurse. The CLI supports all of it: folders inside sections, spread imports under a parent folder, and move --parent with an explicit position for anything that needs rearranging. Emails are de-duplicated by their Message-ID before filing, and each item's note records enough identity to answer "where did this come from" later.

Reading the structure back is one command:

curio assets --section <section-uuid> --recursive --organizer_order --project-id <project-uuid>

which returns the full nested tree, with children under each item and both a top-level count and an all-descendants total_count, so a filing script can verify the hierarchy it just built.

The agent verifies its own work

The most transferable lesson from months of daily use is a discipline, not a command. Treat every write as a transaction with three parts:

  1. Mutate through the CLI, addressing the target with --project-id.
  2. Save once per group. After a group of imports and related edits, issue one explicit curio save --project-id <project-uuid>. The save acts as a settlement and persistence barrier: when it returns, the work is on disk.
  3. Read it back. Confirm the result from the saved file, not from the command's response:
curio get --project "~/Matters/Example Matter/Example Matter (Working Bundle).curio" \
  --id <figure-uuid> --fields id,kind,display_page,asset_file

The --project <path> transport reads the saved package directly, without needing the app's attention, so a build script can sample figures across a large import (first, middle, and last pages) and confirm each one persisted with the right page displayed. Name the fields you need explicitly: the default field set on get and query is deliberately compact (id, kind, title), which is easy on context windows but will make a naive verification script report false failures.

For visual checks, the agent exports an idea space and looks at it:

curio export --format png --id <ideaspace-uuid> ~/scratch/check.png

An agent that can read images can confirm its own layout and rendering without asking the human to look.

Working together on one surface

Automation is half of the picture. The other half is that the human and the agent now share a workspace.

  • A shared margin. Idea-space notes hold observations from both sides. The human writes through the Notes inspector; the agent reads notes and appends through curio set --id <uuid> --note, prefixing its own lines so authorship stays clear. A morning's marks on the evidence become instructions the agent can collect and act on.
  • "This page, these documents." When the human says "summarize the document I've selected," the agent resolves it by reading the live selection with curio get --selected --fields all rather than asking for a filename.
  • Agent deliverables on the canvas. A generated HTML report imported with curio import report.html lands as a live web view figure and renders directly on an idea space, diagrams included. Status dashboards and interactive chronologies can live inside the same project as the evidence they describe.

Lessons, limitations, and what this generalizes to

Lessons.

  • Trust the read-back, not the response. Verification against the saved file caught the few problems that mattered; everything else was noise.
  • Pin the target on every scripted call. Implicit addressing follows the frontmost window, which is right for a person and wrong for a script running while that person works on something else.
  • Make the manifest the artifact. Generating page titles and batch files from a small script, and keeping the JSONL responses and the harvested link map, means every build is reproducible and every link is recoverable.
  • The section structure is semantic. A document's placement tells the agent what role it plays, the way it would tell a human colleague. "The same report" means something different in "Expert reports" than in "Plaintiff's discovery," and the agent can use that.

Limitations.

  • Writes need Curio running; the read-only --project transport works either way.
  • A titling or manifest bug replicated across hundreds of imports is tedious to unwind (move-to-Trash is the delete idiom). Rehearse on a scratch project and verify early.
  • The links are only as durable as the project file's name and location, so renaming the project file is a deliberate act with consequences.

What this generalizes to. Nothing here is specific to law. Any practice that lives on paginated source documents, such as compliance review, medical-record analysis, academic archives, or investigative research, can use the same pattern: sections as meaning, one page per idea space, dated titles, batch builds, link maps, and an agent that files its work into the same project its human reads.

The included snippet, bundle_builder.py, is a compact, sanitized illustration of the build-and-verify loop: it plans a bundle from a folder of PDFs, writes the page-title manifests and a batch file, runs the batch, harvests the returned links into a link map, and verifies a sample of figures against the saved package.

Inspect before downloading

Complete source

1 files

Every text file included in the download is available here. Open a filename to inspect its contents.

bundle_builder.py7 KB
#!/usr/bin/env python3
"""Plan, build, and verify a page-anchored Curio evidence bundle.

A compact, sanitized illustration of the build-and-verify loop described in
the Attorney Assistant gallery entry. It uses only the supported Curio CLI
and the Python standard library (plus macOS's own `mdls` for PDF page
counts). Everything here is an example: adapt the plan format, the title
convention, and the verification sampling to your own work.

Usage:
    python3 bundle_builder.py plan.json

The filing plan is a small JSON file. A fictional example:

    {
      "project": "~/Matters/Example Matter/Example Matter (Working Bundle)",
      "sections": [
        {
          "title": "Pleadings",
          "documents": [
            {"file": "~/Matters/Example Matter/PDFs/notice-of-motion.pdf",
             "date": "14-03-2025", "title": "Notice of Motion"},
            {"file": "~/Matters/Example Matter/PDFs/answering-affidavit.pdf",
             "date": "02-04-2025", "title": "Answering Affidavit"}
          ]
        }
      ]
    }

What it does, in order:
  1. `curio project create` - a fresh project, created and opened from the shell.
  2. Writes a page-title manifest per document (first page dated and described,
     later pages numbered) and one `curio batch` file for the whole build,
     ending with an explicit `save`.
  3. Runs the batch with `--project-id`, so the build never depends on which
     window is frontmost, and harvests every returned id and curio:// link
     into link_map.json.
  4. Verifies a sample of page figures (first, middle, last per document)
     against the SAVED package via the read-only `--project` transport,
     naming the fields explicitly.

Curio must be running for steps 1-3; step 4 reads the saved file directly.
"""

import json
import pathlib
import subprocess
import sys


def curio(*args):
    """Run a curio command and return the parsed `result` object."""
    proc = subprocess.run(["curio", *args], capture_output=True, text=True)
    if proc.returncode != 0:
        raise RuntimeError(f"curio {' '.join(args)} failed: {proc.stderr.strip()}")
    return json.loads(proc.stdout).get("result", {})


def pdf_page_count(pdf: pathlib.Path) -> int:
    """Page count via Spotlight metadata - no third-party libraries needed."""
    out = subprocess.run(
        ["mdls", "-name", "kMDItemNumberOfPages", "-raw", str(pdf)],
        capture_output=True, text=True,
    ).stdout.strip()
    if not out.isdigit():
        raise RuntimeError(f"Could not read page count for {pdf.name}")
    return int(out)


def write_manifest(doc: dict, pages: int, out_dir: pathlib.Path) -> pathlib.Path:
    """Page-title manifest: '<date> — <title> · p1/N' then bare 'pK/N'."""
    suffix = f" · p1/{pages}" if pages > 1 else ""
    titles = [{"page": 1, "title": f"{doc['date']} — {doc['title']}{suffix}"}]
    titles += [{"page": k, "title": f"p{k}/{pages}"} for k in range(2, pages + 1)]
    path = out_dir / (pathlib.Path(doc["file"]).stem + ".titles.json")
    path.write_text(json.dumps(titles, ensure_ascii=False, indent=1))
    return path


def build(plan_path: pathlib.Path):
    plan = json.loads(plan_path.read_text())
    work = plan_path.parent / "_bundle_build"
    work.mkdir(exist_ok=True)

    # 1. Create and open the project; keep the UUID for every later call.
    project_path = pathlib.Path(plan["project"]).expanduser()
    created = curio("project", "create", str(project_path))
    project_id = created["project_uuid"]
    package = project_path.with_suffix(".curio")
    print(f"Created project {project_id}\n  at {package}")

    # 2. One batch file for the whole build: named section creates, spread
    #    imports referencing them via $name.id, and a trailing save.
    lines, doc_names = [], []
    for s_idx, section in enumerate(plan["sections"]):
        ref = f"sec{s_idx}"
        lines.append(f'{ref} = create section --title "{section["title"]}"')
        for doc in section["documents"]:
            pdf = pathlib.Path(doc["file"]).expanduser()
            manifest = write_manifest(doc, pdf_page_count(pdf), work)
            lines.append(
                f'import "{pdf}" --as spread-pdf --section ${ref}.id '
                f'--page-titles "{manifest}"'
            )
            doc_names.append(doc["title"])
    lines.append("save")
    batch_file = work / "build.batch"
    batch_file.write_text("\n".join(lines) + "\n")

    # 3. Run it, addressed at our project regardless of window focus, and
    #    harvest every id + link from the per-line JSONL results.
    proc = subprocess.run(
        ["curio", "batch", str(batch_file), "--project-id", project_id],
        capture_output=True, text=True,
    )
    link_map, doc_figures, import_i = [], [], 0
    for line in proc.stdout.splitlines():
        entry = json.loads(line)
        if not entry.get("ok"):
            raise RuntimeError(f"Batch line failed: {entry}")
        figures = collect_items(entry.get("result", {}))
        if figures:  # an import line: remember its figures for verification
            link_map.append({"document": doc_names[import_i], "items": figures})
            doc_figures.append(figures)
            import_i += 1
    (work / "link_map.json").write_text(json.dumps(link_map, indent=1))
    print(f"Built {import_i} documents; links harvested to {work}/link_map.json")

    # 4. Verify against the SAVED package: sample first / middle / last page
    #    figures per document, naming the fields (the default set is compact).
    for name, figures in zip(doc_names, doc_figures):
        sample = {0, len(figures) // 2, len(figures) - 1}
        for i in sorted(sample):
            got = curio(
                "get", "--project", str(package), "--id", figures[i]["id"],
                "--fields", "id,kind,display_page,asset_file",
            )
            item = (got.get("items") or [got])[0]
            if not item.get("asset_file"):
                raise RuntimeError(f"{name}: figure {figures[i]['id']} did not persist")
        print(f"  verified {name}: {len(sample)} of {len(figures)} pages sampled")
    print("Build verified against the saved package.")


def collect_items(obj, found=None):
    """Walk a result object and collect anything carrying an id (and link)."""
    found = [] if found is None else found
    if isinstance(obj, dict):
        if "id" in obj:
            found.append({"id": obj["id"], "link": obj.get("link"),
                          "title": obj.get("title")})
        for value in obj.values():
            collect_items(value, found)
    elif isinstance(obj, list):
        for value in obj:
            collect_items(value, found)
    return found


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit(__doc__)
    build(pathlib.Path(sys.argv[1]).expanduser())

  • Copyright © Zengobi, Inc.
  • Contact Us
  • Terms of Use
  • Privacy Policy