Skip to content

feat(benzinga): add benzinga#1406

Open
waveriderai wants to merge 6 commits into
mvanhorn:mainfrom
waveriderai:feat/benzinga
Open

feat(benzinga): add benzinga#1406
waveriderai wants to merge 6 commits into
mvanhorn:mainfrom
waveriderai:feat/benzinga

Conversation

@waveriderai

Copy link
Copy Markdown

benzinga

Every Benzinga calendar, news, fundamentals, and signal endpoint as a typed command — plus an offline SQLite store, full-text search, cross-entity queries, and the first Benzinga MCP server.

API: benzinga | Category: other | Press version: 4.27.0
Spec: Official Benzinga OpenAPI specs (Benzinga/benzinga-docs), all on api.benzinga.com with ?token= auth

Publication Path

New print

CLI Shape

$ benzinga-pp-cli --help
(eval):8: no such file or directory: /Users/waverider/printing-press/.publish-repo-benzinga-pp-cli-fa39fa20/library/other/benzinga/benzinga-pp-cli

Novel Commands

Command Name Description
watch Overnight watchlist change scan See everything that changed on your tickers since you last looked — new ratings, price-target moves, breaking news, and signals in one diff.
why Single-ticker move explainer Build one time-ordered catalyst timeline for a ticker by merging unusual options, block trades, halts, rating changes, and news.
catalysts Unified forward catalyst agenda One forward-dated agenda per ticker set unioning earnings, dividends, splits, IPOs, FDA dates, conference calls, guidance, and offerings.
analyst-accuracy Analyst & firm accuracy scorecard Rank rating-issuing firms and analysts by Benzinga's historical accuracy, and tag today's rating changes with the issuer's hit rate.
earnings-season Earnings surprise tracker Compute EPS and revenue beat/miss and surprise % from the earnings calendar and link each name to its conference call and transcript.
insider-cluster Clustered congressional buying Flag tickers where several distinct members of Congress filed purchases within a window — cluster detection beyond a single disclosure.

What This CLI Does

Benzinga's licensed financial-data API is powerful but fragmented across ~60 endpoints and has only one complete client — a Python library with no CLI, no offline store, and no agent surface. This CLI covers the full documented REST surface as first-class commands across news, 16 calendar products, signals, analyst intelligence, congressional/insider trades, fundamentals, market data, logos, and ticker trends. It delta-syncs the calendar/news/signal families into a local SQLite database via the API's own updated cursors (offline FTS5 search), and adds six cross-entity commands the REST API cannot express in a single call: watch, why, catalysts, analyst-accuracy, earnings-season, and insider-cluster. Ships the first-ever Benzinga MCP server (Cloudflare search+execute pattern for the 60+ tool surface).

Manuscripts

Validation Results

Check Result
Manifest PASS
Phase 5 PASS
go mod tidy PASS
govulncheck (this CLI only, reachable findings) PASS
go vet PASS
go build PASS
--help PASS
--version PASS
Manuscripts PRESENT

Publish Live Gate

Full live dogfood reran at publish time and passed (220/220, status: pass) against the real Benzinga API. Proof: phase5 acceptance marker.

Known Gaps

Two Benzinga endpoints were upstream-down at build time and were intentionally omitted (documented in the CLI README ## Known Gaps): the standalone earnings-call-transcripts endpoints (HTTP 503 across all tokens — delivery-service outage) and the deprecated fundamentals operationRatios-v2 path (HTTP 500; the working v2.1 variant ships). Benzinga tokens are product-scoped, so a valid token can return 403 on a product not in your plan — a licensing boundary the CLI surfaces clearly.

@github-actions

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for b6e6614adce6fe93a335743367de2d981d50349d after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a complete Benzinga financial-data CLI (benzinga-pp-cli) and the first Benzinga MCP server, covering ~60 REST endpoints across news, 16 calendar products, signals, fundamentals, analyst intelligence, congressional/insider trades, logos, and quote data. It also ships a local SQLite mirror with FTS5 full-text search and six cross-entity novel commands that join data the REST API cannot combine in a single call.

  • Novel commands (watch, why, catalysts, analyst-accuracy, earnings-season, insider-cluster) all correctly open the store read-only via OpenReadOnlyContext, include zero-timestamp guards, and are annotated mcp:read-only.
  • MCP layer exposes typed endpoint tools, a read-only SQL gate with thorough multi-statement and injection protection, and a Cobra-tree command mirror with arg validation and blocked root-level flag overrides.
  • SQLite store uses WAL mode, FTS5 with Porter stemming, schema-version gating, and SQLITE_BUSY retry logic throughout migrations.

Confidence Score: 5/5

Safe to merge; novel commands, store layer, SQL gate, and shellout security are all well-constructed, and previous thread issues are resolved in the current diff.

Novel commands open the store read-only, zero-timestamp rows are correctly skipped in --since/--window filters, the SQL injection gate is carefully layered (noise-strip + allowlist + multi-statement check + driver mode=ro), and the MCP shellout passes args as an exec slice with flag-injection blocking. The one new finding — the search tool overstating FTS5 operator support — is a documentation mismatch that misleads agents composing OR/NOT queries but does not break functionality or expose data.

The search tool parameter description in internal/mcp/tools.go and the ftsMatchQuery implementation in internal/store/store.go are the only area warranting a follow-up correction.

Important Files Changed

Filename Overview
library/other/benzinga/internal/mcp/tools.go MCP tool registration, search/SQL/context handlers, and binary-response logic. SQL gate is carefully implemented; search description overstates FTS5 operator support.
library/other/benzinga/internal/cli/novel_shared.go Shared helpers for all six novel commands. Store opened via OpenReadOnlyContext; novelEventTime correctly returns zero for unparseable rows.
library/other/benzinga/internal/store/store.go SQLite store with WAL mode, FTS5, migrations, and read-only open path. Identifier validation guards all dynamic SQL.
library/other/benzinga/internal/client/client.go HTTP client with retry, rate limiting, cache, and binary-response wrapping. Auth token redacted in all error paths.
library/other/benzinga/internal/mcp/cobratree/shellout.go CLI shellout via exec slice (no shell injection); flag-like tokens rejected; blockedRootFlags prevents auth/config overrides.
library/other/benzinga/internal/cliutil/credentials.go BenzingaApiKey and CalendarApiKey carry distinct TOML tags resolving the duplicate-tag issue from earlier threads.
library/other/benzinga/internal/config/config.go Config struct; BenzingaApiKey (api_key) and CalendarApiKey (calendar_api_key) carry distinct TOML tags in the current diff.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent as MCP Agent / CLI
    participant MCP as benzinga-pp-mcp
    participant CLI as benzinga-pp-cli
    participant Client as HTTP Client
    participant API as api.benzinga.com
    participant Store as SQLite Store (FTS5)

    Agent->>MCP: tool call (typed endpoint or command mirror)
    alt Typed endpoint tool
        MCP->>Client: "GET/POST with ?token="
        Client->>API: HTTP request
        API-->>Client: JSON / binary response
        Client-->>MCP: json.RawMessage
        MCP-->>Agent: EndpointResponse JSON
    else Command-mirror tool
        MCP->>CLI: exec.Command(args...)
        CLI->>Store: OpenReadOnlyContext
        Store-->>CLI: rows
        CLI-->>MCP: stdout JSON
        MCP-->>Agent: bound.Text result
    end
    Agent->>CLI: sync --resources X --since Nd
    CLI->>API: paginated GET
    API-->>CLI: page of items
    CLI->>Store: UpsertBatch (WAL write)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Agent as MCP Agent / CLI
    participant MCP as benzinga-pp-mcp
    participant CLI as benzinga-pp-cli
    participant Client as HTTP Client
    participant API as api.benzinga.com
    participant Store as SQLite Store (FTS5)

    Agent->>MCP: tool call (typed endpoint or command mirror)
    alt Typed endpoint tool
        MCP->>Client: "GET/POST with ?token="
        Client->>API: HTTP request
        API-->>Client: JSON / binary response
        Client-->>MCP: json.RawMessage
        MCP-->>Agent: EndpointResponse JSON
    else Command-mirror tool
        MCP->>CLI: exec.Command(args...)
        CLI->>Store: OpenReadOnlyContext
        Store-->>CLI: rows
        CLI-->>MCP: stdout JSON
        MCP-->>Agent: bound.Text result
    end
    Agent->>CLI: sync --resources X --since Nd
    CLI->>API: paginated GET
    API-->>CLI: page of items
    CLI->>Store: UpsertBatch (WAL write)
Loading

Reviews (6): Last reviewed commit: "fix(benzinga): read-only novel store + z..." | Re-trigger Greptile

Comment thread library/other/benzinga/internal/cliutil/credentials.go Outdated
Comment thread library/other/benzinga/internal/config/config.go Outdated
Comment thread library/other/benzinga/internal/cli/watch.go
Comment thread library/other/benzinga/internal/cli/novel_shared.go Outdated
Add two read-only commands to the Benzinga CLI:

- `wiims` -> GET /api/v1/wiims (Why Is It Moving): structured
  explanations of why a security is moving, tagged to a security.
  NOTE: endpoint is not in Benzinga's official OpenAPI specs;
  reverse-engineered and verified live (see patch entry's
  deferred_to_upstream note).
- `news-quantified` -> GET /api/v2/newsquantified (Quantitative
  News Analytics): sentiment, volume ratios, per-ticker price
  reactions at 30s-120m horizons. Endpoint was in the captured
  spec but the generator never emitted a command for it.

Both verified live (200, source: live). Registered in root.go,
documented in README/SKILL, added to tools-manifest.json (auto-
exposed via the curated MCP server's generic executor). Patch
records under .printing-press-patches/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 8d66859c8d64c8d877be150aefbba4aa0759512f after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

WIIM is not a standalone endpoint. The dedicated /api/v2/wiims path
404s on the gateway and /api/v1/wiims (previously used) is a dead
legacy route returning a single stale item. Benzinga delivers "Why
Is It Moving" as the `WIIM` channel of the news feed, verified
against a live production integration.

Repoint the wiims command to GET /api/v2/news with channels=WIIM
preset, mirroring news.get's camelCase query params (pageSize,
dateFrom, dateTo, updatedSince) and its bare-array response. Drop
the {"wiims":[...]} envelope unwrap (news returns a plain array).

WIIM requires a Benzinga News token entitled to the WIIM channel;
the market-data tokens return no items. Documented in the command
help, README, SKILL, and the patch record.

Verified live with a WIIM-entitled token: `wiims --date 2026-07-01
--page-size 50 --json` returns 50 items, all on the WIIM channel.
go build/vet/test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 182abf27502b7603fa1db93ffbc31834a59fa68a after 183s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

The WIIM feed carries no numeric price move (only prose in the
title). Add --with-points to wiims: it collects the tickers from
the returned WIIM items, fetches GET /api/v1/quoteDelayed in
batches of 50, and attaches price_move:[{ticker, points, percent,
last}] to each item (the day's move in points vs previous close).

Both calls use the SAME credential — a Benzinga Pro token entitles
the WIIM news channel and delayed quotes alike — so no second token
slot is needed. Verified live with a Pro token: each WIIM item is
enriched with its delayed-quote move. go build/vet/test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for b44d8cd3532bb61ea0a8aedc3b14a954f057c280 after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Benzinga entitlements are split and no single token covers
everything: the news/Pro token serves news, WIIM, fundamentals,
signals, and delayed quotes but 401s on market data; the market/
super-token serves movers, bars, short interest, logos, and the
calendar but returns nothing for the WIIM channel. With one
credential slot, users had to swap tokens between commands.

Add a distinct market token (MarketApiKey, env
BENZINGA_MARKET_API_KEY, `auth set-token --market`) and resolve
the request token per path via Config.TokenForPath: market-data
and calendar paths use the market token when set, everything else
uses the default. Unset product tokens fall back to the default,
so single-token setups are unchanged.

This also fixes the duplicate `toml:"api_key"` tag that
CalendarApiKey shared with BenzingaApiKey (silent credential loss
/ invalid TOML on round-trip) in both config.go and
cliutil/credentials.go — CalendarApiKey now uses
toml:"calendar_api_key". No migration needed: CalendarApiKey was
never independently persisted, and existing api_key files read
unchanged.

Verified live: with api_key=<Pro> and market_api_key=<Market>
stored as distinct TOML keys, `wiims` (Pro) and `market` movers
(Market) both work in one session with no swap. go build/vet/test
pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 72315ff9f263eebd1abb686c367752d23888749e after 186s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Comment on lines +299 to +312
if binaryResponse {
encoded := base64.StdEncoding.EncodeToString(data)
out, err := json.Marshal(map[string]any{
"content_encoding": "base64",
"data_base64": encoded,
"byte_count": len(data),
})
if err != nil {
return mcplib.NewToolResultError(fmt.Sprintf("encoding binary result: %v", err)), nil
}
if len(out) > bound.MaxBytes {
return mcplib.NewToolResultError(fmt.Sprintf("binary response is too large for MCP text output: %d response bytes encode to %d base64 bytes and %d MCP result bytes, exceeding the %d byte budget. Use the companion CLI command with --output <file> to save the payload locally.", len(data), len(encoded), len(out), bound.MaxBytes)), nil
}
return mcplib.NewToolResultText(string(out)), nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 MCP binary response is double-encoded

When binaryResponse=true, data is already the JSON envelope produced by client.wrapBinaryResponse{"_pp_binary":true,"content_type":"...","encoding":"base64","bytes":N,"data":"<base64 of raw bytes>"}. The handler then base64-encodes the entire JSON envelope string a second time and places it in data_base64. To recover the actual binary, a caller would have to: (1) base64-decode data_base64 → JSON string, (2) JSON-parse that envelope, (3) base64-decode the inner data field — three layers instead of one.

Additionally, byte_count is set to len(data) (length of the JSON envelope bytes, which includes the base64 overhead and JSON syntax) rather than the raw binary byte count that lives in the envelope's bytes field.

The wrapBinaryResponse envelope is already a valid JSON result; the MCP handler should return it directly via mcpToolResultText rather than adding another encoding layer.

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Two correctness fixes for the hand-built novel commands:

- novelStore now opens the mirror with OpenReadOnlyContext instead
  of OpenWithContext. All six novel commands (watch/why/catalysts/
  analyst-accuracy/earnings-season/insider-cluster) are read-only
  and only SELECT, so opening read-only enforces the contract at
  the driver level, skips the schema-migration write path, and
  lets a concurrent `sync` proceed without blocking on the WAL
  write lock.

- watch and why filtered with `!when.IsZero() && when.Before(
  cutoff)`, letting rows whose timestamp doesn't parse bypass the
  always-active --since/--window cutoff and surface as if recent.
  Zero-time rows are now skipped. (catalysts already drops them.)

Verified read-only why/watch against a 1.6GB synced mirror (exit
0, results returned). go build/vet/test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 44cdde3d9923ee1e36391f261ba1155f70684031 after 183s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant