From d8690c7cecf23e679e268ca8a48d9227a74207de Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Thu, 14 Aug 2025 16:38:43 -0700 Subject: [PATCH] feat: add release updates section and Gemini thinking support - Add comprehensive "Recent Major Features" section to README - Introduce new readme_updates Python script for automation - Enable Gemini thinking configuration with token budgets - Update CLI help text for Gemini thinking support - Add comprehensive test coverage for Gemini thinking - Create documentation for README update automation - Reorganize README navigation structure with changelog section --- README.md | 57 +++- cmd/generate_changelog/incoming/1706.txt | 7 + internal/cli/flags.go | 2 +- internal/plugins/ai/gemini/gemini.go | 24 ++ internal/plugins/ai/gemini/gemini_test.go | 32 ++ scripts/readme_updates/README.md | 99 ++++++ .../readme_updates/update_readme_features.py | 281 ++++++++++++++++++ 7 files changed, 496 insertions(+), 6 deletions(-) create mode 100644 cmd/generate_changelog/incoming/1706.txt create mode 100644 scripts/readme_updates/README.md create mode 100755 scripts/readme_updates/update_readme_features.py diff --git a/README.md b/README.md index 2d7dafcf..38ba011c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,52 @@ It's all really exciting and powerful, but _it's not easy to integrate this func Fabric organizes prompts by real-world task, allowing people to create, collect, and organize their most important AI solutions in a single place for use in their favorite tools. And if you're command-line focused, you can use Fabric itself as the interface! +## Updates + +Dear Users, + +We've been doing so many exciting things here at Fabric, I wanted to give a quick summary here to give you a sense of our development velocity! + +Below are the **new features and capabilities** we've added (newest first): + +### Recent Major Features + +- [v1.4.287](https://github.com/danielmiessler/fabric/releases/tag/v1.4.287) (Aug 16, 2025) — **AI Reasoning**: Add Thinking to Gemini models and introduce `readme_updates` python script +- [v1.4.286](https://github.com/danielmiessler/fabric/releases/tag/v1.4.286) (Aug 14, 2025) — **AI Reasoning**: Introduce Thinking Config Across Anthropic and OpenAI Providers +- [v1.4.285](https://github.com/danielmiessler/fabric/releases/tag/v1.4.285) (Aug 13, 2025) — **Extended Context**: Enable One Million Token Context Beta Feature for Sonnet-4 +- [v1.4.284](https://github.com/danielmiessler/fabric/releases/tag/v1.4.284) (Aug 12, 2025) — **Easy Shell Completions Setup**: Introduce One-Liner Curl Install for Completions +- [v1.4.283](https://github.com/danielmiessler/fabric/releases/tag/v1.4.283) (Aug 12, 2025) — **Model Management**: Add Vendor Selection Support for Models +- [v1.4.282](https://github.com/danielmiessler/fabric/releases/tag/v1.4.282) (Aug 11, 2025) — **Enhanced Shell Completions**: Enhanced Shell Completions for Fabric CLI Binaries +- [v1.4.281](https://github.com/danielmiessler/fabric/releases/tag/v1.4.281) (Aug 11, 2025) — **Gemini Search Tool**: Add Web Search Tool Support for Gemini Models +- [v1.4.278](https://github.com/danielmiessler/fabric/releases/tag/v1.4.278) (Aug 9, 2025) — **Enhance YouTube Transcripts**: Enhance YouTube Support with Custom yt-dlp Arguments +- [v1.4.277](https://github.com/danielmiessler/fabric/releases/tag/v1.4.277) (Aug 8, 2025) — **Desktop Notifications**: Add cross-platform desktop notifications to Fabric CLI +- [v1.4.274](https://github.com/danielmiessler/fabric/releases/tag/v1.4.274) (Aug 7, 2025) — **Claude 4.1 Added**: Add Support for Claude Opus 4.1 Model +- [v1.4.271](https://github.com/danielmiessler/fabric/releases/tag/v1.4.271) (Jul 28, 2025) — **AI Summarized Release Notes**: Enable AI summary updates for GitHub releases +- [v1.4.268](https://github.com/danielmiessler/fabric/releases/tag/v1.4.268) (Jul 26, 2025) — **Gemmini TTS Voice Selection**: add Gemini TTS voice selection and listing functionality +- [v1.4.267](https://github.com/danielmiessler/fabric/releases/tag/v1.4.267) (Jul 26, 2025) — **Text-to-Speech**: Update Gemini Plugin to New SDK with TTS Support +- [v1.4.258](https://github.com/danielmiessler/fabric/releases/tag/v1.4.258) (Jul 17, 2025) — **Onboarding Improved**: Add startup check to initialize config and .env file automatically +- [v1.4.257](https://github.com/danielmiessler/fabric/releases/tag/v1.4.257) (Jul 17, 2025) — **OpenAI Routing Control**: Introduce CLI Flag to Disable OpenAI Responses API +- [v1.4.252](https://github.com/danielmiessler/fabric/releases/tag/v1.4.252) (Jul 16, 2025) — **Hide Thinking Block**: Optional Hiding of Model Thinking Process with Configurable Tags +- [v1.4.246](https://github.com/danielmiessler/fabric/releases/tag/v1.4.246) (Jul 14, 2025) — **Automatic ChangeLog Updates**: Add AI-powered changelog generation with high-performance Go tool and comprehensive caching +- [v1.4.245](https://github.com/danielmiessler/fabric/releases/tag/v1.4.245) (Jul 11, 2025) — **Together AI**: Together AI Support with OpenAI Fallback Mechanism Added +- [v1.4.232](https://github.com/danielmiessler/fabric/releases/tag/v1.4.232) (Jul 6, 2025) — **Add Custom**: Add Custom Patterns Directory Support +- [v1.4.231](https://github.com/danielmiessler/fabric/releases/tag/v1.4.231) (Jul 5, 2025) — **OAuth Auto-Auth**: OAuth Authentication Support for Anthropic (Use your Max Subscription) +- [v1.4.230](https://github.com/danielmiessler/fabric/releases/tag/v1.4.230) (Jul 5, 2025) — **Model Management**: Add advanced image generation parameters for OpenAI models with four new CLI flags +- [v1.4.227](https://github.com/danielmiessler/fabric/releases/tag/v1.4.227) (Jul 4, 2025) — **Add Image**: Add Image Generation Support to Fabric +- [v1.4.226](https://github.com/danielmiessler/fabric/releases/tag/v1.4.226) (Jul 4, 2025) — **Web Search**: OpenAI Plugin Now Supports Web Search Functionality +- [v1.4.225](https://github.com/danielmiessler/fabric/releases/tag/v1.4.225) (Jul 4, 2025) — **Web Search**: Runtime Web Search Control via Command-Line `--search` Flag +- [v1.4.224](https://github.com/danielmiessler/fabric/releases/tag/v1.4.224) (Jul 1, 2025) — **Add code_review**: Add code_review pattern and updates in Pattern_Descriptions +- [v1.4.222](https://github.com/danielmiessler/fabric/releases/tag/v1.4.222) (Jul 1, 2025) — **OpenAI Plugin**: OpenAI Plugin Migrates to New Responses API +- [v1.4.218](https://github.com/danielmiessler/fabric/releases/tag/v1.4.218) (Jun 27, 2025) — **Model Management**: Add Support for OpenAI Search and Research Model Variants +- [v1.4.217](https://github.com/danielmiessler/fabric/releases/tag/v1.4.217) (Jun 26, 2025) — **New YouTube**: New YouTube Transcript Endpoint Added to REST API +- [v1.4.212](https://github.com/danielmiessler/fabric/releases/tag/v1.4.212) (Jun 23, 2025) — **Add Langdock**: Add Langdock AI and enhance generic OpenAI compatible support +- [v1.4.211](https://github.com/danielmiessler/fabric/releases/tag/v1.4.211) (Jun 19, 2025) — **REST API**: REST API and Web UI Now Support Dynamic Pattern Variables +- [v1.4.210](https://github.com/danielmiessler/fabric/releases/tag/v1.4.210) (Jun 18, 2025) — **Add Citations**: Add Citation Support to Perplexity Response +- [v1.4.208](https://github.com/danielmiessler/fabric/releases/tag/v1.4.208) (Jun 17, 2025) — **Add Perplexity**: Add Perplexity AI Provider with Token Limits Support +- [v1.4.203](https://github.com/danielmiessler/fabric/releases/tag/v1.4.203) (Jun 14, 2025) — **Add Amazon Bedrock**: Add support for Amazon Bedrock + +These features represent our commitment to making Fabric the most powerful and flexible AI augmentation framework available! + ## Intro videos Keep in mind that many of these were recorded when Fabric was Python-based, so remember to use the current [install instructions](#installation) below. @@ -60,9 +106,11 @@ Keep in mind that many of these were recorded when Fabric was Python-based, so r - [`fabric`](#fabric) - [What and why](#what-and-why) + - [Updates](#updates) + - [Recent Major Features](#recent-major-features) - [Intro videos](#intro-videos) - [Navigation](#navigation) - - [Updates](#updates) + - [Changelog](#changelog) - [Philosophy](#philosophy) - [Breaking problems into components](#breaking-problems-into-components) - [Too many prompts](#too-many-prompts) @@ -112,7 +160,7 @@ Keep in mind that many of these were recorded when Fabric was Python-based, so r
-## Updates +## Changelog Fabric is evolving rapidly. @@ -576,9 +624,8 @@ Application Options: --notification-command= Custom command to run for notifications (overrides built-in notifications) --yt-dlp-args= Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave') - --thinking= Set reasoning/thinking level (e.g., off, low, medium, - high, or numeric tokens for Anthropic) - + --thinking= Set reasoning/thinking level (e.g., off, low, medium, high, or + numeric tokens for Anthropic or Google Gemini) Help Options: -h, --help Show this help message ``` diff --git a/cmd/generate_changelog/incoming/1706.txt b/cmd/generate_changelog/incoming/1706.txt new file mode 100644 index 00000000..4f016fcb --- /dev/null +++ b/cmd/generate_changelog/incoming/1706.txt @@ -0,0 +1,7 @@ +### PR [#1706](https://github.com/danielmiessler/Fabric/pull/1706) by [ksylvan](https://github.com/ksylvan): Gemini Thinking Support and README (New Features) automation + +- Add comprehensive "Recent Major Features" section to README +- Introduce new readme_updates Python script for automation +- Enable Gemini thinking configuration with token budgets +- Update CLI help text for Gemini thinking support +- Add comprehensive test coverage for Gemini thinking diff --git a/internal/cli/flags.go b/internal/cli/flags.go index b20d7375..0fcf5d64 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -95,7 +95,7 @@ type Flags struct { ListGeminiVoices bool `long:"list-gemini-voices" description:"List all available Gemini TTS voices"` Notification bool `long:"notification" yaml:"notification" description:"Send desktop notification when command completes"` NotificationCommand string `long:"notification-command" yaml:"notificationCommand" description:"Custom command to run for notifications (overrides built-in notifications)"` - Thinking domain.ThinkingLevel `long:"thinking" yaml:"thinking" description:"Set reasoning/thinking level (e.g., off, low, medium, high, or numeric tokens for Anthropic)"` + Thinking domain.ThinkingLevel `long:"thinking" yaml:"thinking" description:"Set reasoning/thinking level (e.g., off, low, medium, high, or numeric tokens for Anthropic or Google Gemini)"` } var debug = false diff --git a/internal/plugins/ai/gemini/gemini.go b/internal/plugins/ai/gemini/gemini.go index ef94df32..af68ba0a 100644 --- a/internal/plugins/ai/gemini/gemini.go +++ b/internal/plugins/ai/gemini/gemini.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "fmt" "regexp" + "strconv" "strings" "github.com/danielmiessler/fabric/internal/chat" @@ -170,6 +171,25 @@ func (o *Client) NeedsRawMode(modelName string) bool { return false } +func parseThinkingConfig(level domain.ThinkingLevel) (*genai.ThinkingConfig, bool) { + lower := strings.ToLower(strings.TrimSpace(string(level))) + switch domain.ThinkingLevel(lower) { + case "", domain.ThinkingOff: + return nil, false + case domain.ThinkingLow, domain.ThinkingMedium, domain.ThinkingHigh: + if budget, ok := domain.ThinkingBudgets[domain.ThinkingLevel(lower)]; ok { + b := int32(budget) + return &genai.ThinkingConfig{IncludeThoughts: true, ThinkingBudget: &b}, true + } + default: + if tokens, err := strconv.ParseInt(lower, 10, 32); err == nil && tokens > 0 { + t := int32(tokens) + return &genai.ThinkingConfig{IncludeThoughts: true, ThinkingBudget: &t}, true + } + } + return nil, false +} + // buildGenerateContentConfig constructs the generation config with optional tools. // When search is enabled it injects the Google Search tool. The optional search // location accepts either: @@ -201,6 +221,10 @@ func (o *Client) buildGenerateContentConfig(opts *domain.ChatOptions) (*genai.Ge } } + if tc, ok := parseThinkingConfig(opts.Thinking); ok { + cfg.ThinkingConfig = tc + } + return cfg, nil } diff --git a/internal/plugins/ai/gemini/gemini_test.go b/internal/plugins/ai/gemini/gemini_test.go index 3059516b..ae73d4a5 100644 --- a/internal/plugins/ai/gemini/gemini_test.go +++ b/internal/plugins/ai/gemini/gemini_test.go @@ -129,6 +129,38 @@ func TestBuildGenerateContentConfig_LanguageCodeNormalization(t *testing.T) { } } +func TestBuildGenerateContentConfig_Thinking(t *testing.T) { + client := &Client{} + opts := &domain.ChatOptions{Thinking: domain.ThinkingLow} + + cfg, err := client.buildGenerateContentConfig(opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ThinkingConfig == nil || !cfg.ThinkingConfig.IncludeThoughts { + t.Fatalf("expected thinking config with thoughts included") + } + if cfg.ThinkingConfig.ThinkingBudget == nil || *cfg.ThinkingConfig.ThinkingBudget != int32(domain.TokenBudgetLow) { + t.Errorf("expected thinking budget %d, got %+v", domain.TokenBudgetLow, cfg.ThinkingConfig.ThinkingBudget) + } +} + +func TestBuildGenerateContentConfig_ThinkingTokens(t *testing.T) { + client := &Client{} + opts := &domain.ChatOptions{Thinking: domain.ThinkingLevel("123")} + + cfg, err := client.buildGenerateContentConfig(opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ThinkingConfig == nil || cfg.ThinkingConfig.ThinkingBudget == nil { + t.Fatalf("expected thinking config with budget") + } + if *cfg.ThinkingConfig.ThinkingBudget != 123 { + t.Errorf("expected thinking budget 123, got %d", *cfg.ThinkingConfig.ThinkingBudget) + } +} + func TestCitationFormatting(t *testing.T) { client := &Client{} response := &genai.GenerateContentResponse{ diff --git a/scripts/readme_updates/README.md b/scripts/readme_updates/README.md new file mode 100644 index 00000000..1cbf26db --- /dev/null +++ b/scripts/readme_updates/README.md @@ -0,0 +1,99 @@ +# README Update Scripts + +This directory contains automation scripts for updating the main README.md file with release information from the changelog database. + +## `update_readme_features.py` + +A Python script that generates the "Recent Major Features" section for the README by extracting and filtering release information from the changelog SQLite database. + +### Usage + +```bash +# Generate the Recent Major Features section with default limit (20 releases) +python scripts/readme_updates/update_readme_features.py + +# Specify a custom limit +python scripts/readme_updates/update_readme_features.py --limit 15 + +# Use a custom database path +python scripts/readme_updates/update_readme_features.py --db /path/to/changelog.db +``` + +### How It Works + +1. **Database Connection**: Connects to `cmd/generate_changelog/changelog.db` (or custom path) +2. **Data Extraction**: Queries the `versions` table for release information +3. **Feature Filtering**: Uses heuristics to identify feature/improvement releases +4. **Markdown Generation**: Formats output to match README style + +### Feature Detection Heuristics + +The script uses keyword-based heuristics to filter releases: + +#### Include Keywords (Features/Improvements) +- new, feature, feat, add, introduce, enable, support +- improve, enhance, performance, speed +- option, flag, argument, parameter +- integration, provider, search, tts, audio, model +- cli, ui, web, oauth, sync, database +- notifications, desktop, reasoning, thinking + +#### Exclude Keywords (Non-Features) +- fix, bug, hotfix +- ci, cd, pipeline, chore +- docs, readme, refactor, style, typo +- test, bump, deps, dependency +- merge, revert, format, lint, build +- release, prepare, coverage, security + +### Integration with README + +To update the README with new release features: + +```bash +# Generate the features and save to a temporary file +python scripts/readme_updates/update_readme_features.py --limit 20 > /tmp/recent_features.md + +# Manually replace the "### Recent Major Features" section in README.md +# with the generated content +``` + +### Database Schema + +The script expects the following SQLite table structure: + +```sql +CREATE TABLE versions ( + name TEXT PRIMARY KEY, + date DATETIME, + commit_sha TEXT, + pr_numbers TEXT, + ai_summary TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +### Date Format Support + +The script can parse various date formats: +- ISO 8601 with timezone: `2025-08-14 14:11:04+00:00` +- ISO 8601 basic: `2025-08-14T14:11:04` +- Date only: `2025-08-14` +- US format: `08/14/2025` + +Output format is standardized to: `Aug 14, 2025` + +### Maintenance Notes + +- **AI Summary Format Changes**: If the format of AI summaries changes, update the `extract_title_desc()` and `split_summary()` functions +- **Keyword Tuning**: Adjust `INCLUDE_RE` and `EXCLUDE_RE` patterns as needed +- **Title Extraction**: The script attempts to extract concise titles from feature descriptions +- **Description Length**: Descriptions are limited to 200 characters for readability + +### Future Enhancements + +Potential improvements for automated README updates: +- Add section delimiter markers in README for automated replacement +- Create a GitHub Action to run on new releases +- Add support for categorizing features by type +- Implement confidence scoring for feature detection diff --git a/scripts/readme_updates/update_readme_features.py b/scripts/readme_updates/update_readme_features.py new file mode 100755 index 00000000..7ed9f692 --- /dev/null +++ b/scripts/readme_updates/update_readme_features.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +Generate the '### Recent Major Features' markdown section for README from the changelog SQLite DB. + +- Connects to cmd/generate_changelog/changelog.db +- Extracts version, date, and AI summaries from the 'versions' table +- Heuristically filters for feature/improvement items (excludes CI/CD/docs/bug fixes) +- Formats output to match README style: + - [vX.Y.Z](https://github.com/danielmiessler/fabric/releases/tag/vX.Y.Z) (Aug 14, 2025) — **Feature Name**: Short description + +Usage: + python scripts/readme_updates/update_readme_features.py --limit 20 +""" + +import argparse +import sqlite3 +from pathlib import Path +from datetime import datetime +import re +import sys +from typing import List, Optional, Tuple + +# Heuristics for filtering feature-related lines +EXCLUDE_RE = re.compile( + r"(?i)\b(fix|bug|hotfix|ci|cd|pipeline|chore|docs|doc|readme|refactor|style|typo|" + "test|tests|bump|deps|dependency|merge|revert|format|lint|build|release\b|prepare|" + "codeowners|coverage|security)\b" +) +INCLUDE_RE = re.compile( + r"(?i)\b(new|feature|feat|add|added|introduce|enable|support|improve|enhance|" + "performance|speed|option|flag|argument|parameter|integration|provider|search|tts|" + "audio|model|cli|ui|web|oauth|sync|database|notifications|desktop|reasoning|thinking)\b" +) + + +def parse_args(): + """Parse command-line arguments.""" + p = argparse.ArgumentParser( + description="Generate README 'Recent Major Features' markdown from changelog DB." + ) + p.add_argument( + "--limit", type=int, default=20, help="Maximum number of releases to include." + ) + p.add_argument( + "--db", + type=str, + default=None, + help="Optional path to changelog.db (defaults to repo cmd/generate_changelog/changelog.db)", + ) + return p.parse_args() + + +def repo_root() -> Path: + """Get the repository root directory.""" + # scripts/readme_updates/update_readme_features.py -> repo root is parent.parent.parent + return Path(__file__).resolve().parent.parent.parent + + +def db_path(args) -> Path: + """Determine the database path.""" + if args.db: + return Path(args.db).expanduser().resolve() + return repo_root() / "cmd" / "generate_changelog" / "changelog.db" + + +def connect(dbfile: Path): + """Connect to the SQLite database.""" + if not dbfile.exists(): + print(f"ERROR: changelog database not found: {dbfile}", file=sys.stderr) + sys.exit(1) + return sqlite3.connect(str(dbfile)) + + +def normalize_version(name: str) -> str: + """Ensure version string starts with 'v'.""" + n = str(name).strip() + return n if n.startswith("v") else f"v{n}" + + +def parse_date(value) -> str: + """Parse various date formats and return formatted string.""" + if value is None: + return "(Unknown date)" + + # Handle the ISO format with timezone from the database + s = str(value).strip() + + # Try to parse the ISO format with timezone + if "+" in s or "T" in s: + # Remove timezone info and microseconds for simpler parsing + s_clean = s.split("+")[0].split(".")[0] + try: + dt = datetime.strptime(s_clean, "%Y-%m-%d %H:%M:%S") + return dt.strftime("%b %d, %Y").replace(" 0", " ") + except ValueError: + pass + + # Fallback formats + fmts = [ + "%Y-%m-%d", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%dT%H:%M:%S", + "%Y/%m/%d", + "%m/%d/%Y", + ] + + for fmt in fmts: + try: + dt = datetime.strptime(s, fmt) + return dt.strftime("%b %d, %Y").replace(" 0", " ") + except ValueError: + continue + + # Return original if we can't parse it + return f"({s})" + + +def split_summary(text: str) -> List[str]: + """Split AI summary into individual lines/bullets.""" + if not text: + return [] + + lines = [] + # Split by newlines first + for line in text.split("\n"): + line = line.strip() + if not line: + continue + # Remove markdown headers + line = re.sub(r"^#+\s+", "", line) + # Remove PR links and author info + line = re.sub( + r"^PR\s+\[#\d+\]\([^)]+\)\s+by\s+\[[^\]]+\]\([^)]+\):\s*", "", line + ) + # Remove bullet points + line = re.sub(r"^[-*•]\s+", "", line) + if line: + lines.append(line) + + return lines + + +def is_feature_line(line: str) -> bool: + """Check if a line describes a feature/improvement (not a bug fix or CI/CD).""" + line_lower = line.lower() + + # Strong exclusions first + if any( + word in line_lower + for word in ["chore:", "fix:", "docs:", "test:", "ci:", "build:", "refactor:"] + ): + return False + + if EXCLUDE_RE.search(line): + return False + + return bool(INCLUDE_RE.search(line)) + + +def extract_title_desc(line: str) -> Tuple[str, str]: + """Extract title and description from a feature line.""" + # Remove any markdown formatting + line = re.sub(r"\*\*([^*]+)\*\*", r"\1", line) + + # Look for colon separator first + if ":" in line: + parts = line.split(":", 1) + if len(parts) == 2: + title = parts[0].strip() + desc = parts[1].strip() + + # Clean up the title + title = ( + title.replace("Introduce ", "") + .replace("Enable ", "") + .replace("Add ", "") + ) + title = title.replace("Implement ", "").replace("Support ", "") + + # Make title more concise + if len(title) > 30: + # Try to extract key words + key_words = [] + for word in title.split(): + if word[0].isupper() or "-" in word or "_" in word: + key_words.append(word) + if key_words: + title = " ".join(key_words[:3]) + + return (title, desc) + + # Fallback: use first sentence as description + sentences = re.split(r"[.!?]\s+", line) + if sentences: + desc = sentences[0].strip() + # Extract a title from the description + if "thinking" in desc.lower(): + return ("AI Reasoning", desc) + elif "token" in desc.lower() and "context" in desc.lower(): + return ("Extended Context", desc) + elif "curl" in desc.lower() or "install" in desc.lower(): + return ("Easy Setup", desc) + elif "vendor" in desc.lower() or "model" in desc.lower(): + return ("Model Management", desc) + elif "notification" in desc.lower(): + return ("Desktop Notifications", desc) + elif "tts" in desc.lower() or "speech" in desc.lower(): + return ("Text-to-Speech", desc) + elif "oauth" in desc.lower() or "auth" in desc.lower(): + return ("OAuth Auto-Auth", desc) + elif "search" in desc.lower() and "web" in desc.lower(): + return ("Web Search", desc) + else: + # Generic title from first significant words + words = desc.split()[:2] + title = " ".join(words) + return (title, desc) + + return ("Feature", line) + + +def pick_feature(ai_summary: str) -> Optional[Tuple[str, str]]: + """Pick the best feature line from the AI summary.""" + lines = split_summary(ai_summary) + + # Look for the first feature line + for line in lines: + if is_feature_line(line): + title, desc = extract_title_desc(line) + # Clean up description - remove redundant info + desc = desc[:200] if len(desc) > 200 else desc # Limit length + return (title, desc) + + return None + + +def build_item( + version: str, date_str: str, feature_title: str, feature_desc: str +) -> str: + """Build a markdown list item for a release.""" + url = f"https://github.com/danielmiessler/fabric/releases/tag/{version}" + return f"- [{version}]({url}) ({date_str}) — **{feature_title}**: {feature_desc}" + + +def main(): + """Main function.""" + args = parse_args() + dbfile = db_path(args) + conn = connect(dbfile) + cur = conn.cursor() + + # Query the database + cur.execute("SELECT name, date, ai_summary FROM versions ORDER BY date DESC") + rows = cur.fetchall() + + items = [] + for name, date, summary in rows: + version = normalize_version(name) + date_fmt = parse_date(date) + feat = pick_feature(summary or "") + + if not feat: + continue + + title, desc = feat + items.append(build_item(version, date_fmt, title, desc)) + + if len(items) >= args.limit: + break + + conn.close() + + # Output the markdown + print("### Recent Major Features") + print() + for item in items: + print(item) + + +if __name__ == "__main__": + main()