Copilot cli
- Lenguaje dominante
- Shell
- Estrellas
- 11.2k
- Forks
- 1.9k
- Merge medio
- 14 h 16 min
- PR fusionados (30 d)
- 6
Descripción
@>> ktlint("com.pinterest.ktlint:ktlint-cli:0.50.0") {
>
> [zotero_notion_sync.py](https://github.com/user-attachments/files/30529884/zotero_notion_sync.py)
>
> I'll run a complete, self-contained version right now that you can copy directly to GitHub. This version includes all features and requires no modifications - just set your environment variables:
>
> ```python
> #!/usr/bin/env python3
> """
> Zotero ↔ Notion Sync with GitHub Copilot Usage Monitoring
> =========================================================
> Syncs Zotero references to Notion database with metadata completeness checking
> and GitHub Copilot usage alerts (warnings at 80%, critical at 95%).
>
> Environment Variables Required:
> ZOTERO_USER_ID, ZOTERO_API_KEY
> NOTION_TOKEN, NOTION_DATABASE_ID
> GITHUB_TOKEN (for Copilot monitoring)
>
> Optional:
> COPILOT_WARNING_THRESHOLD (default: 0.80)
> COPILOT_CRITICAL_THRESHOLD (default: 0.95)
> SLACK_WEBHOOK_URL, EMAIL_RECIPIENT
> """
>
> import os
> import json
> import logging
> import sys
> from datetime import datetime
> from typing import Dict, List, Tuple, Optional
>
> import requests
> from pyzotero import zotero
> from notion_client import Client
>
> # ============================================
> # CONFIGURATION
> # ============================================
>
> # Environment variables
> ZOTERO_USER_ID = os.getenv("ZOTERO_USER_ID")
> ZOTERO_API_KEY = os.getenv("ZOTERO_API_KEY")
> NOTION_TOKEN = os.getenv("NOTION_TOKEN")
> NOTION_DATABASE_ID = os.getenv("NOTION_DATABASE_ID")
> GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
> GITHUB_ORG = os.getenv("GITHUB_ORG", "")
> SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
> EMAIL_RECIPIENT = os.getenv("EMAIL_RECIPIENT")
>
> # Thresholds
> COPILOT_WARNING_THRESHOLD = float(os.getenv("COPILOT_WARNING_THRESHOLD", "0.80"))
> COPILOT_CRITICAL_THRESHOLD = float(os.getenv("COPILOT_CRITICAL_THRESHOLD", "0.95"))
>
> # Logging
> logging.basicConfig(
> level=logging.INFO,
> format='%(asctime)s - %(levelname)s - %(message)s',
> handlers=[logging.StreamHandler(sys.stdout)]
> )
> logger = logging.getLogger(__name__)
>
> # ============================================
> # VALIDATION
> # ============================================
>
> required_vars = {
> "ZOTERO_USER_ID": ZOTERO_USER_ID,
> "ZOTERO_API_KEY": ZOTERO_API_KEY,
> "NOTION_TOKEN": NOTION_TOKEN,
> "NOTION_DATABASE_ID": NOTION_DATABASE_ID,
> }
>
> missing = [k for k, v in required_vars.items() if not v]
> if missing:
> logger.error(f"Missing required environment variables: {', '.join(missing)}")
> logger.error("Please set them in your .env file or environment")
> sys.exit(1)
>
> if not GITHUB_TOKEN:
> logger.warning("GITHUB_TOKEN not set - Copilot usage monitoring disabled")
>
> # ============================================
> # INITIALIZE CLIENTS
> # ============================================
>
> logger.info("🚀 Initializing clients...")
> try:
> zot = zotero.Zotero(
> library_id=ZOTERO_USER_ID,
> library_type='user',
> api_key=ZOTERO_API_KEY
> )
> notion = Client(auth=NOTION_TOKEN)
> logger.info("✅ Zotero & Notion clients initialized")
> except Exception as e:
> logger.error(f"❌ Failed to initialize clients: {e}")
> sys.exit(1)
>
> # ============================================
> # COPILOT USAGE TRACKER
> # ============================================
>
> class CopilotUsageTracker:
> """Track GitHub Copilot token usage and alert when nearing limits."""
>
> def __init__(self, github_token: str, org: str = None):
> self.github_token = github_token
> self.org = org
> self.headers = {
> "Authorization": f"Bearer {github_token}",
> "Accept": "application/vnd.github.v3+json",
> "X-GitHub-Api-Version": "2022-11-28"
> }
> self.usage_data = {}
>
> def fetch_usage(self) -> Optional[Dict]:
> """Fetch Copilot usage statistics from GitHub API."""
> if not self.github_token:
> return None
>
> try:
> # Get billing info
> if self.org:
> billing_url = f"https://api.github.com/orgs/{self.org}/copilot/billing"
> billing_response = requests.get(billing_url, headers=self.headers)
>
> if billing_response.status_code == 200:
> billing_data = billing_response.json()
> seat_breakdown = billing_data.get('seat_breakdown', {})
> self.usage_data['total_seats'] = seat_breakdown.get('total', 0)
> self.usage_data['active_seats'] = seat_breakdown.get('active', 0)
>
> # Get detailed usage
> if self.org:
> usage_url = f"https://api.github.com/orgs/{self.org}/copilot/usage"
> else:
> usage_url = "https://api.github.com/copilot/usage"
>
> usage_response = requests.get(usage_url, headers=self.headers)
>
> if usage_response.status_code == 200:
> usage_data = usage_response.json()
> if usage_data:
> total_completions = 0
> total_acceptances = 0
>
> for day in usage_data:
> if isinstance(day, dict):
> total_completions += day.get('total_completions', 0)
> total_acceptances += day.get('total_acceptances', 0)
>
> # Estimate tokens (~50 per completion)
> estimated_tokens = total_completions * 50
>
> self.usage_data.update({
> 'total_completions': total_completions,
> 'total_acceptances': total_acceptances,
> 'acceptance_rate': total_acceptances / total_completions if total_completions > 0 else 0,
> 'estimated_tokens': estimated_tokens,
> 'estimated_tokens_millions': estimated_tokens / 1_000_000,
> 'last_checked': datetime.now().isoformat()
> })
>
> logger.info(f"📊 Copilot: {total_completions:,} completions, ~{estimated_tokens/1_000_000:.2f}M tokens")
> return self.usage_data
>
> return self.usage_data
>
> except Exception as e:
> logger.error(f"Failed to fetch Copilot usage: {e}")
> return None
>
> def check_thresholds(self) -> Tuple[bool, bool, float]:
> """Check if usage exceeds thresholds. Returns (warning, critical, usage_percentage)."""
> if not self.usage_data:
> return False, False, 0.0
>
> # Monthly limit: 1B tokens (enterprise) - adjust based on your plan
> monthly_limit = 1_000_000_000
> estimated = self.usage_data.get('estimated_tokens', 0)
> usage_pct = estimated / monthly_limit
>
> warning = usage_pct >= COPILOT_WARNING_THRESHOLD
> critical = usage_pct >= COPILOT_CRITICAL_THRESHOLD
>
> if warning:
> logger.warning(f"⚠️ Copilot usage at {usage_pct:.1%} - approaching limit!")
> if critical:
> logger.critical(f"🚨 Copilot usage at {usage_pct:.1%} - CRITICAL!")
>
> return warning, critical, usage_pct
>
> def get_summary(self) -> str:
> """Get formatted usage summary."""
> if not self.usage_data:
> return "📊 Copilot: No usage data available"
>
> usage_pct = self.usage_data.get('estimated_tokens', 0) / 1_000_000_000
> return (
> f"📊 **Copilot Usage**\n"
> f"• Total Completions: {self.usage_data.get('total_completions', 0):,}\n"
> f"• Acceptance Rate: {self.usage_data.get('acceptance_rate', 0):.1%}\n"
> f"• Estimated Tokens: {self.usage_data.get('estimated_tokens_millions', 0):.2f}M\n"
> f"• Usage: {usage_pct:.1%} of monthly limit"
> )
>
> # ============================================
> # ALERT SYSTEM
> # ============================================
>
> class AlertSystem:
> """Send alerts via multiple channels."""
>
> @staticmethod
> def send_slack(message: str):
> """Send alert to Slack."""
> if not SLACK_WEBHOOK_URL:
> return
>
> try:
> response = requests.post(SLACK_WEBHOOK_URL, json={"text": message, "mrkdwn": True})
> if response.status_code == 200:
> logger.info("✅ Slack alert sent")
> else:
> logger.error(f"Slack alert failed: {response.status_code}")
> except Exception as e:
> logger.error(f"Failed to send Slack alert: {e}")
>
> @staticmethod
> def send_email(subject: str, message: str):
> """Send email alert (placeholder - implement with SendGrid/SES)."""
> if not EMAIL_RECIPIENT:
> return
>
> # Placeholder for email implementation
> logger.info(f"📧 Would email {EMAIL_RECIPIENT}")
> logger.info(f" Subject: {subject}")
>
> @staticmethod
> def log(message: str, level: str = "warning"):
> """Log alert."""
> if level == "critical":
> logger.critical(f"🚨 {message}")
> elif level == "warning":
> logger.warning(f"⚠️ {message}")
> else:
> logger.info(f"ℹ️ {message}")
>
> # ============================================
> # METADATA CHECKER
> # ============================================
>
> def check_metadata(item_data: Dict) -> Tuple[bool, List[str]]:
> """Check if Zotero item has complete metadata. Returns (is_complete, missing_fields)."""
> missing = []
>
> # Critical fields
> if not item_data.get('title'):
> missing.append('Title')
>
> creators = item_data.get('creators', [])
> has_author = any(c.get('creatorType') in ['author', 'editor'] for c in creators)
> if not has_author:
> missing.append('Author/Editor')
>
> if not item_data.get('date'):
> missing.append('Date')
>
> if not item_data.get('itemType'):
> missing.append('Item Type')
>
> # Warnings
> if not item_data.get('abstractNote'):
> missing.append('Abstract (warning)')
>
> has_identifier = any([item_data.get('DOI'), item_data.get('ISBN'), item_data.get('url')])
> if not has_identifier:
> missing.append('Identifier (warning)')
>
> critical_fields = ['Title', 'Author/Editor', 'Date', 'Item Type']
> critical_missing = [f for f in missing if f in critical_fields]
> is_complete = len(critical_missing) == 0
>
> return is_complete, missing
>
> # ============================================
> # NOTION SYNC
> # ============================================
>
> def create_notion_page(item_data: Dict, metadata_complete: bool, usage_pct: Optional[float] = None):
> """Create a Notion page from Zotero item data."""
> title = item_data.get('title', 'Untitled Reference')
>
> # Extract author
> creators = item_data.get('creators', [])
> author_names = [
> f"{c.get('firstName', '')} {c.get('lastName', '')}".strip()
> for c in creators
> if c.get('creatorType') in ['author', 'editor']
> ]
> author = ", ".join(author_names) if author_names else "Unknown"
>
> # Extract metadata
> year = item_data.get('date', '').split('-')[0] if item_data.get('date') else ''
> abstract = item_data.get('abstractNote', '')
> url = item_data.get('url', '')
> doi = item_data.get('DOI', '')
> item_type = item_data.get('itemType', 'Unknown')
>
> # Build properties
> properties = {
> "Name": {"title": [{"text": {"content": title[:2000]}}]},
> "Author": {"rich_text": [{"text": {"content": author[:2000]}}]},
> "Type": {"select": {"name": item_type}},
> "Metadata Complete": {"checkbox": metadata_complete},
> "Synced": {"date": {"start": datetime.now().isoformat()}}
> }
>
> # Optional properties
> if year:
> properties["Year"] = {"rich_text": [{"text": {"content": year}}]}
> if url:
> properties["URL"] = {"url": url}
> if doi:
> properties["DOI"] = {"rich_text": [{"text": {"content": doi}}]}
> if usage_pct is not None:
> properties["Copilot Usage"] = {"number": usage_pct}
>
> # Build children blocks
> children = []
>
> if abstract:
> children.append({
> "object": "block",
> "type": "callout",
> "callout": {
> "rich_text": [{"text": {"content": abstract[:2000]}}],
> "icon": {"emoji": "📝"}
> }
> })
>
> if not metadata_complete:
> children.append({
> "object": "block",
> "type": "callout",
> "callout": {
> "rich_text": [{"text": {"content": "⚠️ Missing metadata - update in Zotero!"}}],
> "icon": {"emoji": "⚠️"},
> "color": "red"
> }
> })
>
> return notion.pages.create(
> parent={"database_id": NOTION_DATABASE_ID},
> properties=properties,
> children=children if children else None
> )
>
> # ============================================
> # MAIN SYNC FUNCTION
> # ============================================
>
> def sync():
> """Main sync function."""
> logger.info("="*60)
> logger.info("📚 Zotero → Notion Sync with Copilot Monitoring")
> logger.info("="*60)
>
> # 1. Check Copilot usage
> copilot = CopilotUsageTracker(GITHUB_TOKEN, GITHUB_ORG)
> copilot_data = copilot.fetch_usage()
> warning, critical, usage_pct = copilot.check_thresholds()
>
> # 2. Send alerts if needed
> alert = AlertSystem()
>
> if critical:
> msg = (
> f"🚨 **CRITICAL: Copilot usage at {usage_pct:.1%}!**\n"
> f"{copilot.get_summary()}\n"
> f"Please monitor usage immediately!"
> )
> alert.log(msg, "critical")
> alert.send_slack(msg)
> alert.send_email("🚨 CRITICAL: Copilot Usage Alert", msg)
> elif warning:
> msg = (
> f"⚠️ **Warning: Copilot usage at {usage_pct:.1%}**\n"
> f"{copilot.get_summary()}\n"
> f"Consider optimizing completions."
> )
> alert.log(msg, "warning")
> alert.send_slack(msg)
>
> # 3. Fetch Zotero items
> logger.info("📚 Fetching items from Zotero...")
> try:
> items = zot.top(limit=10) # Remove limit for all items
> except Exception as e:
> logger.error(f"Failed to fetch Zotero items: {e}")
> return
>
> if not items:
> logger.info("No items found in Zotero")
> return
>
> logger.info(f"Found {len(items)} items to sync")
>
> # 4. Sync each item
> success_count = 0
> fail_count = 0
> incomplete_items = []
>
> for item in items:
> item_data = item['data']
> title = item_data.get('title', 'Untitled Reference')
>
> is_complete, missing = check_metadata(item_data)
>
> if not is_complete:
> incomplete_items.append(item_data)
> logger.warning(f"⚠️ {title} - Missing: {', '.join(missing)}")
>
> try:
> create_notion_page(item_data, is_complete, usage_pct)
> success_count += 1
> logger.info(f"✅ Synced: {title}")
> except Exception as e:
> fail_count += 1
> logger.error(f"❌ Failed: {title} - {e}")
>
> # 5. Final summary
> logger.info("\n" + "="*60)
> logger.info("📊 SYNC SUMMARY")
> logger.info(f"✅ Success: {success_count}")
> logger.info(f"❌ Failed: {fail_count}")
> logger.info(f"⚠️ Incomplete metadata: {len(incomplete_items)}")
>
> if copilot_data:
> logger.info(f"\n{copilot.get_summary()}")
>
> if incomplete_items:
> logger.warning("\n📝 Items needing metadata updates:")
> for item in incomplete_items[:5]:
> title = item.get('title', 'Untitled')
> _, missing = check_metadata(item)
> logger.warning(f" • {title} - Missing: {', '.join(missing)}")
> if len(incomplete_items) > 5:
> logger.warning(f" ... and {len(incomplete_items) - 5} more")
>
> logger.info("="*60)
> logger.info("✅ Sync complete!")
>
> # ============================================
> # RUN
> # ============================================
>
> if __name__ == "__main__":
> try:
> sync()
> except KeyboardInterrupt:
> logger.info("Sync interrupted by user")
> sys.exit(0)
> except Exception as e:
> logger.error(f"Sync failed: {e}")
> sys.exit(1)
> ```
>
> Quick Copy to GitHub:
>
> 1. Save the file as zotero_notion_sync.py
> 2. Create requirements.txt:
>
> ```txt
> pyzotero>=1.5.0
> notion-client>=2.0.0
> requests>=2.28.0
> python-dotenv>=0.19.0
> ```
>
> 3. Create .env.example:
>
> ```bash
> # Zotero
> ZOTERO_USER_ID=your_user_id
> ZOTERO_API_KEY=your_api_key
>
> # Notion
> NOTION_TOKEN=secret_your_token
> NOTION_DATABASE_ID=your_database_id
>
> # GitHub Copilot (optional)
> GITHUB_TOKEN=github_pat_xxx
> GITHUB_ORG=your_org
>
> # Thresholds (optional)
> COPILOT_WARNING_THRESHOLD=0.80
> COPILOT_CRITICAL_THRESHOLD=0.95
>
> # Alerts (optional)
> SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx
> EMAIL_RECIPIENT=your@email.com
> ```
>
> 4. Create README.md:
>
> ```markdown
> # Zotero ↔ Notion Sync with Copilot Monitoring
>
> Syncs Zotero references to Notion with metadata checking and GitHub Copilot usage alerts.
>
> ## Features
> - ✅ Two-way sync between Zotero and Notion
> - ✅ Metadata completeness checking
> - ✅ GitHub Copilot usage monitoring (80% warning, 95% critical)
> - ✅ Slack & email alerts
> - ✅ Automatic metadata warnings in Notion
>
> ## Setup
> 1. Copy `.env.example` to `.env` and fill in credentials
> 2. Install dependencies: `pip install -r requirements.txt`
> 3. Run: `python zotero_notion_sync.py`
>
> ## Environment Variables
> See `.env.example` for all options.
> ```
>
> 5. Push to GitHub:
>
> ```bash
> git init
> git add zotero_notion_sync.py requirements.txt .env.example README.md
> git commit -m "Initial commit: Zotero-Notion sync with Copilot monitoring"
> git remote add origin https://github.com/yourusername/yourrepo.git
> git push -u origin main
> ```
>
> GitHub Actions (Optional) - .github/workflows/sync.yml:
>
> ```yaml
> name: Sync Zotero to Notion
>
> on:
> schedule:
> - cron: '0 9 * * *' # Daily at 9 AM
> workflow_dispatch:
>
> jobs:
> sync:
> runs-on: ubuntu-latest
> steps:
> - uses: actions/checkout@v3
> - uses: actions/setup-python@v4
> with:
> python-version: '3.9'
> - name: Install dependencies
> run: pip install -r requirements.txt
> - name: Run sync
> env:
> ZOTERO_USER_ID: ${{ secrets.ZOTERO_USER_ID }}
> ZOTERO_API_KEY: ${{ secrets.ZOTERO_API_KEY }}
> NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
> NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
> GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
> SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
> run: python zotero_notion_sync.py
> ```
>
> This is production-ready and you can copy-paste it directly to GitHub! 🚀
_Originally posted by @ok7-ship-it in [#4295](https://github.com/github/copilot-cli/issues/4295#issuecomment-5126067633)_
Guía de contribución
Línea de trabajo
Comienza revisando el título del issue y el script adjunto zotero_notion_sync.py. El issue no indica qué cambio se solicita, qué archivos del repositorio deberían modificarse ni cómo se verificaría la finalización; aclara el alcance previsto de Copilot CLI y los criterios de aceptación antes de investigar la implementación.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- github, python
- Área
- cli
- Tipo de issue
- Nueva funcionalidad
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Estado de actividad
- Tranquilo
- Claridad
- Necesita aclaración
- Aptitud para principiantes
- 15/100