ffx-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import typer
6
+ from ffx.config import Config
7
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
8
+ from ffx.output import json_envelope, json_error, transcripts_table
9
+
10
+
11
+ def list_cmd(
12
+ days: Optional[int] = typer.Option(None, "--days", help="Filter to last N days"),
13
+ limit: int = typer.Option(10, "--limit", help="Max results"),
14
+ participant: Optional[list[str]] = typer.Option(None, "--participant", help="Filter by attendee email (repeatable)"),
15
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
16
+ ) -> None:
17
+ """List your Fireflies meetings."""
18
+ json_mode = not table
19
+ config = Config()
20
+ if not config.api_key:
21
+ if json_mode:
22
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
23
+ else:
24
+ typer.echo("API key not set. Run: ffx auth", err=True)
25
+ raise typer.Exit(2)
26
+
27
+ from_date = None
28
+ if days:
29
+ from_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
30
+
31
+ try:
32
+ client = FirefliesClient(config.api_key)
33
+ transcripts = client.list_transcripts(limit=limit, from_date=from_date, participants=participant or None)
34
+ except AuthError:
35
+ if json_mode:
36
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
37
+ else:
38
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
39
+ raise typer.Exit(2)
40
+ except ApiError as e:
41
+ if json_mode:
42
+ typer.echo(json_error(str(e), "API_ERROR"))
43
+ else:
44
+ typer.echo(f"API error: {e}", err=True)
45
+ raise typer.Exit(2)
46
+
47
+ if not transcripts:
48
+ if json_mode:
49
+ typer.echo(json_envelope([], {"limit": limit}))
50
+ else:
51
+ typer.echo("No meetings found.")
52
+ return
53
+
54
+ if json_mode:
55
+ results = [
56
+ {
57
+ "id": t.id,
58
+ "title": t.title,
59
+ "date": t.display_date,
60
+ "duration_seconds": t.duration,
61
+ "organizer_email": t.organizer_email,
62
+ "participants": t.participants,
63
+ }
64
+ for t in transcripts
65
+ ]
66
+ typer.echo(json_envelope(results, {"limit": limit}))
67
+ else:
68
+ transcripts_table(transcripts)
ffx/commands/search.py ADDED
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import typer
6
+ from ffx.config import Config
7
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
8
+ from ffx.output import json_envelope, json_error, transcripts_table
9
+
10
+
11
+ def search(
12
+ query: str = typer.Argument(..., help="Search query"),
13
+ days: Optional[int] = typer.Option(None, "--days"),
14
+ limit: int = typer.Option(20, "--limit"),
15
+ participant: Optional[list[str]] = typer.Option(None, "--participant", help="Filter by attendee email (repeatable)"),
16
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
17
+ ) -> None:
18
+ """Search meetings by keyword."""
19
+ json_mode = not table
20
+ config = Config()
21
+ if not config.api_key:
22
+ if json_mode:
23
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
24
+ else:
25
+ typer.echo("API key not set. Run: ffx auth", err=True)
26
+ raise typer.Exit(2)
27
+
28
+ from_date = None
29
+ if days:
30
+ from_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+ try:
33
+ client = FirefliesClient(config.api_key)
34
+ transcripts = client.search_transcripts(query, limit=limit, from_date=from_date, participants=participant or None)
35
+ except AuthError:
36
+ if json_mode:
37
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
38
+ else:
39
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
40
+ raise typer.Exit(2)
41
+ except ApiError as e:
42
+ if json_mode:
43
+ typer.echo(json_error(str(e), "API_ERROR"))
44
+ else:
45
+ typer.echo(f"API error: {e}", err=True)
46
+ raise typer.Exit(2)
47
+
48
+ if not transcripts:
49
+ if json_mode:
50
+ typer.echo(json_envelope([], {"query": query}))
51
+ else:
52
+ typer.echo(f"No results found for '{query}'.")
53
+ return
54
+
55
+ if json_mode:
56
+ results = [
57
+ {"id": t.id, "title": t.title, "date": t.display_date, "duration_seconds": t.duration}
58
+ for t in transcripts
59
+ ]
60
+ typer.echo(json_envelope(results, {"query": query, "limit": limit}))
61
+ else:
62
+ transcripts_table(transcripts)
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+ import json
3
+
4
+ import typer
5
+ from ffx.config import Config
6
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
7
+ from ffx.output import json_error, console
8
+
9
+
10
+ def speaker(
11
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
12
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
13
+ ) -> None:
14
+ """Show speaker analytics for a meeting."""
15
+ json_mode = not table
16
+ config = Config()
17
+ if not config.api_key:
18
+ if json_mode:
19
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
20
+ else:
21
+ typer.echo("API key not set. Run: ffx auth", err=True)
22
+ raise typer.Exit(2)
23
+
24
+ try:
25
+ client = FirefliesClient(config.api_key)
26
+ t = client.get_transcript(transcript_id)
27
+ except AuthError:
28
+ if json_mode:
29
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
30
+ else:
31
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
32
+ raise typer.Exit(2)
33
+ except ApiError as e:
34
+ if json_mode:
35
+ typer.echo(json_error(str(e), "API_ERROR"))
36
+ else:
37
+ typer.echo(f"API error: {e}", err=True)
38
+ raise typer.Exit(2)
39
+
40
+ if t is None:
41
+ if json_mode:
42
+ typer.echo(json_error("Transcript not found", "NOT_FOUND"))
43
+ else:
44
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
45
+ raise typer.Exit(3)
46
+
47
+ if not t.speakers:
48
+ if json_mode:
49
+ typer.echo(json.dumps({"id": t.id, "title": t.title, "speakers": []}, indent=2))
50
+ else:
51
+ typer.echo("No speaker analytics available for this meeting.")
52
+ return
53
+
54
+ if json_mode:
55
+ typer.echo(json.dumps({
56
+ "id": t.id,
57
+ "title": t.title,
58
+ "speakers": [
59
+ {
60
+ "name": s.name,
61
+ "duration_seconds": s.duration,
62
+ "duration_pct": round(s.duration_pct, 1),
63
+ "word_count": s.word_count,
64
+ "words_per_minute": s.words_per_minute,
65
+ "filler_words": s.filler_words,
66
+ "questions": s.questions,
67
+ "longest_monologue_seconds": s.longest_monologue,
68
+ "monologues_count": s.monologues_count,
69
+ }
70
+ for s in t.speakers
71
+ ],
72
+ }, indent=2))
73
+ else:
74
+ console.print(f"\n[bold]{t.title}[/bold] [dim]{t.display_date}[/dim]\n")
75
+ for s in sorted(t.speakers, key=lambda x: x.duration_pct, reverse=True):
76
+ pct = f"{s.duration_pct:.0f}%"
77
+ bar_len = max(0, min(20, int(s.duration_pct / 5)))
78
+ bar = "\u2588" * bar_len + "\u2591" * (20 - bar_len)
79
+ console.print(f" [bold]{s.name}[/bold]")
80
+ console.print(f" {bar} {pct}")
81
+ console.print(f" {s.word_count} words | {s.words_per_minute:.0f} wpm | {s.filler_words} fillers | {s.questions} questions")
82
+ if s.longest_monologue:
83
+ console.print(f" Longest monologue: {s.longest_monologue:.0f}s")
84
+ console.print()
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import typer
4
+ from ffx.config import Config
5
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
6
+ from ffx.output import json_error
7
+
8
+
9
+ def summary(
10
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
11
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
12
+ ) -> None:
13
+ """Show the AI-generated summary for a meeting."""
14
+ json_mode = not table
15
+ config = Config()
16
+ if not config.api_key:
17
+ if json_mode:
18
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
19
+ else:
20
+ typer.echo("API key not set. Run: ffx auth", err=True)
21
+ raise typer.Exit(2)
22
+
23
+ try:
24
+ client = FirefliesClient(config.api_key)
25
+ t = client.get_transcript(transcript_id)
26
+ except AuthError:
27
+ if json_mode:
28
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
29
+ else:
30
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
31
+ raise typer.Exit(2)
32
+ except ApiError as e:
33
+ if json_mode:
34
+ typer.echo(json_error(str(e), "API_ERROR"))
35
+ else:
36
+ typer.echo(f"API error: {e}", err=True)
37
+ raise typer.Exit(2)
38
+
39
+ if t is None:
40
+ if json_mode:
41
+ typer.echo(json_error("Transcript not found", "NOT_FOUND"))
42
+ else:
43
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
44
+ raise typer.Exit(3)
45
+
46
+ s = t.summary
47
+ has_content = s and (s.overview or s.gist or s.keywords or s.action_items_list or s.topics_list)
48
+ if not has_content:
49
+ if json_mode:
50
+ typer.echo(json.dumps({"id": t.id, "title": t.title, "summary": None}, indent=2))
51
+ else:
52
+ typer.echo("No summary available for this meeting.")
53
+ return
54
+
55
+ if json_mode:
56
+ typer.echo(json.dumps({
57
+ "id": t.id,
58
+ "title": t.title,
59
+ "overview": t.summary.overview,
60
+ "gist": t.summary.gist,
61
+ "keywords": t.summary.keywords,
62
+ "topics_discussed": t.summary.topics_list,
63
+ "action_items": t.summary.action_items_list,
64
+ }, indent=2))
65
+ else:
66
+ typer.echo(f"\n{t.title}\n")
67
+ if t.summary.overview:
68
+ typer.echo(f"Overview:\n{t.summary.overview}\n")
69
+ if t.summary.gist:
70
+ typer.echo(f"Gist: {t.summary.gist}\n")
71
+ if t.summary.keywords:
72
+ typer.echo(f"Keywords: {', '.join(t.summary.keywords)}\n")
73
+ if t.summary.topics_list:
74
+ typer.echo(f"Topics: {', '.join(t.summary.topics_list)}\n")
75
+ if t.summary.action_items_list:
76
+ typer.echo("Action items:")
77
+ for item in t.summary.action_items_list:
78
+ typer.echo(f" - {item}")
ffx/commands/topics.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import typer
6
+ from ffx.config import Config
7
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
8
+ from ffx.output import json_envelope, json_error
9
+
10
+
11
+ def topics(
12
+ transcript_id: Optional[str] = typer.Argument(None, help="Transcript ID (omit for recent meetings)"),
13
+ days: Optional[int] = typer.Option(7, "--days", help="Last N days (when no ID given)"),
14
+ limit: int = typer.Option(20, "--limit"),
15
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
16
+ ) -> None:
17
+ """List topics discussed in meetings."""
18
+ json_mode = not table
19
+ config = Config()
20
+ if not config.api_key:
21
+ if json_mode:
22
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
23
+ else:
24
+ typer.echo("API key not set. Run: ffx auth", err=True)
25
+ raise typer.Exit(2)
26
+
27
+ try:
28
+ client = FirefliesClient(config.api_key)
29
+
30
+ if transcript_id:
31
+ t = client.get_transcript(transcript_id)
32
+ if t is None:
33
+ if json_mode:
34
+ typer.echo(json_error("Transcript not found", "NOT_FOUND"))
35
+ else:
36
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
37
+ raise typer.Exit(3)
38
+ transcripts = [t]
39
+ else:
40
+ from_date = None
41
+ if days:
42
+ from_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
43
+ transcripts = client.list_transcripts(limit=limit, from_date=from_date)
44
+
45
+ except AuthError:
46
+ if json_mode:
47
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
48
+ else:
49
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
50
+ raise typer.Exit(2)
51
+ except ApiError as e:
52
+ if json_mode:
53
+ typer.echo(json_error(str(e), "API_ERROR"))
54
+ else:
55
+ typer.echo(f"API error: {e}", err=True)
56
+ raise typer.Exit(2)
57
+
58
+ all_topics: dict[str, list[str]] = {}
59
+ for t in transcripts:
60
+ topic_list = t.summary.topics_list if t.summary else []
61
+ if not topic_list and t.summary and t.summary.keywords:
62
+ topic_list = t.summary.keywords
63
+ for topic in topic_list:
64
+ all_topics.setdefault(topic, []).append(t.title)
65
+
66
+ if not all_topics:
67
+ if json_mode:
68
+ typer.echo(json_envelope([], {"days": days}))
69
+ else:
70
+ typer.echo("No topics found.")
71
+ return
72
+
73
+ if json_mode:
74
+ results = [
75
+ {"topic": topic, "count": len(meetings), "meetings": meetings}
76
+ for topic, meetings in sorted(all_topics.items(), key=lambda x: -len(x[1]))
77
+ ]
78
+ typer.echo(json_envelope(results, {"days": days}))
79
+ else:
80
+ for topic, meetings in sorted(all_topics.items(), key=lambda x: -len(x[1])):
81
+ count = f" ({len(meetings)})" if len(meetings) > 1 else ""
82
+ typer.echo(f" {topic}{count}")
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+ import json
3
+
4
+ import typer
5
+ from ffx.config import Config
6
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
7
+ from ffx.output import json_error, console
8
+
9
+
10
+ def transcript(
11
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
12
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
13
+ timestamps: bool = typer.Option(True, "--timestamps/--no-timestamps", help="Show timestamps"),
14
+ speakers: bool = typer.Option(True, "--speakers/--no-speakers", help="Show speaker names"),
15
+ ) -> None:
16
+ """Show the full transcript text with speaker names and timestamps."""
17
+ json_mode = not table
18
+ config = Config()
19
+ if not config.api_key:
20
+ if json_mode:
21
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
22
+ else:
23
+ typer.echo("API key not set. Run: ffx auth", err=True)
24
+ raise typer.Exit(2)
25
+
26
+ try:
27
+ client = FirefliesClient(config.api_key)
28
+ t = client.get_transcript_with_sentences(transcript_id)
29
+ except AuthError:
30
+ if json_mode:
31
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
32
+ else:
33
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
34
+ raise typer.Exit(2)
35
+ except ApiError as e:
36
+ if json_mode:
37
+ typer.echo(json_error(str(e), "API_ERROR"))
38
+ else:
39
+ typer.echo(f"API error: {e}", err=True)
40
+ raise typer.Exit(2)
41
+
42
+ if t is None:
43
+ if json_mode:
44
+ typer.echo(json_error("Transcript not found", "NOT_FOUND"))
45
+ else:
46
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
47
+ raise typer.Exit(3)
48
+
49
+ if not t.sentences:
50
+ if json_mode:
51
+ typer.echo(json.dumps({"id": t.id, "title": t.title, "sentences": []}, indent=2))
52
+ else:
53
+ typer.echo("No transcript text available for this meeting.")
54
+ return
55
+
56
+ if json_mode:
57
+ typer.echo(json.dumps({
58
+ "id": t.id,
59
+ "title": t.title,
60
+ "date": t.display_date,
61
+ "sentences": [
62
+ {
63
+ "index": s.index,
64
+ "speaker": s.speaker_name,
65
+ "text": s.text,
66
+ "start_time": s.start_time,
67
+ "end_time": s.end_time,
68
+ }
69
+ for s in t.sentences
70
+ ],
71
+ }, indent=2))
72
+ return
73
+
74
+ console.print(f"\n[bold]{t.title}[/bold] [dim]{t.display_date}[/dim]\n")
75
+
76
+ last_speaker = None
77
+ for s in t.sentences:
78
+ parts = []
79
+ if timestamps:
80
+ parts.append(f"[dim]{s.timestamp}[/dim]")
81
+ if speakers and s.speaker_name != last_speaker:
82
+ parts.append(f"[bold]{s.speaker_name}:[/bold]")
83
+ last_speaker = s.speaker_name
84
+ elif speakers:
85
+ parts.append(" " * (len(s.speaker_name) + 1))
86
+
87
+ prefix = " ".join(parts)
88
+ if prefix:
89
+ console.print(f"{prefix} {s.text}")
90
+ else:
91
+ console.print(s.text)
ffx/commands/week.py ADDED
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import typer
6
+ from ffx.config import Config
7
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
8
+ from ffx.output import json_envelope, json_error, console
9
+
10
+
11
+ def _meeting_summary(transcripts, period_label: str, json_mode: bool) -> None:
12
+ if not transcripts:
13
+ if json_mode:
14
+ typer.echo(json_envelope([], {"period": period_label}))
15
+ else:
16
+ typer.echo(f"No meetings found for {period_label}.")
17
+ return
18
+
19
+ total_duration = sum(t.duration or 0 for t in transcripts)
20
+ total_hours = total_duration / 3600
21
+
22
+ all_action_items = []
23
+ all_topics = {}
24
+ speaker_time: dict[str, float] = {}
25
+
26
+ for t in transcripts:
27
+ all_action_items.extend(t.action_items)
28
+ if t.summary:
29
+ for topic in t.summary.topics_list:
30
+ all_topics[topic] = all_topics.get(topic, 0) + 1
31
+ for kw in t.summary.keywords:
32
+ all_topics[kw] = all_topics.get(kw, 0) + 1
33
+ for s in t.speakers:
34
+ speaker_time[s.name] = speaker_time.get(s.name, 0) + s.duration
35
+
36
+ if json_mode:
37
+ typer.echo(json.dumps({
38
+ "period": period_label,
39
+ "meeting_count": len(transcripts),
40
+ "total_hours": round(total_hours, 1),
41
+ "meetings": [
42
+ {"id": t.id, "title": t.title, "date": t.display_date, "duration_seconds": t.duration}
43
+ for t in transcripts
44
+ ],
45
+ "action_items_count": len(all_action_items),
46
+ "top_topics": sorted(all_topics.items(), key=lambda x: -x[1])[:10],
47
+ "speaker_time": {k: round(v, 1) for k, v in sorted(speaker_time.items(), key=lambda x: -x[1])},
48
+ }, indent=2))
49
+ else:
50
+ console.print(f"\n[bold]{period_label}[/bold]\n")
51
+ console.print(f" {len(transcripts)} meetings | {total_hours:.1f} hours | {len(all_action_items)} action items\n")
52
+
53
+ console.print(" [bold]Meetings:[/bold]")
54
+ for t in transcripts:
55
+ dur = f"{(t.duration or 0) // 60}m"
56
+ console.print(f" {t.display_date} {t.title} ({dur})")
57
+ console.print()
58
+
59
+ if all_topics:
60
+ top = sorted(all_topics.items(), key=lambda x: -x[1])[:8]
61
+ topics_str = ", ".join(f"{t}" for t, c in top)
62
+ console.print(f" [bold]Top topics:[/bold] {topics_str}\n")
63
+
64
+ if speaker_time:
65
+ console.print(" [bold]Time by speaker:[/bold]")
66
+ total = sum(speaker_time.values())
67
+ for name, secs in sorted(speaker_time.items(), key=lambda x: -x[1]):
68
+ pct = (secs / total * 100) if total > 0 else 0
69
+ console.print(f" {name}: {secs / 60:.0f}m ({pct:.0f}%)")
70
+ console.print()
71
+
72
+ if all_action_items:
73
+ console.print(f" [bold]Action items ({len(all_action_items)}):[/bold]")
74
+ for i, item in enumerate(all_action_items[:10], 1):
75
+ typer.echo(f" {i}. {item.text}")
76
+ if len(all_action_items) > 10:
77
+ typer.echo(f" ... and {len(all_action_items) - 10} more")
78
+
79
+
80
+ def week(
81
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
82
+ ) -> None:
83
+ """Show a summary of this week's meetings."""
84
+ _run_period(7, "This Week", not table)
85
+
86
+
87
+ def month(
88
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
89
+ ) -> None:
90
+ """Show a summary of this month's meetings."""
91
+ _run_period(30, "This Month", not table)
92
+
93
+
94
+ def _run_period(days: int, label: str, json_mode: bool) -> None:
95
+ config = Config()
96
+ if not config.api_key:
97
+ if json_mode:
98
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
99
+ else:
100
+ typer.echo("API key not set. Run: ffx auth", err=True)
101
+ raise typer.Exit(2)
102
+
103
+ from_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
104
+
105
+ try:
106
+ client = FirefliesClient(config.api_key)
107
+ transcripts = client.list_transcripts(limit=100, from_date=from_date)
108
+ except AuthError:
109
+ if json_mode:
110
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
111
+ else:
112
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
113
+ raise typer.Exit(2)
114
+ except ApiError as e:
115
+ if json_mode:
116
+ typer.echo(json_error(str(e), "API_ERROR"))
117
+ else:
118
+ typer.echo(f"API error: {e}", err=True)
119
+ raise typer.Exit(2)
120
+
121
+ _meeting_summary(transcripts, label, json_mode)
ffx/config.py ADDED
@@ -0,0 +1,46 @@
1
+ import os
2
+ import stat
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+
8
+ def _ffx_home() -> Path:
9
+ home = os.environ.get("FFX_HOME")
10
+ if home:
11
+ return Path(home)
12
+ return Path.home() / ".ffx"
13
+
14
+
15
+ class Config:
16
+ def __init__(self) -> None:
17
+ self._home = _ffx_home()
18
+ self._home.mkdir(parents=True, exist_ok=True)
19
+ self._path = self._home / "config.yaml"
20
+
21
+ def _load(self) -> dict:
22
+ if not self._path.exists():
23
+ return {}
24
+ with open(self._path) as f:
25
+ return yaml.safe_load(f) or {}
26
+
27
+ def _save(self, data: dict) -> None:
28
+ with open(self._path, "w") as f:
29
+ yaml.safe_dump(data, f)
30
+ self._path.chmod(stat.S_IRUSR | stat.S_IWUSR)
31
+
32
+ def get(self, key: str):
33
+ return self._load().get(key)
34
+
35
+ def set(self, key: str, value) -> None:
36
+ data = self._load()
37
+ data[key] = value
38
+ self._save(data)
39
+
40
+ @property
41
+ def api_key(self) -> str | None:
42
+ return os.environ.get("FIREFLIES_API_KEY") or self.get("api_key")
43
+
44
+ @property
45
+ def home(self) -> Path:
46
+ return self._home