feat(skills): add Fireflies API skill and CLI (#49)

This commit is contained in:
Magnus Hedemark
2026-07-15 17:19:10 -04:00
committed by GitHub
parent 082fd698e5
commit c64b3ce348
14 changed files with 702 additions and 165 deletions
+1
View File
@@ -117,6 +117,7 @@ When the user mentions these keywords, load the corresponding skill:
| "langgraph", "multi-agent", "state machine", "graph-based workflow", "LangGraph", "supervisor pattern", "swarm pattern", "agent orchestration", "graph state", "subgraph", "agent routing", "tool-calling loop", "agent loop", "stateful agent", "durable execution", "human in the loop langgraph", "checkpointer", "langgraph persistence" | [langgraph](langgraph/SKILL.md) |
| "debate", "council", "multi-perspective", "structured debate", "get multiple perspectives", "expert panel", "decision landscape", "what would experts say", "what are we missing", "convergence", "false consensus", "agent-council", "pre-mortem" | [agent-council](agent-council/SKILL.md) |
| "skill format", "how do I make a skill", "agentskills.io" | [agent-skills](agent-skills/SKILL.md) |
| "Fireflies", "Fireflies.ai", "meeting transcripts", "meeting notes", "audio upload", "webhook verification", "AskFred", "AI meeting analytics" | [fireflies](fireflies/SKILL.md) |
| "flaresolverr-cli", "FlareSolverr session", "request.get", "request.post", "return-only-cookies" | [flaresolverr-cli](flaresolverr-cli/SKILL.md) |
| "Cloudflare challenge", "DDoS-GUARD", "browser-backed request" | [flaresolverr](flaresolverr/SKILL.md) |
| "Three.js", "three.js", "WebGL", "3D scene", "GLTF", "browser 3D" | [three](three/SKILL.md) |
+5 -1
View File
@@ -106,13 +106,17 @@ fixed-layout, accessibility, and media overlays. Portable across any AgentSkills
Build and review assumptions-led financial models, unit economics, pricing, fundraising scenarios, and SaaS operating metrics.
### [fireflies](fireflies/SKILL.md)
Fireflies.ai meeting intelligence from the terminal. Query transcripts, summaries, analytics, AskFred, and workspace data through source-faithful GraphQL documents; safely preview confirmed mutations and verify Webhooks V2 signatures locally.
### [flaresolverr](flaresolverr/SKILL.md)
Use a private FlareSolverr service through a dependency-free JSON CLI when ordinary HTTP retrieval is blocked by a browser challenge.
### [flaresolverr-cli](flaresolverr-cli/SKILL.md)
Use the full stdlib FlareSolverr CLI for health checks, browser-session lifecycle, and challenge-solving GET or form-POST requests with structured output and dry-run support.
Use a small FlareSolverr JSON CLI for browser-backed GET and POST requests, readiness checks, and session lifecycle management.
### [forgejo-cli](forgejo-cli/SKILL.md)
+54
View File
@@ -0,0 +1,54 @@
# Fireflies.ai: meeting intelligence from the terminal
## Why Install This Skill
Turn Fireflies meetings into usable data without hand-copying notes. Search transcripts, inspect
summaries and action items, review analytics, and ask focused questions with AskFred from a single
dependency-free command line tool.
It also makes sensitive operations deliberate: every mutation requires an explicit confirmation,
and every mutation can be previewed locally before it is sent.
## What You Get
| Path | What it provides |
|---|---|
| `scripts/fireflies` | Python 3.9+ GraphQL CLI with safe reads, mutations, dry-runs, and webhook verification |
| `references/api-reference.md` | API model, operation families, limits, and source links |
| `references/cli-reference.md` | Complete CLI contract and examples |
| `references/workflows.md` | Transcript, analytics, upload, AskFred, and GraphQL recipes |
| `references/webhook-security.md` | Webhooks V2 verification guidance |
| `references/troubleshooting.md` | Failure diagnosis and escalation boundaries |
## Quick Start
```bash
export FIREFLIES_API_KEY='...'
python3 scripts/fireflies transcripts list --keyword roadmap --limit 10 --json
```
Output is the Fireflies GraphQL response, for example:
```json
{"data":{"transcripts":[{"id":"...","title":"Roadmap review"}]}}
```
Preview a change without calling the API:
```bash
python3 scripts/fireflies meetings rename transcript-id --title "Q3 roadmap" --dry-run --json
```
## Triggers
- Fireflies.ai transcripts, summaries, notes, contacts, channels, or analytics
- AskFred questions about meeting content
- Remote audio upload to Fireflies
- Fireflies Webhooks V2 signature verification
## Requirements
- Python 3.9 or newer
- A Fireflies API key in `FIREFLIES_API_KEY` for API calls
- Network access to `https://api.fireflies.ai/graphql`
- No third-party Python packages
+72
View File
@@ -0,0 +1,72 @@
---
name: fireflies
description: >-
Query Fireflies.ai meeting transcripts, meeting notes, summaries, contacts, channels,
AI meeting analytics, AskFred, audio uploads, and webhook signatures through its GraphQL API.
Use when a user mentions Fireflies, Fireflies.ai, meeting transcripts or notes stored in
Fireflies, AskFred, or Fireflies webhooks. Do not use for local audio transcription, calendar
management, or meetings that are not Fireflies data.
license: MIT
compatibility: Requires Python 3.9+, network access for API calls, and FIREFLIES_API_KEY for non-dry-run API requests.
metadata:
service: fireflies.ai
api: graphql
allowed-tools: Bash Read
---
# Fireflies.ai
Use `scripts/fireflies` from this skill directory. It uses the Fireflies GraphQL endpoint directly
and emits the API response as JSON. Read-only discovery is safe. Before any state change, confirm
the target, scope, and rollback path; then route the change through the CLI's literal `--confirm`.
`--dry-run` previews a payload only and never authorizes a write.
## First Use
1. Check the command schema: `scripts/fireflies --help` and the relevant subcommand help.
2. Set `FIREFLIES_API_KEY` only for an actual API request. Do not expose or persist it.
3. Start with read-only discovery, such as `scripts/fireflies transcripts list --limit 10 --json`.
4. Use `--dry-run --json` before each mutation to inspect the exact GraphQL payload.
## Command Map
| Need | Command |
|---|---|
| Find transcript metadata | `scripts/fireflies transcripts list --keyword TEXT --limit 10 --json` |
| Read a meeting, summary, sentences, and analytics | `scripts/fireflies transcripts get ID --json` |
| People, channels, groups, contacts, apps | `users`, `channels`, `groups`, `contacts`, `apps` |
| Meeting analytics or live meetings | `analytics --start DATE --end DATE`, `meetings active` |
| Ask a transcript question | `askfred create --question TEXT --transcript-id ID --confirm` |
| Run an exact current/future API operation | `query --document GRAPHQL --variables JSON` |
| Verify a delivered webhook locally | `webhook verify --secret SECRET --signature sha256=... --body FILE` |
## Safe Mutation Workflow
1. Identify the Fireflies object ID and present the intended change.
2. Confirm target, scope, and rollback path with the user. Deletion may not be reversible.
3. Preview the exact document using `--dry-run`; this makes no HTTP request.
4. Run exactly the approved mutation with `--confirm`.
5. Return the response without printing credentials.
Examples:
```bash
scripts/fireflies meetings rename transcript-id --title "Weekly product review" --dry-run --json
scripts/fireflies meetings rename transcript-id --title "Weekly product review" --confirm --json
scripts/fireflies transcripts delete transcript-id --confirm --json
```
## Reference Routing
- Read [references/cli-reference.md](references/cli-reference.md) for syntax, output, exit codes, or generic GraphQL execution.
- Read [references/api-reference.md](references/api-reference.md) for the API model, exact documented operation families, limits, permissions, and source links.
- Read [references/workflows.md](references/workflows.md) for transcript, analytics, upload, AskFred, and generic-operation recipes.
- Read [references/webhook-security.md](references/webhook-security.md) when receiving or implementing Webhooks V2.
- Read [references/troubleshooting.md](references/troubleshooting.md) after an auth, GraphQL, limit, permission, pagination, or webhook failure.
- Read [references/source-index.md](references/source-index.md) when validating source scope or documentation freshness.
## Boundaries
The CLI never starts a webhook server, stores API keys, or guesses undocumented GraphQL fields.
If an ergonomic command does not cover the needed current operation, use `query` or `mutation`
with a documented GraphQL document. The generic mutation command still requires `--confirm`.
+48
View File
@@ -0,0 +1,48 @@
# Fireflies API Reference
## Model and Auth
Fireflies exposes GraphQL at `https://api.fireflies.ai/graphql`. Send JSON with `query`, optional
`variables`, and optional `operationName`; authenticate with `Authorization: Bearer <API key>`.
Queries are read-only. Mutations create, update, or delete server-side state.
Use `scripts/fireflies query` and `scripts/fireflies mutation` as the compatibility escape hatch
for every documented current or future operation. The CLI rejects mutations on `query`, queries on
`mutation`, and requires `--confirm` for a non-dry-run generic mutation.
## Documented Families
| Family | Documented operation |
|---|---|
| Meetings | `transcripts`, `transcript`, `deleteTranscript`, `updateMeetingTitle`, `updateMeetingPrivacy`, `updateMeetingState`, `shareMeeting`, `revokeSharedMeetingAccess` |
| Workspace | `user`, `users`, `contacts`, `channels`, `channel`, `user_groups` |
| Content | `bites`, `bite`, `createBite`, `apps`, `analytics` |
| Live | `active_meetings`, `live_action_items`, `createLiveActionItem` |
| Automation | `auditEvents`, `rule_executions_by_meeting`, `uploadAudio` |
| AskFred | `askfred_threads`, `askfred_thread`, `createAskFredThread`, `continueAskFredThread`, `deleteAskFredThread` |
The ergonomic CLI documents use conservative selections. `live add` calls
`createLiveActionItem(input: CreateLiveActionItemInput!)` with `meeting_id` and `prompt` only.
Use the generic `mutation` escape hatch for Add to Live (`addToLiveMeeting`) or any operation not
represented by an ergonomic command. Use introspection only through `schema introspect`; availability
depends on the deployment.
## Pagination and Limits
`transcripts` uses `limit` plus `skip`, with a documented maximum limit of 50. The CLI rejects a
higher value. Bites also support `limit` and `skip`; their list query requires one of `mine`,
`transcript_id`, or `my_team`. Respect plan limits: Free is 50 requests/day, Pro 500/day, and
Business/Enterprise 60/minute. Add to Live is 3 per 20 minutes, meeting sharing is 10/hour, and
`deleteTranscript` is 10/minute.
## Errors and Access
GraphQL errors are returned in `errors` and can include `message`, `code`, `friendly`, and
`extensions.helpUrls`. The CLI preserves that response on stdout in JSON mode and exits 5.
`too_many_requests` can include `retryAfter`; delay before retrying. Audit events and rule execution
logs have Enterprise/admin restrictions. Do not infer the caller's plan or authorization locally.
AskFred mutations require AI credits.
## Primary Sources
See [source-index.md](source-index.md) for dated primary documentation URLs and claim scope.
+34
View File
@@ -0,0 +1,34 @@
# CLI Reference
Run `scripts/fireflies --help` for the authoritative interface. Global flags work before or after
subcommands: `--json`, `--api-key`, `--endpoint`, `--timeout`, `--dry-run`, `--quiet`, and `--verbose`.
`--json` writes one JSON document to stdout; diagnostics are stderr-only.
## Commands
| Command | Purpose |
|---|---|
| `query --document DOC [--variables JSON|--variables-file PATH]` | Generic read-only GraphQL |
| `mutation --document DOC ... --confirm` | Generic mutation |
| `transcripts list|get|delete` | Search, inspect, or delete meetings |
| `users`, `contacts`, `channels`, `groups`, `bites`, `apps` | Workspace/content reads |
| `analytics`, `meetings active`, `live-action-items` | Analytics and live data |
| `meetings rename|privacy|state|share|revoke-share` | Meeting mutations |
| `bites create`, `live add`, `audio upload` | Documented creation/upload mutations; `live add` creates a live action item from a meeting ID and prompt |
| `askfred threads|create|continue|delete` | AskFred workflow |
| `audit-events`, `rule-executions` | Enterprise/admin queries |
| `webhook verify` | Local signature check, no network |
| `schema introspect` | Explicit GraphQL introspection |
## Examples
```bash
scripts/fireflies query --document 'query { user { name } }' --json
scripts/fireflies transcripts list --from-date 2026-01-01 --to-date 2026-01-31 --limit 50 --json
scripts/fireflies mutation --document 'mutation X { ... }' --confirm --dry-run --json
scripts/fireflies webhook verify --secret "$WEBHOOK_SECRET" --signature sha256=... --body payload.json --json
```
Variables must be a JSON object, inline or from a file. `--dry-run` returns the exact endpoint and
payload and does not need an API key. Exit codes: 2 usage/input, 3 configuration, 4 transport/HTTP,
5 GraphQL errors, 6 confirmation refused.
+23
View File
@@ -0,0 +1,23 @@
# Source Index
Access date: 2026-07-15. Sources are primary Fireflies documentation only.
| Source | Relevant claim and scope |
|---|---|
| https://docs.fireflies.ai/getting-started/introduction | API introduction and GraphQL model |
| https://docs.fireflies.ai/fundamentals/authorization | Bearer authorization and API endpoint |
| https://docs.fireflies.ai/fundamentals/limits | Plan, sharing, and Add to Live limits |
| https://docs.fireflies.ai/fundamentals/errors | GraphQL `errors` response shape |
| https://docs.fireflies.ai/fundamentals/introspection | Introspection behavior |
| https://docs.fireflies.ai/graphql-api/query/transcripts | Transcript filters, pagination, and fields |
| https://docs.fireflies.ai/graphql-api/query/transcript | Single transcript fields |
| https://docs.fireflies.ai/graphql-api/query/analytics | Analytics variables and selections |
| https://docs.fireflies.ai/graphql-api/mutation/create-live-action-item | `createLiveActionItem` input and response |
| https://docs.fireflies.ai/graphql-api/mutation/delete-askfred-thread | `deleteAskFredThread` response selection |
| https://docs.fireflies.ai/graphql-api/query/audit-events | Conservative audit event query selection |
| https://docs.fireflies.ai/graphql-api/webhooks-v2 | V2 event names, HMAC signature, response deadline |
| https://docs.fireflies.ai/graphql-api/mutation/delete-transcript | `deleteTranscript` mutation and limit |
| https://docs.fireflies.ai/graphql-api/mutation/update-meeting-title | `updateMeetingTitle` input |
| https://docs.fireflies.ai/graphql-api/mutation/upload-audio | `uploadAudio` input |
| https://docs.fireflies.ai/askfred/overview | AskFred operation family and AI-credit requirement |
| https://docs.fireflies.ai/llms-full.txt | Current primary query/mutation schema documentation used for CLI documents |
+10
View File
@@ -0,0 +1,10 @@
# Troubleshooting
- **Missing key:** set `FIREFLIES_API_KEY`, pass `--api-key`, or use `--dry-run`. Help and local webhook checks need no key.
- **Authentication failure:** verify `Authorization: Bearer <key>` and regenerate/check the key in Fireflies Integrations.
- **GraphQL error:** inspect JSON `errors`, especially `code` and `extensions.helpUrls`; do not retry malformed documents unchanged.
- **`too_many_requests`:** honor `retryAfter` when present and reduce request rate.
- **Permission or plan restriction:** audit events/rule executions can require Enterprise/admin access; AskFred mutations need AI credits. Escalate access, do not infer it.
- **Pagination:** use transcript `limit` (50 or less) and `skip`; use documented cursors for audit/rule queries.
- **Webhook verification failure:** compare the raw body, unmodified signature header, and the correct shared secret. Do not verify re-serialized JSON.
- **Safe escalation:** capture the command (without credentials), exit code, response error code, and help URL; provide these to Fireflies support.
+14
View File
@@ -0,0 +1,14 @@
# Webhook Security
Webhooks V2 events are `meeting.transcribed`, `meeting.summarized`, and `meeting.bot_joined`.
Fireflies sends `X-Hub-Signature: sha256=<hex>`, an HMAC-SHA256 over the exact raw request body.
Verify it before parsing or acting on the event. Use timing-safe comparison.
```bash
scripts/fireflies webhook verify --secret "$WEBHOOK_SECRET" --signature "$X_HUB_SIGNATURE" --body request-body.json --json
```
Use `--body -` to read raw bytes from stdin. This command is a local verifier, never makes a
network call, and does not print the secret. A receiving endpoint should return a 2xx response
within 10 seconds. Persist or queue work after verification; handle retries idempotently and avoid
assuming delivery order. Test valid and invalid signatures before deployment.
+57
View File
@@ -0,0 +1,57 @@
# Workflows
## List, Filter, and Read a Transcript
Use this to locate meetings and then inspect their content.
```bash
scripts/fireflies transcripts list --keyword roadmap --organizers owner@example.com --limit 10 --json
scripts/fireflies transcripts get transcript-id --json
```
The detailed query includes summary, speakers, sentences, and analytics.
## Extract Actions and Summary
Use this for a currently live meeting or for a completed transcript.
```bash
scripts/fireflies live-action-items meeting-id --json
scripts/fireflies transcripts get transcript-id --json
```
## Team Analytics
Use a bounded time range for trend reporting.
```bash
scripts/fireflies analytics --start 2026-01-01 --end 2026-01-31 --json
```
## Upload Audio with Webhook Correlation
Use a publicly retrievable HTTPS media URL and a webhook endpoint you control. Preview first.
```bash
scripts/fireflies audio upload --url https://media.example/call.mp3 --webhook https://app.example/fireflies --client-reference-id import-42 --dry-run --json
scripts/fireflies audio upload --url https://media.example/call.mp3 --webhook https://app.example/fireflies --client-reference-id import-42 --confirm --json
```
## AskFred Conversation
Use AskFred for natural-language analysis when the account has AI credits.
```bash
scripts/fireflies askfred create --question 'What decisions were made?' --transcript-id transcript-id --confirm --json
scripts/fireflies askfred continue thread-id --question 'Who owns the follow-up?' --confirm --json
```
## Generic GraphQL
Use this when a documented operation is newer than the CLI's ergonomic commands. Copy the document
from Fireflies primary documentation, pass variables as JSON, and preview mutations before confirmation.
```bash
scripts/fireflies query --document 'query { user { name } }' --json
scripts/fireflies mutation --document 'mutation Example($input: SomeInput!) { someMutation(input: $input) { success } }' --variables '{"input":{}}' --dry-run --json
```
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""Fireflies GraphQL CLI. Requires Python 3.9+ and no third-party packages."""
import argparse
import hashlib
import hmac
import json
import os
import re
import sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
DEFAULT_ENDPOINT = "https://api.fireflies.ai/graphql"
EX_USAGE, EX_CONFIG, EX_TRANSPORT, EX_GRAPHQL, EX_CONFIRM = 2, 3, 4, 5, 6
DOCS = {
"transcripts_list": "query Transcripts($keyword: String, $scope: TranscriptsQueryScope, $fromDate: DateTime, $toDate: DateTime, $limit: Int, $skip: Int, $hostEmail: String, $userId: String, $mine: Boolean, $organizers: [String], $participants: [String], $channelId: String) { transcripts(keyword: $keyword, scope: $scope, fromDate: $fromDate, toDate: $toDate, limit: $limit, skip: $skip, host_email: $hostEmail, user_id: $userId, mine: $mine, organizers: $organizers, participants: $participants, channel_id: $channelId) { id title date duration organizer_email participants transcript_url } }",
"transcript": "query Transcript($id: String!) { transcript(id: $id) { id title date duration organizer_email participants transcript_url summary { overview } speakers { id name } sentences { index text speaker_name start_time end_time } analytics { sentiments { negative_pct neutral_pct positive_pct } } } }",
"users": "query Users { users { user_id email name is_admin integrations } }",
"me": "query User { user { user_id email name is_admin integrations } }",
"user": "query User($id: String!) { user(id: $id) { user_id email name is_admin integrations } }",
"contacts": "query Contacts { contacts { email name picture last_meeting_date } }",
"channels": "query Channels { channels { id title is_private created_at updated_at created_by members { user_id email name } } }",
"channel": "query Channel($id: ID!) { channel(id: $id) { id title is_private created_at updated_at created_by members { user_id email name } } }",
"groups": "query UserGroups($mine: Boolean) { user_groups(mine: $mine) { id name handle members { user_id first_name last_name email } } }",
"bites": "query Bites($mine: Boolean, $transcriptId: ID, $myTeam: Boolean, $limit: Int, $skip: Int) { bites(mine: $mine, transcript_id: $transcriptId, my_team: $myTeam, limit: $limit, skip: $skip) { id transcript_id name status summary start_time end_time } }",
"bite": "query Bite($id: ID!) { bite(id: $id) { id transcript_id name status summary start_time end_time } }",
"apps": "query GetAIAppsOutputs($appId: String, $transcriptId: String, $skip: Float, $limit: Float) { apps(app_id: $appId, transcript_id: $transcriptId, skip: $skip, limit: $limit) { outputs { transcript_id user_id app_id created_at title prompt response } } }",
"analytics": "query Analytics($startTime: String, $endTime: String) { analytics(start_time: $startTime, end_time: $endTime) { team { conversation { average_filler_words } meeting { count duration } } users { user_id user_name user_email conversation { talk_listen_pct } meeting { count duration } } } }",
"active": "query ActiveMeetings { active_meetings { id title organizer_email meeting_link start_time end_time privacy state } }",
"live_items": "query LiveActionItems($id: ID!) { live_action_items(meeting_id: $id) { name action_item } }",
"audit": "query AuditEvents($limit: Int, $cursor: String, $filters: AuditEventFiltersInput!) { auditEvents(limit: $limit, cursor: $cursor, filters: $filters) { events { id time action actor { user_id } resource { type id } } has_more next_cursor } }",
"rules": "query RuleExecutions($limit: Int, $cursor: String, $filters: RuleExecutionFiltersInput) { rule_executions_by_meeting(limit: $limit, cursor: $cursor, filters: $filters) { meetings { meeting_id meeting { id title organizer_email } executions { extension_id extension_title stopped_at user_name } } has_more next_cursor } }",
"askfred_threads": "query GetAskFredThreads { askfred_threads { id title transcript_id user_id created_at } }",
"delete": "mutation DeleteTranscript($id: String!) { deleteTranscript(id: $id) { id title organizer_email date duration } }",
"rename": "mutation UpdateMeetingTitle($input: UpdateMeetingTitleInput!) { updateMeetingTitle(input: $input) { title } }",
"privacy": "mutation UpdateMeetingPrivacy($input: UpdateMeetingPrivacyInput!) { updateMeetingPrivacy(input: $input) { id title privacy } }",
"state": "mutation UpdateMeetingState($input: UpdateMeetingStateInput!) { updateMeetingState(input: $input) { success action } }",
"share": "mutation ShareMeeting($input: ShareMeetingInput!) { shareMeeting(input: $input) { success message } }",
"revoke": "mutation RevokeSharedMeetingAccess($input: RevokeSharedMeetingAccessInput!) { revokeSharedMeetingAccess(input: $input) { success message } }",
"bite_create": "mutation CreateBite($transcriptId: ID!, $start: Float!, $end: Float!, $name: String, $type: String, $summary: String, $privacy: [String]) { createBite(transcript_id: $transcriptId, start_time: $start, end_time: $end, name: $name, media_type: $type, summary: $summary, privacies: $privacy) { id name status } }",
"live_add": "mutation CreateLiveActionItem($input: CreateLiveActionItemInput!) { createLiveActionItem(input: $input) { success } }",
"upload": "mutation UploadAudio($input: AudioUploadInput) { uploadAudio(input: $input) { success title message } }",
"askfred_create": "mutation CreateThread($input: CreateAskFredThreadInput!) { createAskFredThread(input: $input) { message { id thread_id query answer suggested_queries status created_at } } }",
"askfred_continue": "mutation ContinueThread($input: ContinueAskFredThreadInput!) { continueAskFredThread(input: $input) { message { id thread_id query answer suggested_queries status created_at } } }",
"askfred_delete": "mutation DeleteThread($id: String!) { deleteAskFredThread(id: $id) { id title transcript_id user_id created_at } }",
"introspection": "query IntrospectionQuery { __schema { queryType { name } mutationType { name } types { name kind } } }",
}
def fail(message, code=EX_USAGE):
print("Error: " + message, file=sys.stderr)
raise SystemExit(code)
def json_value(value, label):
try: return json.loads(value)
except json.JSONDecodeError as exc: fail(f"{label} must be valid JSON: {exc}")
def globals_anywhere(argv):
values = {"json": False, "dry_run": False, "quiet": False, "verbose": False,
"api_key": None, "endpoint": None, "timeout": 30.0}
result, i = [argv[0]], 1
flags = {"--json": ("json", False), "--dry-run": ("dry_run", False), "--quiet": ("quiet", False), "--verbose": ("verbose", False), "--api-key": ("api_key", True), "--endpoint": ("endpoint", True), "--timeout": ("timeout", True)}
while i < len(argv):
arg = argv[i]
if arg in flags:
key, takes_value = flags[arg]
if takes_value:
if i + 1 == len(argv): fail(f"{arg} requires a value")
values[key] = argv[i + 1]; i += 2
else: values[key] = True; i += 1
else: result.append(arg); i += 1
try: values["timeout"] = float(values["timeout"])
except ValueError: fail("--timeout must be a number")
return values, result
def operation_kind(document):
cleaned = re.sub(r"(?:#.*$|\s)+", " ", document).lstrip()
match = re.match(r"(?:query|mutation)\b", cleaned, re.I)
if match: return match.group(0).lower()
return "query" if cleaned.startswith("{") else None
class Client:
def __init__(self, options): self.options = options
def run(self, document, variables=None, operation_name=None):
payload = {"query": document, "variables": variables or {}}
if operation_name: payload["operationName"] = operation_name
if self.options["dry_run"]: return {"dry_run": True, "endpoint": self.endpoint, "payload": payload}, 0
if not self.api_key: fail("FIREFLIES_API_KEY is not set; use --api-key or --dry-run.", EX_CONFIG)
body = json.dumps(payload).encode("utf-8")
request = Request(self.endpoint, body, {"Authorization": "Bearer " + self.api_key, "Content-Type": "application/json", "Accept": "application/json"})
try:
with urlopen(request, timeout=self.options["timeout"]) as response: raw = response.read().decode("utf-8")
except HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
try:
data = json.loads(raw)
return data, EX_GRAPHQL if data.get("errors") else EX_TRANSPORT
except json.JSONDecodeError: fail(f"HTTP {exc.code}: {raw[:300]}", EX_TRANSPORT)
except URLError as exc: fail(f"transport error: {exc.reason}", EX_TRANSPORT)
try: data = json.loads(raw)
except json.JSONDecodeError: fail("server returned invalid JSON", EX_TRANSPORT)
return data, EX_GRAPHQL if data.get("errors") else 0
@property
def endpoint(self): return self.options["endpoint"] or os.getenv("FIREFLIES_ENDPOINT") or DEFAULT_ENDPOINT
@property
def api_key(self): return self.options["api_key"] or os.getenv("FIREFLIES_API_KEY")
def emit(data, options, code=0):
print(json.dumps(data, indent=None if options["json"] else 2, sort_keys=True, default=str))
if code == EX_GRAPHQL:
errors = data.get("errors", []) if isinstance(data, dict) else []
print("GraphQL error: " + str(errors[0].get("message", "unknown error") if errors else "unknown error"), file=sys.stderr)
raise SystemExit(code)
def add_example(parser, text): parser.epilog = "Examples:\n " + text
def add_confirm(parser): parser.add_argument("--confirm", action="store_true", help="Required literal confirmation for a real mutation.")
def add_doc_args(parser):
parser.add_argument("--document", required=True, help="GraphQL document."); parser.add_argument("--variables", help="Variables JSON object."); parser.add_argument("--variables-file", help="Path to variables JSON object."); parser.add_argument("--operation-name", help="GraphQL operationName.")
def ensure_confirm(args, options):
if not options["dry_run"] and not args.confirm: fail("a real mutation requires --confirm", EX_CONFIRM)
def run_doc(client, options, document, variables=None, operation_name=None):
data, code = client.run(document, variables, operation_name); emit(data, options, code)
def transcript_vars(args):
values = {"keyword": args.keyword, "scope": args.scope, "fromDate": args.from_date, "toDate": args.to_date, "limit": args.limit, "skip": args.skip, "hostEmail": args.host_email, "userId": args.user_id, "mine": args.mine, "organizers": split_csv(args.organizers), "participants": split_csv(args.participants), "channelId": args.channel_id}
if args.limit > 50: fail("--limit cannot exceed Fireflies' documented maximum of 50")
return {k:v for k,v in values.items() if v is not None}
def split_csv(value): return value.split(",") if value else None
def build_parser():
parser = argparse.ArgumentParser(prog="fireflies", description="Source-faithful Fireflies.ai GraphQL CLI.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Global flags work before or after commands. Example: fireflies transcripts list --limit 10 --json")
parser.add_argument("--json", action="store_true", help="Emit one JSON document."); parser.add_argument("--api-key", help="Override FIREFLIES_API_KEY."); parser.add_argument("--endpoint", help="Override FIREFLIES_ENDPOINT."); parser.add_argument("--timeout", type=float, help="HTTP timeout seconds."); parser.add_argument("--dry-run", action="store_true", help="Show exact GraphQL payload without HTTP."); parser.add_argument("--quiet", action="store_true", help="Suppress optional diagnostics."); parser.add_argument("--verbose", action="store_true", help="Enable diagnostics on stderr.")
root = parser.add_subparsers(dest="root", required=True)
for name, kind in (("query", "query"), ("mutation", "mutation")):
p = root.add_parser(name, help=f"Run a generic GraphQL {kind}."); add_doc_args(p)
if kind == "mutation": add_confirm(p)
add_example(p, f"fireflies {name} --document '{kind} {{ ... }}'" + (" --confirm" if kind == "mutation" else ""))
t = root.add_parser("transcripts", help="Read or delete transcripts."); ts = t.add_subparsers(dest="action", required=True)
p = ts.add_parser("list", help="List transcripts.");
for flag, dest in (("--keyword","keyword"),("--scope","scope"),("--from-date","from_date"),("--to-date","to_date"),("--host-email","host_email"),("--user-id","user_id"),("--organizers","organizers"),("--participants","participants"),("--channel-id","channel_id")): p.add_argument(flag, dest=dest)
p.add_argument("--limit", type=int, default=10); p.add_argument("--skip", type=int, default=0); p.add_argument("--mine", action="store_true", default=None); add_example(p, "fireflies transcripts list --keyword roadmap --limit 10")
p=ts.add_parser("get", help="Get transcript details."); p.add_argument("id"); add_example(p,"fireflies transcripts get transcript-id")
p=ts.add_parser("delete", help="Delete a transcript."); p.add_argument("id"); add_confirm(p); add_example(p,"fireflies transcripts delete transcript-id --confirm")
u=root.add_parser("users", help="Read users."); us=u.add_subparsers(dest="action",required=True)
for action in ("me","list","get"):
p=us.add_parser(action);
if action == "get": p.add_argument("id")
add_example(p,f"fireflies users {action}" + (" user-id" if action=="get" else ""))
simple = {"contacts":"Contacts","channels":"Channels","groups":"User groups","apps":"AI app outputs"}
for name, help_text in simple.items():
p=root.add_parser(name, help=help_text); sub=p.add_subparsers(dest="action", required=True); q=sub.add_parser("list"); add_example(q,f"fireflies {name} list")
if name == "channels": q=sub.add_parser("get"); q.add_argument("id"); add_example(q,"fireflies channels get channel-id")
if name == "apps": q.add_argument("--app-id"); q.add_argument("--transcript-id"); q.add_argument("--limit",type=float); q.add_argument("--skip",type=float)
if name == "groups": q.add_argument("--mine",action="store_true",default=None)
b=root.add_parser("bites",help="Read or create Bites."); bs=b.add_subparsers(dest="action",required=True); p=bs.add_parser("list"); p.add_argument("--mine",action="store_true",default=None);p.add_argument("--transcript-id");p.add_argument("--my-team",action="store_true",default=None);p.add_argument("--limit",type=int);p.add_argument("--skip",type=int);add_example(p,"fireflies bites list --mine")
p=bs.add_parser("get");p.add_argument("id");add_example(p,"fireflies bites get bite-id")
p=bs.add_parser("create");p.add_argument("--transcript-id",required=True);p.add_argument("--start",type=float,required=True);p.add_argument("--end",type=float,required=True);p.add_argument("--name");p.add_argument("--type");p.add_argument("--summary");p.add_argument("--privacy",action="append");add_confirm(p);add_example(p,"fireflies bites create --transcript-id id --start 0 --end 30 --confirm")
p=root.add_parser("analytics",help="Get team/user analytics.");p.add_argument("--start",required=True);p.add_argument("--end",required=True);add_example(p,"fireflies analytics --start 2026-01-01 --end 2026-01-31")
m=root.add_parser("meetings",help="Manage documented meeting properties."); ms=m.add_subparsers(dest="action",required=True)
for action in ("active","rename","privacy","state","share","revoke-share"):
p=ms.add_parser(action); add_example(p,f"fireflies meetings {action} meeting-id" + (" --confirm" if action!="active" else ""))
if action!="active": p.add_argument("id");add_confirm(p)
if action=="rename":p.add_argument("--title",required=True)
if action=="privacy":p.add_argument("--privacy",required=True)
if action=="state":p.add_argument("--state",required=True,choices=("pause_recording","resume_recording"))
if action=="share":p.add_argument("--emails",required=True)
if action=="revoke-share":p.add_argument("--email",required=True)
p=root.add_parser("live-action-items",help="List a meeting's live action items.");p.add_argument("meeting_id");add_example(p,"fireflies live-action-items meeting-id")
p=root.add_parser("live",help="Create action items during a live meeting.");ls=p.add_subparsers(dest="action",required=True);p=ls.add_parser("add",help="Create a live action item.");p.add_argument("--meeting-id",required=True,help="Live meeting ID.");p.add_argument("--action-item",required=True,help="Natural-language action-item prompt.");add_confirm(p);add_example(p,"fireflies live add --meeting-id meeting-id --action-item 'Follow up' --confirm")
a=root.add_parser("audio",help="Upload remote audio/video.");au=a.add_subparsers(dest="action",required=True);p=au.add_parser("upload");p.add_argument("--url",required=True);p.add_argument("--title");p.add_argument("--webhook");p.add_argument("--language");p.add_argument("--save-video",action="store_true",default=None);p.add_argument("--client-reference-id");add_confirm(p);add_example(p,"fireflies audio upload --url https://example.com/call.mp3 --confirm")
af=root.add_parser("askfred",help="Use AskFred; requires AI credits for mutations.");afs=af.add_subparsers(dest="action",required=True);p=afs.add_parser("threads");add_example(p,"fireflies askfred threads")
p=afs.add_parser("create");p.add_argument("--question",required=True);p.add_argument("--transcript-id");p.add_argument("--transcript-ids");p.add_argument("--language");add_confirm(p);add_example(p,"fireflies askfred create --question 'What changed?' --transcript-id id --confirm")
p=afs.add_parser("continue");p.add_argument("thread_id");p.add_argument("--question",required=True);add_confirm(p);add_example(p,"fireflies askfred continue thread-id --question 'Why?' --confirm")
p=afs.add_parser("delete");p.add_argument("thread_id");add_confirm(p);add_example(p,"fireflies askfred delete thread-id --confirm")
for name, title in (("audit-events","Enterprise/admin audit event query"),("rule-executions","Enterprise/admin rule execution query")):
p=root.add_parser(name,help=title);p.add_argument("--limit",type=int);p.add_argument("--cursor");p.add_argument("--filter",required=True,help="Documented filter JSON.");add_example(p,f"fireflies {name} --filter '{{\"category\":\"...\"}}'")
w=root.add_parser("webhook",help="Local Webhooks V2 verification.");ws=w.add_subparsers(dest="action",required=True);p=ws.add_parser("verify");p.add_argument("--secret",required=True);p.add_argument("--signature",required=True);p.add_argument("--body",required=True);add_example(p,"fireflies webhook verify --secret secret --signature sha256=... --body payload.json")
s=root.add_parser("schema",help="Explicit schema tools.");ss=s.add_subparsers(dest="action",required=True);p=ss.add_parser("introspect",help="Run GraphQL introspection if deployment permits it.");add_example(p,"fireflies schema introspect")
def add_fallback_examples(current):
for action in current._actions:
if isinstance(action, argparse._SubParsersAction):
for child in action.choices.values():
if not child.epilog:
child.epilog = "Examples:\n fireflies --help"
add_fallback_examples(child)
add_fallback_examples(parser)
return parser
def main(argv=None):
options, filtered = globals_anywhere(argv or sys.argv)
parser = build_parser()
args = parser.parse_args(filtered[1:])
client = Client(options)
if args.root in ("query", "mutation"):
document = args.document
kind = operation_kind(document)
if kind != args.root: fail(f"{args.root} accepts only GraphQL {args.root} documents")
if args.root == "mutation": ensure_confirm(args, options)
if args.variables and args.variables_file: fail("use only one of --variables and --variables-file")
variables = json_value(args.variables, "--variables") if args.variables else (json_value(Path(args.variables_file).read_text(), "variables file") if args.variables_file else {})
if not isinstance(variables, dict): fail("variables must be a JSON object")
run_doc(client, options, document, variables, args.operation_name)
if args.root == "transcripts":
if args.action == "list": run_doc(client, options, DOCS["transcripts_list"], transcript_vars(args))
if args.action == "get": run_doc(client, options, DOCS["transcript"], {"id":args.id})
ensure_confirm(args, options); run_doc(client, options, DOCS["delete"], {"id":args.id})
if args.root == "users": run_doc(client, options, DOCS["users"] if args.action=="list" else (DOCS["me"] if args.action=="me" else DOCS["user"]), {} if args.action!="get" else {"id":args.id})
if args.root in ("contacts","channels","groups","apps"):
key = args.root if args.root != "channels" or args.action=="list" else "channel"; variables = {}
if args.root=="channels" and args.action=="get":variables={"id":args.id}
if args.root=="groups":variables={"mine":args.mine}
if args.root=="apps":variables={"appId":args.app_id,"transcriptId":args.transcript_id,"limit":args.limit,"skip":args.skip}
run_doc(client,options,DOCS[key],{k:v for k,v in variables.items() if v is not None})
if args.root == "bites":
if args.action=="list":
if not any((args.mine,args.transcript_id,args.my_team)): fail("bites list requires --mine, --transcript-id, or --my-team")
run_doc(client,options,DOCS["bites"],{k:v for k,v in {"mine":args.mine,"transcriptId":args.transcript_id,"myTeam":args.my_team,"limit":args.limit,"skip":args.skip}.items() if v is not None})
if args.action=="get":run_doc(client,options,DOCS["bite"],{"id":args.id})
ensure_confirm(args,options);run_doc(client,options,DOCS["bite_create"],{"transcriptId":args.transcript_id,"start":args.start,"end":args.end,"name":args.name,"type":args.type,"summary":args.summary,"privacy":args.privacy})
if args.root=="analytics":run_doc(client,options,DOCS["analytics"],{"startTime":args.start,"endTime":args.end})
if args.root=="meetings":
if args.action=="active":run_doc(client,options,DOCS["active"])
ensure_confirm(args,options)
if args.action=="rename": run_doc(client,options,DOCS["rename"],{"input":{"id":args.id,"title":args.title}})
if args.action=="privacy": run_doc(client,options,DOCS["privacy"],{"input":{"id":args.id,"privacy":args.privacy}})
if args.action=="state": run_doc(client,options,DOCS["state"],{"input":{"meeting_id":args.id,"action":args.state}})
if args.action=="share": run_doc(client,options,DOCS["share"],{"input":{"meeting_id":args.id,"emails":split_csv(args.emails)}})
run_doc(client,options,DOCS["revoke"],{"input":{"meeting_id":args.id,"email":args.email}})
if args.root=="live-action-items":run_doc(client,options,DOCS["live_items"],{"id":args.meeting_id})
if args.root=="live": ensure_confirm(args,options);run_doc(client,options,DOCS["live_add"],{"input":{"meeting_id":args.meeting_id,"prompt":args.action_item}})
if args.root=="audio":
ensure_confirm(args,options); inp={"url":args.url,"title":args.title,"webhook":args.webhook,"custom_language":args.language,"save_video":args.save_video,"client_reference_id":args.client_reference_id};run_doc(client,options,DOCS["upload"],{"input":{k:v for k,v in inp.items() if v is not None}})
if args.root=="askfred":
if args.action=="threads":run_doc(client,options,DOCS["askfred_threads"])
ensure_confirm(args,options)
if args.action=="create":
inp={"query":args.question,"transcript_id":args.transcript_id,"response_language":args.language}
if args.transcript_ids:inp["filters"]={"transcript_ids":json_value(args.transcript_ids,"--transcript-ids")}
run_doc(client,options,DOCS["askfred_create"],{"input":{k:v for k,v in inp.items() if v is not None}})
if args.action=="continue":run_doc(client,options,DOCS["askfred_continue"],{"input":{"thread_id":args.thread_id,"query":args.question}})
run_doc(client,options,DOCS["askfred_delete"],{"id":args.thread_id})
if args.root in ("audit-events","rule-executions"):run_doc(client,options,DOCS["audit" if args.root=="audit-events" else "rules"],{"limit":args.limit,"cursor":args.cursor,"filters":json_value(args.filter,"--filter")})
if args.root=="webhook":
body=sys.stdin.buffer.read() if args.body=="-" else Path(args.body).read_bytes(); prefix="sha256="; supplied=args.signature[len(prefix):] if args.signature.startswith(prefix) else ""; expected=hmac.new(args.secret.encode(),body,hashlib.sha256).hexdigest(); result={"valid":bool(supplied) and hmac.compare_digest(expected,supplied)}
try: result["event"]=json.loads(body).get("event")
except (json.JSONDecodeError,AttributeError): pass
emit(result,options)
if args.root=="schema":run_doc(client,options,DOCS["introspection"],{},"IntrospectionQuery")
if __name__ == "__main__":
main()
+85
View File
@@ -0,0 +1,85 @@
import hashlib
import hmac
import json
import os
import subprocess
import tempfile
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
CLI = Path(__file__).parents[1] / "scripts" / "fireflies"
class Handler(BaseHTTPRequestHandler):
calls = []
response = {"data": {"ok": True}}
status = 200
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
self.__class__.calls.append((dict(self.headers), json.loads(body)))
self.send_response(self.__class__.status); self.send_header("Content-Type", "application/json"); self.end_headers()
self.wfile.write(json.dumps(self.__class__.response).encode())
def log_message(self, *_): pass
class FirefliesTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = HTTPServer(("127.0.0.1", 0), Handler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True); cls.thread.start()
cls.endpoint = "http://127.0.0.1:%s/graphql" % cls.server.server_port
@classmethod
def tearDownClass(cls): cls.server.shutdown()
def run_cli(self, *args, key=False):
env = os.environ.copy(); env.pop("FIREFLIES_API_KEY", None)
if key: env["FIREFLIES_API_KEY"] = "test-key"
return subprocess.run(["python3", str(CLI), *args], text=True, capture_output=True, env=env)
def test_dry_run_needs_no_key_and_no_http(self):
Handler.calls.clear(); result=self.run_cli("transcripts","list","--dry-run","--json")
self.assertEqual(result.returncode,0); self.assertEqual(Handler.calls,[]); self.assertTrue(json.loads(result.stdout)["dry_run"])
def test_read_uses_bearer_and_payload(self):
Handler.calls.clear(); Handler.response={"data":{"users":[]}}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,0); self.assertEqual(Handler.calls[-1][0]["Authorization"],"Bearer test-key"); self.assertIn("query Users",Handler.calls[-1][1]["query"])
def test_graphql_error_json_and_exit_five(self):
Handler.response={"errors":[{"message":"denied","code":"auth_failed"}]}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,5); self.assertEqual(json.loads(result.stdout)["errors"][0]["message"],"denied"); self.assertIn("GraphQL error",result.stderr)
Handler.response={"data":{"ok":True}}
def test_http_error_with_graphql_errors_exits_five(self):
Handler.status=400; Handler.response={"errors":[{"message":"denied","code":"auth_failed"}]}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,5); self.assertEqual(json.loads(result.stdout)["errors"][0]["message"],"denied"); self.assertIn("GraphQL error",result.stderr)
Handler.status=200; Handler.response={"data":{"ok":True}}
def test_generic_guards(self):
self.assertEqual(self.run_cli("query","--document","mutation { x }","--json").returncode,2)
self.assertEqual(self.run_cli("mutation","--document","mutation { x }","--json").returncode,6)
def test_limit_json_and_help_examples(self):
self.assertEqual(self.run_cli("transcripts","list","--limit","51","--json").returncode,2)
result=self.run_cli("transcripts","list","--dry-run","--json"); json.loads(result.stdout); self.assertEqual(result.stderr,"")
self.assertIn("Examples:",self.run_cli("transcripts","list","--help").stdout)
def test_webhook_signatures(self):
with tempfile.NamedTemporaryFile("wb",delete=False) as f: f.write(b'{"event":"meeting.transcribed"}'); path=f.name
digest=hmac.new(b"test",Path(path).read_bytes(),hashlib.sha256).hexdigest()
self.assertTrue(json.loads(self.run_cli("webhook","verify","--secret","test","--signature","sha256="+digest,"--body",path,"--json").stdout)["valid"])
self.assertFalse(json.loads(self.run_cli("webhook","verify","--secret","test","--signature","sha256=bad","--body",path,"--json").stdout)["valid"])
os.unlink(path)
def test_mutation_dry_run_payload(self):
result=self.run_cli("meetings","rename","abc","--title","New","--dry-run","--json")
payload=json.loads(result.stdout)["payload"]; self.assertIn("updateMeetingTitle",payload["query"]); self.assertEqual(payload["variables"]["input"]["id"],"abc")
def test_source_faithful_dry_run_payloads(self):
live=json.loads(self.run_cli("live","add","--meeting-id","meeting-id","--action-item","Follow up","--dry-run","--json").stdout)["payload"]
self.assertIn("createLiveActionItem(input: $input) { success }",live["query"])
self.assertEqual(live["variables"],{"input":{"meeting_id":"meeting-id","prompt":"Follow up"}})
deleted=json.loads(self.run_cli("askfred","delete","thread-id","--dry-run","--json").stdout)["payload"]
self.assertIn("id title transcript_id user_id created_at",deleted["query"])
self.assertEqual(deleted["variables"],{"id":"thread-id"})
analytics=json.loads(self.run_cli("analytics","--start","2026-01-01","--end","2026-01-31","--dry-run","--json").stdout)["payload"]
self.assertIn("$startTime",analytics["query"]); self.assertIn("$endTime",analytics["query"])
self.assertEqual(analytics["variables"],{"startTime":"2026-01-01","endTime":"2026-01-31"})
transcript=json.loads(self.run_cli("transcripts","get","transcript-id","--dry-run","--json").stdout)["payload"]
self.assertIn("negative_pct neutral_pct positive_pct",transcript["query"])
audit=json.loads(self.run_cli("audit-events","--filter",'{"category":"MEETING_OPERATIONS"}',"--dry-run","--json").stdout)["payload"]
self.assertIn("events { id time action actor { user_id } resource { type id } }",audit["query"])
if __name__ == "__main__": unittest.main()
+20 -25
View File
@@ -1,44 +1,39 @@
# Cloudflare Bypass Proxy from the Terminal
Drive a FlareSolverr instance from the command line. FlareSolverr is an open-source proxy server that launches a headless Chrome browser to solve Cloudflare and DDoS-GUARD JavaScript challenges, returning the unblocked HTML and cookies to your client.
# FlareSolverr CLI: Browser-Backed Requests From the Terminal
## Why Install This Skill
When your agent loads this skill, it can **interact with any FlareSolverr proxy server** without writing raw HTTP requests. That means:
Use this skill when an authorized site requires a browser-backed request to handle a Cloudflare or DDoS-GUARD challenge. It gives an agent a small terminal interface for sending GET and POST requests through an existing private FlareSolverr service.
- **Check server health** — verify the FlareSolverr instance is running and ready before sending traffic
- **Manage browser sessions** — create, list, and destroy persistent headless Chrome sessions for fast, stateful scraping
- **Solve Cloudflare challenges** — fetch pages behind Cloudflare and DDoS-GUARD protection with a single command, getting back unblocked HTML, cookies, and user-agent strings
- **Structured output** — every command supports `--json` for piping into scripts and `--dry-run` for previewing API calls before making them
FlareSolverr is widely used in the *arr ecosystem (Prowlarr, Jackett) and by anyone who needs reliable access to Cloudflare-protected sites. This CLI makes it first-class for agent workflows.
It also provides a readiness probe and browser-session lifecycle commands, without requiring a Python package or raw HTTP request construction. Results are JSON, making them practical to inspect or pass to another tool.
## What You Get
| Directory | Purpose |
|-----------|---------|
| `SKILL.md` | Complete command reference with flag tables, examples, and gotchas |
| `scripts/flaresolverr-cli` | Single-file Python CLI (stdlib only, zero pip dependencies) |
| `tests/test_cli.py` | Deterministic smoke tests covering all commands in dry-run mode |
| Path | Purpose |
|---|---|
| `SKILL.md` | Agent-facing command reference and safe-use guidance. |
| `scripts/flaresolverr-cli` | Dependency-free Python CLI for FlareSolverr's `/v1` API. |
## Quick Start
```bash
# Start FlareSolverr (one-time setup)
docker run -d --name=flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
```sh
# Run FlareSolverr separately, then optionally set its address.
export FLARESOLVERR_SERVER="http://localhost:8191"
# Point the CLI at it
export FLARESOLVERR_URL="http://localhost:8191"
# Verify the service is ready.
python3 scripts/flaresolverr-cli health
# Verify it works
./scripts/flaresolverr-cli health
./scripts/flaresolverr-cli info
# Send a browser-backed request.
python3 scripts/flaresolverr-cli get https://example.com
```
`FLARESOLVERR_SERVER` defaults to `http://localhost:8191`. Add `--server URL` for a one-command override or `--timeout SECONDS` to change the 60-second HTTP timeout. Successful commands write JSON to stdout; request failures write JSON to stderr and exit nonzero.
## Triggers
Load this when the user mentions FlareSolverr, Cloudflare bypass, anti-bot proxy, headless browser proxy, or needs to fetch a page behind Cloudflare or DDoS-GUARD protection.
- A Cloudflare or DDoS-GUARD browser challenge blocks an authorized request.
- A workflow needs browser-backed GET or POST requests through an existing FlareSolverr service.
- A workflow needs to check FlareSolverr readiness or create, list, or destroy a session.
## Requirements
Python 3.8+ (stdlib only, no pip installs needed). A running [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) instance (Docker: `ghcr.io/flaresolverr/flaresolverr`). Set `FLARESOLVERR_URL` to the server address (defaults to `http://localhost:8191`).
Python 3.8+ with no pip dependencies. A running private [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) instance is required. Do not expose that service publicly, and use it only for sites you are authorized to access.
+28 -139
View File
@@ -1,161 +1,50 @@
---
name: flaresolverr-cli
description: 'Operate the full flaresolverr-cli command surface: named browser-session
lifecycle, structured health and service information, dry-run planning, cookie-only
returns, and challenge-solving GET or form-POST requests. Use for explicit
flaresolverr-cli or session-management requests; use the smaller flaresolverr skill
for a one-off health check or basic browser-backed retrieval.'
description: Use a small FlareSolverr JSON CLI for browser-backed GET and POST requests, readiness checks, and session lifecycle management when a site requires Cloudflare or DDoS-GUARD challenge handling.
license: MIT
compatibility: >-
Python 3.8+ (stdlib only, no pip deps). Requires a running FlareSolverr instance
(Docker: ghcr.io/flaresolverr/flaresolverr) and the FLARESOLVERR_URL env var set to
the server address (defaults to http://localhost:8191).
compatibility: Python 3.8+ (stdlib only). Requires a running FlareSolverr instance; FLARESOLVERR_SERVER defaults to http://localhost:8191.
metadata:
tags: flaresolverr, cloudflare, proxy, anti-bot, headless-browser, selenium, web-scraping
sources: "https://github.com/FlareSolverr/FlareSolverr, https://hub.docker.com/r/flaresolverr/flaresolverr"
tags: flaresolverr, cloudflare, proxy, anti-bot, headless-browser, web-scraping
sources: https://github.com/FlareSolverr/FlareSolverr, https://hub.docker.com/r/flaresolverr/flaresolverr
---
# flaresolverr-cli — Cloudflare Bypass Proxy from the Terminal
# FlareSolverr CLI
Drive a FlareSolverr instance from the command line. FlareSolverr is a proxy server that launches a headless Chrome browser to solve Cloudflare and DDoS-GUARD JavaScript challenges, returning the unblocked HTML, cookies, and user-agent to your client.
Use this CLI with a running private FlareSolverr service when ordinary HTTP requests encounter a browser challenge. It sends JSON commands to FlareSolverr's `/v1` API.
The CLI wraps all four API endpoints: service info, health check, the three `/v1` session commands (create/list/destroy), and both challenge-solving request commands (`request.get` and `request.post`). Every command supports `--json`, `--dry-run`, and `--timeout`.
## Quick Start
## Mutation Gate
`health`, `info`, and `sessions list` are read-only discovery. Creating or destroying a session changes FlareSolverr state, and `request get` or `request post` sends traffic to an external target.
> Confirm the target, scope, and rollback path before acting. Read-only discovery may proceed without confirmation.
Use `--dry-run` to review a planned mutation first. Session destruction and any request intended to change target state require an explicit user directive; this skill does not authorize deletion, privilege changes, authentication bypass, or irreversible cleanup.
## When not to use
Use [flaresolverr](../flaresolverr/SKILL.md) for a one-off health check or basic GET/POST when named sessions, cookie-only returns, dry-run planning, and the full command surface are unnecessary. Do not use either skill to bypass authentication, authorization, paywalls, or access controls.
## Setup
1. Start a FlareSolverr instance (Docker):
```bash
docker run -d --name=flaresolverr -p 8191:8191 \
ghcr.io/flaresolverr/flaresolverr:latest
```sh
export FLARESOLVERR_SERVER="http://localhost:8191"
python3 scripts/flaresolverr-cli health
```
2. Set the server URL:
`FLARESOLVERR_SERVER` is optional and defaults to `http://localhost:8191`. Use `--server URL` to override it for one command and `--timeout SECONDS` to set the HTTP timeout; the default is 60 seconds.
```bash
export FLARESOLVERR_URL="http://localhost:8191"
## Commands
```text
flaresolverr-cli [--server URL] [--timeout SECONDS] health
flaresolverr-cli [--server URL] [--timeout SECONDS] get URL [--session ID]
flaresolverr-cli [--server URL] [--timeout SECONDS] post URL [--session ID] [--data FORM_BODY]
flaresolverr-cli [--server URL] [--timeout SECONDS] session create
flaresolverr-cli [--server URL] [--timeout SECONDS] session list
flaresolverr-cli [--server URL] [--timeout SECONDS] session destroy [SESSION_ID]
```
`--help` and `--dry-run` work without a running server.
`health` is a readiness probe that sends the `sessions.list` JSON command to `/v1`; it does not call `GET /health`.
## Essential Commands
`get` sends `request.get`. `post` sends `request.post` and includes `FORM_BODY` as `postData` when `--data` is supplied. `--session ID` adds the given session to either request. Session commands create, list, or destroy FlareSolverr sessions; omitting `SESSION_ID` from `session destroy` sends an empty session value.
### health — Server health check
## Output And Errors
```bash
flaresolverr-cli health # check if server is reachable
flaresolverr-cli health --json # {"status": "ok"}
```
Successful commands print one JSON result to stdout. Request failures print a JSON error to stderr and return a nonzero status.
Calls `GET /health`. Returns `ok` when the server is running. Use as a readiness probe or pre-flight check before session/request commands.
## Requirements
### info — Service information
```bash
flaresolverr-cli info # version, user-agent, ready message
flaresolverr-cli info --json # machine-readable
```
Calls `GET /`. Returns the FlareSolverr version, the Chrome user-agent string, and whether the service is ready to accept requests.
### sessions create — Create a persistent browser session
```bash
flaresolverr-cli sessions create # auto-generated session ID
flaresolverr-cli sessions create --session my-session # custom session name
flaresolverr-cli sessions create --proxy socks5://proxy:1080 # with proxy
```
Creates a long-lived headless browser instance. Reuse the returned session ID in subsequent `request get` / `request post` calls for 10-100x faster requests (no browser startup overhead per call).
### sessions list — List active sessions
```bash
flaresolverr-cli sessions list # all active session IDs
```
Returns the IDs of every active persistent session. Each session holds a browser process — use this to audit resource usage before creating more.
### sessions destroy — Tear down a session
```bash
flaresolverr-cli sessions destroy --session my-session
```
Closes the browser and frees memory. Always destroy sessions when done — each idle session consumes significant RAM.
### request get — Fetch a URL through the solver
```bash
flaresolverr-cli request get --url https://example.com # basic
flaresolverr-cli request get --url https://example.com --session my-session # reuse session
flaresolverr-cli request get --url https://example.com --return-only-cookies # cookies only
flaresolverr-cli request get --url https://example.com --timeout 120 # 120s timeout
```
Sends `request.get` to the `/v1` endpoint. FlareSolverr launches Chrome (or reuses a session), navigates to the URL, solves any Cloudflare/DDoS-GUARD challenge, and returns the resolved HTML, cookies, and user-agent.
Flags:
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--url` | string | required | Target URL |
| `--session` | string | — | Reuse existing session (faster) |
| `--max-timeout` | int | 60000 | Challenge solve timeout (ms) |
| `--return-only-cookies` | flag | false | Omit HTML from response |
| `--proxy` | string | — | Per-request proxy URL |
| `--wait` | int | 0 | Extra wait after solve (seconds) |
### request post — POST through the solver
```bash
flaresolverr-cli request post --url https://example.com/form --data "a=1&b=2"
```
Same as `request get` but sends an `application/x-www-form-urlencoded` POST body. Accepts the same flags plus `--data` (the form-encoded body string).
## Global Flags
All flags work in any position:
```bash
flaresolverr-cli --json health
flaresolverr-cli --dry-run sessions create --session test
flaresolverr-cli --quiet request get --url https://example.com
flaresolverr-cli --timeout 30 request get --url https://example.com
```
| Flag | Effect |
|------|--------|
| `--json` | Output one JSON value to stdout (all diagnostics go to stderr) |
| `--dry-run` | Print the planned API call without making it |
| `--quiet` | Suppress non-essential output |
| `--timeout N` | HTTP request timeout in seconds (default 60) |
## Known Gotchas
- **No authentication** — FlareSolverr has no built-in auth. Expose it only on localhost or behind a reverse proxy with auth.
- **HTTP 200 for errors** — FlareSolverr always returns HTTP 200. Check the JSON `status` field (`"ok"` vs `"error"`) to determine success.
- **Session proxy precedence** — When a `--session` is provided, any `--proxy` flag is ignored. The session's proxy (set at create time) takes precedence.
- **Memory per session** — Each persistent session runs a full Chrome browser (~200-500 MB RAM). Destroy sessions promptly.
- **First request latency** — A stateless request (no `--session`) pays a browser cold-start cost of 3-10 seconds. Persistent sessions amortize this.
- **POST body format** — `--data` must be `application/x-www-form-urlencoded` format (`key=value&key2=value2`). Multipart and JSON bodies are not supported by FlareSolverr.
- **Selenium status limitation** — The `status` field in responses is always 200 (Selenium does not expose the real HTTP status). Trust the response body, not the status code.
Python 3.8+ with no pip dependencies and a running FlareSolverr instance. Keep the service private and use it only for sites you are authorized to access.
## References
- [Related FlareSolverr CLI](../flaresolverr/scripts/flaresolverr) Stdlib-only reference implementation for the same API.
- [FlareSolverr GitHub](https://github.com/FlareSolverr/FlareSolverr) Source, API docs, Docker Compose examples.
- [FlareSolverr Docker Hub](https://hub.docker.com/r/flaresolverr/flaresolverr) — Prebuilt images.
- [scripts/flaresolverr-cli](scripts/flaresolverr-cli) - Stdlib-only CLI implementation.
- [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) - Service and API documentation.