From 780c2ec22827567fa5547133d5aa24b3c14e6fa7 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Mon, 27 Oct 2025 14:18:35 -0400 Subject: [PATCH 01/14] initial setup --- .github/workflows/rill-extensions.yml | 24 + bids_report.md | 34 + extensions/README.md | 7 + extensions/gemini/.example.env | 3 + extensions/gemini/.gitignore | 4 + extensions/gemini/.prettierrc | 5 + extensions/gemini/GEMINI.md | 197 +++++ extensions/gemini/README.md | 97 +++ extensions/gemini/commands/rill/analyze.toml | 94 +++ extensions/gemini/commands/rill/generate.toml | 62 ++ extensions/gemini/docs/LOCAL.md | 31 + extensions/gemini/docs/rill-icon.png | Bin 0 -> 1368 bytes extensions/gemini/eslint.config.js | 26 + extensions/gemini/gemini-extension.json | 26 + extensions/gemini/package.json | 45 ++ extensions/gemini/reports/.gitkeep | 0 extensions/gemini/src/lib/auth.ts | 47 ++ extensions/gemini/src/lib/drive/docs.ts | 165 +++++ extensions/gemini/src/lib/drive/drive.ts | 44 ++ extensions/gemini/src/lib/drive/sheets.ts | 112 +++ extensions/gemini/src/lib/utils.ts | 150 ++++ extensions/gemini/src/server.ts | 22 + extensions/gemini/src/tools/generate.ts | 156 ++++ extensions/gemini/src/tools/tools.ts | 10 + extensions/gemini/tsconfig.json | 14 + package-lock.json | 682 +++++++++++++++--- package.json | 3 +- 27 files changed, 1945 insertions(+), 115 deletions(-) create mode 100644 .github/workflows/rill-extensions.yml create mode 100644 bids_report.md create mode 100644 extensions/README.md create mode 100644 extensions/gemini/.example.env create mode 100644 extensions/gemini/.gitignore create mode 100644 extensions/gemini/.prettierrc create mode 100644 extensions/gemini/GEMINI.md create mode 100644 extensions/gemini/README.md create mode 100644 extensions/gemini/commands/rill/analyze.toml create mode 100644 extensions/gemini/commands/rill/generate.toml create mode 100644 extensions/gemini/docs/LOCAL.md create mode 100644 extensions/gemini/docs/rill-icon.png create mode 100644 extensions/gemini/eslint.config.js create mode 100644 extensions/gemini/gemini-extension.json create mode 100644 extensions/gemini/package.json create mode 100644 extensions/gemini/reports/.gitkeep create mode 100644 extensions/gemini/src/lib/auth.ts create mode 100644 extensions/gemini/src/lib/drive/docs.ts create mode 100644 extensions/gemini/src/lib/drive/drive.ts create mode 100644 extensions/gemini/src/lib/drive/sheets.ts create mode 100644 extensions/gemini/src/lib/utils.ts create mode 100644 extensions/gemini/src/server.ts create mode 100644 extensions/gemini/src/tools/generate.ts create mode 100644 extensions/gemini/src/tools/tools.ts create mode 100644 extensions/gemini/tsconfig.json diff --git a/.github/workflows/rill-extensions.yml b/.github/workflows/rill-extensions.yml new file mode 100644 index 000000000000..b29f99c8842e --- /dev/null +++ b/.github/workflows/rill-extensions.yml @@ -0,0 +1,24 @@ +name: Build and Release Extensions +on: + pull_request: + paths: + - '.github/workflows/rill-extensions.yml' + - 'extensions/gemini/**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up NodeJS + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: npm install + + - name: Build extensions + run: npm run build -w extensions/gemini diff --git a/bids_report.md b/bids_report.md new file mode 100644 index 000000000000..de4365782d8c --- /dev/null +++ b/bids_report.md @@ -0,0 +1,34 @@ + +# Bids Metrics Report: Last Week (2025-09-26 to 2025-10-02) + +## Executive Summary + +This report summarizes the key bidding metrics for the last week. The total advertising spend across all campaigns was **$673,000**. The top-performing advertiser was **Hyundai**, with their **Hyundai_Revitalize** campaign generating **$246,883.26** in spend. + +## Top 10 Campaigns by Spend + +| Advertiser | Campaign | Spend (USD) | Total Bids | Impressions | Win Rate | CTR | +|---|---|---|---|---|---|---| +| Hyundai | Hyundai_Revitalize | 246,883.26 | 277,694 | 44,302 | 15.95% | 0.13% | +| drakesoftware | drakesoftware_BrandBlast | 94,569.04 | 42,233 | 11,003 | 26.05% | 0.09% | +| Leafly | Leafly_MarketMight | 56,278.38 | 16,830 | 11,439 | 67.97% | 0.03% | +| Instacart | Instacart_BrandBounce | 53,203.38 | 43,636 | 11,353 | 26.02% | 0.10% | +| Amgen Inc | Amgen Inc_BrandRamp | 47,923.38 | 133,030 | 3,435 | 2.58% | 0.47% | +| ESPACIO URBANO | ESPACIO URBANO_PrimeProfit | 47,807.05 | 30,457 | 3,952 | 12.98% | 0.13% | +| Hennessy | Hennessy_MarketMaster | 34,117.65 | 39,971 | 2,460 | 6.15% | 0.04% | +| Instacart | Instacart_PrimePinnacle | 32,784.02 | 53,770 | 3,453 | 6.42% | 0.03% | +| GlaxoSmithKline | GlaxoSmithKline_ExpandNow | 32,074.83 | 36,033 | 2,309 | 6.41% | 0.00% | +| Johnson & Johnson | Johnson & Johnson_GrowthGlow | 27,405.06 | 47,280 | 1,970 | 4.17% | 0.71% | + + +## Key Insights + +* **Hyundai's** campaign has the highest spend, but a relatively low win rate compared to other top campaigns. This suggests they are bidding aggressively but not always winning the auction. +* **Leafly's** campaign has an exceptionally high win rate (67.97%), indicating their bids are very competitive. +* **Johnson & Johnson's** campaign has a very high CTR (0.71%) despite a low win rate, suggesting their creatives are highly effective at capturing user attention. + +## Recommendations + +* **Hyundai:** Analyze bidding strategy to improve win rate and optimize spend. +* **Leafly:** Continue with the current strategy as it appears to be very effective. +* **All Advertisers:** Investigate the creative strategies of campaigns with high CTRs to identify best practices. diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 000000000000..ebf4e2a15838 --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,7 @@ +# Rill Extensions + +Extend the capabilities of Rill Data with a variety of extensions designed to enhance your data workflows. + +## Google Gemini Extension + +Enhance your Rill Data experience with the Gemini extension, enabling advanced data analysis and report generation. Explore the [Gemini README](gemini/README.md) for detailed information on installation, configuration, and usage. diff --git a/extensions/gemini/.example.env b/extensions/gemini/.example.env new file mode 100644 index 000000000000..72f25ea1a228 --- /dev/null +++ b/extensions/gemini/.example.env @@ -0,0 +1,3 @@ +# Rill Extension Development Configuration +RILL_MCP_SERVER_URL="https://api.rilldata.com/v1/organizations/demo/projects/demo-project/runtime/mcp" +RILL_AUTH_TOKEN="rill_usr_1234567890abcdef" diff --git a/extensions/gemini/.gitignore b/extensions/gemini/.gitignore new file mode 100644 index 000000000000..23865b4f6f6f --- /dev/null +++ b/extensions/gemini/.gitignore @@ -0,0 +1,4 @@ +.env +*.log +node_modules/ +dist/ diff --git a/extensions/gemini/.prettierrc b/extensions/gemini/.prettierrc new file mode 100644 index 000000000000..7de69380e7c5 --- /dev/null +++ b/extensions/gemini/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100 +} diff --git a/extensions/gemini/GEMINI.md b/extensions/gemini/GEMINI.md new file mode 100644 index 000000000000..b7f3a3dc6c79 --- /dev/null +++ b/extensions/gemini/GEMINI.md @@ -0,0 +1,197 @@ +# Rill Analytics Extension - Data Analysis & Report Generation + +This extension enables comprehensive data analysis and professional report generation using Rill's metrics layer combined with Google Docs integration. + +## Core Capabilities + +### Analytics Functions + +- **Data Discovery**: Systematic exploration of available metrics views and data sources +- **Trend Analysis**: Time-series analysis with period-over-period comparisons +- **Anomaly Detection**: Identification of unusual patterns and outliers +- **Metrics Comparison**: Side-by-side analysis of different time periods or segments +- **Business Intelligence**: Actionable insights generation for executive decision-making + +### Report Generation + +- **Google Docs Integration**: Professional document creation with proper formatting +- **Automated Sharing**: Configurable document sharing with stakeholders +- **Multiple Report Types**: Summary reports, comparison analyses, and detailed insights +- **Executive-Ready Format**: Clear structure with executive summaries and recommendations + +## Analytical Methodology + +### Phase 1: Data Discovery (Systematic Approach) + +The extension follows a structured discovery process: + +1. **Dataset Identification**: Use `list_metrics_views` to catalog available data sources +2. **Schema Understanding**: Analyze measures, dimensions, and relationships via `get_metrics_view` +3. **Data Scope Assessment**: Determine time ranges and data coverage with `query_metrics_view_summary` + +### Phase 2: Analytical Investigation (OODA Loop) + +Execute iterative analysis cycles: + +- **Observe**: Query data systematically, starting broad then drilling into specifics +- **Orient**: Evaluate patterns, identify anomalies, and assess business context +- **Decide**: Prioritize analytical angles based on emerging insights +- **Act**: Execute targeted queries to validate hypotheses and uncover insights + +### Phase 3: Insight Synthesis & Documentation + +Transform findings into actionable intelligence: + +- **Pattern Recognition**: Identify trends, seasonality, and behavioral changes +- **Impact Quantification**: Measure business impact with specific metrics and percentages +- **Contextual Analysis**: Connect data patterns to business operations and decisions +- **Report Generation**: Create professional documentation using Google Docs tools + +## Data Analysis Best Practices + +### Query Strategy + +- **Minimum 4-6 distinct analytical queries** per analysis session +- **Progressive refinement**: Build each query based on previous results +- **Time-based analysis**: Always include period-over-period comparisons +- **Multi-dimensional exploration**: Analyze across different segments and dimensions + +### Insight Quality Standards + +- **Quantified findings**: All insights must include specific numbers and percentages +- **Business relevance**: Connect patterns to operational decisions and outcomes +- **Surprise factor**: Prioritize unexpected findings that challenge assumptions +- **Actionability**: Focus on insights that enable concrete business actions + +### Data Accuracy Requirements + +- **Tool-based calculations only**: All numbers must come from `query_metrics_view_summary` results +- **No manual computations**: Avoid performing calculations outside of the metrics tools +- **Source attribution**: Clearly indicate data sources and calculation methods +- **Limitation acknowledgment**: State when desired calculations cannot be performed + +## Report Structure & Formatting + +### Executive Summary Format + +```markdown +# [Report Title] + +## Executive Summary + +- **Key Finding 1**: [Quantified insight with business impact] +- **Key Finding 2**: [Trend or pattern with specific metrics] +- **Key Finding 3**: [Actionable recommendation with expected outcome] + +## Data Overview + +- **Analysis Period**: [Time range analyzed] +- **Metrics Covered**: [Primary measures and dimensions] +- **Data Sources**: [Metrics views and underlying datasets] +``` + +### Detailed Analysis Structure + +1. **Methodology**: Approach and tools used +2. **Key Findings**: Primary insights with supporting data +3. **Trend Analysis**: Time-based patterns and changes +4. **Segment Analysis**: Performance across different dimensions +5. **Recommendations**: Actionable next steps based on findings +6. **Appendix**: Additional data and methodology notes + +## Google Cloud Prerequisites + +### Authentication Setup + +Users must have proper Google Cloud credentials configured: + +1. **Install Google Cloud SDK**: + + ```bash + curl https://sdk.cloud.google.com | bash + exec -l $SHELL + ``` + +2. **Authenticate with Google Cloud**: + + ```bash + gcloud auth login + gcloud auth application-default login + ``` + +3. **Set Default Project** (if needed): + ```bash + gcloud config set project YOUR_PROJECT_ID + ``` + +### Required Permissions + +The service account or user must have: + +- **Google Drive API access**: For document creation and management +- **Google Docs API access**: For content formatting and updates +- **Sharing permissions**: To generate shareable links and manage access + +## Communication Guidelines + +### Analysis Presentation + +- **Conversational tone**: Use "I" and "you" to speak directly to users +- **Confident delivery**: Present findings with authority and enthusiasm +- **Business focus**: Frame technical findings in business context +- **Collaborative approach**: Invite questions and deeper exploration + +### Report Documentation + +- **Executive-friendly language**: Avoid technical jargon in summaries +- **Visual descriptions**: Include references to charts and visualizations +- **Clear structure**: Use headers, bullet points, and numbered lists +- **Methodology transparency**: Document analysis approach and data sources + +## Quality Assurance + +### Analysis Validation + +- **Cross-verification**: Validate findings across multiple query approaches +- **Logical consistency**: Ensure insights align with business reality +- **Completeness check**: Confirm all relevant dimensions are explored +- **Bias awareness**: Acknowledge potential limitations and biases + +### Report Review + +- **Accuracy verification**: Confirm all numbers match tool outputs +- **Clarity assessment**: Ensure findings are understandable to target audience +- **Actionability test**: Verify recommendations are specific and implementable +- **Stakeholder relevance**: Align content with recipient needs and priorities + +## Extension Usage Examples + +### Basic Analysis Request + +"Analyze our GitHub repository activity for the last quarter and identify key productivity trends." + +### Comparative Analysis + +"Generate a metrics comparison report between Q3 and Q4 sales performance, focusing on conversion rates and revenue per customer." + +### Custom Report Generation + +"Create an executive summary of user engagement metrics with recommendations for improving retention, and share it with the product team." + +## Troubleshooting + +### Common Issues + +- **Authentication failures**: Verify Google Cloud credentials and API enablement +- **Query errors**: Check metrics view availability and parameter validity +- **Report creation problems**: Confirm Google Docs API permissions and quota limits +- **Sharing issues**: Validate email addresses and sharing permission settings + +### Error Resolution + +1. **Check authentication status**: `gcloud auth list` +2. **Verify API enablement**: Ensure Drive and Docs APIs are enabled +3. **Review error messages**: Look for specific permission or quota issues +4. **Test connectivity**: Verify network access to Google Cloud services + +This extension transforms complex analytics into clear, actionable business intelligence while maintaining the highest standards of data accuracy and professional presentation. diff --git a/extensions/gemini/README.md b/extensions/gemini/README.md new file mode 100644 index 000000000000..1d7787732b31 --- /dev/null +++ b/extensions/gemini/README.md @@ -0,0 +1,97 @@ +# Gemini CLI Extension for Rill Data + +Professional data analysis and report generation using Rill's metrics layer with Google Docs integration. + +## Features + +- **Comprehensive Analytics**: Discover, analyze, and visualize metrics data +- **Professional Reports**: Generate executive-ready Google Docs with automated formatting +- **Local & Cloud Storage**: Create reports locally first, then optionally upload to Google Drive +- **Stakeholder Sharing**: Automatically share reports with configurable permissions + +Check out the [Rill Data website](https://www.rilldata.com) for more information about Rill. + +## Installation + +Install the extension via GitHub: + +```bash +gemini extensions install https://github.com/rilldata/gemini +``` + +Install Rill CLI if you haven't already: + +```bash +curl https://rill.sh | sh +``` + +## Configuration + +Generate a Rill authentication token: + +```bash +rill token issue --display-name "Gemini Extension" +``` + +Update `gemini-extension.json` with your Rill credentials: + +- `RILL_PROJECT_URL`: Your Rill project's MCP endpoint +- `RILL_AUTH_HEADER`: Your authentication token + +For Google Drive integration, ensure you have: + +- Valid Google Cloud credentials configured (see [GEMINI.md](GEMINI.md)) +- Appropriate scopes: `drive`, `documents` + +## Usage + +### Quick Example + +> Generate a change report comparing sales metrics from last month to this month using the `sales_dashboard` metrics view + +This extension will: + +1. Query your Rill metrics for both time periods +2. Analyze the differences in key metrics +3. Create a formatted Google Doc with the comparison +4. Share it and return the link + +### Basic Analysis + +```bash +gemini rill analyze "Show me user engagement trends for the last quarter" +``` + +### Report Generation + +#### Create a Report + +```bash +gemini rill generate "Create a monthly performance report" +``` + +#### Create Local Report, Then Upload Later + +```bash +# First, create locally +gemini rill generate "Create quarterly analysis" + +# Review the report, then upload when ready +gemini rill upload_local_report --filePath "./reports/quarterly-analysis-2025-10-03.md" +``` + +## Available Tools + +- **`generate`** - Create comprehensive analytics reports + - Required: `title`, `content` + - Options: `saveLocalOnly`, `localFilePath`, `shareEmail` + +- **`export_to_sheet`** - Export Rill query results to Google Sheets + - Required: `title`, `data` + - Options: `sheetName`, `shareEmail` + +> **Note**: Additional tools like `create_metrics_comparison_report`, `update_report`, `upload_local_report`, and `list_reports` are planned for future releases. + +## Documentation + +For detailed documentation, visit the [Rill Docs](https://docs.rilldata.com/), and refer to the [Local Development Guide](LOCAL.md) for setting up a development environment. diff --git a/extensions/gemini/commands/rill/analyze.toml b/extensions/gemini/commands/rill/analyze.toml new file mode 100644 index 000000000000..f8500adb609a --- /dev/null +++ b/extensions/gemini/commands/rill/analyze.toml @@ -0,0 +1,94 @@ +description = "Analyzes Rill query results and generates actionable business insights with optional Google Doc report generation" + +prompt = """ +You are a senior data analyst specializing in business intelligence and actionable insights generation. Your role is to systematically explore data, uncover patterns, and create professional documentation. + +## Analysis Framework + +### Step 1: Data Discovery +1. **Catalog Available Data**: Use `list_metrics_views` to identify all available datasets +2. **Understand Schema**: Use `get_metrics_view` for each relevant view to understand measures and dimensions +3. **Assess Data Scope**: Use `query_metrics_view_summary` to determine available time periods + +### Step 2: Systematic Analysis +4. **Baseline Analysis**: Query high-level metrics to establish current state +5. **Trend Analysis**: Examine time-based patterns and seasonality +6. **Segment Analysis**: Break down metrics by key dimensions +7. **Comparative Analysis**: Compare periods, cohorts, or segments +8. **Anomaly Detection**: Identify unusual patterns or outliers + +### Step 3: Insight Generation +9. **Pattern Recognition**: Identify significant trends and relationships +10. **Business Impact**: Quantify the business implications of findings +11. **Root Cause Analysis**: Investigate underlying drivers of key patterns +12. **Recommendations**: Develop specific, actionable next steps + +### Step 4: Documentation (Optional) +13. **Report Creation**: Use `create_analytics_report` for comprehensive documentation +14. **Stakeholder Sharing**: Generate shareable links for team collaboration + +## Quality Standards + +- **Minimum 6 distinct queries** per analysis session +- **Quantified insights**: Every finding must include specific metrics +- **Business context**: Connect data patterns to operational decisions +- **Progressive depth**: Each query should build on previous results +- **Tool-based calculations**: Only use numbers directly from query results + +## Communication Style + +- Speak confidently and directly using "I" and "you" +- Present findings with authority while remaining collaborative +- Focus on business impact and actionability +- Use clear, executive-friendly language + +## Report Structure (when generating documents) + +1. **Executive Summary**: 3 key findings with quantified impact +2. **Methodology**: Data sources and analysis approach +3. **Key Findings**: Detailed insights with supporting data +4. **Trends & Patterns**: Time-based analysis results +5. **Recommendations**: Specific, actionable next steps +6. **Appendix**: Additional data and technical notes + +Always validate findings across multiple query approaches and ensure all insights are grounded in the actual data results. +- ALL numbers and calculations must come from "get_metrics_view" tool results +- NEVER perform manual calculations or mathematical operations +- If a desired calculation cannot be achieved through the metrics tools, explicitly state this limitation +- Use only the exact numbers returned by the tools in your analysis + + + +- If the user asks an unrelated question (e.g., general knowledge, politics, entertainment, trivia): + 1. Politely decline to answer the unrelated question + 2. Briefly explain that you specialize only in business data analysis + 3. Redirect the conversation back to datasets, metrics, or insights + 4. Do **NOT** attempt to answer the unrelated question + +Example response style: +*"I focus specifically on analyzing your business data and uncovering insights — so I won't be the right source for general knowledge questions. Let's pivot back to your project: would you like me to explore the available datasets so we can start surfacing insights?"* + + + +After each query in Step 2, think through: +- What patterns or anomalies did this reveal? +- How does this connect to previous findings? +- What new questions does this raise? +- What's the most valuable next query to run? +- Are there any surprising insights worth highlighting? + + + +Format your analysis as follows: +`+"```markdown"+` +Based on the data analysis, here are the key insights: + +1. ## [Headline with specific impact/number] + [Finding with business context and implications] + +2. ## [Headline with specific impact/number] + [Finding with business context and implications] + +3. ## [Headline with specific impact/number] + [Finding with business context and implications] +""" diff --git a/extensions/gemini/commands/rill/generate.toml b/extensions/gemini/commands/rill/generate.toml new file mode 100644 index 000000000000..b30610263d96 --- /dev/null +++ b/extensions/gemini/commands/rill/generate.toml @@ -0,0 +1,62 @@ +description = "Generates professional Google Doc reports from Rill data analysis" + +prompt = """ +You are a professional report writer specializing in transforming complex analytics into executive-ready business intelligence documents. + +## Report Generation Process + +### Step 1: Requirements Gathering +- Ask the user to specify report type, target audience, and key metrics of interest +- Determine time periods, dimensions, and specific business questions to address +- Clarify sharing requirements (public, specific emails, or private) + +### Step 2: Data Collection +- Use Rill MCP tools to systematically gather relevant data +- Execute comprehensive queries covering all requested metrics and dimensions +- Ensure data spans appropriate time periods for meaningful analysis + +### Step 3: Analysis & Synthesis +- Identify key patterns, trends, and anomalies in the data +- Calculate period-over-period changes and growth rates +- Develop business-relevant insights and implications +- Formulate specific, actionable recommendations + +### Step 4: Document Creation +- Structure content using clear executive summary format +- Use `create_analytics_report` or `create_metrics_comparison_report` tools +- Apply professional formatting with headers, bullet points, and data tables +- Include methodology section documenting data sources and approach + +### Step 5: Quality Assurance +- Verify all numbers match query results exactly +- Ensure insights are business-relevant and actionable +- Confirm document structure serves target audience needs +- Test sharing functionality and provide access instructions + +## Available Report Types + +1. **Executive Dashboard**: High-level KPI overview with key insights +2. **Performance Review**: Comprehensive business performance assessment +3. **Trend Analysis**: Time-series patterns and forecasting insights +4. **Comparison Report**: Side-by-side analysis of periods, segments, or cohorts +5. **Anomaly Investigation**: Deep dive into unusual patterns or outliers + +## Document Standards + +- **Executive Summary**: Maximum 3 bullet points with quantified findings +- **Data Visualization**: Describe charts and tables that would enhance understanding +- **Actionable Recommendations**: Specific next steps with expected outcomes +- **Source Attribution**: Clear documentation of data sources and methodology +- **Professional Tone**: Business-appropriate language for executive audience + +## Quality Checklist + +- [ ] All metrics sourced directly from Rill query results +- [ ] Insights include specific percentages and quantified impact +- [ ] Recommendations are actionable and time-bound +- [ ] Document structure serves target audience effectively +- [ ] Sharing permissions configured correctly +- [ ] Methodology and limitations clearly documented + +Focus on creating documents that enable informed decision-making and drive business action. +""" diff --git a/extensions/gemini/docs/LOCAL.md b/extensions/gemini/docs/LOCAL.md new file mode 100644 index 000000000000..28cadcc04240 --- /dev/null +++ b/extensions/gemini/docs/LOCAL.md @@ -0,0 +1,31 @@ +## Local Development + +## Link a Local Extension + +```bash +# Clone the Gemini extension repository +git clone https://github.com/rilldata/gemini.git && cd gemini +# Install dependencies +npm install +``` + +The `gemini extensions link` command creates a symbolic link from the extension installation directory to your development path. This is useful for testing changes without running `gemini extensions update` repeatedly. + +```bash +gemini extensions link $(pwd) +``` + +### How It Works + +On startup, Gemini CLI looks for extensions in `/.gemini/extensions` + +Extensions exist as a directory containing a `gemini-extension.json` file. For example: + +``` +/.gemini/extensions/my-extension/gemini-extension.json +``` + +## Documentation + +- [Gemini CLI Extension Documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/extension.md) +- [Google Cloud Configuration Guide](GEMINI.md) diff --git a/extensions/gemini/docs/rill-icon.png b/extensions/gemini/docs/rill-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e7040a73ab327a80655995ecafd73ef6819077 GIT binary patch literal 1368 zcmV-e1*iInP)&fmX=l51}ogod~laTU>YV-b9SO~n@|1t|)ywYu_(ZftaAp^HKYB0><(mH#fvefec)gDk=G1oFXG8YN6 zf;_#eLS0Ktm?%aaL4nYz0aO~H&%p!ioJ)+FNFX08Lr8^)S40W1EGw0)=hDJ}hd>wi z#n93#UV-2NlXFm6RJ&5cVewB^7a@}$HNr5$RP!`Y1_e4O&G9aX1b8aY_OnN_CsNon z8wx(Mr?NB@tuP7CtT>AQeN>{w7Hd<&o`X;(=QMp$x<19fj3PtM8cnOrjJSv(RHngQgVjFHJb4V5klTABho8RtAM2M#7;f%I_YYQ& zQ#z0HPyC6&#XgpnZ=>{1mp_=t!s3z0@6r2)C6=Nwjc6XMi*dk5?~gET^=@x)`r#RT z^5q>^y8kE=fGsJT}qDlRbEJ(wVnI|^Bm3%{*usVEFqtzFu}+9lamgopLq`5s<=eJ(x+w=ONuunz^{ zd#%Wl;wG?e6yDgXhy^8DYZ%r%EZqWH2@)RS&psZ}vtEgX#WpEHbno&ptUNUv-+wr} zNwvkU_DA*rrDEhLV!-}55eXigi*+jzI$Bt4sxS<%?=!|q@AOv}7-ktUMY$+M$QA6t zM`qDG-o)yu#6j@N)4i-f$b#Rl{)ZLJb}YnH4+#3p)Xs&dtlz}14CYDQJZ){PZ((nD zpSZH}@GP+E8Py5Tlq;bzV8S8mnzwpm{cn8p^$lUd-IyPykSiNkq;FH4b7d<*d+q8? z@A4xaY)9jJaQmw-U1wjAXe`_!p@zg*9dNP4i!A9GjpZsAAe6PY>+sfv&G;Ku2+Ng? zJ4wTr zkXpr6m4 z+Z|ACNnLExT_TO`HkfiTo0KCzq#L@bRivf&+?wrs(Zfu)pHa&oO~eL*P|PWOPs$oBNM zqytF-G{8n@y;Ml!DE6zN!>(+U_kemF7q(_wnhE_tLT(FkGwyt}3x>s94a{NVN!9}; zra)bxO_M>m(~#qL{D*C3>jjc(TB2i`rf(q&4pIubI((u!JU_hua}` atG57jf?d9m*EEv=0000=18.0.0" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/rilldata/rill", + "directory": "extensions/gemini" + }, + "author": { + "name": "Rill Data", + "email": "support@rilldata.com", + "url": "https://rilldata.com" + } +} diff --git a/extensions/gemini/reports/.gitkeep b/extensions/gemini/reports/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/extensions/gemini/src/lib/auth.ts b/extensions/gemini/src/lib/auth.ts new file mode 100644 index 000000000000..a16837e7f5dc --- /dev/null +++ b/extensions/gemini/src/lib/auth.ts @@ -0,0 +1,47 @@ +import { GoogleAuth } from 'google-auth-library'; + +// Singleton Google Auth instance +let googleAuthInstance: GoogleAuth | null = null; + +/** + * Gets or creates a single Google Auth instance with all required scopes. + */ +export async function getGoogleAuth(): Promise { + if (!googleAuthInstance) { + googleAuthInstance = new GoogleAuth({ + scopes: [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/spreadsheets', + ], + }); + + // Verify credentials work with required scopes + const hasCredentials = await ensureGCPCredentials(); + if (!hasCredentials) { + googleAuthInstance = null; + throw new Error('Google Cloud credentials are not properly configured'); + } + } + + return googleAuthInstance; +} + +/** + * Ensures that Google Cloud credentials are set up correctly with the required scopes. + */ +export async function ensureGCPCredentials(): Promise { + try { + if (!googleAuthInstance) { + return false; + } + + const client = await googleAuthInstance.getClient(); + const accessToken = await client.getAccessToken(); + + return accessToken.token !== null && accessToken.token !== undefined; + } catch (error) { + console.error('Google Cloud authentication failed:', error); + return false; + } +} diff --git a/extensions/gemini/src/lib/drive/docs.ts b/extensions/gemini/src/lib/drive/docs.ts new file mode 100644 index 000000000000..1be332960973 --- /dev/null +++ b/extensions/gemini/src/lib/drive/docs.ts @@ -0,0 +1,165 @@ +import { google, docs_v1, drive_v3 } from 'googleapis'; +import { getGoogleAuth } from '../auth.js'; +import { setupDriveClient } from './drive.js'; + +/** + * Sets up the Google Docs client. + */ +export async function setupDocsClient(): Promise { + const auth = await getGoogleAuth(); + return google.docs({ version: 'v1', auth }); +} + +/** Creates a new Google Document with the specified title. + * @param title - The title of the document to be created. + */ +export async function createDocument(title: string): Promise { + const docs = await setupDocsClient(); + const response = await docs.documents.create({ + requestBody: { title }, + }); + return response.data; +} + +/** + * Updates the content of a Google Document. + * @param documentId + * @param content + */ +export async function updateDocument( + documentId: string, + content: docs_v1.Schema$Request[], +): Promise { + const docs = await setupDocsClient(); + await docs.documents.batchUpdate({ + documentId, + requestBody: { requests: content }, + }); +} + +/** + * Shares a Google Document with the specified email or makes it public. + * @param documentId + * @param email + */ +export async function shareDocument( + documentId: string, + email?: string, +): Promise { + const drive = await setupDriveClient(); + + const permission: drive_v3.Schema$Permission = { + type: 'anyone', + role: 'reader', + }; + + if (email) { + permission.type = 'user'; + permission.emailAddress = email; + } + + await drive.permissions.create({ + fileId: documentId, + requestBody: permission, + }); + + const file = await drive.files.get({ + fileId: documentId, + fields: 'webViewLink', + }); + + return file.data.webViewLink; +} + +/** + * Gets the content of a Google Document. + * @param documentId - The ID of the document to retrieve + */ +export async function getDocument( + documentId: string, +): Promise { + const docs = await setupDocsClient(); + const response = await docs.documents.get({ + documentId, + }); + return response.data; +} + +/** + * Lists Google Drive documents (reports) created by this extension. + * @param maxResults - Maximum number of results to return (default: 10) + */ +export async function listDriveReports(maxResults: number = 10): Promise { + const drive = await setupDriveClient(); + const response = await drive.files.list({ + q: "mimeType='application/vnd.google-apps.document' and trashed=false", + fields: 'files(id, name, modifiedTime, webViewLink, createdTime)', + pageSize: maxResults, + orderBy: 'modifiedTime desc', + }); + return response.data.files || []; +} + +/** + * Uploads a local file to Google Drive and converts it to a Google Doc. + * @param filePath - Path to the local file + * @param title - Title for the Google Doc + * @param mimeType - MIME type of the source file (default: text/markdown) + */ +export async function uploadFileToGoogleDocs( + content: string, + title: string, + mimeType: string = 'text/markdown', +): Promise { + const drive = await setupDriveClient(); + + const fileMetadata = { + name: title, + mimeType: 'application/vnd.google-apps.document', + }; + + const media = { + mimeType: mimeType, + body: content, + }; + + const response = await drive.files.create({ + requestBody: fileMetadata, + media: media, + fields: 'id, name, webViewLink', + }); + + return response.data; +} + +/** + * Deletes all content from a Google Document and replaces it with new content. + * @param documentId - The ID of the document to update + * @param _content - The new content in markdown format (to be implemented) + */ +export async function replaceDocumentContent(documentId: string, _content: string): Promise { + const docs = await setupDocsClient(); + + const doc = await getDocument(documentId); + if (!doc?.body?.content) { + throw new Error('Could not retrieve document content'); + } + + const endIndex = doc.body.content[doc.body.content.length - 1]?.endIndex || 1; + + const deleteRequest = { + deleteContentRange: { + range: { + startIndex: 1, + endIndex: endIndex - 1, + }, + }, + }; + + await docs.documents.batchUpdate({ + documentId, + requestBody: { + requests: [deleteRequest], + }, + }); +} diff --git a/extensions/gemini/src/lib/drive/drive.ts b/extensions/gemini/src/lib/drive/drive.ts new file mode 100644 index 000000000000..3d959de630aa --- /dev/null +++ b/extensions/gemini/src/lib/drive/drive.ts @@ -0,0 +1,44 @@ +import { google, drive_v3 } from 'googleapis'; +import { getGoogleAuth } from '../auth.js'; + +/** + * Sets up the Google Drive client. + */ +export async function setupDriveClient(): Promise { + const auth = await getGoogleAuth(); + return google.drive({ version: 'v3', auth }); +} + +/** + * Shares a Google Document with the specified email or makes it public. + * @param documentId + * @param email + */ +export async function shareDocument( + documentId: string, + email?: string, +): Promise { + const drive = await setupDriveClient(); + + const permission: drive_v3.Schema$Permission = { + type: 'anyone', + role: 'reader', + }; + + if (email) { + permission.type = 'user'; + permission.emailAddress = email; + } + + await drive.permissions.create({ + fileId: documentId, + requestBody: permission, + }); + + const file = await drive.files.get({ + fileId: documentId, + fields: 'webViewLink', + }); + + return file.data.webViewLink; +} diff --git a/extensions/gemini/src/lib/drive/sheets.ts b/extensions/gemini/src/lib/drive/sheets.ts new file mode 100644 index 000000000000..a54aad091589 --- /dev/null +++ b/extensions/gemini/src/lib/drive/sheets.ts @@ -0,0 +1,112 @@ +import { google, sheets_v4 } from 'googleapis'; +import { getGoogleAuth } from '../auth.js'; +import { shareDocument } from './drive.js'; + +/** + * Sets up the Google Sheets client. + */ +export async function setupSheetsClient(): Promise { + const auth = await getGoogleAuth(); + return google.sheets({ version: 'v4', auth }); +} + +/** + * Creates a new Google Sheet with the specified title. + * @param title - The title of the sheet to be created. + */ +export async function createSheet( + title: string, +): Promise { + const sheets = await setupSheetsClient(); + const response = await sheets.spreadsheets.create({ + requestBody: { + properties: { title }, + }, + }); + return response.data; +} + +/** + * Updates the data in a Google Sheet. + * @param spreadsheetId - The ID of the spreadsheet to update. + * @param sheetName - The name of the sheet within the spreadsheet. + * @param headers - An array of header strings. + * @param rows - A 2D array representing the rows of data. + */ +export async function updateSheetData( + spreadsheetId: string, + sheetName: string, + headers: string[], + rows: any[][], +): Promise { + const sheets = await setupSheetsClient(); + + // Write headers + await sheets.spreadsheets.values.update({ + spreadsheetId, + range: `${sheetName}!A1`, + valueInputOption: 'RAW', + requestBody: { + values: [headers], + }, + }); + + // Write data rows + await sheets.spreadsheets.values.update({ + spreadsheetId, + range: `${sheetName}!A2`, + valueInputOption: 'RAW', + requestBody: { + values: rows, + }, + }); + + // Format headers (bold, frozen) + await sheets.spreadsheets.batchUpdate({ + spreadsheetId, + requestBody: { + requests: [ + { + repeatCell: { + range: { + sheetId: 0, + startRowIndex: 0, + endRowIndex: 1, + }, + cell: { + userEnteredFormat: { + textFormat: { bold: true }, + backgroundColor: { red: 0.9, green: 0.9, blue: 0.9 }, + }, + }, + fields: 'userEnteredFormat(textFormat,backgroundColor)', + }, + }, + { + updateSheetProperties: { + properties: { + sheetId: 0, + gridProperties: { + frozenRowCount: 1, + }, + }, + fields: 'gridProperties.frozenRowCount', + }, + }, + ], + }, + }); +} + +/** + * Shares the Google Sheet with the specified email or makes it publicly accessible. + * @param spreadsheetId - The ID of the spreadsheet to share. + * @param email - Optional email address to share the sheet with. + * @returns The web view link of the shared sheet. + */ +export async function shareSheet( + spreadsheetId: string, + email?: string, +): Promise { + return shareDocument(spreadsheetId, email); +} diff --git a/extensions/gemini/src/lib/utils.ts b/extensions/gemini/src/lib/utils.ts new file mode 100644 index 000000000000..2e682a31b475 --- /dev/null +++ b/extensions/gemini/src/lib/utils.ts @@ -0,0 +1,150 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { join, dirname } from 'path'; +import { docs_v1 } from 'googleapis'; + +export interface MetricsData { + currentPeriod: Record; + previousPeriod: Record; + periodLabels: { + current: string; + previous: string; + }; +} + +/** + * Writes report content to a local file + */ +export async function writeLocalReport(content: string, filePath: string): Promise { + try { + const dir = dirname(filePath); + + await mkdir(dir, { recursive: true }); + + await writeFile(filePath, content, 'utf8'); + + return filePath; + } catch (error) { + throw new Error( + `Failed to write local file: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } +} + +/** + * Generates a default local file path if none is provided + */ +export function getDefaultLocalPath(title: string, extension: string = 'md'): string { + const sanitizedTitle = title + .replace(/[^a-zA-Z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .toLowerCase(); + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); + return join(process.cwd(), 'reports', `${sanitizedTitle}-${timestamp}.${extension}`); +} + +/** + * Converts markdown content to Google Docs format requests + */ +export function convertMarkdownToGoogleDocsRequests(markdown: string): docs_v1.Schema$Request[] { + const requests = []; + const lines = markdown.split('\n'); + let insertIndex = 1; + + for (const line of lines) { + if (line.startsWith('# ')) { + // Header 1 + requests.push({ + insertText: { + location: { index: insertIndex }, + text: line.substring(2) + '\n', + }, + }); + requests.push({ + updateTextStyle: { + range: { + startIndex: insertIndex, + endIndex: insertIndex + line.substring(2).length, + }, + textStyle: { + fontSize: { magnitude: 20, unit: 'PT' }, + bold: true, + }, + fields: 'fontSize,bold', + }, + }); + insertIndex += line.substring(2).length + 1; + } else if (line.startsWith('## ')) { + // Header 2 + requests.push({ + insertText: { + location: { index: insertIndex }, + text: line.substring(3) + '\n', + }, + }); + requests.push({ + updateTextStyle: { + range: { + startIndex: insertIndex, + endIndex: insertIndex + line.substring(3).length, + }, + textStyle: { + fontSize: { magnitude: 16, unit: 'PT' }, + bold: true, + }, + fields: 'fontSize,bold', + }, + }); + insertIndex += line.substring(3).length + 1; + } else { + // Regular text + requests.push({ + insertText: { + location: { index: insertIndex }, + text: line + '\n', + }, + }); + insertIndex += line.length + 1; + } + } + + return requests; +} + +/** + * Generates comparison report content from metrics data + */ +export function generateComparisonReportContent( + metricsData: MetricsData, + insights: string[], +): string { + const { currentPeriod, previousPeriod, periodLabels } = metricsData; + + let content = `# Metrics Comparison Report\n\n`; + content += `## Executive Summary\n\n`; + content += `This report compares key metrics between ${periodLabels.previous} and ${periodLabels.current}.\n\n`; + + content += `## Metrics Overview\n\n`; + + // Compare each metric + for (const [metric, currentValue] of Object.entries(currentPeriod)) { + const previousValue = previousPeriod[metric]; + if (previousValue !== undefined) { + const change = ((currentValue - previousValue) / previousValue) * 100; + const direction = change > 0 ? '↑' : change < 0 ? '↓' : '→'; + + content += `### ${metric}\n`; + content += `- ${periodLabels.current}: ${currentValue}\n`; + content += `- ${periodLabels.previous}: ${previousValue}\n`; + content += `- Change: ${direction} ${Math.abs(change).toFixed(2)}%\n\n`; + } + } + + content += `## Key Insights\n\n`; + insights.forEach((insight, index) => { + content += `${index + 1}. ${insight}\n`; + }); + + content += `\n---\n\nReport generated on ${new Date().toLocaleDateString()}\n`; + + return content; +} diff --git a/extensions/gemini/src/server.ts b/extensions/gemini/src/server.ts new file mode 100644 index 000000000000..5eff99ea911b --- /dev/null +++ b/extensions/gemini/src/server.ts @@ -0,0 +1,22 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { registerTools } from './tools/tools.js'; + +const server = new McpServer({ + name: 'Rill Report Generator', + websiteUrl: 'https://rilldata.com', + version: '0.1.0', + title: 'Rill Report Generator', +}); + +// Register tools +registerTools(server); + +// Starts the MCP server and connects it to the standard I/O transport. +async function start() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +// Start the server +void start(); diff --git a/extensions/gemini/src/tools/generate.ts b/extensions/gemini/src/tools/generate.ts new file mode 100644 index 000000000000..2369c1b65689 --- /dev/null +++ b/extensions/gemini/src/tools/generate.ts @@ -0,0 +1,156 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import { shareDocument } from '../lib/drive/drive.js'; +import { createDocument, updateDocument } from '../lib/drive/docs.js'; +import { + writeLocalReport, + getDefaultLocalPath, + convertMarkdownToGoogleDocsRequests, +} from '../lib/utils.js'; +import { createSheet, shareSheet, updateSheetData } from '../lib/drive/sheets.js'; + +export function registerGenerateTool(server: McpServer) { + server.registerTool( + 'generate', + { + description: + 'Creates a comprehensive analytics report in Google Docs based on Rill data analysis. Optionally saves a local copy first.', + inputSchema: { + title: z.string().describe('The title of the report document.'), + content: z.string().describe('The main content of the report in markdown format.'), + shareEmail: z + .string() + .optional() + .describe( + 'Email address to share the document with (optional - if not provided, document will be public).', + ), + localFilePath: z + .string() + .optional() + .describe( + 'Optional local file path to save the report before creating Google Doc. If not provided, will generate a default path.', + ), + saveLocalOnly: z + .boolean() + .optional() + .describe( + 'If true, only saves locally and skips Google Docs creation. Defaults to false.', + ), + }, + }, + async (input) => { + try { + const { title, content, shareEmail, localFilePath, saveLocalOnly = false } = input; + + const results: string[] = []; + let localPath: string | null = null; + + // Write local file if requested or if saveLocalOnly is true + if (localFilePath || saveLocalOnly) { + const filePath = localFilePath || getDefaultLocalPath(title); + localPath = await writeLocalReport(content, filePath); + results.push(`Local report saved: ${localPath}`); + } + + // Skip Google Docs creation if saveLocalOnly is true + if (!saveLocalOnly) { + // Create the document + const document = await createDocument(title); + if (!document?.documentId) { + throw new Error('Failed to create document'); + } + + // Convert markdown content to Google Docs format + const requests = convertMarkdownToGoogleDocsRequests(content); + + // Update document with content + await updateDocument(document.documentId, requests); + + // Share the document + const shareLink = await shareDocument(document.documentId, shareEmail); + + results.push(`Google Docs report created successfully!`); + results.push(`Document ID: ${document.documentId}`); + results.push(`Share Link: ${shareLink}`); + } + + results.push(`Title: ${title}`); + + return { + content: [ + { + type: 'text', + text: results.join('\n\n'), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error creating report: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + ], + }; + } + }, + ); +} + +export function registerExportSheetTool(server: McpServer) { + server.registerTool( + 'export_to_sheet', + { + description: + 'Exports Rill query results to a Google Sheet with formatted tables and optional sharing.', + inputSchema: { + title: z.string().describe('The title of the Google Sheet.'), + data: z + .array(z.record(z.any())) + .describe('Array of query result objects to export as rows.'), + sheetName: z.string().optional().describe('Name for the worksheet tab (default: Sheet1).'), + shareEmail: z.string().optional().describe('Email address to share the sheet with.'), + }, + }, + async (input) => { + try { + const { title, data, sheetName = 'Sheet1', shareEmail } = input; + + // Create new spreadsheet + const sheet = await createSheet(title); + if (!sheet?.spreadsheetId) { + throw new Error('Failed to create spreadsheet'); + } + + // Convert data to rows format + const headers = Object.keys(data[0] || {}); + const rows = data.map((row) => headers.map((h) => row[h])); + + // Update sheet with data + await updateSheetData(sheet.spreadsheetId, sheetName, headers, rows); + + // Share the sheet + const shareLink = await shareSheet(sheet.spreadsheetId, shareEmail); + + return { + content: [ + { + type: 'text', + text: `Sheet exported successfully!\n\nSpreadsheet ID: ${sheet.spreadsheetId}\nTitle: ${title}\nShare Link: ${shareLink}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error exporting to sheet: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + ], + }; + } + }, + ); +} diff --git a/extensions/gemini/src/tools/tools.ts b/extensions/gemini/src/tools/tools.ts new file mode 100644 index 000000000000..3e24ce58608d --- /dev/null +++ b/extensions/gemini/src/tools/tools.ts @@ -0,0 +1,10 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerExportSheetTool, registerGenerateTool } from './generate.js'; + +/** + * Registers all tools with the MCP server. + */ +export const registerTools = (server: McpServer) => { + registerGenerateTool(server); + registerExportSheetTool(server); +}; diff --git a/extensions/gemini/tsconfig.json b/extensions/gemini/tsconfig.json new file mode 100644 index 000000000000..707155e8d049 --- /dev/null +++ b/extensions/gemini/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "lib"] +} diff --git a/package-lock.json b/package-lock.json index 42dd535b6087..d56a2caab7a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "web-admin", "web-common", "web-integration", - "web-local" + "web-local", + "extensions/gemini" ], "devDependencies": { "@types/eslint": "^8.56.9", @@ -206,6 +207,323 @@ "url": "https://paulmillr.com/funding/" } }, + "extensions/gemini": { + "name": "rill-analytics-extension", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.19.1", + "google-auth-library": "^10.4.0", + "googleapis": "^161.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^24.6.2", + "eslint": "^9.24.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.30.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "extensions/gemini/node_modules/@modelcontextprotocol/sdk": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz", + "integrity": "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "extensions/gemini/node_modules/@types/node": { + "version": "24.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.13.0" + } + }, + "extensions/gemini/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "extensions/gemini/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "extensions/gemini/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "extensions/gemini/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "extensions/gemini/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "extensions/gemini/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "extensions/gemini/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "extensions/gemini/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "extensions/gemini/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "extensions/gemini/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "extensions/gemini/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "extensions/gemini/node_modules/undici-types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "dev": true, + "license": "MIT" + }, + "extensions/gemini/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", @@ -13973,7 +14291,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -14033,7 +14350,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -14057,7 +14373,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -14526,7 +14841,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, "license": "MIT" }, "node_modules/array-union": { @@ -15056,7 +15370,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -15130,6 +15443,15 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -15177,7 +15499,6 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -15202,7 +15523,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -15212,14 +15532,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/body-parser/node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -15235,7 +15553,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -15418,6 +15735,12 @@ "node": ">=8.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -15449,7 +15772,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -15530,7 +15852,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15544,7 +15865,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -16777,7 +17097,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -16790,7 +17109,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16817,7 +17135,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, "license": "MIT" }, "node_modules/copy-text-to-clipboard": { @@ -16953,7 +17270,6 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -17105,7 +17421,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -18248,6 +18563,15 @@ "lodash-es": "^4.17.21" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -18334,7 +18658,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -18761,7 +19084,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -18791,7 +19113,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -19297,7 +19618,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -19385,11 +19705,19 @@ "ebnf": "dist/bin.js" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/effect": { @@ -19472,7 +19800,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -19681,7 +20008,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -19691,7 +20017,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -19729,7 +20054,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -19980,7 +20304,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -20604,7 +20927,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -20665,7 +20987,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -20678,7 +20999,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -20722,7 +21042,6 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -20769,7 +21088,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 16" @@ -20785,7 +21103,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -20795,7 +21112,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -20805,14 +21121,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/express/node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -20845,7 +21159,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, "license": "MIT" }, "node_modules/extend-shallow": { @@ -20928,7 +21241,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -20967,7 +21279,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -21097,6 +21408,29 @@ "node": ">=0.4.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", @@ -21256,7 +21590,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -21275,7 +21608,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -21285,7 +21617,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/find-cache-dir": { @@ -21539,11 +21870,22 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -21567,7 +21909,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -21661,7 +22002,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21705,6 +22045,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz", + "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21752,7 +22138,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -21827,7 +22212,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -22025,11 +22409,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.4.0.tgz", + "integrity": "sha512-CmIrSy1bqMQUsPmA9+hcSbAXL80cFhu40cGMUjCaLpNKVzzvi+0uAHq8GNZxkoGYIsTX4ZQ7e4aInAqWxgn4fg==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "161.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-161.0.0.tgz", + "integrity": "sha512-JZy2cWMxgUF8E09KHzplI+z+FVG8NWDB/bsf4xevt9Um4bInb0X1qaG9qpDn49DHT5HsU0mOp3EOBGb8+AdE3Q==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz", + "integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -22161,6 +22600,19 @@ ], "license": "MIT" }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -22297,7 +22749,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -22339,7 +22790,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -22952,7 +23402,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -23071,7 +23520,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -23111,7 +23559,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -23278,7 +23725,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -23334,7 +23780,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -24096,7 +24541,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -24498,6 +24942,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -24564,7 +25017,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -24653,6 +25105,27 @@ "node": "*" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/katex": { "version": "0.16.22", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", @@ -25606,7 +26079,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -26118,7 +26590,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -26178,7 +26649,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -26261,7 +26731,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -28163,7 +28632,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -28176,7 +28644,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -28186,7 +28653,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -28488,7 +28954,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/multicast-dns": { @@ -28585,7 +29050,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -28681,6 +29145,26 @@ "node": "*" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -29154,7 +29638,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29174,7 +29657,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29242,7 +29724,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -29265,7 +29746,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -30003,7 +30483,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -30065,7 +30544,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -30106,7 +30584,6 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, "license": "MIT" }, "node_modules/path-type": { @@ -30231,7 +30708,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -32445,7 +32921,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -32500,7 +32975,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -32682,7 +33156,6 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -32791,7 +33264,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -32801,7 +33273,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -32817,7 +33288,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -34331,6 +34801,10 @@ "dev": true, "license": "MIT" }, + "node_modules/rill-analytics-extension": { + "resolved": "extensions/gemini", + "link": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -34435,7 +34909,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -34452,14 +34925,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, "license": "MIT" }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -34559,7 +35030,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -34622,7 +35092,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sander": { @@ -34968,7 +35437,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -34993,7 +35461,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -35003,14 +35470,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -35216,7 +35681,6 @@ "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -35288,7 +35752,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { @@ -35315,7 +35778,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -35328,7 +35790,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -35428,7 +35889,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -35448,7 +35908,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -35465,7 +35924,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35484,7 +35942,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35915,7 +36372,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -38403,7 +38859,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -39206,7 +39661,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -39460,9 +39914,9 @@ } }, "node_modules/undici": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.11.0.tgz", - "integrity": "sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "dev": true, "license": "MIT", "engines": { @@ -39677,7 +40131,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -39833,7 +40286,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -39904,7 +40356,6 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "dev": true, "license": "BSD" }, "node_modules/use-callback-ref": { @@ -40018,7 +40469,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -40109,7 +40559,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -41672,6 +42121,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", @@ -42176,7 +42634,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -42433,7 +42890,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -42751,7 +43207,6 @@ "version": "3.24.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -42761,7 +43216,6 @@ "version": "3.24.5", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.24.1" diff --git a/package.json b/package.json index 9a33c5cbd181..ea6369a33bae 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "web-admin", "web-common", "web-integration", - "web-local" + "web-local", + "extensions/gemini" ], "scripts": { "prepare": "husky || true", From 79d0366a9f42b2c26cae549fc948c6180264e6b9 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Mon, 27 Oct 2025 14:23:38 -0400 Subject: [PATCH 02/14] address prettier issues --- .prettierignore | 1 + bids_report.md | 38 ++++++++++++++++++------------------- extensions/gemini/README.md | 1 - 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.prettierignore b/.prettierignore index 93696b4ac490..80fd4b5fee42 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,6 +20,7 @@ web-common/tests/projects web-local/tests/data .env +.example.env *.sql *.bin *.csv diff --git a/bids_report.md b/bids_report.md index de4365782d8c..799cb56b14df 100644 --- a/bids_report.md +++ b/bids_report.md @@ -1,4 +1,3 @@ - # Bids Metrics Report: Last Week (2025-09-26 to 2025-10-02) ## Executive Summary @@ -7,28 +6,27 @@ This report summarizes the key bidding metrics for the last week. The total adve ## Top 10 Campaigns by Spend -| Advertiser | Campaign | Spend (USD) | Total Bids | Impressions | Win Rate | CTR | -|---|---|---|---|---|---|---| -| Hyundai | Hyundai_Revitalize | 246,883.26 | 277,694 | 44,302 | 15.95% | 0.13% | -| drakesoftware | drakesoftware_BrandBlast | 94,569.04 | 42,233 | 11,003 | 26.05% | 0.09% | -| Leafly | Leafly_MarketMight | 56,278.38 | 16,830 | 11,439 | 67.97% | 0.03% | -| Instacart | Instacart_BrandBounce | 53,203.38 | 43,636 | 11,353 | 26.02% | 0.10% | -| Amgen Inc | Amgen Inc_BrandRamp | 47,923.38 | 133,030 | 3,435 | 2.58% | 0.47% | -| ESPACIO URBANO | ESPACIO URBANO_PrimeProfit | 47,807.05 | 30,457 | 3,952 | 12.98% | 0.13% | -| Hennessy | Hennessy_MarketMaster | 34,117.65 | 39,971 | 2,460 | 6.15% | 0.04% | -| Instacart | Instacart_PrimePinnacle | 32,784.02 | 53,770 | 3,453 | 6.42% | 0.03% | -| GlaxoSmithKline | GlaxoSmithKline_ExpandNow | 32,074.83 | 36,033 | 2,309 | 6.41% | 0.00% | -| Johnson & Johnson | Johnson & Johnson_GrowthGlow | 27,405.06 | 47,280 | 1,970 | 4.17% | 0.71% | - +| Advertiser | Campaign | Spend (USD) | Total Bids | Impressions | Win Rate | CTR | +| ----------------- | ---------------------------- | ----------- | ---------- | ----------- | -------- | ----- | +| Hyundai | Hyundai_Revitalize | 246,883.26 | 277,694 | 44,302 | 15.95% | 0.13% | +| drakesoftware | drakesoftware_BrandBlast | 94,569.04 | 42,233 | 11,003 | 26.05% | 0.09% | +| Leafly | Leafly_MarketMight | 56,278.38 | 16,830 | 11,439 | 67.97% | 0.03% | +| Instacart | Instacart_BrandBounce | 53,203.38 | 43,636 | 11,353 | 26.02% | 0.10% | +| Amgen Inc | Amgen Inc_BrandRamp | 47,923.38 | 133,030 | 3,435 | 2.58% | 0.47% | +| ESPACIO URBANO | ESPACIO URBANO_PrimeProfit | 47,807.05 | 30,457 | 3,952 | 12.98% | 0.13% | +| Hennessy | Hennessy_MarketMaster | 34,117.65 | 39,971 | 2,460 | 6.15% | 0.04% | +| Instacart | Instacart_PrimePinnacle | 32,784.02 | 53,770 | 3,453 | 6.42% | 0.03% | +| GlaxoSmithKline | GlaxoSmithKline_ExpandNow | 32,074.83 | 36,033 | 2,309 | 6.41% | 0.00% | +| Johnson & Johnson | Johnson & Johnson_GrowthGlow | 27,405.06 | 47,280 | 1,970 | 4.17% | 0.71% | ## Key Insights -* **Hyundai's** campaign has the highest spend, but a relatively low win rate compared to other top campaigns. This suggests they are bidding aggressively but not always winning the auction. -* **Leafly's** campaign has an exceptionally high win rate (67.97%), indicating their bids are very competitive. -* **Johnson & Johnson's** campaign has a very high CTR (0.71%) despite a low win rate, suggesting their creatives are highly effective at capturing user attention. +- **Hyundai's** campaign has the highest spend, but a relatively low win rate compared to other top campaigns. This suggests they are bidding aggressively but not always winning the auction. +- **Leafly's** campaign has an exceptionally high win rate (67.97%), indicating their bids are very competitive. +- **Johnson & Johnson's** campaign has a very high CTR (0.71%) despite a low win rate, suggesting their creatives are highly effective at capturing user attention. ## Recommendations -* **Hyundai:** Analyze bidding strategy to improve win rate and optimize spend. -* **Leafly:** Continue with the current strategy as it appears to be very effective. -* **All Advertisers:** Investigate the creative strategies of campaigns with high CTRs to identify best practices. +- **Hyundai:** Analyze bidding strategy to improve win rate and optimize spend. +- **Leafly:** Continue with the current strategy as it appears to be very effective. +- **All Advertisers:** Investigate the creative strategies of campaigns with high CTRs to identify best practices. diff --git a/extensions/gemini/README.md b/extensions/gemini/README.md index 1d7787732b31..ef1fac25f74d 100644 --- a/extensions/gemini/README.md +++ b/extensions/gemini/README.md @@ -85,7 +85,6 @@ gemini rill upload_local_report --filePath "./reports/quarterly-analysis-2025-10 - **`generate`** - Create comprehensive analytics reports - Required: `title`, `content` - Options: `saveLocalOnly`, `localFilePath`, `shareEmail` - - **`export_to_sheet`** - Export Rill query results to Google Sheets - Required: `title`, `data` - Options: `sheetName`, `shareEmail` From 39ed6e2139ece0de26e1b7f86edd5cae619249b8 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Mon, 27 Oct 2025 14:26:25 -0400 Subject: [PATCH 03/14] move docs --- extensions/gemini/README.md | 2 ++ bids_report.md => extensions/gemini/docs/bids_report.md | 0 2 files changed, 2 insertions(+) rename bids_report.md => extensions/gemini/docs/bids_report.md (100%) diff --git a/extensions/gemini/README.md b/extensions/gemini/README.md index ef1fac25f74d..c9448331a524 100644 --- a/extensions/gemini/README.md +++ b/extensions/gemini/README.md @@ -11,6 +11,8 @@ Professional data analysis and report generation using Rill's metrics layer with Check out the [Rill Data website](https://www.rilldata.com) for more information about Rill. +Example report generated using this extension can be found in the [Gemini Documentation](docs/bids_report.md). + ## Installation Install the extension via GitHub: diff --git a/bids_report.md b/extensions/gemini/docs/bids_report.md similarity index 100% rename from bids_report.md rename to extensions/gemini/docs/bids_report.md From be569bd15cf39a4e3f0578693a83180dcb00abfc Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Thu, 30 Oct 2025 16:54:14 -0400 Subject: [PATCH 04/14] initial review pass --- ...l-extensions.yml => rill-integrations.yml} | 10 +- extensions/README.md | 7 - extensions/gemini/.example.env | 3 - extensions/gemini/.gitignore | 4 - extensions/gemini/.prettierrc | 5 - extensions/gemini/README.md | 98 ----------- extensions/gemini/commands/rill/analyze.toml | 94 ---------- extensions/gemini/commands/rill/generate.toml | 62 ------- extensions/gemini/docs/LOCAL.md | 31 ---- extensions/gemini/docs/bids_report.md | 32 ---- extensions/gemini/docs/rill-icon.png | Bin 1368 -> 0 bytes extensions/gemini/eslint.config.js | 26 --- extensions/gemini/gemini-extension.json | 26 --- extensions/gemini/package.json | 45 ----- extensions/gemini/reports/.gitkeep | 0 extensions/gemini/src/lib/auth.ts | 47 ----- extensions/gemini/src/lib/drive/docs.ts | 165 ------------------ extensions/gemini/src/lib/drive/drive.ts | 44 ----- extensions/gemini/src/lib/drive/sheets.ts | 112 ------------ extensions/gemini/src/lib/utils.ts | 150 ---------------- extensions/gemini/src/server.ts | 22 --- extensions/gemini/src/tools/generate.ts | 156 ----------------- extensions/gemini/src/tools/tools.ts | 10 -- extensions/gemini/tsconfig.json | 14 -- integrations/README.md | 5 + integrations/gemini/.example.env | 4 + {extensions => integrations}/gemini/GEMINI.md | 161 ++++++++++------- integrations/gemini/README.md | 37 ++++ integrations/gemini/gemini-extension.json | 40 +++++ integrations/gemini/package.json | 23 +++ package-lock.json | 38 ++++ package.json | 4 +- 32 files changed, 252 insertions(+), 1223 deletions(-) rename .github/workflows/{rill-extensions.yml => rill-integrations.yml} (61%) delete mode 100644 extensions/README.md delete mode 100644 extensions/gemini/.example.env delete mode 100644 extensions/gemini/.gitignore delete mode 100644 extensions/gemini/.prettierrc delete mode 100644 extensions/gemini/README.md delete mode 100644 extensions/gemini/commands/rill/analyze.toml delete mode 100644 extensions/gemini/commands/rill/generate.toml delete mode 100644 extensions/gemini/docs/LOCAL.md delete mode 100644 extensions/gemini/docs/bids_report.md delete mode 100644 extensions/gemini/docs/rill-icon.png delete mode 100644 extensions/gemini/eslint.config.js delete mode 100644 extensions/gemini/gemini-extension.json delete mode 100644 extensions/gemini/package.json delete mode 100644 extensions/gemini/reports/.gitkeep delete mode 100644 extensions/gemini/src/lib/auth.ts delete mode 100644 extensions/gemini/src/lib/drive/docs.ts delete mode 100644 extensions/gemini/src/lib/drive/drive.ts delete mode 100644 extensions/gemini/src/lib/drive/sheets.ts delete mode 100644 extensions/gemini/src/lib/utils.ts delete mode 100644 extensions/gemini/src/server.ts delete mode 100644 extensions/gemini/src/tools/generate.ts delete mode 100644 extensions/gemini/src/tools/tools.ts delete mode 100644 extensions/gemini/tsconfig.json create mode 100644 integrations/README.md create mode 100644 integrations/gemini/.example.env rename {extensions => integrations}/gemini/GEMINI.md (62%) create mode 100644 integrations/gemini/README.md create mode 100644 integrations/gemini/gemini-extension.json create mode 100644 integrations/gemini/package.json diff --git a/.github/workflows/rill-extensions.yml b/.github/workflows/rill-integrations.yml similarity index 61% rename from .github/workflows/rill-extensions.yml rename to .github/workflows/rill-integrations.yml index b29f99c8842e..c8192b15cd28 100644 --- a/.github/workflows/rill-extensions.yml +++ b/.github/workflows/rill-integrations.yml @@ -1,9 +1,9 @@ -name: Build and Release Extensions +name: Build and Release Integrations on: pull_request: paths: - - '.github/workflows/rill-extensions.yml' - - 'extensions/gemini/**' + - '.github/workflows/rill-integrations.yml' + - 'integrations/gemini/**' jobs: build: @@ -20,5 +20,5 @@ jobs: - name: Install dependencies run: npm install - - name: Build extensions - run: npm run build -w extensions/gemini + - name: Build integrations + run: npm run build -w integrations/gemini diff --git a/extensions/README.md b/extensions/README.md deleted file mode 100644 index ebf4e2a15838..000000000000 --- a/extensions/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Rill Extensions - -Extend the capabilities of Rill Data with a variety of extensions designed to enhance your data workflows. - -## Google Gemini Extension - -Enhance your Rill Data experience with the Gemini extension, enabling advanced data analysis and report generation. Explore the [Gemini README](gemini/README.md) for detailed information on installation, configuration, and usage. diff --git a/extensions/gemini/.example.env b/extensions/gemini/.example.env deleted file mode 100644 index 72f25ea1a228..000000000000 --- a/extensions/gemini/.example.env +++ /dev/null @@ -1,3 +0,0 @@ -# Rill Extension Development Configuration -RILL_MCP_SERVER_URL="https://api.rilldata.com/v1/organizations/demo/projects/demo-project/runtime/mcp" -RILL_AUTH_TOKEN="rill_usr_1234567890abcdef" diff --git a/extensions/gemini/.gitignore b/extensions/gemini/.gitignore deleted file mode 100644 index 23865b4f6f6f..000000000000 --- a/extensions/gemini/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.env -*.log -node_modules/ -dist/ diff --git a/extensions/gemini/.prettierrc b/extensions/gemini/.prettierrc deleted file mode 100644 index 7de69380e7c5..000000000000 --- a/extensions/gemini/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "printWidth": 100 -} diff --git a/extensions/gemini/README.md b/extensions/gemini/README.md deleted file mode 100644 index c9448331a524..000000000000 --- a/extensions/gemini/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Gemini CLI Extension for Rill Data - -Professional data analysis and report generation using Rill's metrics layer with Google Docs integration. - -## Features - -- **Comprehensive Analytics**: Discover, analyze, and visualize metrics data -- **Professional Reports**: Generate executive-ready Google Docs with automated formatting -- **Local & Cloud Storage**: Create reports locally first, then optionally upload to Google Drive -- **Stakeholder Sharing**: Automatically share reports with configurable permissions - -Check out the [Rill Data website](https://www.rilldata.com) for more information about Rill. - -Example report generated using this extension can be found in the [Gemini Documentation](docs/bids_report.md). - -## Installation - -Install the extension via GitHub: - -```bash -gemini extensions install https://github.com/rilldata/gemini -``` - -Install Rill CLI if you haven't already: - -```bash -curl https://rill.sh | sh -``` - -## Configuration - -Generate a Rill authentication token: - -```bash -rill token issue --display-name "Gemini Extension" -``` - -Update `gemini-extension.json` with your Rill credentials: - -- `RILL_PROJECT_URL`: Your Rill project's MCP endpoint -- `RILL_AUTH_HEADER`: Your authentication token - -For Google Drive integration, ensure you have: - -- Valid Google Cloud credentials configured (see [GEMINI.md](GEMINI.md)) -- Appropriate scopes: `drive`, `documents` - -## Usage - -### Quick Example - -> Generate a change report comparing sales metrics from last month to this month using the `sales_dashboard` metrics view - -This extension will: - -1. Query your Rill metrics for both time periods -2. Analyze the differences in key metrics -3. Create a formatted Google Doc with the comparison -4. Share it and return the link - -### Basic Analysis - -```bash -gemini rill analyze "Show me user engagement trends for the last quarter" -``` - -### Report Generation - -#### Create a Report - -```bash -gemini rill generate "Create a monthly performance report" -``` - -#### Create Local Report, Then Upload Later - -```bash -# First, create locally -gemini rill generate "Create quarterly analysis" - -# Review the report, then upload when ready -gemini rill upload_local_report --filePath "./reports/quarterly-analysis-2025-10-03.md" -``` - -## Available Tools - -- **`generate`** - Create comprehensive analytics reports - - Required: `title`, `content` - - Options: `saveLocalOnly`, `localFilePath`, `shareEmail` -- **`export_to_sheet`** - Export Rill query results to Google Sheets - - Required: `title`, `data` - - Options: `sheetName`, `shareEmail` - -> **Note**: Additional tools like `create_metrics_comparison_report`, `update_report`, `upload_local_report`, and `list_reports` are planned for future releases. - -## Documentation - -For detailed documentation, visit the [Rill Docs](https://docs.rilldata.com/), and refer to the [Local Development Guide](LOCAL.md) for setting up a development environment. diff --git a/extensions/gemini/commands/rill/analyze.toml b/extensions/gemini/commands/rill/analyze.toml deleted file mode 100644 index f8500adb609a..000000000000 --- a/extensions/gemini/commands/rill/analyze.toml +++ /dev/null @@ -1,94 +0,0 @@ -description = "Analyzes Rill query results and generates actionable business insights with optional Google Doc report generation" - -prompt = """ -You are a senior data analyst specializing in business intelligence and actionable insights generation. Your role is to systematically explore data, uncover patterns, and create professional documentation. - -## Analysis Framework - -### Step 1: Data Discovery -1. **Catalog Available Data**: Use `list_metrics_views` to identify all available datasets -2. **Understand Schema**: Use `get_metrics_view` for each relevant view to understand measures and dimensions -3. **Assess Data Scope**: Use `query_metrics_view_summary` to determine available time periods - -### Step 2: Systematic Analysis -4. **Baseline Analysis**: Query high-level metrics to establish current state -5. **Trend Analysis**: Examine time-based patterns and seasonality -6. **Segment Analysis**: Break down metrics by key dimensions -7. **Comparative Analysis**: Compare periods, cohorts, or segments -8. **Anomaly Detection**: Identify unusual patterns or outliers - -### Step 3: Insight Generation -9. **Pattern Recognition**: Identify significant trends and relationships -10. **Business Impact**: Quantify the business implications of findings -11. **Root Cause Analysis**: Investigate underlying drivers of key patterns -12. **Recommendations**: Develop specific, actionable next steps - -### Step 4: Documentation (Optional) -13. **Report Creation**: Use `create_analytics_report` for comprehensive documentation -14. **Stakeholder Sharing**: Generate shareable links for team collaboration - -## Quality Standards - -- **Minimum 6 distinct queries** per analysis session -- **Quantified insights**: Every finding must include specific metrics -- **Business context**: Connect data patterns to operational decisions -- **Progressive depth**: Each query should build on previous results -- **Tool-based calculations**: Only use numbers directly from query results - -## Communication Style - -- Speak confidently and directly using "I" and "you" -- Present findings with authority while remaining collaborative -- Focus on business impact and actionability -- Use clear, executive-friendly language - -## Report Structure (when generating documents) - -1. **Executive Summary**: 3 key findings with quantified impact -2. **Methodology**: Data sources and analysis approach -3. **Key Findings**: Detailed insights with supporting data -4. **Trends & Patterns**: Time-based analysis results -5. **Recommendations**: Specific, actionable next steps -6. **Appendix**: Additional data and technical notes - -Always validate findings across multiple query approaches and ensure all insights are grounded in the actual data results. -- ALL numbers and calculations must come from "get_metrics_view" tool results -- NEVER perform manual calculations or mathematical operations -- If a desired calculation cannot be achieved through the metrics tools, explicitly state this limitation -- Use only the exact numbers returned by the tools in your analysis - - - -- If the user asks an unrelated question (e.g., general knowledge, politics, entertainment, trivia): - 1. Politely decline to answer the unrelated question - 2. Briefly explain that you specialize only in business data analysis - 3. Redirect the conversation back to datasets, metrics, or insights - 4. Do **NOT** attempt to answer the unrelated question - -Example response style: -*"I focus specifically on analyzing your business data and uncovering insights — so I won't be the right source for general knowledge questions. Let's pivot back to your project: would you like me to explore the available datasets so we can start surfacing insights?"* - - - -After each query in Step 2, think through: -- What patterns or anomalies did this reveal? -- How does this connect to previous findings? -- What new questions does this raise? -- What's the most valuable next query to run? -- Are there any surprising insights worth highlighting? - - - -Format your analysis as follows: -`+"```markdown"+` -Based on the data analysis, here are the key insights: - -1. ## [Headline with specific impact/number] - [Finding with business context and implications] - -2. ## [Headline with specific impact/number] - [Finding with business context and implications] - -3. ## [Headline with specific impact/number] - [Finding with business context and implications] -""" diff --git a/extensions/gemini/commands/rill/generate.toml b/extensions/gemini/commands/rill/generate.toml deleted file mode 100644 index b30610263d96..000000000000 --- a/extensions/gemini/commands/rill/generate.toml +++ /dev/null @@ -1,62 +0,0 @@ -description = "Generates professional Google Doc reports from Rill data analysis" - -prompt = """ -You are a professional report writer specializing in transforming complex analytics into executive-ready business intelligence documents. - -## Report Generation Process - -### Step 1: Requirements Gathering -- Ask the user to specify report type, target audience, and key metrics of interest -- Determine time periods, dimensions, and specific business questions to address -- Clarify sharing requirements (public, specific emails, or private) - -### Step 2: Data Collection -- Use Rill MCP tools to systematically gather relevant data -- Execute comprehensive queries covering all requested metrics and dimensions -- Ensure data spans appropriate time periods for meaningful analysis - -### Step 3: Analysis & Synthesis -- Identify key patterns, trends, and anomalies in the data -- Calculate period-over-period changes and growth rates -- Develop business-relevant insights and implications -- Formulate specific, actionable recommendations - -### Step 4: Document Creation -- Structure content using clear executive summary format -- Use `create_analytics_report` or `create_metrics_comparison_report` tools -- Apply professional formatting with headers, bullet points, and data tables -- Include methodology section documenting data sources and approach - -### Step 5: Quality Assurance -- Verify all numbers match query results exactly -- Ensure insights are business-relevant and actionable -- Confirm document structure serves target audience needs -- Test sharing functionality and provide access instructions - -## Available Report Types - -1. **Executive Dashboard**: High-level KPI overview with key insights -2. **Performance Review**: Comprehensive business performance assessment -3. **Trend Analysis**: Time-series patterns and forecasting insights -4. **Comparison Report**: Side-by-side analysis of periods, segments, or cohorts -5. **Anomaly Investigation**: Deep dive into unusual patterns or outliers - -## Document Standards - -- **Executive Summary**: Maximum 3 bullet points with quantified findings -- **Data Visualization**: Describe charts and tables that would enhance understanding -- **Actionable Recommendations**: Specific next steps with expected outcomes -- **Source Attribution**: Clear documentation of data sources and methodology -- **Professional Tone**: Business-appropriate language for executive audience - -## Quality Checklist - -- [ ] All metrics sourced directly from Rill query results -- [ ] Insights include specific percentages and quantified impact -- [ ] Recommendations are actionable and time-bound -- [ ] Document structure serves target audience effectively -- [ ] Sharing permissions configured correctly -- [ ] Methodology and limitations clearly documented - -Focus on creating documents that enable informed decision-making and drive business action. -""" diff --git a/extensions/gemini/docs/LOCAL.md b/extensions/gemini/docs/LOCAL.md deleted file mode 100644 index 28cadcc04240..000000000000 --- a/extensions/gemini/docs/LOCAL.md +++ /dev/null @@ -1,31 +0,0 @@ -## Local Development - -## Link a Local Extension - -```bash -# Clone the Gemini extension repository -git clone https://github.com/rilldata/gemini.git && cd gemini -# Install dependencies -npm install -``` - -The `gemini extensions link` command creates a symbolic link from the extension installation directory to your development path. This is useful for testing changes without running `gemini extensions update` repeatedly. - -```bash -gemini extensions link $(pwd) -``` - -### How It Works - -On startup, Gemini CLI looks for extensions in `/.gemini/extensions` - -Extensions exist as a directory containing a `gemini-extension.json` file. For example: - -``` -/.gemini/extensions/my-extension/gemini-extension.json -``` - -## Documentation - -- [Gemini CLI Extension Documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/extension.md) -- [Google Cloud Configuration Guide](GEMINI.md) diff --git a/extensions/gemini/docs/bids_report.md b/extensions/gemini/docs/bids_report.md deleted file mode 100644 index 799cb56b14df..000000000000 --- a/extensions/gemini/docs/bids_report.md +++ /dev/null @@ -1,32 +0,0 @@ -# Bids Metrics Report: Last Week (2025-09-26 to 2025-10-02) - -## Executive Summary - -This report summarizes the key bidding metrics for the last week. The total advertising spend across all campaigns was **$673,000**. The top-performing advertiser was **Hyundai**, with their **Hyundai_Revitalize** campaign generating **$246,883.26** in spend. - -## Top 10 Campaigns by Spend - -| Advertiser | Campaign | Spend (USD) | Total Bids | Impressions | Win Rate | CTR | -| ----------------- | ---------------------------- | ----------- | ---------- | ----------- | -------- | ----- | -| Hyundai | Hyundai_Revitalize | 246,883.26 | 277,694 | 44,302 | 15.95% | 0.13% | -| drakesoftware | drakesoftware_BrandBlast | 94,569.04 | 42,233 | 11,003 | 26.05% | 0.09% | -| Leafly | Leafly_MarketMight | 56,278.38 | 16,830 | 11,439 | 67.97% | 0.03% | -| Instacart | Instacart_BrandBounce | 53,203.38 | 43,636 | 11,353 | 26.02% | 0.10% | -| Amgen Inc | Amgen Inc_BrandRamp | 47,923.38 | 133,030 | 3,435 | 2.58% | 0.47% | -| ESPACIO URBANO | ESPACIO URBANO_PrimeProfit | 47,807.05 | 30,457 | 3,952 | 12.98% | 0.13% | -| Hennessy | Hennessy_MarketMaster | 34,117.65 | 39,971 | 2,460 | 6.15% | 0.04% | -| Instacart | Instacart_PrimePinnacle | 32,784.02 | 53,770 | 3,453 | 6.42% | 0.03% | -| GlaxoSmithKline | GlaxoSmithKline_ExpandNow | 32,074.83 | 36,033 | 2,309 | 6.41% | 0.00% | -| Johnson & Johnson | Johnson & Johnson_GrowthGlow | 27,405.06 | 47,280 | 1,970 | 4.17% | 0.71% | - -## Key Insights - -- **Hyundai's** campaign has the highest spend, but a relatively low win rate compared to other top campaigns. This suggests they are bidding aggressively but not always winning the auction. -- **Leafly's** campaign has an exceptionally high win rate (67.97%), indicating their bids are very competitive. -- **Johnson & Johnson's** campaign has a very high CTR (0.71%) despite a low win rate, suggesting their creatives are highly effective at capturing user attention. - -## Recommendations - -- **Hyundai:** Analyze bidding strategy to improve win rate and optimize spend. -- **Leafly:** Continue with the current strategy as it appears to be very effective. -- **All Advertisers:** Investigate the creative strategies of campaigns with high CTRs to identify best practices. diff --git a/extensions/gemini/docs/rill-icon.png b/extensions/gemini/docs/rill-icon.png deleted file mode 100644 index e2e7040a73ab327a80655995ecafd73ef6819077..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1368 zcmV-e1*iInP)&fmX=l51}ogod~laTU>YV-b9SO~n@|1t|)ywYu_(ZftaAp^HKYB0><(mH#fvefec)gDk=G1oFXG8YN6 zf;_#eLS0Ktm?%aaL4nYz0aO~H&%p!ioJ)+FNFX08Lr8^)S40W1EGw0)=hDJ}hd>wi z#n93#UV-2NlXFm6RJ&5cVewB^7a@}$HNr5$RP!`Y1_e4O&G9aX1b8aY_OnN_CsNon z8wx(Mr?NB@tuP7CtT>AQeN>{w7Hd<&o`X;(=QMp$x<19fj3PtM8cnOrjJSv(RHngQgVjFHJb4V5klTABho8RtAM2M#7;f%I_YYQ& zQ#z0HPyC6&#XgpnZ=>{1mp_=t!s3z0@6r2)C6=Nwjc6XMi*dk5?~gET^=@x)`r#RT z^5q>^y8kE=fGsJT}qDlRbEJ(wVnI|^Bm3%{*usVEFqtzFu}+9lamgopLq`5s<=eJ(x+w=ONuunz^{ zd#%Wl;wG?e6yDgXhy^8DYZ%r%EZqWH2@)RS&psZ}vtEgX#WpEHbno&ptUNUv-+wr} zNwvkU_DA*rrDEhLV!-}55eXigi*+jzI$Bt4sxS<%?=!|q@AOv}7-ktUMY$+M$QA6t zM`qDG-o)yu#6j@N)4i-f$b#Rl{)ZLJb}YnH4+#3p)Xs&dtlz}14CYDQJZ){PZ((nD zpSZH}@GP+E8Py5Tlq;bzV8S8mnzwpm{cn8p^$lUd-IyPykSiNkq;FH4b7d<*d+q8? z@A4xaY)9jJaQmw-U1wjAXe`_!p@zg*9dNP4i!A9GjpZsAAe6PY>+sfv&G;Ku2+Ng? zJ4wTr zkXpr6m4 z+Z|ACNnLExT_TO`HkfiTo0KCzq#L@bRivf&+?wrs(Zfu)pHa&oO~eL*P|PWOPs$oBNM zqytF-G{8n@y;Ml!DE6zN!>(+U_kemF7q(_wnhE_tLT(FkGwyt}3x>s94a{NVN!9}; zra)bxO_M>m(~#qL{D*C3>jjc(TB2i`rf(q&4pIubI((u!JU_hua}` atG57jf?d9m*EEv=0000=18.0.0" - }, - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/rilldata/rill", - "directory": "extensions/gemini" - }, - "author": { - "name": "Rill Data", - "email": "support@rilldata.com", - "url": "https://rilldata.com" - } -} diff --git a/extensions/gemini/reports/.gitkeep b/extensions/gemini/reports/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/extensions/gemini/src/lib/auth.ts b/extensions/gemini/src/lib/auth.ts deleted file mode 100644 index a16837e7f5dc..000000000000 --- a/extensions/gemini/src/lib/auth.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { GoogleAuth } from 'google-auth-library'; - -// Singleton Google Auth instance -let googleAuthInstance: GoogleAuth | null = null; - -/** - * Gets or creates a single Google Auth instance with all required scopes. - */ -export async function getGoogleAuth(): Promise { - if (!googleAuthInstance) { - googleAuthInstance = new GoogleAuth({ - scopes: [ - 'https://www.googleapis.com/auth/drive', - 'https://www.googleapis.com/auth/documents', - 'https://www.googleapis.com/auth/spreadsheets', - ], - }); - - // Verify credentials work with required scopes - const hasCredentials = await ensureGCPCredentials(); - if (!hasCredentials) { - googleAuthInstance = null; - throw new Error('Google Cloud credentials are not properly configured'); - } - } - - return googleAuthInstance; -} - -/** - * Ensures that Google Cloud credentials are set up correctly with the required scopes. - */ -export async function ensureGCPCredentials(): Promise { - try { - if (!googleAuthInstance) { - return false; - } - - const client = await googleAuthInstance.getClient(); - const accessToken = await client.getAccessToken(); - - return accessToken.token !== null && accessToken.token !== undefined; - } catch (error) { - console.error('Google Cloud authentication failed:', error); - return false; - } -} diff --git a/extensions/gemini/src/lib/drive/docs.ts b/extensions/gemini/src/lib/drive/docs.ts deleted file mode 100644 index 1be332960973..000000000000 --- a/extensions/gemini/src/lib/drive/docs.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { google, docs_v1, drive_v3 } from 'googleapis'; -import { getGoogleAuth } from '../auth.js'; -import { setupDriveClient } from './drive.js'; - -/** - * Sets up the Google Docs client. - */ -export async function setupDocsClient(): Promise { - const auth = await getGoogleAuth(); - return google.docs({ version: 'v1', auth }); -} - -/** Creates a new Google Document with the specified title. - * @param title - The title of the document to be created. - */ -export async function createDocument(title: string): Promise { - const docs = await setupDocsClient(); - const response = await docs.documents.create({ - requestBody: { title }, - }); - return response.data; -} - -/** - * Updates the content of a Google Document. - * @param documentId - * @param content - */ -export async function updateDocument( - documentId: string, - content: docs_v1.Schema$Request[], -): Promise { - const docs = await setupDocsClient(); - await docs.documents.batchUpdate({ - documentId, - requestBody: { requests: content }, - }); -} - -/** - * Shares a Google Document with the specified email or makes it public. - * @param documentId - * @param email - */ -export async function shareDocument( - documentId: string, - email?: string, -): Promise { - const drive = await setupDriveClient(); - - const permission: drive_v3.Schema$Permission = { - type: 'anyone', - role: 'reader', - }; - - if (email) { - permission.type = 'user'; - permission.emailAddress = email; - } - - await drive.permissions.create({ - fileId: documentId, - requestBody: permission, - }); - - const file = await drive.files.get({ - fileId: documentId, - fields: 'webViewLink', - }); - - return file.data.webViewLink; -} - -/** - * Gets the content of a Google Document. - * @param documentId - The ID of the document to retrieve - */ -export async function getDocument( - documentId: string, -): Promise { - const docs = await setupDocsClient(); - const response = await docs.documents.get({ - documentId, - }); - return response.data; -} - -/** - * Lists Google Drive documents (reports) created by this extension. - * @param maxResults - Maximum number of results to return (default: 10) - */ -export async function listDriveReports(maxResults: number = 10): Promise { - const drive = await setupDriveClient(); - const response = await drive.files.list({ - q: "mimeType='application/vnd.google-apps.document' and trashed=false", - fields: 'files(id, name, modifiedTime, webViewLink, createdTime)', - pageSize: maxResults, - orderBy: 'modifiedTime desc', - }); - return response.data.files || []; -} - -/** - * Uploads a local file to Google Drive and converts it to a Google Doc. - * @param filePath - Path to the local file - * @param title - Title for the Google Doc - * @param mimeType - MIME type of the source file (default: text/markdown) - */ -export async function uploadFileToGoogleDocs( - content: string, - title: string, - mimeType: string = 'text/markdown', -): Promise { - const drive = await setupDriveClient(); - - const fileMetadata = { - name: title, - mimeType: 'application/vnd.google-apps.document', - }; - - const media = { - mimeType: mimeType, - body: content, - }; - - const response = await drive.files.create({ - requestBody: fileMetadata, - media: media, - fields: 'id, name, webViewLink', - }); - - return response.data; -} - -/** - * Deletes all content from a Google Document and replaces it with new content. - * @param documentId - The ID of the document to update - * @param _content - The new content in markdown format (to be implemented) - */ -export async function replaceDocumentContent(documentId: string, _content: string): Promise { - const docs = await setupDocsClient(); - - const doc = await getDocument(documentId); - if (!doc?.body?.content) { - throw new Error('Could not retrieve document content'); - } - - const endIndex = doc.body.content[doc.body.content.length - 1]?.endIndex || 1; - - const deleteRequest = { - deleteContentRange: { - range: { - startIndex: 1, - endIndex: endIndex - 1, - }, - }, - }; - - await docs.documents.batchUpdate({ - documentId, - requestBody: { - requests: [deleteRequest], - }, - }); -} diff --git a/extensions/gemini/src/lib/drive/drive.ts b/extensions/gemini/src/lib/drive/drive.ts deleted file mode 100644 index 3d959de630aa..000000000000 --- a/extensions/gemini/src/lib/drive/drive.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { google, drive_v3 } from 'googleapis'; -import { getGoogleAuth } from '../auth.js'; - -/** - * Sets up the Google Drive client. - */ -export async function setupDriveClient(): Promise { - const auth = await getGoogleAuth(); - return google.drive({ version: 'v3', auth }); -} - -/** - * Shares a Google Document with the specified email or makes it public. - * @param documentId - * @param email - */ -export async function shareDocument( - documentId: string, - email?: string, -): Promise { - const drive = await setupDriveClient(); - - const permission: drive_v3.Schema$Permission = { - type: 'anyone', - role: 'reader', - }; - - if (email) { - permission.type = 'user'; - permission.emailAddress = email; - } - - await drive.permissions.create({ - fileId: documentId, - requestBody: permission, - }); - - const file = await drive.files.get({ - fileId: documentId, - fields: 'webViewLink', - }); - - return file.data.webViewLink; -} diff --git a/extensions/gemini/src/lib/drive/sheets.ts b/extensions/gemini/src/lib/drive/sheets.ts deleted file mode 100644 index a54aad091589..000000000000 --- a/extensions/gemini/src/lib/drive/sheets.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { google, sheets_v4 } from 'googleapis'; -import { getGoogleAuth } from '../auth.js'; -import { shareDocument } from './drive.js'; - -/** - * Sets up the Google Sheets client. - */ -export async function setupSheetsClient(): Promise { - const auth = await getGoogleAuth(); - return google.sheets({ version: 'v4', auth }); -} - -/** - * Creates a new Google Sheet with the specified title. - * @param title - The title of the sheet to be created. - */ -export async function createSheet( - title: string, -): Promise { - const sheets = await setupSheetsClient(); - const response = await sheets.spreadsheets.create({ - requestBody: { - properties: { title }, - }, - }); - return response.data; -} - -/** - * Updates the data in a Google Sheet. - * @param spreadsheetId - The ID of the spreadsheet to update. - * @param sheetName - The name of the sheet within the spreadsheet. - * @param headers - An array of header strings. - * @param rows - A 2D array representing the rows of data. - */ -export async function updateSheetData( - spreadsheetId: string, - sheetName: string, - headers: string[], - rows: any[][], -): Promise { - const sheets = await setupSheetsClient(); - - // Write headers - await sheets.spreadsheets.values.update({ - spreadsheetId, - range: `${sheetName}!A1`, - valueInputOption: 'RAW', - requestBody: { - values: [headers], - }, - }); - - // Write data rows - await sheets.spreadsheets.values.update({ - spreadsheetId, - range: `${sheetName}!A2`, - valueInputOption: 'RAW', - requestBody: { - values: rows, - }, - }); - - // Format headers (bold, frozen) - await sheets.spreadsheets.batchUpdate({ - spreadsheetId, - requestBody: { - requests: [ - { - repeatCell: { - range: { - sheetId: 0, - startRowIndex: 0, - endRowIndex: 1, - }, - cell: { - userEnteredFormat: { - textFormat: { bold: true }, - backgroundColor: { red: 0.9, green: 0.9, blue: 0.9 }, - }, - }, - fields: 'userEnteredFormat(textFormat,backgroundColor)', - }, - }, - { - updateSheetProperties: { - properties: { - sheetId: 0, - gridProperties: { - frozenRowCount: 1, - }, - }, - fields: 'gridProperties.frozenRowCount', - }, - }, - ], - }, - }); -} - -/** - * Shares the Google Sheet with the specified email or makes it publicly accessible. - * @param spreadsheetId - The ID of the spreadsheet to share. - * @param email - Optional email address to share the sheet with. - * @returns The web view link of the shared sheet. - */ -export async function shareSheet( - spreadsheetId: string, - email?: string, -): Promise { - return shareDocument(spreadsheetId, email); -} diff --git a/extensions/gemini/src/lib/utils.ts b/extensions/gemini/src/lib/utils.ts deleted file mode 100644 index 2e682a31b475..000000000000 --- a/extensions/gemini/src/lib/utils.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { writeFile, mkdir } from 'fs/promises'; -import { join, dirname } from 'path'; -import { docs_v1 } from 'googleapis'; - -export interface MetricsData { - currentPeriod: Record; - previousPeriod: Record; - periodLabels: { - current: string; - previous: string; - }; -} - -/** - * Writes report content to a local file - */ -export async function writeLocalReport(content: string, filePath: string): Promise { - try { - const dir = dirname(filePath); - - await mkdir(dir, { recursive: true }); - - await writeFile(filePath, content, 'utf8'); - - return filePath; - } catch (error) { - throw new Error( - `Failed to write local file: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - } -} - -/** - * Generates a default local file path if none is provided - */ -export function getDefaultLocalPath(title: string, extension: string = 'md'): string { - const sanitizedTitle = title - .replace(/[^a-zA-Z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .toLowerCase(); - const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); - return join(process.cwd(), 'reports', `${sanitizedTitle}-${timestamp}.${extension}`); -} - -/** - * Converts markdown content to Google Docs format requests - */ -export function convertMarkdownToGoogleDocsRequests(markdown: string): docs_v1.Schema$Request[] { - const requests = []; - const lines = markdown.split('\n'); - let insertIndex = 1; - - for (const line of lines) { - if (line.startsWith('# ')) { - // Header 1 - requests.push({ - insertText: { - location: { index: insertIndex }, - text: line.substring(2) + '\n', - }, - }); - requests.push({ - updateTextStyle: { - range: { - startIndex: insertIndex, - endIndex: insertIndex + line.substring(2).length, - }, - textStyle: { - fontSize: { magnitude: 20, unit: 'PT' }, - bold: true, - }, - fields: 'fontSize,bold', - }, - }); - insertIndex += line.substring(2).length + 1; - } else if (line.startsWith('## ')) { - // Header 2 - requests.push({ - insertText: { - location: { index: insertIndex }, - text: line.substring(3) + '\n', - }, - }); - requests.push({ - updateTextStyle: { - range: { - startIndex: insertIndex, - endIndex: insertIndex + line.substring(3).length, - }, - textStyle: { - fontSize: { magnitude: 16, unit: 'PT' }, - bold: true, - }, - fields: 'fontSize,bold', - }, - }); - insertIndex += line.substring(3).length + 1; - } else { - // Regular text - requests.push({ - insertText: { - location: { index: insertIndex }, - text: line + '\n', - }, - }); - insertIndex += line.length + 1; - } - } - - return requests; -} - -/** - * Generates comparison report content from metrics data - */ -export function generateComparisonReportContent( - metricsData: MetricsData, - insights: string[], -): string { - const { currentPeriod, previousPeriod, periodLabels } = metricsData; - - let content = `# Metrics Comparison Report\n\n`; - content += `## Executive Summary\n\n`; - content += `This report compares key metrics between ${periodLabels.previous} and ${periodLabels.current}.\n\n`; - - content += `## Metrics Overview\n\n`; - - // Compare each metric - for (const [metric, currentValue] of Object.entries(currentPeriod)) { - const previousValue = previousPeriod[metric]; - if (previousValue !== undefined) { - const change = ((currentValue - previousValue) / previousValue) * 100; - const direction = change > 0 ? '↑' : change < 0 ? '↓' : '→'; - - content += `### ${metric}\n`; - content += `- ${periodLabels.current}: ${currentValue}\n`; - content += `- ${periodLabels.previous}: ${previousValue}\n`; - content += `- Change: ${direction} ${Math.abs(change).toFixed(2)}%\n\n`; - } - } - - content += `## Key Insights\n\n`; - insights.forEach((insight, index) => { - content += `${index + 1}. ${insight}\n`; - }); - - content += `\n---\n\nReport generated on ${new Date().toLocaleDateString()}\n`; - - return content; -} diff --git a/extensions/gemini/src/server.ts b/extensions/gemini/src/server.ts deleted file mode 100644 index 5eff99ea911b..000000000000 --- a/extensions/gemini/src/server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { registerTools } from './tools/tools.js'; - -const server = new McpServer({ - name: 'Rill Report Generator', - websiteUrl: 'https://rilldata.com', - version: '0.1.0', - title: 'Rill Report Generator', -}); - -// Register tools -registerTools(server); - -// Starts the MCP server and connects it to the standard I/O transport. -async function start() { - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -// Start the server -void start(); diff --git a/extensions/gemini/src/tools/generate.ts b/extensions/gemini/src/tools/generate.ts deleted file mode 100644 index 2369c1b65689..000000000000 --- a/extensions/gemini/src/tools/generate.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import { shareDocument } from '../lib/drive/drive.js'; -import { createDocument, updateDocument } from '../lib/drive/docs.js'; -import { - writeLocalReport, - getDefaultLocalPath, - convertMarkdownToGoogleDocsRequests, -} from '../lib/utils.js'; -import { createSheet, shareSheet, updateSheetData } from '../lib/drive/sheets.js'; - -export function registerGenerateTool(server: McpServer) { - server.registerTool( - 'generate', - { - description: - 'Creates a comprehensive analytics report in Google Docs based on Rill data analysis. Optionally saves a local copy first.', - inputSchema: { - title: z.string().describe('The title of the report document.'), - content: z.string().describe('The main content of the report in markdown format.'), - shareEmail: z - .string() - .optional() - .describe( - 'Email address to share the document with (optional - if not provided, document will be public).', - ), - localFilePath: z - .string() - .optional() - .describe( - 'Optional local file path to save the report before creating Google Doc. If not provided, will generate a default path.', - ), - saveLocalOnly: z - .boolean() - .optional() - .describe( - 'If true, only saves locally and skips Google Docs creation. Defaults to false.', - ), - }, - }, - async (input) => { - try { - const { title, content, shareEmail, localFilePath, saveLocalOnly = false } = input; - - const results: string[] = []; - let localPath: string | null = null; - - // Write local file if requested or if saveLocalOnly is true - if (localFilePath || saveLocalOnly) { - const filePath = localFilePath || getDefaultLocalPath(title); - localPath = await writeLocalReport(content, filePath); - results.push(`Local report saved: ${localPath}`); - } - - // Skip Google Docs creation if saveLocalOnly is true - if (!saveLocalOnly) { - // Create the document - const document = await createDocument(title); - if (!document?.documentId) { - throw new Error('Failed to create document'); - } - - // Convert markdown content to Google Docs format - const requests = convertMarkdownToGoogleDocsRequests(content); - - // Update document with content - await updateDocument(document.documentId, requests); - - // Share the document - const shareLink = await shareDocument(document.documentId, shareEmail); - - results.push(`Google Docs report created successfully!`); - results.push(`Document ID: ${document.documentId}`); - results.push(`Share Link: ${shareLink}`); - } - - results.push(`Title: ${title}`); - - return { - content: [ - { - type: 'text', - text: results.join('\n\n'), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error creating report: ${error instanceof Error ? error.message : 'Unknown error'}`, - }, - ], - }; - } - }, - ); -} - -export function registerExportSheetTool(server: McpServer) { - server.registerTool( - 'export_to_sheet', - { - description: - 'Exports Rill query results to a Google Sheet with formatted tables and optional sharing.', - inputSchema: { - title: z.string().describe('The title of the Google Sheet.'), - data: z - .array(z.record(z.any())) - .describe('Array of query result objects to export as rows.'), - sheetName: z.string().optional().describe('Name for the worksheet tab (default: Sheet1).'), - shareEmail: z.string().optional().describe('Email address to share the sheet with.'), - }, - }, - async (input) => { - try { - const { title, data, sheetName = 'Sheet1', shareEmail } = input; - - // Create new spreadsheet - const sheet = await createSheet(title); - if (!sheet?.spreadsheetId) { - throw new Error('Failed to create spreadsheet'); - } - - // Convert data to rows format - const headers = Object.keys(data[0] || {}); - const rows = data.map((row) => headers.map((h) => row[h])); - - // Update sheet with data - await updateSheetData(sheet.spreadsheetId, sheetName, headers, rows); - - // Share the sheet - const shareLink = await shareSheet(sheet.spreadsheetId, shareEmail); - - return { - content: [ - { - type: 'text', - text: `Sheet exported successfully!\n\nSpreadsheet ID: ${sheet.spreadsheetId}\nTitle: ${title}\nShare Link: ${shareLink}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error exporting to sheet: ${error instanceof Error ? error.message : 'Unknown error'}`, - }, - ], - }; - } - }, - ); -} diff --git a/extensions/gemini/src/tools/tools.ts b/extensions/gemini/src/tools/tools.ts deleted file mode 100644 index 3e24ce58608d..000000000000 --- a/extensions/gemini/src/tools/tools.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { registerExportSheetTool, registerGenerateTool } from './generate.js'; - -/** - * Registers all tools with the MCP server. - */ -export const registerTools = (server: McpServer) => { - registerGenerateTool(server); - registerExportSheetTool(server); -}; diff --git a/extensions/gemini/tsconfig.json b/extensions/gemini/tsconfig.json deleted file mode 100644 index 707155e8d049..000000000000 --- a/extensions/gemini/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "lib"] -} diff --git a/integrations/README.md b/integrations/README.md new file mode 100644 index 000000000000..171709800a33 --- /dev/null +++ b/integrations/README.md @@ -0,0 +1,5 @@ +# Rill Integration + +## Google Gemini Integration + +Enhance your Rill Data experience with the Gemini integration, enabling advanced data analysis. Explore the [Gemini README](gemini/README.md) for detailed information on installation, configuration, and usage. diff --git a/integrations/gemini/.example.env b/integrations/gemini/.example.env new file mode 100644 index 000000000000..4078761e14a3 --- /dev/null +++ b/integrations/gemini/.example.env @@ -0,0 +1,4 @@ +# Rill Configuration +RILL_ORG="demo" +RILL_PROJECT="demo-project" +RILL_ACCESS_TOKEN="rill_usr_1234567890abcdef" diff --git a/extensions/gemini/GEMINI.md b/integrations/gemini/GEMINI.md similarity index 62% rename from extensions/gemini/GEMINI.md rename to integrations/gemini/GEMINI.md index b7f3a3dc6c79..6bc5df031ed1 100644 --- a/extensions/gemini/GEMINI.md +++ b/integrations/gemini/GEMINI.md @@ -1,6 +1,4 @@ -# Rill Analytics Extension - Data Analysis & Report Generation - -This extension enables comprehensive data analysis and professional report generation using Rill's metrics layer combined with Google Docs integration. +# Rill - Data Analysis ## Core Capabilities @@ -10,31 +8,25 @@ This extension enables comprehensive data analysis and professional report gener - **Trend Analysis**: Time-series analysis with period-over-period comparisons - **Anomaly Detection**: Identification of unusual patterns and outliers - **Metrics Comparison**: Side-by-side analysis of different time periods or segments -- **Business Intelligence**: Actionable insights generation for executive decision-making - -### Report Generation - -- **Google Docs Integration**: Professional document creation with proper formatting -- **Automated Sharing**: Configurable document sharing with stakeholders -- **Multiple Report Types**: Summary reports, comparison analyses, and detailed insights -- **Executive-Ready Format**: Clear structure with executive summaries and recommendations +- **Actionable Intelligence**: Actionable insights generation for executive decision-making ## Analytical Methodology ### Phase 1: Data Discovery (Systematic Approach) -The extension follows a structured discovery process: +Follow the structured discovery process: 1. **Dataset Identification**: Use `list_metrics_views` to catalog available data sources 2. **Schema Understanding**: Analyze measures, dimensions, and relationships via `get_metrics_view` 3. **Data Scope Assessment**: Determine time ranges and data coverage with `query_metrics_view_summary` +4. **Preliminary Queries**: Execute broad queries to gauge data quality and trends `query_metrics_view` ### Phase 2: Analytical Investigation (OODA Loop) Execute iterative analysis cycles: - **Observe**: Query data systematically, starting broad then drilling into specifics -- **Orient**: Evaluate patterns, identify anomalies, and assess business context +- **Orient**: Evaluate patterns, identify anomalies, and assess the context - **Decide**: Prioritize analytical angles based on emerging insights - **Act**: Execute targeted queries to validate hypotheses and uncover insights @@ -43,9 +35,9 @@ Execute iterative analysis cycles: Transform findings into actionable intelligence: - **Pattern Recognition**: Identify trends, seasonality, and behavioral changes -- **Impact Quantification**: Measure business impact with specific metrics and percentages -- **Contextual Analysis**: Connect data patterns to business operations and decisions -- **Report Generation**: Create professional documentation using Google Docs tools +- **Impact Quantification**: Measure impact with specific metrics and percentages +- **Contextual Analysis**: Connect data patterns to operations and decisions +- **Report Generation**: Create professional documentation with clear structure and visualizations ## Data Analysis Best Practices @@ -59,9 +51,9 @@ Transform findings into actionable intelligence: ### Insight Quality Standards - **Quantified findings**: All insights must include specific numbers and percentages -- **Business relevance**: Connect patterns to operational decisions and outcomes +- **Relevance**: Connect patterns to operational decisions and outcomes - **Surprise factor**: Prioritize unexpected findings that challenge assumptions -- **Actionability**: Focus on insights that enable concrete business actions +- **Actionability**: Focus on insights that enable concrete actions ### Data Accuracy Requirements @@ -70,6 +62,92 @@ Transform findings into actionable intelligence: - **Source attribution**: Clearly indicate data sources and calculation methods - **Limitation acknowledgment**: State when desired calculations cannot be performed +## Chart Generation + +When you include data in your responses, either from tool use or using your own analysis capabilities, do not build web pages or React apps. For visualizing data, you can use text-based techniques for data visualization: + +Bar Charts using block characters: + +Q1 ████████░░ 411 + +Q2 ██████████ 514 + +Q3 ██████░░░░ 300 + +Q4 ████████░░ 400 + +Horizontal progress bars: Project Progress: + +Frontend ▓▓▓▓▓▓▓▓░░ 80% + +Backend ▓▓▓▓▓▓░░░░ 60% + +Testing ▓▓░░░░░░░░ 20% + +Using different block densities: Trends: + +Jan ▁▂▃▄▅▆▇█ High + +Feb ▁▂▃▄▅░░░ Medium + +Mar ▁▂░░░░░░ Low + +Sparklines with Unicode Basic sparklines: + +Stock prices: ▁▂▃▅▂▇▆▃▅▇ + +Website traffic: ▁▁▂▃▅▄▆▇▆▅▄▂▁ + +CPU usage: ▂▄▆█▇▅▃▂▄▆█▇▄▂ + +Trend indicators: + +AAPL ▲ +2.3% + +GOOG ▼ -1.2% + +MSFT ► +0.5% + +TSLA ▼ -3.1% + +Simple trend arrows: Sales ↗️ (+15%) Costs ↘️ (-8%) Profit ⤴️ (+28%) + +Pivot tables using text formatting: + +| Region | Q1 Sales | Q2 Sales | Q3 Sales | Q4 Sales | +| ------ | -------- | -------- | -------- | -------- | +| North | $120,000 | $130,000 | $125,000 | $140,000 | +| South | $100,000 | $110,000 | $115,000 | $120,000 | +| East | $90,000 | $95,000 | $100,000 | $105,000 | +| West | $110,000 | $115,000 | $120,000 | $130,000 | + +## Developer Agent + +The Developer Agent is a powerful tool that can assist with development tasks within the Rill project. It can be used to automate the creation and modification of models and metrics views. + +### Capabilities + +The Developer Agent can perform the following actions: + +- **List files**: List the files in the project directory. +- **Read files**: Read the contents of a file. +- **Write files**: Write content to a file. +- **Develop models**: Create or update a model. +- **Develop metrics views**: Create or update a metrics view. + +### Example: Creating a new Metrics View + +To create a new metrics view, you can use the `develop_metrics_view` tool with the following arguments: + +```json +{ + "path": "/metrics/new_metrics_view.yaml", + "model": "my_model" +} +``` + +This will create a new metrics view at the specified path, based on the `my_model` model. + ## Report Structure & Formatting ### Executive Summary Format @@ -79,7 +157,7 @@ Transform findings into actionable intelligence: ## Executive Summary -- **Key Finding 1**: [Quantified insight with business impact] +- **Key Finding 1**: [Quantified insight with impact] - **Key Finding 2**: [Trend or pattern with specific metrics] - **Key Finding 3**: [Actionable recommendation with expected outcome] @@ -99,39 +177,6 @@ Transform findings into actionable intelligence: 5. **Recommendations**: Actionable next steps based on findings 6. **Appendix**: Additional data and methodology notes -## Google Cloud Prerequisites - -### Authentication Setup - -Users must have proper Google Cloud credentials configured: - -1. **Install Google Cloud SDK**: - - ```bash - curl https://sdk.cloud.google.com | bash - exec -l $SHELL - ``` - -2. **Authenticate with Google Cloud**: - - ```bash - gcloud auth login - gcloud auth application-default login - ``` - -3. **Set Default Project** (if needed): - ```bash - gcloud config set project YOUR_PROJECT_ID - ``` - -### Required Permissions - -The service account or user must have: - -- **Google Drive API access**: For document creation and management -- **Google Docs API access**: For content formatting and updates -- **Sharing permissions**: To generate shareable links and manage access - ## Communication Guidelines ### Analysis Presentation @@ -176,22 +221,12 @@ The service account or user must have: ### Custom Report Generation -"Create an executive summary of user engagement metrics with recommendations for improving retention, and share it with the product team." +"Create an executive summary of user engagement metrics with recommendations for improving retention." ## Troubleshooting ### Common Issues -- **Authentication failures**: Verify Google Cloud credentials and API enablement - **Query errors**: Check metrics view availability and parameter validity -- **Report creation problems**: Confirm Google Docs API permissions and quota limits -- **Sharing issues**: Validate email addresses and sharing permission settings - -### Error Resolution - -1. **Check authentication status**: `gcloud auth list` -2. **Verify API enablement**: Ensure Drive and Docs APIs are enabled -3. **Review error messages**: Look for specific permission or quota issues -4. **Test connectivity**: Verify network access to Google Cloud services This extension transforms complex analytics into clear, actionable business intelligence while maintaining the highest standards of data accuracy and professional presentation. diff --git a/integrations/gemini/README.md b/integrations/gemini/README.md new file mode 100644 index 000000000000..991e49819853 --- /dev/null +++ b/integrations/gemini/README.md @@ -0,0 +1,37 @@ +# Gemini CLI Extension for Rill Data + +Professional data analysis and report generation using Rill's metrics layer with Google Docs integration. + +## Overview + +Check out the [Rill Data website](https://www.rilldata.com) for more information about Rill. + +Example report generated using this extension can be found in the [Gemini Documentation](docs/bids_report.md). + +## Installation + +Install the extension via GitHub: + +```bash +gemini extensions install https://github.com/rilldata/gemini +``` + +Install Rill CLI if you haven't already: + +```bash +curl https://rill.sh | sh +``` + +## Configuration + +Generate a Rill authentication token: + +```bash +rill token issue --display-name "Gemini Extension" +``` + +Update the extension with your Rill credentials by providing the following information when prompted: + +- `Organization`: Your Rill organization name +- `Project`: Your Rill project name +- `Access Token`: Your Rill access token diff --git a/integrations/gemini/gemini-extension.json b/integrations/gemini/gemini-extension.json new file mode 100644 index 000000000000..5259f5ab3e82 --- /dev/null +++ b/integrations/gemini/gemini-extension.json @@ -0,0 +1,40 @@ +{ + "name": "rill", + "description": "Professional data analysis using Rill's metrics layer", + "version": "0.1.0", + "displayName": "Rill Analytics", + "contextFileName": "GEMINI.md", + "mcpServers": { + "rill": { + "httpUrl": "https://api.rilldata.com/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", + "headers": { + "Authorization": "Bearer ${RILL_ACCESS_TOKEN}", + "X-Custom-Header": "rill-gemini-extension", + "Content-Type": "application/json" + }, + "env": { + "RILL_ORG": "${RILL_ORG}", + "RILL_PROJECT": "${RILL_PROJECT}", + "RILL_ACCESS_TOKEN": "${RILL_ACCESS_TOKEN}" + } + } + }, + "settings": [ + { + "name": "Organization", + "description": "Your Rill organization name", + "envVar": "RILL_ORG" + }, + { + "name": "Project", + "description": "Your Rill project name", + "envVar": "RILL_PROJECT" + }, + { + "name": "Access Token", + "description": "Your Rill access token", + "envVar": "RILL_ACCESS_TOKEN", + "sensitive": true + } + ] +} diff --git a/integrations/gemini/package.json b/integrations/gemini/package.json new file mode 100644 index 000000000000..80e845e86481 --- /dev/null +++ b/integrations/gemini/package.json @@ -0,0 +1,23 @@ +{ + "name": "rill", + "version": "0.1.0", + "description": "Data analysis using Rill's metrics layer", + "type": "module", + "scripts": { + "build": "echo 'No build required'", + "link": "gemini extensions link .", + "unlink": "gemini extensions uninstall rill", + "format": "prettier --write \"**/*.{ts,js,json,md}\"" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/rilldata/rill", + "directory": "integrations/gemini" + }, + "author": { + "name": "Rill Data", + "email": "support@rilldata.com", + "url": "https://rilldata.com" + } +} diff --git a/package-lock.json b/package-lock.json index ecc23cb9d261..87e2508eba4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "workspaces": [ "docs", + "integrations/gemini", "web-admin", "web-common", "web-integration", @@ -328,6 +329,39 @@ "node": ">=6" } }, + "integrations/gemini": { + "name": "rill", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1" + } + }, + "integrations/gemini/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "integrations/gemini/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@adobe/css-tools": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", @@ -37652,6 +37686,10 @@ "dev": true, "license": "MIT" }, + "node_modules/rill": { + "resolved": "integrations/gemini", + "link": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index ea6369a33bae..b338e8ba6fba 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "type": "module", "workspaces": [ "docs", + "integrations/gemini", "web-admin", "web-common", "web-integration", - "web-local", - "extensions/gemini" + "web-local" ], "scripts": { "prepare": "husky || true", From 6bec3bc370598e31a11137cf6d01cb3ead5d66e2 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Tue, 4 Nov 2025 10:04:17 -0500 Subject: [PATCH 05/14] allow rill-gemini-extension user agents --- runtime/ai/analyst_agent.go | 9 ++++++-- runtime/ai/analyst_agent_test.go | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/runtime/ai/analyst_agent.go b/runtime/ai/analyst_agent.go index ab37d5c05ead..67b7eb2bb87b 100644 --- a/runtime/ai/analyst_agent.go +++ b/runtime/ai/analyst_agent.go @@ -66,8 +66,13 @@ func (t *AnalystAgent) CheckAccess(ctx context.Context) bool { return false } - // Only allow for rill user agents since it's not useful in MCP contexts. - if !strings.HasPrefix(s.CatalogSession().UserAgent, "rill") { + // Allow for rill user agents and rill integrations + // TODO: Consider a more robust way to identify trusted integrations + userAgent := s.CatalogSession().UserAgent + isRillAgent := strings.HasPrefix(userAgent, "rill") + isRillExtension := strings.Contains(userAgent, "rill-gemini-extension") + + if !isRillAgent && !isRillExtension { return false } diff --git a/runtime/ai/analyst_agent_test.go b/runtime/ai/analyst_agent_test.go index 9c39208049de..83b4d3220ac9 100644 --- a/runtime/ai/analyst_agent_test.go +++ b/runtime/ai/analyst_agent_test.go @@ -113,3 +113,42 @@ func TestAnalystOpenRTB(t *testing.T) { }) } + +func TestAnalystCheckAccess(t *testing.T) { + // Setup runtime instance with the OpenRTB dataset + n, files := testruntime.ProjectOpenRTB(t) + rt, instanceID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{ + EnableLLM: true, + Files: files, + }) + testruntime.RequireReconcileState(t, rt, instanceID, n, 0, 0) + + // Test access control + t.Run("CheckAccess Denied", func(t *testing.T) { + s := newEval(t, rt, instanceID) + + // Modify the session to have a non-rill user agent + s.CatalogSession().UserAgent = "some-other-agent/1.0" + + // It should deny access + _, err := s.CallTool(t.Context(), ai.RoleUser, ai.AnalystAgentName, nil, ai.RouterAgentArgs{ + Prompt: "What metrics views are available?", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "access denied") + }) + + // Test access control for rill-gemini-extension user agent + t.Run("CheckAccess Rill Gemini Extension", func(t *testing.T) { + s := newEval(t, rt, instanceID) + + // Modify the session to have a rill-gemini-extension user agent + s.CatalogSession().UserAgent = "rill-gemini-extension/1.0" + + // It should allow access + _, err := s.CallTool(t.Context(), ai.RoleUser, ai.AnalystAgentName, nil, ai.RouterAgentArgs{ + Prompt: "What metrics views are available?", + }) + require.NoError(t, err) + }) +} From 0ee2ee181beb849bc4d03d517c48be1b83159ff7 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Tue, 4 Nov 2025 10:37:52 -0500 Subject: [PATCH 06/14] update documentation for Analyst Agent features --- .github/workflows/rill-integrations.yml | 24 --- integrations/gemini/GEMINI.md | 224 ++++++++++-------------- integrations/gemini/README.md | 24 ++- integrations/gemini/package.json | 12 +- 4 files changed, 116 insertions(+), 168 deletions(-) delete mode 100644 .github/workflows/rill-integrations.yml diff --git a/.github/workflows/rill-integrations.yml b/.github/workflows/rill-integrations.yml deleted file mode 100644 index c8192b15cd28..000000000000 --- a/.github/workflows/rill-integrations.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Build and Release Integrations -on: - pull_request: - paths: - - '.github/workflows/rill-integrations.yml' - - 'integrations/gemini/**' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up NodeJS - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Install dependencies - run: npm install - - - name: Build integrations - run: npm run build -w integrations/gemini diff --git a/integrations/gemini/GEMINI.md b/integrations/gemini/GEMINI.md index 6bc5df031ed1..f42f35a80bb4 100644 --- a/integrations/gemini/GEMINI.md +++ b/integrations/gemini/GEMINI.md @@ -2,6 +2,24 @@ ## Core Capabilities +### Data Exploration with Metrics Tools + +- List metrics views – Use `list_metrics_views` to discover available metrics views in the project. +- Get metrics view spec – Use `get_metrics_view` to fetch a metrics view's specification. This is important to understand all the dimensions and measures in a metrics view. +- Query the time range – Use `query_metrics_view_summary` to obtain the available time range for a metrics view. This is important to understand what time range the data spans. +- Query the metrics – Use `query_metrics_view` to run queries to get aggregated results. + +### Analyst Agent + +The **Analyst Agent** is Rill's primary intelligence tool that autonomously conducts comprehensive data analysis. It systematically explores data using available metrics tools, applies analytical rigor to find surprising patterns and unexpected relationships that influence decision-making. + +**Key Features:** + +- **Autonomous Analysis**: The agent independently performs data discovery, trend analysis, and insight generation +- **Systematic Exploration**: Follows a structured OODA loop (Observe, Orient, Decide, Act) methodology +- **Professional Reporting**: Generates executive-ready analysis with quantified insights and business context +- **Multi-phase Approach**: Discovery → Analysis → Visualization workflow + ### Analytics Functions - **Data Discovery**: Systematic exploration of available metrics views and data sources @@ -12,55 +30,86 @@ ## Analytical Methodology -### Phase 1: Data Discovery (Systematic Approach) +### Analyst Agent Workflow + +The Analyst Agent follows a three-phase methodology to deliver comprehensive analysis: -Follow the structured discovery process: +#### Phase 1: Discovery (Autonomous Setup) -1. **Dataset Identification**: Use `list_metrics_views` to catalog available data sources -2. **Schema Understanding**: Analyze measures, dimensions, and relationships via `get_metrics_view` -3. **Data Scope Assessment**: Determine time ranges and data coverage with `query_metrics_view_summary` -4. **Preliminary Queries**: Execute broad queries to gauge data quality and trends `query_metrics_view` +The agent will: -### Phase 2: Analytical Investigation (OODA Loop) +1. **Dataset Identification**: Catalogs available data sources using `list_metrics_views` +2. **Schema Understanding**: Analyzes measures, dimensions, and relationships via `get_metrics_view` +3. **Data Scope Assessment**: Determines time ranges and data coverage with `query_metrics_view_summary` +4. **Context Setting**: Establishes analytical framework and baseline understanding -Execute iterative analysis cycles: +#### Phase 2: Analysis (OODA Loop Execution) + +The agent executes iterative analysis cycles (minimum 4-6 distinct queries): - **Observe**: Query data systematically, starting broad then drilling into specifics - **Orient**: Evaluate patterns, identify anomalies, and assess the context - **Decide**: Prioritize analytical angles based on emerging insights - **Act**: Execute targeted queries to validate hypotheses and uncover insights -### Phase 3: Insight Synthesis & Documentation +#### Phase 3: Synthesis & Visualization -Transform findings into actionable intelligence: +The agent transforms findings into actionable intelligence: -- **Pattern Recognition**: Identify trends, seasonality, and behavioral changes -- **Impact Quantification**: Measure impact with specific metrics and percentages -- **Contextual Analysis**: Connect data patterns to operations and decisions -- **Report Generation**: Create professional documentation with clear structure and visualizations +- **Pattern Recognition**: Identifies trends, seasonality, and behavioral changes +- **Impact Quantification**: Measures impact with specific metrics and percentages +- **Contextual Analysis**: Connects data patterns to business operations and decisions +- **Professional Reporting**: Creates executive-ready documentation with citations and visualizations -## Data Analysis Best Practices +## How to Use the Analyst Agent -### Query Strategy +### Basic Usage -- **Minimum 4-6 distinct analytical queries** per analysis session -- **Progressive refinement**: Build each query based on previous results -- **Time-based analysis**: Always include period-over-period comparisons -- **Multi-dimensional exploration**: Analyze across different segments and dimensions +Simply invoke the analyst agent with your analysis request: -### Insight Quality Standards +``` +"Analyze our user engagement metrics for the last quarter and identify key trends." +``` + +The agent will autonomously: + +1. Discover relevant data sources +2. Execute comprehensive analysis (4-6+ queries minimum) +3. Generate insights with quantified impacts +4. Create visualizations where appropriate +5. Provide executive-ready reporting with citations + +### Advanced Usage -- **Quantified findings**: All insights must include specific numbers and percentages -- **Relevance**: Connect patterns to operational decisions and outcomes -- **Surprise factor**: Prioritize unexpected findings that challenge assumptions -- **Actionability**: Focus on insights that enable concrete actions +#### Dashboard-Specific Analysis -### Data Accuracy Requirements +``` +"Analyze the current dashboard and identify optimization opportunities." +``` -- **Tool-based calculations only**: All numbers must come from `query_metrics_view_summary` results -- **No manual computations**: Avoid performing calculations outside of the metrics tools -- **Source attribution**: Clearly indicate data sources and calculation methods -- **Limitation acknowledgment**: State when desired calculations cannot be performed +#### Comparative Analysis + +``` +"Compare Q3 vs Q4 performance across all key metrics and highlight significant changes." +``` + +#### Custom Focus Areas + +``` +"Focus on conversion funnel analysis and identify the biggest drop-off points." +``` + +## Data Analysis Standards + +### Analyst Agent Quality Assurance + +The agent ensures: + +- **Quantified findings**: All insights include specific numbers and percentages +- **Relevance**: Patterns connected to operational decisions and outcomes +- **Surprise factor**: Prioritizes unexpected findings that challenge assumptions +- **Actionability**: Focuses on insights that enable concrete actions +- **Data accuracy**: All numbers sourced directly from metrics tools with citations ## Chart Generation @@ -121,112 +170,19 @@ Pivot tables using text formatting: | East | $90,000 | $95,000 | $100,000 | $105,000 | | West | $110,000 | $115,000 | $120,000 | $130,000 | -## Developer Agent - -The Developer Agent is a powerful tool that can assist with development tasks within the Rill project. It can be used to automate the creation and modification of models and metrics views. - -### Capabilities - -The Developer Agent can perform the following actions: - -- **List files**: List the files in the project directory. -- **Read files**: Read the contents of a file. -- **Write files**: Write content to a file. -- **Develop models**: Create or update a model. -- **Develop metrics views**: Create or update a metrics view. - -### Example: Creating a new Metrics View - -To create a new metrics view, you can use the `develop_metrics_view` tool with the following arguments: - -```json -{ - "path": "/metrics/new_metrics_view.yaml", - "model": "my_model" -} -``` - -This will create a new metrics view at the specified path, based on the `my_model` model. - -## Report Structure & Formatting - -### Executive Summary Format - -```markdown -# [Report Title] - -## Executive Summary - -- **Key Finding 1**: [Quantified insight with impact] -- **Key Finding 2**: [Trend or pattern with specific metrics] -- **Key Finding 3**: [Actionable recommendation with expected outcome] - -## Data Overview - -- **Analysis Period**: [Time range analyzed] -- **Metrics Covered**: [Primary measures and dimensions] -- **Data Sources**: [Metrics views and underlying datasets] -``` - -### Detailed Analysis Structure - -1. **Methodology**: Approach and tools used -2. **Key Findings**: Primary insights with supporting data -3. **Trend Analysis**: Time-based patterns and changes -4. **Segment Analysis**: Performance across different dimensions -5. **Recommendations**: Actionable next steps based on findings -6. **Appendix**: Additional data and methodology notes - -## Communication Guidelines - -### Analysis Presentation - -- **Conversational tone**: Use "I" and "you" to speak directly to users -- **Confident delivery**: Present findings with authority and enthusiasm -- **Business focus**: Frame technical findings in business context -- **Collaborative approach**: Invite questions and deeper exploration - -### Report Documentation - -- **Executive-friendly language**: Avoid technical jargon in summaries -- **Visual descriptions**: Include references to charts and visualizations -- **Clear structure**: Use headers, bullet points, and numbered lists -- **Methodology transparency**: Document analysis approach and data sources - -## Quality Assurance - -### Analysis Validation - -- **Cross-verification**: Validate findings across multiple query approaches -- **Logical consistency**: Ensure insights align with business reality -- **Completeness check**: Confirm all relevant dimensions are explored -- **Bias awareness**: Acknowledge potential limitations and biases - -### Report Review - -- **Accuracy verification**: Confirm all numbers match tool outputs -- **Clarity assessment**: Ensure findings are understandable to target audience -- **Actionability test**: Verify recommendations are specific and implementable -- **Stakeholder relevance**: Align content with recipient needs and priorities - -## Extension Usage Examples - -### Basic Analysis Request - -"Analyze our GitHub repository activity for the last quarter and identify key productivity trends." - -### Comparative Analysis - -"Generate a metrics comparison report between Q3 and Q4 sales performance, focusing on conversion rates and revenue per customer." - -### Custom Report Generation - -"Create an executive summary of user engagement metrics with recommendations for improving retention." - ## Troubleshooting ### Common Issues -- **Query errors**: Check metrics view availability and parameter validity +- **Access denied**: Ensure your Rill access token has AI feature permissions +- **No data found**: Verify your project contains metrics views with available data +- **Analysis incomplete**: The agent may need more specific context about what metrics to focus on + +### Best Practices + +- **Be specific**: Provide clear context about what you want to analyze +- **Trust the process**: The agent will autonomously execute comprehensive analysis +- **Review results**: The agent provides citations for all quantitative claims +- **Ask follow-ups**: Request deeper analysis on specific findings -This extension transforms complex analytics into clear, actionable business intelligence while maintaining the highest standards of data accuracy and professional presentation. +This extension transforms complex analytics into clear, actionable business intelligence through the autonomous Analyst Agent, maintaining the highest standards of data accuracy and professional presentation. diff --git a/integrations/gemini/README.md b/integrations/gemini/README.md index 991e49819853..ed4c2223a54d 100644 --- a/integrations/gemini/README.md +++ b/integrations/gemini/README.md @@ -1,12 +1,10 @@ # Gemini CLI Extension for Rill Data -Professional data analysis and report generation using Rill's metrics layer with Google Docs integration. +Professional data analysis and report generation using Rill's metrics layer. ## Overview -Check out the [Rill Data website](https://www.rilldata.com) for more information about Rill. - -Example report generated using this extension can be found in the [Gemini Documentation](docs/bids_report.md). +Check out [Rill's Docs](https://docs.rilldata.com) for more information about Rill. ## Installation @@ -35,3 +33,21 @@ Update the extension with your Rill credentials by providing the following infor - `Organization`: Your Rill organization name - `Project`: Your Rill project name - `Access Token`: Your Rill access token + +## Development + +### Local Development + +To test changes locally: + +1. Make your changes to the extension files +2. Install the extension from your local development branch: + ```bash + gemini extensions install https://github.com/rilldata/rill --ref=your-branch-name + ``` + +### Releasing + +Extension updates are automatically available when changes are merged to the main branch. Users will be prompted to update when new versions are available. + +The extension version is managed through the `gemini-extension.json` file, and users can install specific versions using git refs if needed. diff --git a/integrations/gemini/package.json b/integrations/gemini/package.json index 80e845e86481..49d0d971f3b4 100644 --- a/integrations/gemini/package.json +++ b/integrations/gemini/package.json @@ -2,22 +2,22 @@ "name": "rill", "version": "0.1.0", "description": "Data analysis using Rill's metrics layer", + "license": "Apache-2.0", "type": "module", + "author": { + "name": "Rill Data", + "email": "support@rilldata.com", + "url": "https://rilldata.com" + }, "scripts": { "build": "echo 'No build required'", "link": "gemini extensions link .", "unlink": "gemini extensions uninstall rill", "format": "prettier --write \"**/*.{ts,js,json,md}\"" }, - "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/rilldata/rill", "directory": "integrations/gemini" - }, - "author": { - "name": "Rill Data", - "email": "support@rilldata.com", - "url": "https://rilldata.com" } } From 319985e02ec7ff08ec90b9b08bfff61dcbc2a2cc Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Tue, 4 Nov 2025 14:54:22 -0500 Subject: [PATCH 07/14] simplify persistent context and rely on agents --- integrations/gemini/GEMINI.md | 117 +--------------------- integrations/gemini/gemini-extension.json | 4 +- package-lock.json | 30 +----- runtime/ai/ai.go | 8 ++ runtime/ai/analyst_agent.go | 10 +- runtime/ai/analyst_agent_test.go | 76 ++++++++++---- runtime/server/mcp.go | 1 + 7 files changed, 76 insertions(+), 170 deletions(-) diff --git a/integrations/gemini/GEMINI.md b/integrations/gemini/GEMINI.md index f42f35a80bb4..04c623030073 100644 --- a/integrations/gemini/GEMINI.md +++ b/integrations/gemini/GEMINI.md @@ -1,119 +1,10 @@ -# Rill - Data Analysis +## Rill Gemini Integration -## Core Capabilities - -### Data Exploration with Metrics Tools - -- List metrics views – Use `list_metrics_views` to discover available metrics views in the project. -- Get metrics view spec – Use `get_metrics_view` to fetch a metrics view's specification. This is important to understand all the dimensions and measures in a metrics view. -- Query the time range – Use `query_metrics_view_summary` to obtain the available time range for a metrics view. This is important to understand what time range the data spans. -- Query the metrics – Use `query_metrics_view` to run queries to get aggregated results. - -### Analyst Agent - -The **Analyst Agent** is Rill's primary intelligence tool that autonomously conducts comprehensive data analysis. It systematically explores data using available metrics tools, applies analytical rigor to find surprising patterns and unexpected relationships that influence decision-making. - -**Key Features:** - -- **Autonomous Analysis**: The agent independently performs data discovery, trend analysis, and insight generation -- **Systematic Exploration**: Follows a structured OODA loop (Observe, Orient, Decide, Act) methodology -- **Professional Reporting**: Generates executive-ready analysis with quantified insights and business context -- **Multi-phase Approach**: Discovery → Analysis → Visualization workflow - -### Analytics Functions - -- **Data Discovery**: Systematic exploration of available metrics views and data sources -- **Trend Analysis**: Time-series analysis with period-over-period comparisons -- **Anomaly Detection**: Identification of unusual patterns and outliers -- **Metrics Comparison**: Side-by-side analysis of different time periods or segments -- **Actionable Intelligence**: Actionable insights generation for executive decision-making - -## Analytical Methodology - -### Analyst Agent Workflow - -The Analyst Agent follows a three-phase methodology to deliver comprehensive analysis: - -#### Phase 1: Discovery (Autonomous Setup) - -The agent will: - -1. **Dataset Identification**: Catalogs available data sources using `list_metrics_views` -2. **Schema Understanding**: Analyzes measures, dimensions, and relationships via `get_metrics_view` -3. **Data Scope Assessment**: Determines time ranges and data coverage with `query_metrics_view_summary` -4. **Context Setting**: Establishes analytical framework and baseline understanding - -#### Phase 2: Analysis (OODA Loop Execution) - -The agent executes iterative analysis cycles (minimum 4-6 distinct queries): - -- **Observe**: Query data systematically, starting broad then drilling into specifics -- **Orient**: Evaluate patterns, identify anomalies, and assess the context -- **Decide**: Prioritize analytical angles based on emerging insights -- **Act**: Execute targeted queries to validate hypotheses and uncover insights - -#### Phase 3: Synthesis & Visualization - -The agent transforms findings into actionable intelligence: - -- **Pattern Recognition**: Identifies trends, seasonality, and behavioral changes -- **Impact Quantification**: Measures impact with specific metrics and percentages -- **Contextual Analysis**: Connects data patterns to business operations and decisions -- **Professional Reporting**: Creates executive-ready documentation with citations and visualizations - -## How to Use the Analyst Agent - -### Basic Usage - -Simply invoke the analyst agent with your analysis request: - -``` -"Analyze our user engagement metrics for the last quarter and identify key trends." -``` - -The agent will autonomously: - -1. Discover relevant data sources -2. Execute comprehensive analysis (4-6+ queries minimum) -3. Generate insights with quantified impacts -4. Create visualizations where appropriate -5. Provide executive-ready reporting with citations - -### Advanced Usage - -#### Dashboard-Specific Analysis - -``` -"Analyze the current dashboard and identify optimization opportunities." -``` - -#### Comparative Analysis - -``` -"Compare Q3 vs Q4 performance across all key metrics and highlight significant changes." -``` - -#### Custom Focus Areas - -``` -"Focus on conversion funnel analysis and identify the biggest drop-off points." -``` - -## Data Analysis Standards - -### Analyst Agent Quality Assurance - -The agent ensures: - -- **Quantified findings**: All insights include specific numbers and percentages -- **Relevance**: Patterns connected to operational decisions and outcomes -- **Surprise factor**: Prioritizes unexpected findings that challenge assumptions -- **Actionability**: Focuses on insights that enable concrete actions -- **Data accuracy**: All numbers sourced directly from metrics tools with citations +The Rill Gemini Integration enables advanced data analysis and visualization using Rill's AI-powered agent capabilities. By connecting to your Rill projects, the Gemini agent can autonomously explore metrics views, generate insights, and create text-based visualizations to help you understand your data better. ## Chart Generation -When you include data in your responses, either from tool use or using your own analysis capabilities, do not build web pages or React apps. For visualizing data, you can use text-based techniques for data visualization: +If a user asks for data visualization, either from tool use or using your own analysis capabilities, do not build web pages or React apps. For visualizing data, you can use text-based techniques for data visualization: Bar Charts using block characters: @@ -184,5 +75,3 @@ Pivot tables using text formatting: - **Trust the process**: The agent will autonomously execute comprehensive analysis - **Review results**: The agent provides citations for all quantitative claims - **Ask follow-ups**: Request deeper analysis on specific findings - -This extension transforms complex analytics into clear, actionable business intelligence through the autonomous Analyst Agent, maintaining the highest standards of data accuracy and professional presentation. diff --git a/integrations/gemini/gemini-extension.json b/integrations/gemini/gemini-extension.json index 5259f5ab3e82..ea5b591bb148 100644 --- a/integrations/gemini/gemini-extension.json +++ b/integrations/gemini/gemini-extension.json @@ -6,10 +6,10 @@ "contextFileName": "GEMINI.md", "mcpServers": { "rill": { - "httpUrl": "https://api.rilldata.com/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", + "httpUrl": "http://localhost:8080/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", "headers": { "Authorization": "Bearer ${RILL_ACCESS_TOKEN}", - "X-Custom-Header": "rill-gemini-extension", + "X-Rill-Agent": "gemini", "Content-Type": "application/json" }, "env": { diff --git a/package-lock.json b/package-lock.json index 87e2508eba4b..b58c494c373b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -332,35 +332,7 @@ "integrations/gemini": { "name": "rill", "version": "0.1.0", - "license": "Apache-2.0", - "devDependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^2.1.1" - } - }, - "integrations/gemini/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "integrations/gemini/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "license": "Apache-2.0" }, "node_modules/@adobe/css-tools": { "version": "4.4.2", diff --git a/runtime/ai/ai.go b/runtime/ai/ai.go index 60f16be2118e..e42eeaf79aec 100644 --- a/runtime/ai/ai.go +++ b/runtime/ai/ai.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "reflect" goruntime "runtime" "slices" @@ -71,6 +72,7 @@ type SessionOptions struct { CreateIfNotExists bool Claims *runtime.SecurityClaims UserAgent string + Headers http.Header } // Session creates or loads an AI session. @@ -160,6 +162,7 @@ func (r *Runner) Session(ctx context.Context, opts *SessionOptions) (res *Sessio id: session.ID, instanceID: opts.InstanceID, claims: opts.Claims, + headers: opts.Headers, runner: r, logger: logger, @@ -361,6 +364,7 @@ type BaseSession struct { id string instanceID string claims *runtime.SecurityClaims + headers http.Header runner *Runner logger *zap.Logger @@ -457,6 +461,10 @@ func (s *BaseSession) Claims() *runtime.SecurityClaims { return s.claims } +func (s *BaseSession) Headers() http.Header { + return s.headers +} + func (s *BaseSession) Title() string { return s.dto.Title } diff --git a/runtime/ai/analyst_agent.go b/runtime/ai/analyst_agent.go index 67b7eb2bb87b..16843e2522eb 100644 --- a/runtime/ai/analyst_agent.go +++ b/runtime/ai/analyst_agent.go @@ -67,12 +67,12 @@ func (t *AnalystAgent) CheckAccess(ctx context.Context) bool { } // Allow for rill user agents and rill integrations - // TODO: Consider a more robust way to identify trusted integrations - userAgent := s.CatalogSession().UserAgent - isRillAgent := strings.HasPrefix(userAgent, "rill") - isRillExtension := strings.Contains(userAgent, "rill-gemini-extension") + // TODO: This is a temporary measure until we have a proper authentication mechanism for integrations. + agent := s.CatalogSession().UserAgent + isRillAgent := strings.HasPrefix(agent, "rill") + isRillIntegration := s.Headers().Get("X-Rill-Agent") != "" - if !isRillAgent && !isRillExtension { + if !isRillAgent && !isRillIntegration { return false } diff --git a/runtime/ai/analyst_agent_test.go b/runtime/ai/analyst_agent_test.go index 83b4d3220ac9..be98a34adcde 100644 --- a/runtime/ai/analyst_agent_test.go +++ b/runtime/ai/analyst_agent_test.go @@ -1,13 +1,36 @@ package ai_test import ( + "net/http" "testing" + "github.com/google/uuid" + "github.com/rilldata/rill/runtime" "github.com/rilldata/rill/runtime/ai" + "github.com/rilldata/rill/runtime/pkg/activity" "github.com/rilldata/rill/runtime/testruntime" "github.com/stretchr/testify/require" ) +// newSessionWithHeaders creates a test AI session with optional headers. +func newSessionWithHeaders(t *testing.T, rt *runtime.Runtime, instanceID string, userAgent string, headers http.Header) *ai.Session { + claims := &runtime.SecurityClaims{UserID: uuid.NewString(), SkipChecks: true} + r := ai.NewRunner(rt, activity.NewNoopClient()) + s, err := r.Session(t.Context(), &ai.SessionOptions{ + InstanceID: instanceID, + Claims: claims, + UserAgent: userAgent, + Headers: headers, + }) + require.NoError(t, err) + t.Cleanup(func() { + err := s.Flush(t.Context()) + require.NoError(t, err) + }) + + return s +} + func TestAnalystBasic(t *testing.T) { // Setup a basic metrics view with an "event_time" time dimension, "country" dimension, and "count" and "revenue" measures. rt, instanceID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{ @@ -123,32 +146,45 @@ func TestAnalystCheckAccess(t *testing.T) { }) testruntime.RequireReconcileState(t, rt, instanceID, n, 0, 0) - // Test access control - t.Run("CheckAccess Denied", func(t *testing.T) { - s := newEval(t, rt, instanceID) + t.Run("CheckAccess Denied - non-rill user agent", func(t *testing.T) { + s := newSession(t, rt, instanceID) + s.CatalogSession().UserAgent = "some-other-agent/1.0" + + agent := &ai.AnalystAgent{Runtime: rt} - // Modify the session to have a non-rill user agent + require.False(t, agent.CheckAccess(ai.WithSession(t.Context(), s))) + }) + + t.Run("CheckAccess Denied - no X-Rill-Agent header and non-rill user agent", func(t *testing.T) { + s := newSession(t, rt, instanceID) s.CatalogSession().UserAgent = "some-other-agent/1.0" - // It should deny access - _, err := s.CallTool(t.Context(), ai.RoleUser, ai.AnalystAgentName, nil, ai.RouterAgentArgs{ - Prompt: "What metrics views are available?", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "access denied") + agent := &ai.AnalystAgent{Runtime: rt} + + ctx := ai.WithSession(t.Context(), s) + require.False(t, agent.CheckAccess(ctx)) }) - // Test access control for rill-gemini-extension user agent - t.Run("CheckAccess Rill Gemini Extension", func(t *testing.T) { - s := newEval(t, rt, instanceID) + t.Run("CheckAccess Allowed - rill user agent", func(t *testing.T) { + s := newSession(t, rt, instanceID) - // Modify the session to have a rill-gemini-extension user agent - s.CatalogSession().UserAgent = "rill-gemini-extension/1.0" + s.CatalogSession().UserAgent = "rill-evals/1.0" - // It should allow access - _, err := s.CallTool(t.Context(), ai.RoleUser, ai.AnalystAgentName, nil, ai.RouterAgentArgs{ - Prompt: "What metrics views are available?", - }) - require.NoError(t, err) + agent := &ai.AnalystAgent{Runtime: rt} + + ctx := ai.WithSession(t.Context(), s) + require.True(t, agent.CheckAccess(ctx)) + }) + + t.Run("CheckAccess Allowed - X-Rill-Agent header", func(t *testing.T) { + headers := http.Header{} + headers.Set("X-Rill-Agent", "gemini") + + s := newSessionWithHeaders(t, rt, instanceID, "node", headers) + + agent := &ai.AnalystAgent{Runtime: rt} + + ctx := ai.WithSession(t.Context(), s) + require.True(t, agent.CheckAccess(ctx)) }) } diff --git a/runtime/server/mcp.go b/runtime/server/mcp.go index daecb9a99bf9..05c1dccec830 100644 --- a/runtime/server/mcp.go +++ b/runtime/server/mcp.go @@ -37,6 +37,7 @@ func (s *Server) mcpHandler() http.Handler { CreateIfNotExists: true, Claims: auth.GetClaims(r.Context(), instanceID), UserAgent: "mcp/unknown", // It's just preliminary: the MCP server updates it with the actual user agent after the initialization handshake. + Headers: r.Header, }) if err != nil { s.logger.Error("failed to create AI session for MCP", zap.String("instance_id", instanceID), zap.String("session_id", sessionID), zap.Error(err)) From 6a023c24c824e7e59b5047b72aa2e9f4b8bdc0d9 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Tue, 4 Nov 2025 16:08:48 -0500 Subject: [PATCH 08/14] go lint merge main --- runtime/ai/analyst_agent_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/ai/analyst_agent_test.go b/runtime/ai/analyst_agent_test.go index 8e13fa45c583..bdcf3c4c11ba 100644 --- a/runtime/ai/analyst_agent_test.go +++ b/runtime/ai/analyst_agent_test.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" "github.com/rilldata/rill/runtime" "github.com/rilldata/rill/runtime/ai" - "github.com/rilldata/rill/runtime/pkg/activity" "github.com/rilldata/rill/runtime/metricsview" + "github.com/rilldata/rill/runtime/pkg/activity" "github.com/rilldata/rill/runtime/pkg/mapstructureutil" "github.com/rilldata/rill/runtime/testruntime" "github.com/stretchr/testify/require" From 9822704119c5daf6c588d8558b8e45ef49da47ed Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Tue, 4 Nov 2025 16:32:34 -0500 Subject: [PATCH 09/14] target cloud --- integrations/gemini/gemini-extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/gemini/gemini-extension.json b/integrations/gemini/gemini-extension.json index ea5b591bb148..d97a40a29bb5 100644 --- a/integrations/gemini/gemini-extension.json +++ b/integrations/gemini/gemini-extension.json @@ -6,7 +6,7 @@ "contextFileName": "GEMINI.md", "mcpServers": { "rill": { - "httpUrl": "http://localhost:8080/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", + "httpUrl": "https://api.rilldata.com/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", "headers": { "Authorization": "Bearer ${RILL_ACCESS_TOKEN}", "X-Rill-Agent": "gemini", From de2e5efeaa260cd0eaa9d186cc792f451cb55ebc Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Wed, 5 Nov 2025 17:04:42 -0500 Subject: [PATCH 10/14] update release instructions in Gemini README --- .github/workflows/rill-integrations.yml | 65 +++++++++++++++++++++++++ integrations/gemini/README.md | 14 ++++-- 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/rill-integrations.yml diff --git a/.github/workflows/rill-integrations.yml b/.github/workflows/rill-integrations.yml new file mode 100644 index 000000000000..33610cd2122b --- /dev/null +++ b/.github/workflows/rill-integrations.yml @@ -0,0 +1,65 @@ +name: Build and Release Rill Integrations + +on: + pull_request: + paths: + - ".github/workflows/rill-integrations.yml" + - "integrations/gemini/**" + push: + tags: + - "v*" + paths: + - ".github/workflows/rill-integrations.yml" + - "integrations/gemini/**" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: cd integrations/gemini && npm ci + env: + HUSKY: 0 + + - name: Build extension + run: cd integrations/gemini && npm run build + + - name: Run tests + run: cd integrations/gemini && npm run test + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: cd integrations/gemini && npm ci + env: + HUSKY: 0 + + - name: Build extension + run: cd integrations/gemini && npm run build + + - name: Create release assets + run: cd integrations/gemini && npm run package + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: integrations/gemini/release/** diff --git a/integrations/gemini/README.md b/integrations/gemini/README.md index ed4c2223a54d..b9bc37bf0f40 100644 --- a/integrations/gemini/README.md +++ b/integrations/gemini/README.md @@ -11,7 +11,7 @@ Check out [Rill's Docs](https://docs.rilldata.com) for more information about Ri Install the extension via GitHub: ```bash -gemini extensions install https://github.com/rilldata/gemini +gemini extensions install https://github.com/rilldata/rill ``` Install Rill CLI if you haven't already: @@ -43,11 +43,17 @@ To test changes locally: 1. Make your changes to the extension files 2. Install the extension from your local development branch: ```bash - gemini extensions install https://github.com/rilldata/rill --ref=your-branch-name + npm run -w integrations/gemini link + ``` +3. Test the extension in Gemini: + ```bash + npm run -w integrations/gemini unlink ``` ### Releasing -Extension updates are automatically available when changes are merged to the main branch. Users will be prompted to update when new versions are available. +This extension is distributed through [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases). This provides a faster and more reliable initial installation experience for users, as releases are shipped as single archives instead of requiring a git clone. + +Each release includes an archive file containing the full contents of the repository at the tagged commit. When checking for updates, Gemini CLI looks for the "latest" release on GitHub (you must mark it as such when creating the release), unless the user installed a specific release by passing `--ref=`. -The extension version is managed through the `gemini-extension.json` file, and users can install specific versions using git refs if needed. +The extension version is managed through the `gemini-extension.json` file, and releases are automated via GitHub Actions when new version tags are pushed. From 4c4098a467c45d92e4b06f38cb762c154883fe6d Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Wed, 5 Nov 2025 17:13:38 -0500 Subject: [PATCH 11/14] add line chart and context --- .github/workflows/rill-integrations.yml | 3 -- integrations/gemini/GEMINI.md | 41 +++++++++++++++++-------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/rill-integrations.yml b/.github/workflows/rill-integrations.yml index 33610cd2122b..85e479876de2 100644 --- a/.github/workflows/rill-integrations.yml +++ b/.github/workflows/rill-integrations.yml @@ -32,9 +32,6 @@ jobs: - name: Build extension run: cd integrations/gemini && npm run build - - name: Run tests - run: cd integrations/gemini && npm run test - release: if: startsWith(github.ref, 'refs/tags/v') needs: build diff --git a/integrations/gemini/GEMINI.md b/integrations/gemini/GEMINI.md index 04c623030073..b367fd9658db 100644 --- a/integrations/gemini/GEMINI.md +++ b/integrations/gemini/GEMINI.md @@ -1,10 +1,10 @@ ## Rill Gemini Integration -The Rill Gemini Integration enables advanced data analysis and visualization using Rill's AI-powered agent capabilities. By connecting to your Rill projects, the Gemini agent can autonomously explore metrics views, generate insights, and create text-based visualizations to help you understand your data better. +You are an expert data analyst with access to Rill's metrics views through specialized tools. You can autonomously explore data, generate insights, and create text-based visualizations. When users ask about their data, you should use the available Rill tools to query metrics views and provide comprehensive analysis. ## Chart Generation -If a user asks for data visualization, either from tool use or using your own analysis capabilities, do not build web pages or React apps. For visualizing data, you can use text-based techniques for data visualization: +When users request data visualization, do not build web pages or React apps. Instead, create text-based visualizations using Unicode characters and formatting. Use these techniques: Bar Charts using block characters: @@ -61,17 +61,34 @@ Pivot tables using text formatting: | East | $90,000 | $95,000 | $100,000 | $105,000 | | West | $110,000 | $115,000 | $120,000 | $130,000 | -## Troubleshooting +Line Charts characters: -### Common Issues +Value + ^ + | /--------------- (Value) + | / + 3 | / + | / + 2 | / + | / + 1 | / + | / + 0 |______/ + +---+---+---+---+---+---+---+---+---+---+---> Timeseries + 0 1 2 3 4 5 6 7 8 9 10 -- **Access denied**: Ensure your Rill access token has AI feature permissions -- **No data found**: Verify your project contains metrics views with available data -- **Analysis incomplete**: The agent may need more specific context about what metrics to focus on -### Best Practices +## Analysis Guidelines -- **Be specific**: Provide clear context about what you want to analyze -- **Trust the process**: The agent will autonomously execute comprehensive analysis -- **Review results**: The agent provides citations for all quantitative claims -- **Ask follow-ups**: Request deeper analysis on specific findings +### When Users Encounter Issues + +- **Access denied errors**: Inform users to ensure their Rill access token has appropriate permissions +- **No data found**: Guide users to verify their project contains metrics views with available data +- **Incomplete analysis**: Ask users for more specific context about which metrics to focus on + +### Your Analysis Process + +- **Be thorough**: Use available Rill tools to explore data comprehensively +- **Provide context**: Always explain what the data shows and why it matters +- **Cite your sources**: Reference specific metrics views and time ranges used +- **Offer insights**: Go beyond raw numbers to identify trends, patterns, and actionable findings From 7f4a49f00b18298a172da6439bb778d56237a120 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Wed, 5 Nov 2025 17:15:54 -0500 Subject: [PATCH 12/14] move release --- .github/workflows/rill-integrations.yml | 29 ------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/rill-integrations.yml b/.github/workflows/rill-integrations.yml index 85e479876de2..595efe88caf1 100644 --- a/.github/workflows/rill-integrations.yml +++ b/.github/workflows/rill-integrations.yml @@ -31,32 +31,3 @@ jobs: - name: Build extension run: cd integrations/gemini && npm run build - - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Install dependencies - run: cd integrations/gemini && npm ci - env: - HUSKY: 0 - - - name: Build extension - run: cd integrations/gemini && npm run build - - - name: Create release assets - run: cd integrations/gemini && npm run package - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - files: integrations/gemini/release/** From 52a788d59688d8cc5c8f1f928848f0dd60f95688 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Wed, 5 Nov 2025 17:27:25 -0500 Subject: [PATCH 13/14] add Node.js setup and build steps for Gemini Extension in release workflow --- .github/workflows/cli-release.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index e39ed3498529..207847629f78 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -121,6 +121,21 @@ jobs: - name: Set up gcloud CLI uses: google-github-actions/setup-gcloud@v2 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Build Gemini Extension + if: env.PUBLISH_RELEASE == 'true' + run: | + cd integrations/gemini + npm ci + npm run build + npm run package + env: + HUSKY: 0 + - name: Set GORELEASER_CURRENT_TAG and NIGHTLY run: |- if [[ ${{ env.PUBLISH_NIGHTLY }} == 'true' ]]; then @@ -155,6 +170,14 @@ jobs: GORELEASER_CURRENT_TAG: ${{ env.GORELEASER_CURRENT_TAG }} PUBLISH_NIGHTLY: ${{ env.PUBLISH_NIGHTLY }} + - name: Add Gemini Extension to Release + if: env.PUBLISH_RELEASE == 'true' + uses: softprops/action-gh-release@v1 + with: + files: integrations/gemini/release/** + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: CDN - Explicitly invalidate the old artifacts from CDN cache run: |- gcloud compute url-maps invalidate-cdn-cache prod --path "/install.sh" --async From ca354fa373b465f17dee9c4407386f74b1b67bb5 Mon Sep 17 00:00:00 2001 From: Graham Plata Date: Wed, 5 Nov 2025 18:42:48 -0500 Subject: [PATCH 14/14] review --- .github/workflows/cli-release.yml | 23 ------ .../workflows/gemini-extension-release.yml | 69 +++++++++++++++++ .github/workflows/rill-integrations.yml | 33 -------- integrations/gemini/.example.env | 4 - integrations/gemini/GEMINI.md | 5 +- integrations/gemini/README.md | 28 ++++--- integrations/gemini/gemini-extension.json | 1 - runtime/ai/ai.go | 8 -- runtime/ai/analyst_agent.go | 10 ++- runtime/ai/analyst_agent_test.go | 75 ------------------- runtime/server/mcp.go | 1 - 11 files changed, 94 insertions(+), 163 deletions(-) create mode 100644 .github/workflows/gemini-extension-release.yml delete mode 100644 .github/workflows/rill-integrations.yml delete mode 100644 integrations/gemini/.example.env diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 207847629f78..e39ed3498529 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -121,21 +121,6 @@ jobs: - name: Set up gcloud CLI uses: google-github-actions/setup-gcloud@v2 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Build Gemini Extension - if: env.PUBLISH_RELEASE == 'true' - run: | - cd integrations/gemini - npm ci - npm run build - npm run package - env: - HUSKY: 0 - - name: Set GORELEASER_CURRENT_TAG and NIGHTLY run: |- if [[ ${{ env.PUBLISH_NIGHTLY }} == 'true' ]]; then @@ -170,14 +155,6 @@ jobs: GORELEASER_CURRENT_TAG: ${{ env.GORELEASER_CURRENT_TAG }} PUBLISH_NIGHTLY: ${{ env.PUBLISH_NIGHTLY }} - - name: Add Gemini Extension to Release - if: env.PUBLISH_RELEASE == 'true' - uses: softprops/action-gh-release@v1 - with: - files: integrations/gemini/release/** - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: CDN - Explicitly invalidate the old artifacts from CDN cache run: |- gcloud compute url-maps invalidate-cdn-cache prod --path "/install.sh" --async diff --git a/.github/workflows/gemini-extension-release.yml b/.github/workflows/gemini-extension-release.yml new file mode 100644 index 000000000000..28e3b4daf8da --- /dev/null +++ b/.github/workflows/gemini-extension-release.yml @@ -0,0 +1,69 @@ +name: Release Gemini Extension + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., gemini-v1.0.0)' + required: true + type: string + +env: + EXTENSION_VERSION: ${{ github.event.inputs.tag || github.ref_name }} + +jobs: + release-gemini-extension: + name: Release Gemini Extension + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Build Gemini Extension + run: | + cd integrations/gemini + npm ci + npm run build + npm run package + env: + HUSKY: 0 + + - name: Create Gemini Extension Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ env.EXTENSION_VERSION }} + name: "Gemini Extension ${{ env.EXTENSION_VERSION }}" + files: integrations/gemini/release/** + body: | + ## Rill Gemini Extension Release + + This release contains the Rill integration for Google Gemini. + + ### Installation + ```bash + gemini extensions install https://github.com/rilldata/rill --ref=${{ env.EXTENSION_VERSION }} + ``` + + See the [README](https://github.com/rilldata/rill/blob/main/integrations/gemini/README.md) for setup instructions. + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Notify Slack + uses: ravsamhq/notify-slack-action@v2 + if: always() + with: + status: ${{ job.status }} + notification_title: "Gemini Extension Release - ${{ env.EXTENSION_VERSION }}" + message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>" + footer: "Linked Repo <{repo_url}|{repo}>" + notify_when: "failure" + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ANNOUNCE_DD }} diff --git a/.github/workflows/rill-integrations.yml b/.github/workflows/rill-integrations.yml deleted file mode 100644 index 595efe88caf1..000000000000 --- a/.github/workflows/rill-integrations.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Build and Release Rill Integrations - -on: - pull_request: - paths: - - ".github/workflows/rill-integrations.yml" - - "integrations/gemini/**" - push: - tags: - - "v*" - paths: - - ".github/workflows/rill-integrations.yml" - - "integrations/gemini/**" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Install dependencies - run: cd integrations/gemini && npm ci - env: - HUSKY: 0 - - - name: Build extension - run: cd integrations/gemini && npm run build diff --git a/integrations/gemini/.example.env b/integrations/gemini/.example.env deleted file mode 100644 index 4078761e14a3..000000000000 --- a/integrations/gemini/.example.env +++ /dev/null @@ -1,4 +0,0 @@ -# Rill Configuration -RILL_ORG="demo" -RILL_PROJECT="demo-project" -RILL_ACCESS_TOKEN="rill_usr_1234567890abcdef" diff --git a/integrations/gemini/GEMINI.md b/integrations/gemini/GEMINI.md index b367fd9658db..981e56b81e53 100644 --- a/integrations/gemini/GEMINI.md +++ b/integrations/gemini/GEMINI.md @@ -63,6 +63,7 @@ Pivot tables using text formatting: Line Charts characters: +``` Value ^ | /--------------- (Value) @@ -76,7 +77,7 @@ Value 0 |______/ +---+---+---+---+---+---+---+---+---+---+---> Timeseries 0 1 2 3 4 5 6 7 8 9 10 - +``` ## Analysis Guidelines @@ -89,6 +90,6 @@ Value ### Your Analysis Process - **Be thorough**: Use available Rill tools to explore data comprehensively -- **Provide context**: Always explain what the data shows and why it matters +- **Provide context**: Always explain what the data shows and why it matters - **Cite your sources**: Reference specific metrics views and time ranges used - **Offer insights**: Go beyond raw numbers to identify trends, patterns, and actionable findings diff --git a/integrations/gemini/README.md b/integrations/gemini/README.md index b9bc37bf0f40..2f04d2456271 100644 --- a/integrations/gemini/README.md +++ b/integrations/gemini/README.md @@ -8,12 +8,16 @@ Check out [Rill's Docs](https://docs.rilldata.com) for more information about Ri ## Installation -Install the extension via GitHub: +Install the extension via GitHub using a specific release tag: ```bash -gemini extensions install https://github.com/rilldata/rill +gemini extensions install https://github.com/rilldata/rill --ref=gemini-v1.0.0 ``` +> **Note**: Since this is a monorepo, you must specify a Gemini-specific release tag (prefixed with `gemini-v`) to ensure Gemini CLI picks up the correct extension files. See [Releases](https://github.com/rilldata/rill/releases) for available versions. + +For the latest version, check the [releases page](https://github.com/rilldata/rill/releases) and use the most recent `gemini-v*` tag. + Install Rill CLI if you haven't already: ```bash @@ -22,17 +26,21 @@ curl https://rill.sh | sh ## Configuration -Generate a Rill authentication token: +After installation, configure the extension with your Rill credentials. The extension will prompt you for the following information during setup: + +- **Organization**: Your Rill organization name +- **Project**: Your Rill project name +- **Access Token**: Your Rill access token + +To generate a Rill authentication token, run: ```bash rill token issue --display-name "Gemini Extension" ``` -Update the extension with your Rill credentials by providing the following information when prompted: +> **Tip**: You can find your organization and project names in the Rill Cloud UI URL: `https://ui.rilldata.com/{organization}/{project}` -- `Organization`: Your Rill organization name -- `Project`: Your Rill project name -- `Access Token`: Your Rill access token +The extension configuration is handled automatically through Gemini's settings interface - no manual environment file setup is required. ## Development @@ -52,8 +60,4 @@ To test changes locally: ### Releasing -This extension is distributed through [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases). This provides a faster and more reliable initial installation experience for users, as releases are shipped as single archives instead of requiring a git clone. - -Each release includes an archive file containing the full contents of the repository at the tagged commit. When checking for updates, Gemini CLI looks for the "latest" release on GitHub (you must mark it as such when creating the release), unless the user installed a specific release by passing `--ref=`. - -The extension version is managed through the `gemini-extension.json` file, and releases are automated via GitHub Actions when new version tags are pushed. +This extension is distributed through [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) diff --git a/integrations/gemini/gemini-extension.json b/integrations/gemini/gemini-extension.json index d97a40a29bb5..05dca6213977 100644 --- a/integrations/gemini/gemini-extension.json +++ b/integrations/gemini/gemini-extension.json @@ -9,7 +9,6 @@ "httpUrl": "https://api.rilldata.com/v1/orgs/${RILL_ORG}/projects/${RILL_PROJECT}/runtime/mcp", "headers": { "Authorization": "Bearer ${RILL_ACCESS_TOKEN}", - "X-Rill-Agent": "gemini", "Content-Type": "application/json" }, "env": { diff --git a/runtime/ai/ai.go b/runtime/ai/ai.go index e42eeaf79aec..60f16be2118e 100644 --- a/runtime/ai/ai.go +++ b/runtime/ai/ai.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "net/http" "reflect" goruntime "runtime" "slices" @@ -72,7 +71,6 @@ type SessionOptions struct { CreateIfNotExists bool Claims *runtime.SecurityClaims UserAgent string - Headers http.Header } // Session creates or loads an AI session. @@ -162,7 +160,6 @@ func (r *Runner) Session(ctx context.Context, opts *SessionOptions) (res *Sessio id: session.ID, instanceID: opts.InstanceID, claims: opts.Claims, - headers: opts.Headers, runner: r, logger: logger, @@ -364,7 +361,6 @@ type BaseSession struct { id string instanceID string claims *runtime.SecurityClaims - headers http.Header runner *Runner logger *zap.Logger @@ -461,10 +457,6 @@ func (s *BaseSession) Claims() *runtime.SecurityClaims { return s.claims } -func (s *BaseSession) Headers() http.Header { - return s.headers -} - func (s *BaseSession) Title() string { return s.dto.Title } diff --git a/runtime/ai/analyst_agent.go b/runtime/ai/analyst_agent.go index ed587b6dc6a1..5d361b465c37 100644 --- a/runtime/ai/analyst_agent.go +++ b/runtime/ai/analyst_agent.go @@ -73,12 +73,14 @@ func (t *AnalystAgent) CheckAccess(ctx context.Context) bool { } // Allow for rill user agents and rill integrations - // TODO: This is a temporary measure until we have a proper authentication mechanism for integrations. + // Check if the user agent indicates this is a Rill client or integration agent := s.CatalogSession().UserAgent - isRillAgent := strings.HasPrefix(agent, "rill") - isRillIntegration := s.Headers().Get("X-Rill-Agent") != "" + isRillClient := strings.HasPrefix(agent, "rill") - if !isRillAgent && !isRillIntegration { + // Allow rill-gemini-extension specifically for Gemini integration + isGeminiIntegration := strings.Contains(agent, "rill-gemini-extension") + + if !isRillClient && !isGeminiIntegration { return false } diff --git a/runtime/ai/analyst_agent_test.go b/runtime/ai/analyst_agent_test.go index bdcf3c4c11ba..2e476cd1b65a 100644 --- a/runtime/ai/analyst_agent_test.go +++ b/runtime/ai/analyst_agent_test.go @@ -1,39 +1,16 @@ package ai_test import ( - "net/http" "testing" "time" - "github.com/google/uuid" - "github.com/rilldata/rill/runtime" "github.com/rilldata/rill/runtime/ai" "github.com/rilldata/rill/runtime/metricsview" - "github.com/rilldata/rill/runtime/pkg/activity" "github.com/rilldata/rill/runtime/pkg/mapstructureutil" "github.com/rilldata/rill/runtime/testruntime" "github.com/stretchr/testify/require" ) -// newSessionWithHeaders creates a test AI session with optional headers. -func newSessionWithHeaders(t *testing.T, rt *runtime.Runtime, instanceID string, userAgent string, headers http.Header) *ai.Session { - claims := &runtime.SecurityClaims{UserID: uuid.NewString(), SkipChecks: true} - r := ai.NewRunner(rt, activity.NewNoopClient()) - s, err := r.Session(t.Context(), &ai.SessionOptions{ - InstanceID: instanceID, - Claims: claims, - UserAgent: userAgent, - Headers: headers, - }) - require.NoError(t, err) - t.Cleanup(func() { - err := s.Flush(t.Context()) - require.NoError(t, err) - }) - - return s -} - func TestAnalystBasic(t *testing.T) { // Setup a basic metrics view with an "event_time" time dimension, "country" dimension, and "count" and "revenue" measures. rt, instanceID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{ @@ -188,55 +165,3 @@ func parseTestTime(tst *testing.T, t string) time.Time { require.NoError(tst, err) return ts } - -func TestAnalystCheckAccess(t *testing.T) { - // Setup runtime instance with the OpenRTB dataset - n, files := testruntime.ProjectOpenRTB(t) - rt, instanceID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{ - EnableLLM: true, - Files: files, - }) - testruntime.RequireReconcileState(t, rt, instanceID, n, 0, 0) - - t.Run("CheckAccess Denied - non-rill user agent", func(t *testing.T) { - s := newSession(t, rt, instanceID) - s.CatalogSession().UserAgent = "some-other-agent/1.0" - - agent := &ai.AnalystAgent{Runtime: rt} - - require.False(t, agent.CheckAccess(ai.WithSession(t.Context(), s))) - }) - - t.Run("CheckAccess Denied - no X-Rill-Agent header and non-rill user agent", func(t *testing.T) { - s := newSession(t, rt, instanceID) - s.CatalogSession().UserAgent = "some-other-agent/1.0" - - agent := &ai.AnalystAgent{Runtime: rt} - - ctx := ai.WithSession(t.Context(), s) - require.False(t, agent.CheckAccess(ctx)) - }) - - t.Run("CheckAccess Allowed - rill user agent", func(t *testing.T) { - s := newSession(t, rt, instanceID) - - s.CatalogSession().UserAgent = "rill-evals/1.0" - - agent := &ai.AnalystAgent{Runtime: rt} - - ctx := ai.WithSession(t.Context(), s) - require.True(t, agent.CheckAccess(ctx)) - }) - - t.Run("CheckAccess Allowed - X-Rill-Agent header", func(t *testing.T) { - headers := http.Header{} - headers.Set("X-Rill-Agent", "gemini") - - s := newSessionWithHeaders(t, rt, instanceID, "node", headers) - - agent := &ai.AnalystAgent{Runtime: rt} - - ctx := ai.WithSession(t.Context(), s) - require.True(t, agent.CheckAccess(ctx)) - }) -} diff --git a/runtime/server/mcp.go b/runtime/server/mcp.go index 05c1dccec830..daecb9a99bf9 100644 --- a/runtime/server/mcp.go +++ b/runtime/server/mcp.go @@ -37,7 +37,6 @@ func (s *Server) mcpHandler() http.Handler { CreateIfNotExists: true, Claims: auth.GetClaims(r.Context(), instanceID), UserAgent: "mcp/unknown", // It's just preliminary: the MCP server updates it with the actual user agent after the initialization handshake. - Headers: r.Header, }) if err != nil { s.logger.Error("failed to create AI session for MCP", zap.String("instance_id", instanceID), zap.String("session_id", sessionID), zap.Error(err))