Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .claude/skills/quartr/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: quartr
description: Query Quartr Public API v3 via the local `quartr` CLI — companies, events, earnings calls, transcripts, reports, slides, audio, live events. Use when the user asks about a ticker's earnings or fiscal periods, SEC filings (10-K / 10-Q / 8-K / 20-F / proxy) via Quartr, downloading transcripts or reports, streaming live calls, or anything sourced from api.quartr.com / quartr.com.
description: Query Quartr Public API v3 via the local `quartr` CLI — companies, events, earnings calls, transcripts, reports, slides, parsed Markdown of reports and slides, audio, live events. Use when the user asks about a ticker's earnings or fiscal periods, SEC filings (10-K / 10-Q / 8-K / 20-F / proxy) via Quartr, reading or downloading transcripts, reports or slides as text, streaming live calls, or anything sourced from api.quartr.com / quartr.com.
---

# quartr
Expand Down Expand Up @@ -92,8 +92,11 @@ explicitly passed, `--all` raises it to 500 to minimize round-trips.
```bash
# Companies
quartr companies list --tickers AAPL,MSFT --fields id,name,country
quartr companies list --openfigis BBG000B9XRY4
quartr companies resolve CE # every company using that ticker
quartr companies resolve BBG000B9XRY4 # a CIK or OpenFIGI works too
quartr companies get 4742 --format json
quartr companies segments 4742 # legacy dataset; 403 on most plans

# Events (earnings calls, AGMs, etc.)
quartr events list --tickers AAPL --sort-by date --direction desc --limit 10
Expand All @@ -109,12 +112,15 @@ quartr transcripts chapters <id> --levels 1,2

# Reports (10-K, 10-Q, 8-K, etc.)
quartr reports list --tickers AAPL --type-ids 11 --limit 5
quartr reports text <id> # parsed Markdown on stdout — read this, not the PDF
quartr reports text <id> --output 10k.md
quartr reports download <id> --output annual-report.pdf
quartr reports pages <id> --format csv
quartr reports summary <id> --length long --plain

# Slides
quartr slides list --tickers AAPL --limit 5
quartr slides text <id> # parsed Markdown of the deck
quartr slides download <id>
quartr slides pages <id>

Expand Down Expand Up @@ -158,13 +164,22 @@ quartr request get /events --query tickers=AAPL --query limit=3 --format json
Credito Emiliano and Cortus Energy. Run `quartr companies resolve <ticker>`
when a symbol might be shared, then either use `--company-ids` or qualify the
ticker as `NYSE:BLD` (the CLI resolves it to a companyId before querying).
There is no name search in the API — tickers and CIKs only.
There is no name search in the API — tickers, CIKs and OpenFIGIs only
(`--openfigis` on `companies list`; `resolve` recognises a FIGI by shape).
- **`--expand company` is a client-side join.** The API rejects
`expand=company`; the CLI strips it and batch-fetches `/companies` instead.
Use it whenever rows need to be attributable — otherwise they carry only a
bare `companyId` and a collision is invisible.
- **Read reports and slides with `text`, not `download`.** `reports text <id>`
and `slides text <id>` print Quartr's parsed Markdown (headings and tables
preserved) on stdout — pipe it to `head`, redirect it, or feed it to a model.
`download` fetches the PDF and is only right when the user wants the file.
`text` is a separate paid package: without it the endpoint returns 403.
Transcripts have no `text`; `transcripts download --output -` is already JSON.
- **Tier-restricted endpoints** return `403 Forbidden` on the user's API tier.
Observed restrictions: `events summary`, `audio list`, `live transcripts list`.
Observed restrictions: `events summary`, `audio list`, `live transcripts list`,
`companies segments`; `reports text` / `slides text` without the parsed
documents package.
The CLI prints a `hint:` line clarifying that 403 is entitlement, not
authentication. Surface the error verbatim — do not retry, hide, silently
fall back, or start debugging the API key. A rejected key returns 401.
Expand All @@ -186,6 +201,7 @@ For a worked example of each, see `references/recipes.md`:
- Find a company by ticker and grab its ID
- Pull the last N earnings calls for a ticker
- Download the latest annual report (10-K)
- Read a report or slide deck as Markdown
- Fetch all transcripts for a ticker, paginated, with parent event metadata
- Stream a live earnings transcript
- Use the raw `request get` for an unwrapped endpoint
Expand Down
21 changes: 17 additions & 4 deletions .claude/skills/quartr/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ specific flag or endpoint at hand.
| Command | Operations | Base path | Notes |
|--------------------|---------------------------------------------|----------------------------|--------------------------------|
| `auth` | `login`, `show`, `logout` | (local) | Manages `~/.config/quartr/config.json` |
| `companies` | `list`, `get`, `resolve` | `/companies` | Uses `ids` API param, not `companyIds`; `resolve <ticker\|cik>` lists collision candidates |
| `companies` | `list`, `get`, `resolve`, `segments` | `/companies` | Uses `ids` API param, not `companyIds`; `resolve <ticker\|cik\|figi>` lists collision candidates; `segments` is a legacy dataset (403 on most plans) |
| `events` | `list`, `get`, `summary` | `/events` | `summary` is tier-restricted |
| `documents` | `list`, `get`, `download` | `/documents` | Generic parent; prefer typed resources |
| `reports` | `list`, `get`, `summary`, `pages`, `download` | `/documents/reports` | `fileUrl` is the download field |
| `slides` | `list`, `get`, `summary`, `pages`, `download` | `/documents/slides` | `fileUrl` is the download field |
| `reports` | `list`, `get`, `summary`, `pages`, `text`, `download` | `/documents/reports` | `fileUrl` is the download field; `text` streams parsed Markdown (paid package) |
| `slides` | `list`, `get`, `summary`, `pages`, `text`, `download` | `/documents/slides` | `fileUrl` is the download field; `text` streams parsed Markdown (paid package) |
| `transcripts` | `list`, `get`, `summary`, `chapters`, `download` | `/documents/transcripts` | `fileUrl` is the download field |
| `audio` | `list`, `get`, `chapters`, `download` | `/audio` | `list` may be tier-restricted; `fileUrl` |
| `live` | `list`, `get` | `/live` | Honors `transcriptVersion` |
Expand Down Expand Up @@ -57,6 +57,7 @@ Auth precedence: flags > env > config file > defaults.
--exchanges NYSE,NASDAQ exchange symbols
--isins US0378331005 ISINs
--ciks 0000320193 SEC CIKs
--openfigis BBG000B9XRY4 OpenFIGI codes (figi, compositeFigi or shareClassFigi); companies only
--ids foo,bar alias used by companies-only consumers
--start-date 2024-01-01 ISO 8601
--end-date 2024-12-31 ISO 8601
Expand Down Expand Up @@ -96,6 +97,12 @@ company` on rows that carry no companyId. Both exit 2.
| `<r> summary` | `--plain` | Strip embedded document sources |
| `<r> summary` | `--fields` | Output columns |
| `<r> pages` | list flags | reports, slides only |
| `<r> text` | (none) | reports, slides only; prints parsed Markdown on **stdout** |
| `<r> text` | `--output PATH` | Write the Markdown to a file instead; `Saved <path>` on stderr |
| `<r> text` | `--metadata` | Print the envelope (`documentId`, `textUrl`, `updatedAt`) instead of fetching it |
| `<r> text` | `--fields` | Output columns, only with `--metadata` |
| `<r> text` | `--with-api-key` | Send `x-api-key` when fetching `textUrl` |
| `<r> segments` | list flags | companies only (`limit`, `cursor`, `direction`) |
| `<r> chapters` | list flags + `--levels` | transcripts, audio only |
| `<r> download` | `--output PATH` | Defaults to `<resource>-<id>.<ext>` in cwd; `-` streams to stdout |
| `<r> download` | (status line) | `Saved <path>` goes to **stderr**, never stdout |
Expand Down Expand Up @@ -205,12 +212,18 @@ and pass it via `--type-ids`.

## Key code locations (for skill maintenance)

Quartr publishes the live spec at <https://api.quartr.com/public/v3/openapi.json>
and a docs index at <https://quartr.com/docs/llms.txt> (datasets, changelog at
`/docs/changelogs/api-updates.md`). When Quartr announces a new dataset, diff
the spec's `paths` against the `resources` map first — that is how `text`,
`segments`, and `openfigis` were found.

If the CLI gets new commands, refresh this reference from:

- `internal/cli/app.go` — top-level command dispatch
- `internal/cli/resources.go` — resource map, paths, allowed param sets
- `internal/cli/flags.go` — global flags, listFlags, `toParams`
- `internal/cli/handlers.go` — list/get/summary/pages/chapters/download/stream/request
- `internal/cli/handlers.go` — list/get/summary/pages/text/chapters/segments/download/stream/request
- `internal/output/output.go` — format implementations, dotted-path lookup
- `internal/quartr/client.go` — retry, backoff, BuildURL
- `internal/quartr/config.go` — config file precedence and shape
61 changes: 57 additions & 4 deletions .claude/skills/quartr/references/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ quartr companies resolve CE
# 16930 Cortus Energy SE OM:CE
```

`resolve` takes a ticker, an `EXCHANGE:TICKER` pair, or a CIK, and lists every
candidate with the exchange pairs that matched. There is no name search — the
API has no `search`/`query`/`name` parameter — so never try to look a company
up by name.
`resolve` takes a ticker, an `EXCHANGE:TICKER` pair, a CIK, or an OpenFIGI
(`BBG000B9XRY4`), and lists every candidate with the exchange pairs that
matched. There is no name search — the API has no `search`/`query`/`name`
parameter — so never try to look a company up by name. When the user hands you
a FIGI, `quartr companies list --openfigis <figi>` is exact and collision-free.

Once the exchange is known, qualify the ticker anywhere `--tickers` is
accepted and the CLI resolves it to a companyId before querying:
Expand Down Expand Up @@ -145,6 +146,58 @@ country first.
`fileUrl` is publicly fetchable. Only add it if the file URL itself returns
401/403.

**If the user wants to read the filing rather than have the PDF**, skip the
download and use recipe 3a.

---

## 3a. Read a report or slide deck as Markdown

**Intent:** "What does Apple's latest 10-K say about services margins?" /
"Summarize this deck" / anything where the content matters and the PDF is a
detour.

Quartr's parsed documents package renders each report and slide deck to one
Markdown file with headings and tables preserved. `text` fetches it:

```bash
quartr reports text <id> # Markdown on stdout
quartr reports text <id> --output 10k.md # or to a file
quartr slides text <id> | head -80 # first slides of a deck
```

Find the id the same way as recipe 3 (pull wide, sort locally on `createdAt`),
then:

```bash
quartr reports text 105446 > apple-q4-2019.md
```

Tables come through as pipe tables, so financial statements are greppable:

```bash
quartr reports text 105446 | grep -i 'total net sales'
# |Total net sales (1)|64,040|62,900|260,174|265,595|
```

Response shape of `--metadata` (for provenance or freshness checks):

```json
{"data":{"documentId":105446,"textUrl":"https://files.quartr.com/document-artifacts/…/….markdown?ref=…","updatedAt":"2026-08-05T22:36:36.000Z","createdAt":"…"}}
```

**Pitfalls:**

- `text` prints to stdout by default; `download` writes a file by default. Do
not expect a `Saved` line from `text` unless you passed `--output`.
- Parsed text is a separately licensed package. A `403` here with a working
key elsewhere means the plan lacks it — say so, do not debug auth.
- Coverage is per document: a recent filing may have `text` before an old one
does. If `text` fails for one document, fall back to `download` and say the
parsed rendering is not available for it.
- Transcripts have no `text` endpoint: `quartr transcripts download <id>
--output -` already returns structured JSON with the speaker turns.

---

## 4. Fetch all transcripts for a ticker, paginated, with parent event metadata
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- uses: golangci/golangci-lint-action@v7
with:
version: v2.12.0
version: v2.13.2

# Exercises the release recipe on every PR. Without this, a broken cross
# compile only shows up after a tag is pushed, which is the worst time.
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: "2"
run:
go: "1.26"
go: "1.27"
linters:
default: none
enable:
Expand Down
8 changes: 5 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ repos:
hooks:
# Uses the golangci-lint on PATH rather than the upstream pre-commit
# repo, which builds the linter from source with whatever Go it finds.
# A linter built with Go < 1.26 refuses to load this config ("the Go
# A linter built with Go < 1.27 refuses to load this config ("the Go
# language version used to build golangci-lint is lower than the
# targeted Go version"). Install the matching binary with:
# go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.0
# targeted Go version"), and one built with an older toolchain than
# the `go` on PATH panics loading the standard library. Rebuild it
# with the current toolchain:
# go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2
- id: golangci-lint
name: golangci-lint
entry: golangci-lint run --fix
Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ pre-commit run --all-files # lint+test against the whole tree
~/go/bin/golangci-lint run # lint without pre-commit (needs v2.12+)
```

`pre-commit install` was already run in this clone — every commit runs golangci-lint (with `--fix`) and `go test ./...`. The lint hook shells out to the `golangci-lint` on `PATH` instead of the upstream pre-commit repo, which builds the linter from source with whatever Go it finds; a linter built with Go < 1.26 refuses to load this config. Install the matching binary once:
`pre-commit install` was already run in this clone — every commit runs golangci-lint (with `--fix`) and `go test ./...`. The lint hook shells out to the `golangci-lint` on `PATH` instead of the upstream pre-commit repo, which builds the linter from source with whatever Go it finds; a linter built with Go < 1.27 refuses to load this config, and one built with an older patch release than the `go` on `PATH` panics while type-checking the standard library (`file requires newer Go version`). Whenever the toolchain moves, rebuild the linter with it:

```bash
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.0
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2
```

CI pins the same version through `golangci/golangci-lint-action@v7`.
Expand All @@ -32,7 +32,7 @@ CI pins the same version through `golangci/golangci-lint-action@v7`.
Three internal packages, no external deps (Go stdlib only):

- **`internal/quartr`** — HTTP client and config persistence. `Client.GetBytes` retries 429/5xx with backoff (honors `Retry-After`). `LoadConfig`/`SaveConfig` handle the JSON file at `~/.config/quartr/config.json`.
- **`internal/cli`** — command dispatch and request shaping. The whole CLI surface is driven by a single `resources` map in `resources.go` keyed by command name; each entry describes the API path templates, allowed query params (`paramSet`), and download/stream URL fields. `handlers.go` dispatches the operations (list/get/summary/pages/chapters/download/stream) against any resource by reading from that map. Adding a new resource = one map entry, no per-command handler code.
- **`internal/cli`** — command dispatch and request shaping. The whole CLI surface is driven by a single `resources` map in `resources.go` keyed by command name; each entry describes the API path templates, allowed query params (`paramSet`), and download/stream URL fields. `handlers.go` dispatches the operations (list/get/summary/pages/text/chapters/segments/download/stream) against any resource by reading from that map. Adding a new resource = one map entry, no per-command handler code.
- **`internal/output`** — formats results as `table` / `json` / `csv` / `raw`. `--fields` supports dotted paths (`event.title`) via `getPath` recursive traversal.

`cmd/quartr/main.go` is a 3-line entry point that calls `cli.Run`.
Expand All @@ -44,10 +44,12 @@ Three internal packages, no external deps (Go stdlib only):
- **`--all` auto-bumps `--limit` to 500** unless the user passed `--limit` explicitly. Detection lives in `flagWasPassed` (string-scan over the raw args, since the `flag` package can't distinguish "default" from "explicitly default").
- **`parseInterspersed`** in `flags.go` lets users write `cmd <id> --flag value`. The stdlib `flag` package stops at the first positional, so we shuffle flags before positionals before delegating.
- **Downloads do NOT send `x-api-key` by default** — the Quartr `fileUrl` is publicly fetchable. `--with-api-key` is the opt-in.
- **`text` streams to stdout by default; `download` writes a file by default.** Both share `saveURL`. The asymmetry is deliberate: parsed Markdown is meant to be piped or redirected, while PDFs and audio are not. Don't unify them.
- **`/text` returns a link, not text.** `DocumentTextDto` is `{documentId, textUrl, updatedAt, createdAt}`; the Markdown lives at `textUrl` on the CDN. `--metadata` exposes the envelope.

## Lint config notes

- golangci-lint v2 syntax (config has `version: "2"` at top). v1 is built with Go 1.24 and rejects this repo's Go 1.26 target — never downgrade.
- golangci-lint v2 syntax (config has `version: "2"` at top). v1 is built with Go 1.24 and rejects this repo's Go 1.27 target — never downgrade.
- `gomodguard` is referenced as `gomodguard_v2` after the v2.12 deprecation rename.
- `gocritic.hugeParam` is intentionally disabled — passing `resource` (200B) by value is the design, not a perf bug.
- `gosec G304/G602` excluded globally — file paths from CLI args and bounds-checked slice indexes are inherent to the tool.
Expand Down
Loading
Loading