ffx-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ffx_cli-0.1.0/.github/workflows/ci.yml +31 -0
- ffx_cli-0.1.0/.github/workflows/publish.yml +44 -0
- ffx_cli-0.1.0/.gitignore +26 -0
- ffx_cli-0.1.0/CLAUDE.md +136 -0
- ffx_cli-0.1.0/PKG-INFO +9 -0
- ffx_cli-0.1.0/README.md +127 -0
- ffx_cli-0.1.0/ffx/__init__.py +1 -0
- ffx_cli-0.1.0/ffx/__main__.py +4 -0
- ffx_cli-0.1.0/ffx/api_client.py +261 -0
- ffx_cli-0.1.0/ffx/commands/__init__.py +29 -0
- ffx_cli-0.1.0/ffx/commands/action_items.py +74 -0
- ffx_cli-0.1.0/ffx/commands/auth.py +20 -0
- ffx_cli-0.1.0/ffx/commands/brief.py +45 -0
- ffx_cli-0.1.0/ffx/commands/export.py +129 -0
- ffx_cli-0.1.0/ffx/commands/get.py +64 -0
- ffx_cli-0.1.0/ffx/commands/list_cmd.py +68 -0
- ffx_cli-0.1.0/ffx/commands/search.py +62 -0
- ffx_cli-0.1.0/ffx/commands/speaker.py +84 -0
- ffx_cli-0.1.0/ffx/commands/summary.py +78 -0
- ffx_cli-0.1.0/ffx/commands/topics.py +82 -0
- ffx_cli-0.1.0/ffx/commands/transcript.py +91 -0
- ffx_cli-0.1.0/ffx/commands/week.py +121 -0
- ffx_cli-0.1.0/ffx/config.py +46 -0
- ffx_cli-0.1.0/ffx/models.py +107 -0
- ffx_cli-0.1.0/ffx/output.py +106 -0
- ffx_cli-0.1.0/pyproject.toml +30 -0
- ffx_cli-0.1.0/tests/__init__.py +0 -0
- ffx_cli-0.1.0/tests/conftest.py +10 -0
- ffx_cli-0.1.0/tests/test_api_client.py +108 -0
- ffx_cli-0.1.0/tests/test_commands/__init__.py +0 -0
- ffx_cli-0.1.0/tests/test_commands/test_action_items.py +64 -0
- ffx_cli-0.1.0/tests/test_commands/test_auth.py +33 -0
- ffx_cli-0.1.0/tests/test_commands/test_brief.py +58 -0
- ffx_cli-0.1.0/tests/test_commands/test_export.py +79 -0
- ffx_cli-0.1.0/tests/test_commands/test_get.py +61 -0
- ffx_cli-0.1.0/tests/test_commands/test_list.py +101 -0
- ffx_cli-0.1.0/tests/test_commands/test_search.py +72 -0
- ffx_cli-0.1.0/tests/test_commands/test_speaker.py +74 -0
- ffx_cli-0.1.0/tests/test_commands/test_summary.py +54 -0
- ffx_cli-0.1.0/tests/test_commands/test_topics.py +73 -0
- ffx_cli-0.1.0/tests/test_commands/test_transcript.py +100 -0
- ffx_cli-0.1.0/tests/test_commands/test_week.py +75 -0
- ffx_cli-0.1.0/tests/test_config.py +47 -0
- ffx_cli-0.1.0/uv.lock +881 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
with:
|
|
22
|
+
version: "latest"
|
|
23
|
+
|
|
24
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
25
|
+
run: uv python install ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: uv run pytest --cov=ffx --cov-report=term-missing
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v4
|
|
16
|
+
with:
|
|
17
|
+
version: "latest"
|
|
18
|
+
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: uv sync
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: uv run pytest
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: test
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Install uv
|
|
36
|
+
uses: astral-sh/setup-uv@v4
|
|
37
|
+
with:
|
|
38
|
+
version: "latest"
|
|
39
|
+
|
|
40
|
+
- name: Build
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Publish to PyPI
|
|
44
|
+
run: uv publish --trusted-publishing always
|
ffx_cli-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
# uv
|
|
13
|
+
.python-version
|
|
14
|
+
|
|
15
|
+
# ffx local state
|
|
16
|
+
.ffx/
|
|
17
|
+
|
|
18
|
+
# Planning docs (not committed)
|
|
19
|
+
TODOS.md
|
|
20
|
+
PLAN.md
|
|
21
|
+
*.plan.md
|
|
22
|
+
|
|
23
|
+
# Editor
|
|
24
|
+
.DS_Store
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
ffx_cli-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# ffx — Fireflies CLI
|
|
2
|
+
|
|
3
|
+
## Project
|
|
4
|
+
|
|
5
|
+
Python CLI tool for the Fireflies.ai GraphQL API. Wraps both primitive API operations
|
|
6
|
+
and higher-level workflow commands. Designed for developer scripting, shell pipelines,
|
|
7
|
+
and AI agent tooling.
|
|
8
|
+
|
|
9
|
+
## Dev Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync
|
|
13
|
+
uv pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Tests
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv run pytest
|
|
20
|
+
uv run pytest --cov=ffx --cov-report=term-missing # with coverage
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Test strategy: HTTP requests mocked with `respx`. CLI commands tested with
|
|
24
|
+
`typer.testing.CliRunner`. Config tests use `tmp_path` + `FFX_HOME` env override.
|
|
25
|
+
|
|
26
|
+
## Build and Publish
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv build
|
|
30
|
+
uv publish # requires PYPI_API_TOKEN env var
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
CI/CD: GitHub Actions triggers on git tag push → tests → build → publish.
|
|
34
|
+
|
|
35
|
+
## Locked Architecture Decisions
|
|
36
|
+
|
|
37
|
+
These were decided during design review. Do not change without a documented reason.
|
|
38
|
+
|
|
39
|
+
**CLI framework: Typer** (not Click)
|
|
40
|
+
- Type hint-driven argument parsing
|
|
41
|
+
- Auto-generates `--help` from docstrings
|
|
42
|
+
- Better DX for flat command tree design
|
|
43
|
+
|
|
44
|
+
**GraphQL client: gql[httpx]** via `HTTPXTransport` (not raw httpx)
|
|
45
|
+
- Handles GraphQL error envelope automatically
|
|
46
|
+
- Standard Python GraphQL client as of 2025
|
|
47
|
+
|
|
48
|
+
**API-direct architecture** (no local SQLite cache)
|
|
49
|
+
- All commands hit the Fireflies API directly
|
|
50
|
+
- No sync layer, no local DB, no cache
|
|
51
|
+
- Simpler, fewer failure modes, always-fresh data
|
|
52
|
+
|
|
53
|
+
## Dropped from Scope
|
|
54
|
+
|
|
55
|
+
- `ffx objections` — pattern-matched from summary text, unreliable. Do not add back
|
|
56
|
+
without a structured Fireflies API field or explicit --llm flag.
|
|
57
|
+
- `ffx risks` — same reason as objections.
|
|
58
|
+
- `ffx sync` / local SQLite — dropped after review. API-direct is simpler and sufficient.
|
|
59
|
+
Commits exist in git history if ever needed.
|
|
60
|
+
- `ffx serve --webhook` — cron + `ffx` commands covers this.
|
|
61
|
+
- MCP adapter — Fireflies already has one.
|
|
62
|
+
- `ffx export notion` — OAuth complexity for niche use case.
|
|
63
|
+
|
|
64
|
+
## Project Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
ffx/
|
|
68
|
+
├── __init__.py
|
|
69
|
+
├── api_client.py # Fireflies GraphQL client (gql + httpx)
|
|
70
|
+
├── models.py # Dataclasses: Transcript, ActionItem, Speaker, Topic, Summary
|
|
71
|
+
├── config.py # Config file (~/.ffx/config.yaml) read/write
|
|
72
|
+
├── output.py # Rich terminal formatting + JSON envelope
|
|
73
|
+
└── commands/
|
|
74
|
+
├── __init__.py
|
|
75
|
+
├── auth.py # ffx auth
|
|
76
|
+
├── list_cmd.py # ffx list
|
|
77
|
+
├── get.py # ffx get
|
|
78
|
+
├── search.py # ffx search
|
|
79
|
+
├── summary.py # ffx summary
|
|
80
|
+
├── export.py # ffx export
|
|
81
|
+
├── brief.py # ffx brief
|
|
82
|
+
├── action_items.py # ffx action-items
|
|
83
|
+
├── topics.py # ffx topics
|
|
84
|
+
├── speaker.py # ffx speaker
|
|
85
|
+
└── week.py # ffx week / ffx month
|
|
86
|
+
tests/
|
|
87
|
+
├── conftest.py # Shared fixtures
|
|
88
|
+
├── test_api_client.py
|
|
89
|
+
├── test_config.py
|
|
90
|
+
└── test_commands/
|
|
91
|
+
└── test_*.py # One test file per command
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Fireflies API Notes
|
|
95
|
+
|
|
96
|
+
- Endpoint: `https://api.fireflies.ai/graphql`
|
|
97
|
+
- Auth: `Authorization: Bearer <api_key>`
|
|
98
|
+
- `participants` field is `[String!]` (emails), not objects
|
|
99
|
+
- Summary fields (`action_items`, `bullet_gist`, `topics_discussed`, `outline`,
|
|
100
|
+
`shorthand_bullet`) are **strings** (newline-delimited), not lists. Only `keywords`
|
|
101
|
+
is a proper list. Use `Summary.*_list` properties to split into lists.
|
|
102
|
+
- Speaker analytics path: `analytics.speakers` (not `speaker_talk_time_percentage`)
|
|
103
|
+
- Speaker fields: `name`, `speaker_id`, `duration`, `duration_pct`, `word_count`,
|
|
104
|
+
`words_per_minute`, `filler_words`, `questions`, `longest_monologue`, `monologues_count`
|
|
105
|
+
|
|
106
|
+
## Error Handling
|
|
107
|
+
|
|
108
|
+
Standard exit codes:
|
|
109
|
+
- 0: success
|
|
110
|
+
- 1: user error (bad flag, missing required arg)
|
|
111
|
+
- 2: API error (Fireflies unreachable, rate limit exhausted)
|
|
112
|
+
- 3: not found
|
|
113
|
+
|
|
114
|
+
JSON error envelope (always use in `--json` mode):
|
|
115
|
+
```json
|
|
116
|
+
{"error": "message", "code": "ERROR_CODE", "hint": "Run: ffx auth"}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
On AuthError: print "API key may have expired. Run: ffx auth"
|
|
120
|
+
|
|
121
|
+
## Output Conventions
|
|
122
|
+
|
|
123
|
+
Standard JSON wrapper (all list-returning commands):
|
|
124
|
+
```json
|
|
125
|
+
{"source": "fireflies", "generated_at": "ISO_TIMESTAMP", "filters": {}, "results": []}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Global flags all commands support: `--json`
|
|
129
|
+
List commands also support: `--days N`, `--limit N`
|
|
130
|
+
|
|
131
|
+
Default `--limit` for `ffx list`: 10.
|
|
132
|
+
|
|
133
|
+
## References
|
|
134
|
+
|
|
135
|
+
- Fireflies API docs: https://docs.fireflies.ai
|
|
136
|
+
- CEO plan: ~/.gstack/projects/fireflies-cli/ceo-plans/2026-03-27-ffx-cli.md
|
ffx_cli-0.1.0/PKG-INFO
ADDED
ffx_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# ffx — Fireflies CLI
|
|
2
|
+
|
|
3
|
+
A command-line tool for the [Fireflies.ai](https://fireflies.ai) API. List meetings, read transcripts, pull action items, view speaker analytics, and export to JSON/Markdown/Obsidian/CSV.
|
|
4
|
+
|
|
5
|
+
All commands output JSON by default — pipe to `jq`, feed to scripts, or use `--table` for human-readable output.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ffx-cli
|
|
11
|
+
# or
|
|
12
|
+
uv tool install ffx-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Get your API key from [Fireflies Settings](https://app.fireflies.ai/integrations/custom/fireflies) and either:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ffx auth # interactive prompt, stores in ~/.ffx/config.yaml
|
|
21
|
+
export FIREFLIES_API_KEY=your-key # env var, overrides config file
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
### Basics
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ffx list # recent meetings (JSON)
|
|
30
|
+
ffx list --table # rich formatted table
|
|
31
|
+
ffx list --days 7 --limit 20 # last 7 days, up to 20
|
|
32
|
+
ffx list --participant alice@co.com # filter by attendee (repeatable)
|
|
33
|
+
|
|
34
|
+
ffx get <id> # single meeting details
|
|
35
|
+
ffx search "quarterly review" # keyword search
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Summaries & briefs
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ffx summary <id> # AI-generated summary
|
|
42
|
+
ffx brief <id> # formatted brief: gist, key points, action items, topics
|
|
43
|
+
ffx brief <id> --table # rich formatted version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Action items & topics
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ffx action-items # action items from last 7 days
|
|
50
|
+
ffx action-items --days 30 --filter alice
|
|
51
|
+
ffx topics # topics across recent meetings
|
|
52
|
+
ffx topics <id> # topics from one meeting
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Full transcript
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ffx transcript <id> # full text with speakers + timestamps (JSON)
|
|
59
|
+
ffx transcript <id> --table # readable format with timestamps
|
|
60
|
+
ffx transcript <id> --no-timestamps # just speaker: text
|
|
61
|
+
ffx transcript <id> --no-speakers # just text
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Speaker analytics
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ffx speaker <id> # talk time, word count, wpm, fillers, questions
|
|
68
|
+
ffx speaker <id> --table # visual bar chart
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Weekly & monthly rollups
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ffx week # this week: meeting count, hours, action items, top topics
|
|
75
|
+
ffx month # this month
|
|
76
|
+
ffx week --table # rich formatted
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Export
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ffx export <id> # JSON (default)
|
|
83
|
+
ffx export <id> --format md # Markdown
|
|
84
|
+
ffx export <id> --format obsidian # Markdown with YAML frontmatter
|
|
85
|
+
ffx export <id> --format csv # CSV
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Scripting examples
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Get all action items from last week as JSON
|
|
92
|
+
ffx action-items --days 7 | jq '.results[].text'
|
|
93
|
+
|
|
94
|
+
# Export all recent meetings to markdown files
|
|
95
|
+
ffx list --limit 50 | jq -r '.results[].id' | while read id; do
|
|
96
|
+
ffx export "$id" --format md > "meeting-$id.md"
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# Who talks the most in a meeting?
|
|
100
|
+
ffx speaker <id> | jq '.speakers | sort_by(.duration_pct) | reverse | .[0].name'
|
|
101
|
+
|
|
102
|
+
# Search and get briefs
|
|
103
|
+
ffx search "product launch" | jq -r '.results[].id' | xargs -I{} ffx brief {}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Auth
|
|
107
|
+
|
|
108
|
+
Two methods, env var takes precedence:
|
|
109
|
+
|
|
110
|
+
| Method | How |
|
|
111
|
+
|--------|-----|
|
|
112
|
+
| Config file | `ffx auth` stores key in `~/.ffx/config.yaml` (chmod 600) |
|
|
113
|
+
| Env var | `export FIREFLIES_API_KEY=your-key` |
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/bobtista/fireflies-cli.git
|
|
119
|
+
cd fireflies-cli
|
|
120
|
+
uv sync
|
|
121
|
+
uv pip install -e .
|
|
122
|
+
uv run pytest
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from gql import Client, gql
|
|
4
|
+
from gql.transport.httpx import HTTPXTransport
|
|
5
|
+
|
|
6
|
+
from ffx.models import (
|
|
7
|
+
ActionItem,
|
|
8
|
+
Sentence,
|
|
9
|
+
Speaker,
|
|
10
|
+
Summary,
|
|
11
|
+
Topic,
|
|
12
|
+
Transcript,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
FIREFLIES_API_URL = "https://api.fireflies.ai/graphql"
|
|
16
|
+
|
|
17
|
+
# participants is [String!] (emails), not objects
|
|
18
|
+
# analytics.speakers is the correct path (not speaker_talk_time_percentage)
|
|
19
|
+
LIST_TRANSCRIPTS_QUERY = gql("""
|
|
20
|
+
query Transcripts($limit: Int, $skip: Int, $keyword: String, $fromDate: DateTime, $toDate: DateTime, $participants: [String!]) {
|
|
21
|
+
transcripts(limit: $limit, skip: $skip, keyword: $keyword, fromDate: $fromDate, toDate: $toDate, participants: $participants) {
|
|
22
|
+
id
|
|
23
|
+
title
|
|
24
|
+
date
|
|
25
|
+
duration
|
|
26
|
+
organizer_email
|
|
27
|
+
participants
|
|
28
|
+
summary {
|
|
29
|
+
action_items keywords overview gist bullet_gist
|
|
30
|
+
shorthand_bullet outline short_summary short_overview
|
|
31
|
+
meeting_type topics_discussed
|
|
32
|
+
}
|
|
33
|
+
analytics {
|
|
34
|
+
speakers {
|
|
35
|
+
speaker_id name duration duration_pct word_count
|
|
36
|
+
words_per_minute filler_words questions
|
|
37
|
+
longest_monologue monologues_count
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
""")
|
|
43
|
+
|
|
44
|
+
GET_TRANSCRIPT_QUERY = gql("""
|
|
45
|
+
query Transcript($id: String!) {
|
|
46
|
+
transcript(id: $id) {
|
|
47
|
+
id
|
|
48
|
+
title
|
|
49
|
+
date
|
|
50
|
+
duration
|
|
51
|
+
organizer_email
|
|
52
|
+
participants
|
|
53
|
+
summary {
|
|
54
|
+
action_items keywords overview gist bullet_gist
|
|
55
|
+
shorthand_bullet outline short_summary short_overview
|
|
56
|
+
meeting_type topics_discussed
|
|
57
|
+
}
|
|
58
|
+
analytics {
|
|
59
|
+
speakers {
|
|
60
|
+
speaker_id name duration duration_pct word_count
|
|
61
|
+
words_per_minute filler_words questions
|
|
62
|
+
longest_monologue monologues_count
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
""")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
GET_TRANSCRIPT_WITH_SENTENCES_QUERY = gql("""
|
|
71
|
+
query Transcript($id: String!) {
|
|
72
|
+
transcript(id: $id) {
|
|
73
|
+
id
|
|
74
|
+
title
|
|
75
|
+
date
|
|
76
|
+
duration
|
|
77
|
+
organizer_email
|
|
78
|
+
participants
|
|
79
|
+
summary {
|
|
80
|
+
action_items keywords overview gist bullet_gist
|
|
81
|
+
shorthand_bullet outline short_summary short_overview
|
|
82
|
+
meeting_type topics_discussed
|
|
83
|
+
}
|
|
84
|
+
analytics {
|
|
85
|
+
speakers {
|
|
86
|
+
speaker_id name duration duration_pct word_count
|
|
87
|
+
words_per_minute filler_words questions
|
|
88
|
+
longest_monologue monologues_count
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
sentences {
|
|
92
|
+
index
|
|
93
|
+
speaker_name
|
|
94
|
+
speaker_id
|
|
95
|
+
text
|
|
96
|
+
raw_text
|
|
97
|
+
start_time
|
|
98
|
+
end_time
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
""")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AuthError(Exception):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ApiError(Exception):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_transcript(raw: dict) -> Transcript:
|
|
114
|
+
summary_raw = raw.get("summary") or {}
|
|
115
|
+
analytics_raw = raw.get("analytics") or {}
|
|
116
|
+
|
|
117
|
+
# Most summary fields come back as strings (with newlines), not lists.
|
|
118
|
+
# Only keywords is a proper list. We store raw and use .action_items_list etc.
|
|
119
|
+
summary = Summary(
|
|
120
|
+
overview=summary_raw.get("overview"),
|
|
121
|
+
short_overview=summary_raw.get("short_overview"),
|
|
122
|
+
gist=summary_raw.get("gist"),
|
|
123
|
+
short_summary=summary_raw.get("short_summary"),
|
|
124
|
+
bullet_gist=summary_raw.get("bullet_gist"),
|
|
125
|
+
shorthand_bullet=summary_raw.get("shorthand_bullet"),
|
|
126
|
+
action_items=summary_raw.get("action_items"),
|
|
127
|
+
keywords=summary_raw.get("keywords") or [],
|
|
128
|
+
topics_discussed=summary_raw.get("topics_discussed"),
|
|
129
|
+
outline=summary_raw.get("outline"),
|
|
130
|
+
meeting_type=summary_raw.get("meeting_type"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
participants = raw.get("participants") or []
|
|
134
|
+
|
|
135
|
+
speakers = [
|
|
136
|
+
Speaker(
|
|
137
|
+
name=s.get("name", ""),
|
|
138
|
+
speaker_id=s.get("speaker_id"),
|
|
139
|
+
duration=s.get("duration", 0),
|
|
140
|
+
duration_pct=s.get("duration_pct", 0.0),
|
|
141
|
+
word_count=s.get("word_count", 0),
|
|
142
|
+
words_per_minute=s.get("words_per_minute", 0.0),
|
|
143
|
+
filler_words=s.get("filler_words", 0),
|
|
144
|
+
questions=s.get("questions", 0),
|
|
145
|
+
longest_monologue=s.get("longest_monologue", 0),
|
|
146
|
+
monologues_count=s.get("monologues_count", 0),
|
|
147
|
+
)
|
|
148
|
+
for s in (analytics_raw.get("speakers") or [])
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
action_items = [
|
|
152
|
+
ActionItem(text=item, transcript_id=raw.get("id"))
|
|
153
|
+
for item in summary.action_items_list
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
topics = [
|
|
157
|
+
Topic(text=t, transcript_id=raw.get("id"))
|
|
158
|
+
for t in summary.topics_list
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
sentences = [
|
|
162
|
+
Sentence(
|
|
163
|
+
index=s.get("index", 0),
|
|
164
|
+
speaker_name=s.get("speaker_name", ""),
|
|
165
|
+
speaker_id=s.get("speaker_id"),
|
|
166
|
+
text=s.get("text", ""),
|
|
167
|
+
raw_text=s.get("raw_text", ""),
|
|
168
|
+
start_time=s.get("start_time", 0.0),
|
|
169
|
+
end_time=s.get("end_time", 0.0),
|
|
170
|
+
)
|
|
171
|
+
for s in (raw.get("sentences") or [])
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
return Transcript(
|
|
175
|
+
id=raw["id"],
|
|
176
|
+
title=raw.get("title", "Untitled"),
|
|
177
|
+
date=str(raw.get("date", "")),
|
|
178
|
+
duration=raw.get("duration", 0),
|
|
179
|
+
organizer_email=raw.get("organizer_email"),
|
|
180
|
+
participants=participants,
|
|
181
|
+
summary=summary,
|
|
182
|
+
speakers=speakers,
|
|
183
|
+
action_items=action_items,
|
|
184
|
+
topics=topics,
|
|
185
|
+
sentences=sentences,
|
|
186
|
+
raw=raw,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class FirefliesClient:
|
|
191
|
+
def __init__(self, api_key: str) -> None:
|
|
192
|
+
transport = HTTPXTransport(
|
|
193
|
+
url=FIREFLIES_API_URL,
|
|
194
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
195
|
+
)
|
|
196
|
+
self._client = Client(transport=transport, fetch_schema_from_transport=False)
|
|
197
|
+
|
|
198
|
+
def _execute(self, query, variables: dict | None = None) -> dict:
|
|
199
|
+
try:
|
|
200
|
+
result = self._client.execute(query, variable_values=variables or {})
|
|
201
|
+
return result
|
|
202
|
+
except Exception as e:
|
|
203
|
+
msg = str(e).lower()
|
|
204
|
+
if "429" in str(e) or "rate limit" in msg:
|
|
205
|
+
raise ApiError("rate limit exceeded — retry later") from e
|
|
206
|
+
if "unauthenticated" in msg or "unauthorized" in msg or "401" in str(e):
|
|
207
|
+
raise AuthError("API key missing or expired") from e
|
|
208
|
+
if any(code in str(e) for code in ("500", "502", "503")):
|
|
209
|
+
raise ApiError(f"Fireflies API error: {e}") from e
|
|
210
|
+
raise ApiError(str(e)) from e
|
|
211
|
+
|
|
212
|
+
def list_transcripts(
|
|
213
|
+
self,
|
|
214
|
+
limit: int = 10,
|
|
215
|
+
skip: int = 0,
|
|
216
|
+
keyword: str | None = None,
|
|
217
|
+
from_date: str | None = None,
|
|
218
|
+
to_date: str | None = None,
|
|
219
|
+
participants: list[str] | None = None,
|
|
220
|
+
) -> list[Transcript]:
|
|
221
|
+
vars: dict = {"limit": limit, "skip": skip}
|
|
222
|
+
if keyword:
|
|
223
|
+
vars["keyword"] = keyword
|
|
224
|
+
if from_date:
|
|
225
|
+
vars["fromDate"] = from_date
|
|
226
|
+
if to_date:
|
|
227
|
+
vars["toDate"] = to_date
|
|
228
|
+
if participants:
|
|
229
|
+
vars["participants"] = participants
|
|
230
|
+
result = self._execute(LIST_TRANSCRIPTS_QUERY, vars)
|
|
231
|
+
return [_parse_transcript(t) for t in (result.get("transcripts") or [])]
|
|
232
|
+
|
|
233
|
+
def get_transcript(self, transcript_id: str) -> Transcript | None:
|
|
234
|
+
result = self._execute(GET_TRANSCRIPT_QUERY, {"id": transcript_id})
|
|
235
|
+
raw = result.get("transcript")
|
|
236
|
+
if raw is None:
|
|
237
|
+
return None
|
|
238
|
+
return _parse_transcript(raw)
|
|
239
|
+
|
|
240
|
+
def get_transcript_with_sentences(self, transcript_id: str) -> Transcript | None:
|
|
241
|
+
result = self._execute(GET_TRANSCRIPT_WITH_SENTENCES_QUERY, {"id": transcript_id})
|
|
242
|
+
raw = result.get("transcript")
|
|
243
|
+
if raw is None:
|
|
244
|
+
return None
|
|
245
|
+
return _parse_transcript(raw)
|
|
246
|
+
|
|
247
|
+
def search_transcripts(
|
|
248
|
+
self,
|
|
249
|
+
query: str,
|
|
250
|
+
limit: int = 20,
|
|
251
|
+
from_date: str | None = None,
|
|
252
|
+
to_date: str | None = None,
|
|
253
|
+
participants: list[str] | None = None,
|
|
254
|
+
) -> list[Transcript]:
|
|
255
|
+
return self.list_transcripts(
|
|
256
|
+
limit=limit,
|
|
257
|
+
keyword=query,
|
|
258
|
+
from_date=from_date,
|
|
259
|
+
to_date=to_date,
|
|
260
|
+
participants=participants,
|
|
261
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from ffx.commands.auth import auth
|
|
3
|
+
from ffx.commands.list_cmd import list_cmd
|
|
4
|
+
from ffx.commands.get import get
|
|
5
|
+
from ffx.commands.search import search
|
|
6
|
+
from ffx.commands.summary import summary
|
|
7
|
+
from ffx.commands.brief import brief
|
|
8
|
+
from ffx.commands.action_items import action_items
|
|
9
|
+
from ffx.commands.export import export
|
|
10
|
+
from ffx.commands.topics import topics
|
|
11
|
+
from ffx.commands.speaker import speaker
|
|
12
|
+
from ffx.commands.transcript import transcript
|
|
13
|
+
from ffx.commands.week import week, month
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(name="ffx", help="Fireflies.ai CLI", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
app.command()(auth)
|
|
18
|
+
app.command("list")(list_cmd)
|
|
19
|
+
app.command()(get)
|
|
20
|
+
app.command()(search)
|
|
21
|
+
app.command()(summary)
|
|
22
|
+
app.command()(brief)
|
|
23
|
+
app.command("action-items")(action_items)
|
|
24
|
+
app.command()(export)
|
|
25
|
+
app.command()(transcript)
|
|
26
|
+
app.command()(topics)
|
|
27
|
+
app.command()(speaker)
|
|
28
|
+
app.command()(week)
|
|
29
|
+
app.command()(month)
|