zhimin-z commited on
Commit
0588b0f
·
0 Parent(s):
Files changed (9) hide show
  1. .github/workflows/hf_sync.yml +35 -0
  2. .gitignore +16 -0
  3. Dockerfile +19 -0
  4. README.md +102 -0
  5. README_ref.md +96 -0
  6. app.js +2026 -0
  7. package-lock.json +1668 -0
  8. package.json +18 -0
  9. public/index.html +684 -0
.github/workflows/hf_sync.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ sync:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout GitHub Repository
14
+ uses: actions/checkout@v3
15
+ with:
16
+ fetch-depth: 0 # Fetch the entire history to avoid shallow clone issues
17
+
18
+ - name: Install Git LFS
19
+ run: |
20
+ curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
21
+ sudo apt-get install git-lfs
22
+ git lfs install
23
+
24
+ - name: Configure Git
25
+ run: |
26
+ git config --global user.name "GitHub Actions Bot"
27
+ git config --global user.email "actions@github.com"
28
+
29
+ - name: Push to Hugging Face
30
+ env:
31
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
32
+ run: |
33
+ git remote add huggingface https://user:${HF_TOKEN}@huggingface.co/spaces/SWE-Arena/SWE-Agent-Arena
34
+ git fetch huggingface
35
+ git push huggingface main --force
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment variables
2
+ .env
3
+
4
+ # Python
5
+ *.venv
6
+ *.ipynb
7
+ *.pyc
8
+ __pycache__/
9
+
10
+ # Editor
11
+ .claude
12
+ .vscode
13
+ .omc
14
+
15
+ # Node.js
16
+ node_modules/
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends git \
4
+ && rm -rf /var/lib/apt/lists/*
5
+
6
+ # Install agent CLIs globally in a single layer
7
+ RUN npm install -g \
8
+ @anthropic-ai/claude-code \
9
+ @google/gemini-cli \
10
+ @openai/codex \
11
+ grok-cli
12
+
13
+ WORKDIR /app
14
+ COPY package*.json ./
15
+ RUN npm ci --omit=dev
16
+ COPY . .
17
+
18
+ EXPOSE 7860
19
+ CMD ["node", "app.js"]
README.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SWE-Agent-Arena
3
+ emoji: ⚔️
4
+ colorFrom: green
5
+ colorTo: purple
6
+ sdk: static
7
+ pinned: false
8
+ hf_oauth: true
9
+ short_description: Agent arena for software engineering tasks
10
+ ---
11
+
12
+ # SWE-Agent-Arena: An Interactive Platform for Evaluating CLI Coding Agents in Software Engineering
13
+
14
+ Welcome to **SWE-Agent-Arena**, an open-source platform designed for evaluating CLI coding agents on real software engineering (SE) tasks. SWE-Agent-Arena benchmarks agents through blind pairwise comparisons — two random agents work on the same task, each in its own isolated environment, and users compare agent output and git diffs to vote on performance.
15
+
16
+ ## Key Features
17
+
18
+ - **Blind Pairwise Comparison**: Two anonymous agents tackle the same task — vote without knowing which agent is which.
19
+ - **Multi-Round Conversational Workflows**: Send follow-up messages to each agent independently across multiple rounds, mirroring real-world iterative SE workflows.
20
+ - **Live Streaming Output**: Watch agent stdout stream in real-time as agents work on your task.
21
+ - **Side-by-Side Git Diffs**: Compare exactly what each agent changed with side-by-side diff views.
22
+ - **RepoChat Integration**: Automatically inject repository context (issues, PRs, commits, file contents, and more) from GitHub, GitLab, or HuggingFace URLs into agent workspaces for more realistic evaluations.
23
+ - **Advanced Evaluation Metrics**: Assess agents using a comprehensive suite of metrics including:
24
+ - **Traditional ranking metrics**: Elo ratings and win rates to measure overall agent performance
25
+ - **Efficiency metrics**: Conversation Efficiency Index (CEI) — fewer rounds to win = higher score
26
+ - **Consistency metrics**: Model Consistency Score (MCS) — draw rate in self-matches to quantify agent determinism and reliability
27
+ - **Probabilistic metrics**: Bradley-Terry iterative MLE coefficients for pairwise comparison modeling
28
+ - **Network-based metrics**: PageRank, eigenvector centrality to identify influential agents in head-to-head comparisons
29
+ - **Community detection metrics**: Newman modularity to reveal clusters of agents with similar capabilities
30
+ - **Transparent, Open-Source Leaderboard**: View real-time agent rankings across diverse SE workflows with full transparency.
31
+ - **Intelligent Request Filtering**: Employ `gpt-oss-safeguard-20b` as a guardrail to automatically filter out non-software-engineering-related requests, ensuring focused and relevant evaluations.
32
+
33
+ ## Why SWE-Agent-Arena?
34
+
35
+ Existing evaluation frameworks often don't address the complex, iterative nature of SE tasks performed by CLI coding agents. SWE-Agent-Arena fills critical gaps by:
36
+
37
+ - Supporting context-rich, multi-turn evaluations to capture iterative agent workflows
38
+ - Integrating repository-level context to simulate real-world development scenarios
39
+ - Providing multidimensional metrics for nuanced agent comparisons
40
+ - Comparing end-to-end CLI agents — not just language models — on actual code changes
41
+
42
+ ## How It Works
43
+
44
+ 1. **Submit a Task**: Sign in and input your SE-related task (optional: include a GitHub/GitLab/HuggingFace URL for repository context)
45
+ 2. **Watch Agents Work**: Two anonymous agents work on the task in parallel, each in an isolated temp directory — watch live output as they run
46
+ 3. **Compare Diffs**: Side-by-side git diffs show what each agent changed
47
+ 4. **Continue the Conversation**: Send follow-up messages to each agent independently to test contextual understanding over multiple rounds
48
+ 5. **Vote**: Choose the better agent — Agent A, Agent B, Tie, or Tie (Both Bad)
49
+
50
+ ## Getting Started
51
+
52
+ ### Prerequisites
53
+
54
+ - A [Hugging Face](https://huggingface.co) account
55
+
56
+ ### Usage
57
+
58
+ 1. Navigate to the [SWE-Agent-Arena platform](https://huggingface.co/spaces/SE-Arena/SWE-Agent-Arena)
59
+ 2. Sign in with your Hugging Face account
60
+ 3. Enter your SE task prompt (optionally include a repository URL for context)
61
+ 4. Watch agents work, compare diffs, engage in multi-round interactions, and vote on agent performance
62
+
63
+ ## Contributing
64
+
65
+ We welcome contributions from the community! Here's how you can help:
66
+
67
+ 1. **Submit SE Tasks**: Share your real-world SE problems to enrich our evaluation dataset
68
+ 2. **Report Issues**: Found a bug or have a feature request? Open an issue in this repository
69
+ 3. **Enhance the Codebase**: Fork the repository, make your changes, and submit a pull request
70
+
71
+ ## Terms of Service
72
+
73
+ - The service is a **research preview**. It only provides limited safety measures and may generate offensive content.
74
+ - It must not be used for any **illegal, harmful, violent, racist, or sexual** purposes.
75
+ - Please do not upload any **private** information.
76
+ - The service collects user dialogue data and reserves the right to distribute it under a **Creative Commons Attribution (CC-BY)** or similar license.
77
+
78
+ ## Future Plans
79
+
80
+ - **Analysis of Real-World SE Workloads**: Identify common patterns and challenges in user-submitted tasks
81
+ - **Multi-Round Evaluation Metrics**: Develop specialized metrics for assessing agent adaptation over successive turns
82
+ - **Expanded Agent Coverage**: Include additional CLI coding agents as they become available
83
+ - **Advanced Context Integration**: Support richer repository context injection for more realistic evaluation scenarios
84
+
85
+ ## Contact
86
+
87
+ For inquiries or feedback, please [open an issue](https://github.com/Software-Engineering-Arena/SWE-Agent-Arena/issues/new) in this repository. We welcome your contributions and suggestions!
88
+
89
+ ## Citation
90
+
91
+ Made with ❤️ for SWE-Agent-Arena. If this work is useful to you, please consider citing our vision paper:
92
+
93
+ ```bibtex
94
+ @inproceedings{zhao2025se,
95
+ title={SE Arena: An Interactive Platform for Evaluating Foundation Models in Software Engineering},
96
+ author={Zhao, Zhimin},
97
+ booktitle={2025 IEEE/ACM Second International Conference on AI Foundation Models and Software Engineering (Forge)},
98
+ pages={78--81},
99
+ year={2025},
100
+ organization={IEEE}
101
+ }
102
+ ```
README_ref.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SWE-Chatbot-Arena
3
+ emoji: 🎯
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.50.0
8
+ app_file: app.py
9
+ hf_oauth: true
10
+ pinned: false
11
+ short_description: Chatbot arena for software engineering tasks
12
+ ---
13
+
14
+ # SWE-Chatbot-Arena: An Interactive Platform for Evaluating Foundation Models in Software Engineering
15
+
16
+ Welcome to **SWE-Chatbot-Arena**, an open-source platform designed for evaluating software engineering-focused foundation models (FMs), particularly large language models (LLMs). SWE-Chatbot-Arena benchmarks models in iterative, context-rich workflows that are characteristic of software engineering (SE) tasks.
17
+
18
+ ## Key Features
19
+
20
+ - **Multi-Round Conversational Workflows**: Evaluate models through extended, context-dependent interactions that mirror real-world SE processes.
21
+ - **RepoChat Integration**: Automatically inject repository context (issues, commits, PRs) into conversations for more realistic evaluations.
22
+ - **Advanced Evaluation Metrics**: Assess models using a comprehensive suite of metrics including:
23
+ - **Traditional ranking metrics**: Elo ratings and win rates to measure overall model performance
24
+ - **Network-based metrics**: Eigenvector centrality and PageRank to identify influential models in head-to-head comparisons
25
+ - **Community detection metrics**: Newman modularity to reveal clusters of models with similar capabilities
26
+ - **Consistency metrics**: Self-play match analysis to quantify model determinism and reliability
27
+ - **Efficiency metrics**: Conversation efficiency index to measure response quality relative to length
28
+ - **Transparent, Open-Source Leaderboard**: View real-time model rankings across diverse SE workflows with full transparency.
29
+ - **Intelligent Request Filtering**: Employ `gpt-oss-safeguard-20b` as a guardrail to automatically filter out non-software-engineering-related requests, ensuring focused and relevant evaluations.
30
+
31
+ ## Why SWE-Chatbot-Arena?
32
+
33
+ Existing evaluation frameworks (e.g. [LMArena](https://lmarena.ai)) often don't address the complex, iterative nature of SE tasks. SWE-Chatbot-Arena fills critical gaps by:
34
+
35
+ - Supporting context-rich, multi-turn evaluations to capture iterative workflows
36
+ - Integrating repository-level context through RepoChat to simulate real-world development scenarios
37
+ - Providing multidimensional metrics for nuanced model comparisons
38
+ - Focusing on the full breadth of SE tasks beyond just code generation
39
+
40
+ ## How It Works
41
+
42
+ 1. **Submit a Prompt**: Sign in and input your SE-related task (optional: include a repository URL for RepoChat context)
43
+ 2. **Compare Responses**: Two anonymous models provide responses to your query
44
+ 3. **Continue the Conversation**: Test contextual understanding over multiple rounds
45
+ 4. **Vote**: Choose the better model at any point, with ability to re-assess after multiple turns
46
+
47
+ ## Getting Started
48
+
49
+ ### Prerequisites
50
+
51
+ - A [Hugging Face](https://huggingface.co) account
52
+
53
+ ### Usage
54
+
55
+ 1. Navigate to the [SWE-Chatbot-Arena platform](https://huggingface.co/spaces/SE-Arena/SWE-Chatbot-Arena)
56
+ 2. Sign in with your Hugging Face account
57
+ 3. Enter your SE task prompt (optionally include a repository URL for RepoChat)
58
+ 4. Engage in multi-round interactions and vote on model performance
59
+
60
+ ## Contributing
61
+
62
+ We welcome contributions from the community! Here's how you can help:
63
+
64
+ 1. **Submit SE Tasks**: Share your real-world SE problems to enrich our evaluation dataset
65
+ 2. **Report Issues**: Found a bug or have a feature request? Open an issue in this repository
66
+ 3. **Enhance the Codebase**: Fork the repository, make your changes, and submit a pull request
67
+
68
+ ## Privacy Policy
69
+
70
+ Your interactions are anonymized and used solely for improving SWE-Chatbot-Arena and FM benchmarking. By using SWE-Chatbot-Arena, you agree to our Terms of Service.
71
+
72
+ ## Future Plans
73
+
74
+ - **Analysis of Real-World SE Workloads**: Identify common patterns and challenges in user-submitted tasks
75
+ - **Multi-Round Evaluation Metrics**: Develop specialized metrics for assessing model adaptation over successive turns
76
+ - **Expanded FM Coverage**: Include multimodal and domain-specific foundation models
77
+ - **Advanced Context Compression**: Integrate techniques like [LongRope](https://github.com/microsoft/LongRoPE) and [SelfExtend](https://github.com/datamllab/LongLM) to manage long-term memory in multi-round conversations
78
+
79
+ ## Contact
80
+
81
+ For inquiries or feedback, please [open an issue](https://github.com/SE-Arena/SWE-Chatbot-Arena/issues/new) in this repository. We welcome your contributions and suggestions!
82
+
83
+ ## Citation
84
+
85
+ Made with ❤️ for SWE-Chatbot-Arena. If this work is useful to you, please consider citing our vision paper:
86
+
87
+ ```bibtex
88
+ @inproceedings{zhao2025se,
89
+ title={SE Arena: An Interactive Platform for Evaluating Foundation Models in Software Engineering},
90
+ author={Zhao, Zhimin},
91
+ booktitle={2025 IEEE/ACM Second International Conference on AI Foundation Models and Software Engineering (Forge)},
92
+ pages={78--81},
93
+ year={2025},
94
+ organization={IEEE}
95
+ }
96
+ ```
app.js ADDED
@@ -0,0 +1,2026 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // References for model evaluation metrics:
2
+ // - Chatbot Arena: https://colab.research.google.com/drive/1KdwokPjirkTmpO_P1WByFNFiqxWQquwH
3
+ // - Evalica: https://github.com/dustalov/evalica/blob/master/Chatbot-Arena.ipynb
4
+
5
+ import "dotenv/config";
6
+ import { mkdtempSync, rmSync } from "node:fs";
7
+ import { spawn, execFile, execFileSync } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { randomUUID } from "node:crypto";
12
+ import { URL } from "node:url";
13
+
14
+ import express from "express";
15
+ import cookieSession from "cookie-session";
16
+ import OpenAI from "openai";
17
+ import { Octokit } from "@octokit/rest";
18
+ import { Gitlab } from "@gitbeaker/rest";
19
+ import { uploadFile, listFiles, downloadFile } from "@huggingface/hub";
20
+ import whichSync from "which";
21
+
22
+ const execFileAsync = promisify(execFile);
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Environment & constants
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const openaiClient = new OpenAI({
29
+ apiKey: process.env.OPENROUTER_API_KEY,
30
+ baseURL: "https://openrouter.ai/api/v1",
31
+ });
32
+
33
+ const CLI_DATA_REPO = "SWE-Arena/cli_data";
34
+ const LEADERBOARD_REPO = "SWE-Arena/leaderboard_data";
35
+ const VOTE_REPO = "SWE-Arena/vote_data";
36
+ const CONVERSATION_REPO = "SWE-Arena/conversation_data";
37
+ const LEADERBOARD_FILE = "agent_arena";
38
+
39
+ const AGENT_TIMEOUT = 300_000; // 5 minutes per agent (ms)
40
+ const AGENT_TIMEOUT_LABEL = `${AGENT_TIMEOUT / 60_000}min`;
41
+ const LEADERBOARD_UPDATE_TIME_FRAME_DAYS = 365;
42
+
43
+ let leaderboardCache = null; // in-memory cache, populated at startup
44
+
45
+ const SHOW_HINT_STRING = true;
46
+ const HINT_STRING = "Once signed in, your votes will be recorded securely.";
47
+
48
+ const SYSTEM_PREFIX =
49
+ "You MUST operate entirely within the current working directory. " +
50
+ "Do NOT read, write, or execute anything outside this directory.";
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Agent definitions — loaded from HF dataset SWE-Arena/cli_data at startup.
54
+ // Each {id}.json declares CLI binary and two "styles" that drive generic
55
+ // buildAgentCommand() / runFollowup().
56
+ //
57
+ // promptStyle:
58
+ // "flag" → [bin, "-p", <prompt>, ...initArgs]
59
+ // "exec" → [bin, "exec", ...initArgs, <prompt>]
60
+ //
61
+ // followupStyle:
62
+ // "continue" → [bin, "-p", <followup>, ...followupArgs] (e.g. --continue)
63
+ // "resume" → [bin, "exec", ...followupArgs, "resume", "--last", <followup>]
64
+ // "replay" → rebuild full conversation, then use promptStyle
65
+ // ---------------------------------------------------------------------------
66
+
67
+ let agents = [];
68
+ let agentById = {};
69
+ let agentByName = {};
70
+
71
+ async function loadAgentsFromHf() {
72
+ const token = process.env.HF_TOKEN;
73
+ const credentials = token ? { accessToken: token } : undefined;
74
+ const repo = { type: "dataset", name: CLI_DATA_REPO };
75
+ const loaded = [];
76
+
77
+ for await (const file of listFiles({ repo, credentials })) {
78
+ if (!file.path.endsWith(".json")) continue;
79
+ // Skip hidden / nested paths (e.g. .gitattributes)
80
+ if (file.path.includes("/")) continue;
81
+
82
+ const resp = await downloadFile({ repo, path: file.path, credentials });
83
+ if (!resp) continue;
84
+
85
+ const data = JSON.parse(await resp.text());
86
+ const name = file.path.replace(/\.json$/, "");
87
+ loaded.push({ id: data.bin, name, ...data });
88
+ }
89
+
90
+ agents = loaded;
91
+ agentById = Object.fromEntries(agents.map((a) => [a.id, a]));
92
+ agentByName = Object.fromEntries(agents.map((a) => [a.name, a]));
93
+ console.log(`Loaded ${agents.length} agent(s) from ${CLI_DATA_REPO}: ${agents.map((a) => a.name).join(", ")}`);
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // CLI availability
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function availableAgents() {
101
+ return agents.filter((a) => {
102
+ try {
103
+ whichSync.sync(a.bin);
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ });
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // URL parsing helpers
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function parseUrlPath(url) {
116
+ try {
117
+ const parsed = new URL(url);
118
+ const segments = parsed.pathname.split("/").filter(Boolean);
119
+ return { hostname: parsed.hostname || "", segments };
120
+ } catch {
121
+ return { hostname: null, segments: [] };
122
+ }
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // GitHub
127
+ // ---------------------------------------------------------------------------
128
+
129
+ const octokit = process.env.GITHUB_TOKEN
130
+ ? new Octokit({ auth: process.env.GITHUB_TOKEN })
131
+ : new Octokit();
132
+
133
+ function classifyGithubUrl(segments) {
134
+ if (segments.length < 2) return null;
135
+ let repo = segments[1];
136
+ if (repo.endsWith(".git")) repo = repo.slice(0, -4);
137
+ const base = { owner: segments[0], repo };
138
+
139
+ if (segments.length === 2) return { ...base, resource: null };
140
+
141
+ const res = segments[2];
142
+
143
+ if (res === "issues" && segments.length >= 4)
144
+ return { ...base, resource: "issues", id: segments[3] };
145
+ if (res === "pull" && segments.length >= 4)
146
+ return { ...base, resource: "pull", id: segments[3] };
147
+ if (res === "commit" && segments.length >= 4)
148
+ return { ...base, resource: "commit", sha: segments[3] };
149
+ if (res === "blob" && segments.length >= 4)
150
+ return {
151
+ ...base,
152
+ resource: "blob",
153
+ branch: segments[3],
154
+ path: segments.slice(4).join("/"),
155
+ };
156
+ if (res === "tree" && segments.length >= 4)
157
+ return {
158
+ ...base,
159
+ resource: "tree",
160
+ branch: segments[3],
161
+ path: segments.slice(4).join("/"),
162
+ };
163
+ if (res === "discussions" && segments.length >= 4)
164
+ return { ...base, resource: "discussions", id: segments[3] };
165
+ if (res === "releases" && segments.length >= 5 && segments[3] === "tag")
166
+ return { ...base, resource: "releases", tag: segments[4] };
167
+ if (res === "compare" && segments.length >= 4)
168
+ return { ...base, resource: "compare", spec: segments[3] };
169
+ if (res === "actions" && segments.length >= 5 && segments[3] === "runs")
170
+ return { ...base, resource: "actions", run_id: segments[4] };
171
+ if (res === "wiki")
172
+ return {
173
+ ...base,
174
+ resource: "wiki",
175
+ page: segments.length >= 4 ? segments[3] : null,
176
+ };
177
+
178
+ return { ...base, resource: "unknown" };
179
+ }
180
+
181
+ async function fmtGithubRepo(owner, repo) {
182
+ const { data } = await octokit.repos.get({ owner, repo });
183
+ const parts = [`Repository: ${data.full_name}`];
184
+ if (data.description) parts.push(`Description: ${data.description}`);
185
+ try {
186
+ const readme = await octokit.repos.getReadme({ owner, repo });
187
+ const content = Buffer.from(readme.data.content, "base64").toString(
188
+ "utf-8"
189
+ );
190
+ parts.push(`README (first 2000 chars):\n${content.slice(0, 2000)}`);
191
+ } catch {}
192
+ return parts.join("\n\n");
193
+ }
194
+
195
+ async function fmtGithubIssue(owner, repo, issueId) {
196
+ const { data: issue } = await octokit.issues.get({
197
+ owner,
198
+ repo,
199
+ issue_number: Number(issueId),
200
+ });
201
+ const parts = [
202
+ `Issue #${issue.number}: ${issue.title}`,
203
+ `State: ${issue.state}`,
204
+ `Body:\n${issue.body || "(empty)"}`,
205
+ ];
206
+ const { data: comments } = await octokit.issues.listComments({
207
+ owner,
208
+ repo,
209
+ issue_number: Number(issueId),
210
+ per_page: 10,
211
+ });
212
+ if (comments.length) {
213
+ const texts = comments.map(
214
+ (c) => ` Comment by ${c.user.login}:\n ${c.body}`
215
+ );
216
+ parts.push("Comments (first 10):\n" + texts.join("\n---\n"));
217
+ }
218
+ return parts.join("\n\n");
219
+ }
220
+
221
+ async function fmtGithubPr(owner, repo, prId) {
222
+ const { data: pr } = await octokit.pulls.get({
223
+ owner,
224
+ repo,
225
+ pull_number: Number(prId),
226
+ });
227
+ const parts = [
228
+ `Pull Request #${pr.number}: ${pr.title}`,
229
+ `State: ${pr.state} Merged: ${pr.merged}`,
230
+ `Body:\n${pr.body || "(empty)"}`,
231
+ ];
232
+ const { data: files } = await octokit.pulls.listFiles({
233
+ owner,
234
+ repo,
235
+ pull_number: Number(prId),
236
+ });
237
+ const diffParts = files.map((f) => {
238
+ const header = `--- ${f.filename} (${f.status}, +${f.additions}/-${f.deletions})`;
239
+ const patch = f.patch || "(binary or too large)";
240
+ return `${header}\n${patch}`;
241
+ });
242
+ if (diffParts.length) {
243
+ let diffText = diffParts.join("\n\n");
244
+ if (diffText.length > 5000)
245
+ diffText = diffText.slice(0, 5000) + "\n... (diff truncated)";
246
+ parts.push(`Diff:\n${diffText}`);
247
+ }
248
+ return parts.join("\n\n");
249
+ }
250
+
251
+ async function fmtGithubCommit(owner, repo, sha) {
252
+ const { data: commit } = await octokit.repos.getCommit({ owner, repo, ref: sha });
253
+ const parts = [
254
+ `Commit: ${commit.sha}`,
255
+ `Message: ${commit.commit.message}`,
256
+ `Author: ${commit.commit.author.name}`,
257
+ `Stats: +${commit.stats.additions}/-${commit.stats.deletions}`,
258
+ ];
259
+ const fileParts = (commit.files || []).map(
260
+ (f) => ` ${f.filename} (${f.status}): ${f.patch || "(binary)"}`
261
+ );
262
+ if (fileParts.length) {
263
+ let patchText = fileParts.join("\n");
264
+ if (patchText.length > 5000)
265
+ patchText = patchText.slice(0, 5000) + "\n... (patch truncated)";
266
+ parts.push(`Files changed:\n${patchText}`);
267
+ }
268
+ return parts.join("\n\n");
269
+ }
270
+
271
+ async function fmtGithubBlob(owner, repo, branch, path) {
272
+ const { data } = await octokit.repos.getContent({
273
+ owner,
274
+ repo,
275
+ path,
276
+ ref: branch,
277
+ });
278
+ if (Array.isArray(data)) {
279
+ const listing = data.map((c) => ` ${c.path} (${c.type})`).join("\n");
280
+ return `Directory listing at ${branch}/${path}:\n${listing}`;
281
+ }
282
+ let content = Buffer.from(data.content, "base64").toString("utf-8");
283
+ if (content.length > 5000)
284
+ content = content.slice(0, 5000) + "\n... (content truncated)";
285
+ return `File: ${path} (branch: ${branch})\n\n${content}`;
286
+ }
287
+
288
+ async function fmtGithubTree(owner, repo, branch, path) {
289
+ const { data } = await octokit.repos.getContent({
290
+ owner,
291
+ repo,
292
+ path: path || "",
293
+ ref: branch,
294
+ });
295
+ const items = Array.isArray(data) ? data : [data];
296
+ const listing = items
297
+ .map((c) => ` ${c.path} (${c.type}, ${c.size} bytes)`)
298
+ .join("\n");
299
+ return `Tree at ${branch}/${path || "(root)"}:\n${listing}`;
300
+ }
301
+
302
+ async function fmtGithubRelease(owner, repo, tag) {
303
+ const { data: release } = await octokit.repos.getReleaseByTag({
304
+ owner,
305
+ repo,
306
+ tag,
307
+ });
308
+ return [
309
+ `Release: ${release.name || release.tag_name}`,
310
+ `Tag: ${release.tag_name}`,
311
+ `Body:\n${release.body || "(empty)"}`,
312
+ ].join("\n\n");
313
+ }
314
+
315
+ async function fmtGithubCompare(owner, repo, spec) {
316
+ let base, head;
317
+ if (spec.includes("...")) [base, head] = spec.split("...", 2);
318
+ else if (spec.includes("..")) [base, head] = spec.split("..", 2);
319
+ else return null;
320
+ const { data } = await octokit.repos.compareCommits({
321
+ owner,
322
+ repo,
323
+ base,
324
+ head,
325
+ });
326
+ const parts = [
327
+ `Comparison: ${base}...${head}`,
328
+ `Status: ${data.status}`,
329
+ `Ahead by: ${data.ahead_by}, Behind by: ${data.behind_by}`,
330
+ `Total commits: ${data.total_commits}`,
331
+ ];
332
+ const commitSummaries = (data.commits || [])
333
+ .slice(0, 20)
334
+ .map((c) => ` ${c.sha.slice(0, 8)}: ${c.commit.message.split("\n")[0]}`);
335
+ if (commitSummaries.length)
336
+ parts.push("Commits:\n" + commitSummaries.join("\n"));
337
+ const fileSummaries = (data.files || [])
338
+ .slice(0, 30)
339
+ .map(
340
+ (f) =>
341
+ ` ${f.filename} (${f.status}, +${f.additions}/-${f.deletions})`
342
+ );
343
+ if (fileSummaries.length)
344
+ parts.push("Files changed:\n" + fileSummaries.join("\n"));
345
+ return parts.join("\n\n");
346
+ }
347
+
348
+ async function fmtGithubActions(owner, repo, runId) {
349
+ const { data: run } = await octokit.actions.getWorkflowRun({
350
+ owner,
351
+ repo,
352
+ run_id: Number(runId),
353
+ });
354
+ const parts = [
355
+ `Workflow Run: ${run.name} #${run.run_number}`,
356
+ `Status: ${run.status} Conclusion: ${run.conclusion}`,
357
+ `SHA: ${run.head_sha}`,
358
+ ];
359
+ try {
360
+ const { data: jobsData } = await octokit.actions.listJobsForWorkflowRun({
361
+ owner,
362
+ repo,
363
+ run_id: Number(runId),
364
+ });
365
+ for (const job of jobsData.jobs) {
366
+ if (job.conclusion === "failure") {
367
+ parts.push(`Failed job: ${job.name}`);
368
+ for (const step of job.steps || []) {
369
+ if (step.conclusion === "failure")
370
+ parts.push(` Failed step: ${step.name}`);
371
+ }
372
+ }
373
+ }
374
+ } catch {}
375
+ return parts.join("\n\n");
376
+ }
377
+
378
+ function fmtGithubWiki(owner, repo, page) {
379
+ if (page)
380
+ return `Wiki page: ${page} (from ${owner}/${repo}/wiki)\nNote: Wiki content cannot be fetched via API.`;
381
+ return `Wiki: ${owner}/${repo}/wiki\nNote: Wiki content cannot be fetched via API.`;
382
+ }
383
+
384
+ async function fetchGithubContent(url) {
385
+ if (!process.env.GITHUB_TOKEN) {
386
+ console.log("GITHUB_TOKEN not set.");
387
+ return null;
388
+ }
389
+ const { hostname, segments } = parseUrlPath(url);
390
+ if (!hostname || !hostname.includes("github.com")) return null;
391
+ const info = classifyGithubUrl(segments);
392
+ if (!info) return null;
393
+
394
+ try {
395
+ const { owner, repo, resource } = info;
396
+ if (resource === null) return await fmtGithubRepo(owner, repo);
397
+ if (resource === "issues") return await fmtGithubIssue(owner, repo, info.id);
398
+ if (resource === "pull") return await fmtGithubPr(owner, repo, info.id);
399
+ if (resource === "commit") return await fmtGithubCommit(owner, repo, info.sha);
400
+ if (resource === "blob")
401
+ return await fmtGithubBlob(owner, repo, info.branch, info.path);
402
+ if (resource === "tree")
403
+ return await fmtGithubTree(owner, repo, info.branch, info.path);
404
+ if (resource === "releases")
405
+ return await fmtGithubRelease(owner, repo, info.tag);
406
+ if (resource === "compare")
407
+ return await fmtGithubCompare(owner, repo, info.spec);
408
+ if (resource === "actions")
409
+ return await fmtGithubActions(owner, repo, info.run_id);
410
+ if (resource === "wiki") return fmtGithubWiki(owner, repo, info.page);
411
+ return null;
412
+ } catch (err) {
413
+ console.error(`GitHub API error: ${err.message}`);
414
+ return null;
415
+ }
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // GitLab
420
+ // ---------------------------------------------------------------------------
421
+
422
+ const gitlab = process.env.GITLAB_TOKEN
423
+ ? new Gitlab({ token: process.env.GITLAB_TOKEN })
424
+ : null;
425
+
426
+ function classifyGitlabUrl(segments) {
427
+ let dashIdx = segments.indexOf("-");
428
+ if (dashIdx === -1) {
429
+ if (segments.length >= 2)
430
+ return { projectPath: segments.join("/"), resource: null };
431
+ return null;
432
+ }
433
+
434
+ const projectPath = segments.slice(0, dashIdx).join("/");
435
+ const resSegments = segments.slice(dashIdx + 1);
436
+
437
+ if (!projectPath || !resSegments.length)
438
+ return { projectPath, resource: null };
439
+
440
+ const res = resSegments[0];
441
+
442
+ if (res === "issues" && resSegments.length >= 2)
443
+ return { projectPath, resource: "issues", id: resSegments[1] };
444
+ if (res === "merge_requests" && resSegments.length >= 2)
445
+ return { projectPath, resource: "merge_requests", id: resSegments[1] };
446
+ if ((res === "commit" || res === "commits") && resSegments.length >= 2)
447
+ return { projectPath, resource: "commit", sha: resSegments[1] };
448
+ if (res === "blob" && resSegments.length >= 2)
449
+ return {
450
+ projectPath,
451
+ resource: "blob",
452
+ branch: resSegments[1],
453
+ path: resSegments.slice(2).join("/"),
454
+ };
455
+ if (res === "tree" && resSegments.length >= 2)
456
+ return {
457
+ projectPath,
458
+ resource: "tree",
459
+ branch: resSegments[1],
460
+ path: resSegments.slice(2).join("/"),
461
+ };
462
+ if (res === "releases" && resSegments.length >= 2)
463
+ return { projectPath, resource: "releases", tag: resSegments[1] };
464
+ if (res === "compare" && resSegments.length >= 2)
465
+ return { projectPath, resource: "compare", spec: resSegments[1] };
466
+ if (res === "pipelines" && resSegments.length >= 2)
467
+ return { projectPath, resource: "pipelines", id: resSegments[1] };
468
+ if (res === "wikis")
469
+ return {
470
+ projectPath,
471
+ resource: "wikis",
472
+ page: resSegments.length >= 2 ? resSegments[1] : null,
473
+ };
474
+
475
+ return { projectPath, resource: "unknown" };
476
+ }
477
+
478
+ async function fetchGitlabContent(url) {
479
+ if (!gitlab) {
480
+ console.log("GITLAB_TOKEN not set.");
481
+ return null;
482
+ }
483
+ const { hostname, segments } = parseUrlPath(url);
484
+ if (!hostname || !hostname.includes("gitlab.com")) return null;
485
+ const info = classifyGitlabUrl(segments);
486
+ if (!info) return null;
487
+
488
+ try {
489
+ const project = await gitlab.Projects.show(info.projectPath);
490
+ const { resource } = info;
491
+
492
+ if (resource === null) {
493
+ const parts = [`Repository: ${project.path_with_namespace}`];
494
+ if (project.description)
495
+ parts.push(`Description: ${project.description}`);
496
+ try {
497
+ const readme = await gitlab.RepositoryFiles.show(
498
+ project.id,
499
+ "README.md",
500
+ project.default_branch
501
+ );
502
+ const content = Buffer.from(readme.content, "base64").toString("utf-8");
503
+ parts.push(`README (first 2000 chars):\n${content.slice(0, 2000)}`);
504
+ } catch {}
505
+ return parts.join("\n\n");
506
+ }
507
+ if (resource === "issues") {
508
+ const issue = await gitlab.Issues.show(project.id, Number(info.id));
509
+ const parts = [
510
+ `Issue #${issue.iid}: ${issue.title}`,
511
+ `State: ${issue.state}`,
512
+ `Body:\n${issue.description || "(empty)"}`,
513
+ ];
514
+ const notes = await gitlab.IssueNotes.all(project.id, Number(info.id), {
515
+ perPage: 10,
516
+ });
517
+ const noteTexts = notes.map(
518
+ (n) => ` Comment by ${n.author.username}: ${n.body}`
519
+ );
520
+ if (noteTexts.length)
521
+ parts.push("Comments (first 10):\n" + noteTexts.join("\n---\n"));
522
+ return parts.join("\n\n");
523
+ }
524
+ if (resource === "merge_requests") {
525
+ const mr = await gitlab.MergeRequests.show(project.id, Number(info.id));
526
+ const parts = [
527
+ `Merge Request !${mr.iid}: ${mr.title}`,
528
+ `State: ${mr.state}`,
529
+ `Body:\n${mr.description || "(empty)"}`,
530
+ ];
531
+ try {
532
+ const changes = await gitlab.MergeRequests.allDiffs(
533
+ project.id,
534
+ Number(info.id)
535
+ );
536
+ const diffParts = changes
537
+ .slice(0, 30)
538
+ .map(
539
+ (c) =>
540
+ ` ${c.new_path || "?"}: ${(c.diff || "").slice(0, 500)}`
541
+ );
542
+ if (diffParts.length) {
543
+ let diffText = diffParts.join("\n");
544
+ if (diffText.length > 5000)
545
+ diffText = diffText.slice(0, 5000) + "\n... (diff truncated)";
546
+ parts.push(`Changes:\n${diffText}`);
547
+ }
548
+ } catch {}
549
+ return parts.join("\n\n");
550
+ }
551
+ if (resource === "commit") {
552
+ const commit = await gitlab.Commits.show(project.id, info.sha);
553
+ const parts = [
554
+ `Commit: ${commit.id}`,
555
+ `Title: ${commit.title}`,
556
+ `Message: ${commit.message}`,
557
+ `Author: ${commit.author_name}`,
558
+ ];
559
+ try {
560
+ const diffs = await gitlab.Commits.showDiff(project.id, info.sha);
561
+ const diffParts = diffs
562
+ .slice(0, 30)
563
+ .map(
564
+ (d) =>
565
+ ` ${d.new_path || "?"}: ${(d.diff || "").slice(0, 500)}`
566
+ );
567
+ if (diffParts.length) {
568
+ let diffText = diffParts.join("\n");
569
+ if (diffText.length > 5000)
570
+ diffText = diffText.slice(0, 5000) + "\n... (diff truncated)";
571
+ parts.push(`Diff:\n${diffText}`);
572
+ }
573
+ } catch {}
574
+ return parts.join("\n\n");
575
+ }
576
+ if (resource === "blob") {
577
+ const file = await gitlab.RepositoryFiles.show(
578
+ project.id,
579
+ info.path,
580
+ info.branch
581
+ );
582
+ let content = Buffer.from(file.content, "base64").toString("utf-8");
583
+ if (content.length > 5000)
584
+ content = content.slice(0, 5000) + "\n... (content truncated)";
585
+ return `File: ${info.path} (branch: ${info.branch})\n\n${content}`;
586
+ }
587
+ if (resource === "tree") {
588
+ const items = await gitlab.Repositories.allRepositoryTrees(project.id, {
589
+ path: info.path || "",
590
+ ref: info.branch,
591
+ perPage: 100,
592
+ });
593
+ const listing = items
594
+ .map((item) => ` ${item.path} (${item.type})`)
595
+ .join("\n");
596
+ return `Tree at ${info.branch}/${info.path || "(root)"}:\n${listing}`;
597
+ }
598
+ if (resource === "releases") {
599
+ const release = await gitlab.ProjectReleases.show(
600
+ project.id,
601
+ info.tag
602
+ );
603
+ return [
604
+ `Release: ${release.name || release.tag_name}`,
605
+ `Tag: ${release.tag_name}`,
606
+ `Description:\n${release.description || "(empty)"}`,
607
+ ].join("\n\n");
608
+ }
609
+ if (resource === "compare") {
610
+ let base, head;
611
+ if (info.spec.includes("...")) [base, head] = info.spec.split("...", 2);
612
+ else if (info.spec.includes(".."))
613
+ [base, head] = info.spec.split("..", 2);
614
+ else return null;
615
+ const result = await gitlab.Repositories.compare(project.id, base, head);
616
+ const parts = [`Comparison: ${base}...${head}`];
617
+ const commits = (result.commits || [])
618
+ .slice(0, 20)
619
+ .map((c) => ` ${c.short_id || "?"}: ${c.title || ""}`);
620
+ if (commits.length) parts.push("Commits:\n" + commits.join("\n"));
621
+ const diffs = (result.diffs || [])
622
+ .slice(0, 30)
623
+ .map(
624
+ (d) =>
625
+ ` ${d.new_path || "?"}: ${(d.diff || "").slice(0, 500)}`
626
+ );
627
+ if (diffs.length) {
628
+ let diffText = diffs.join("\n");
629
+ if (diffText.length > 5000)
630
+ diffText = diffText.slice(0, 5000) + "\n... (diff truncated)";
631
+ parts.push(`Diffs:\n${diffText}`);
632
+ }
633
+ return parts.join("\n\n");
634
+ }
635
+ if (resource === "pipelines") {
636
+ const pipeline = await gitlab.Pipelines.show(
637
+ project.id,
638
+ Number(info.id)
639
+ );
640
+ const parts = [
641
+ `Pipeline #${pipeline.id}`,
642
+ `Status: ${pipeline.status}`,
643
+ `Ref: ${pipeline.ref}`,
644
+ `SHA: ${pipeline.sha}`,
645
+ ];
646
+ try {
647
+ const jobs = await gitlab.PipelineJobs.all(project.id, pipeline.id, {
648
+ perPage: 20,
649
+ });
650
+ const failed = jobs.filter((j) => j.status === "failed");
651
+ if (failed.length) {
652
+ parts.push("Failed jobs:");
653
+ for (const j of failed)
654
+ parts.push(` ${j.name}: ${j.status} (stage: ${j.stage})`);
655
+ }
656
+ } catch {}
657
+ return parts.join("\n\n");
658
+ }
659
+ if (resource === "wikis") {
660
+ if (info.page) {
661
+ try {
662
+ const page = await gitlab.Wikis.show(project.id, info.page);
663
+ return `Wiki page: ${page.title}\n\n${page.content}`;
664
+ } catch {
665
+ return `Wiki page: ${info.page}\nNote: Could not fetch wiki page content.`;
666
+ }
667
+ }
668
+ try {
669
+ const pages = await gitlab.Wikis.all(project.id, { perPage: 20 });
670
+ const listing = pages.map((p) => ` ${p.slug}: ${p.title}`).join("\n");
671
+ return `Wiki pages:\n${listing}`;
672
+ } catch {
673
+ return "Wiki: Could not fetch wiki pages.";
674
+ }
675
+ }
676
+ return null;
677
+ } catch (err) {
678
+ console.error(`GitLab API error: ${err.message}`);
679
+ return null;
680
+ }
681
+ }
682
+
683
+ // ---------------------------------------------------------------------------
684
+ // HuggingFace
685
+ // ---------------------------------------------------------------------------
686
+
687
+ function classifyHuggingfaceUrl(segments) {
688
+ if (!segments.length) return null;
689
+ const segs = [...segments];
690
+ let repoType = null;
691
+ if (segs[0] === "datasets" || segs[0] === "spaces") {
692
+ repoType = segs[0] === "datasets" ? "dataset" : "space";
693
+ segs.splice(0, 1);
694
+ }
695
+ if (segs.length < 2) return null;
696
+ const repoId = `${segs[0]}/${segs[1]}`;
697
+ const base = { repoId, repoType };
698
+
699
+ if (segs.length === 2) return { ...base, resource: null };
700
+ const res = segs[2];
701
+
702
+ if (res === "blob" && segs.length >= 4)
703
+ return {
704
+ ...base,
705
+ resource: "blob",
706
+ revision: segs[3],
707
+ path: segs.slice(4).join("/"),
708
+ };
709
+ if (res === "resolve" && segs.length >= 4)
710
+ return {
711
+ ...base,
712
+ resource: "resolve",
713
+ revision: segs[3],
714
+ path: segs.slice(4).join("/"),
715
+ };
716
+ if (res === "tree" && segs.length >= 4)
717
+ return {
718
+ ...base,
719
+ resource: "tree",
720
+ revision: segs[3],
721
+ path: segs.slice(4).join("/"),
722
+ };
723
+ if (res === "commit" && segs.length >= 4)
724
+ return { ...base, resource: "commit", sha: segs[3] };
725
+ if (res === "discussions" && segs.length >= 4)
726
+ return { ...base, resource: "discussions", num: segs[3] };
727
+
728
+ return { ...base, resource: "unknown" };
729
+ }
730
+
731
+ async function fetchHuggingfaceContent(url) {
732
+ const token = process.env.HF_TOKEN;
733
+ if (!token) {
734
+ console.log("HF_TOKEN not set.");
735
+ return null;
736
+ }
737
+ const { hostname, segments } = parseUrlPath(url);
738
+ if (!hostname || !hostname.includes("huggingface.co")) return null;
739
+ const info = classifyHuggingfaceUrl(segments);
740
+ if (!info) return null;
741
+
742
+ try {
743
+ const credentials = { accessToken: token };
744
+ const repo = { type: info.repoType || "model", name: info.repoId };
745
+
746
+ if (info.resource === null) {
747
+ const parts = [`Repository: ${info.repoId}`];
748
+ try {
749
+ const resp = await downloadFile({ repo, path: "README.md", credentials });
750
+ if (resp) {
751
+ const content = await resp.text();
752
+ parts.push(
753
+ `README (first 2000 chars):\n${content.slice(0, 2000)}`
754
+ );
755
+ }
756
+ } catch {}
757
+ return parts.join("\n\n");
758
+ }
759
+ if (info.resource === "blob" || info.resource === "resolve") {
760
+ try {
761
+ const resp = await downloadFile({
762
+ repo,
763
+ path: info.path,
764
+ revision: info.revision,
765
+ credentials,
766
+ });
767
+ if (resp) {
768
+ let content = await resp.text();
769
+ if (content.length > 5000)
770
+ content = content.slice(0, 5000) + "\n... (content truncated)";
771
+ return `File: ${info.path} (revision: ${info.revision})\n\n${content}`;
772
+ }
773
+ } catch {
774
+ return `File: ${info.path} (revision: ${info.revision})\n(binary or unreadable file)`;
775
+ }
776
+ }
777
+ if (info.resource === "tree") {
778
+ const items = [];
779
+ for await (const entry of listFiles({
780
+ repo,
781
+ path: info.path || undefined,
782
+ revision: info.revision,
783
+ credentials,
784
+ })) {
785
+ items.push(` ${entry.path} (${entry.type})`);
786
+ if (items.length >= 100) {
787
+ items.push(" ... (truncated)");
788
+ break;
789
+ }
790
+ }
791
+ return `Tree at ${info.revision}/${info.path || "(root)"}:\n${items.join("\n")}`;
792
+ }
793
+ return null;
794
+ } catch (err) {
795
+ console.error(`Hugging Face API error: ${err.message}`);
796
+ return null;
797
+ }
798
+ }
799
+
800
+ // ---------------------------------------------------------------------------
801
+ // URL router
802
+ // ---------------------------------------------------------------------------
803
+
804
+ async function fetchUrlContent(url) {
805
+ if (!url || !url.trim()) return "";
806
+ url = url.trim();
807
+ try {
808
+ const { hostname } = parseUrlPath(url);
809
+ if (hostname && hostname.includes("github.com"))
810
+ return await fetchGithubContent(url);
811
+ if (hostname && hostname.includes("gitlab.com"))
812
+ return await fetchGitlabContent(url);
813
+ if (hostname && hostname.includes("huggingface.co"))
814
+ return await fetchHuggingfaceContent(url);
815
+ } catch (err) {
816
+ console.error(`Error fetching URL content: ${err.message}`);
817
+ }
818
+ return "";
819
+ }
820
+
821
+ // ---------------------------------------------------------------------------
822
+ // Agent execution via CLI
823
+ // ---------------------------------------------------------------------------
824
+
825
+ function buildAgentCommand(agent, prompt) {
826
+ switch (agent.promptStyle) {
827
+ case "flag":
828
+ return [agent.bin, ["-p", prompt, ...agent.initArgs]];
829
+ case "exec":
830
+ return [agent.bin, ["exec", ...agent.initArgs, prompt]];
831
+ default:
832
+ throw new Error(`Unknown promptStyle "${agent.promptStyle}" for ${agent.id}`);
833
+ }
834
+ }
835
+
836
+ // Extract human-readable text from agent output (some CLIs return JSON/JSONL)
837
+ function parseAgentOutput(raw) {
838
+ if (!raw || typeof raw !== "string") return raw || "";
839
+ const trimmed = raw.trim();
840
+
841
+ // Try JSONL first (one JSON object per line — e.g. Grok CLI chat format)
842
+ const lines = trimmed.split("\n").filter((l) => l.trim());
843
+ const hasJsonLines = lines.length > 0 && lines.every((l) => {
844
+ const t = l.trim();
845
+ return t.startsWith("{") || t.startsWith("[");
846
+ });
847
+
848
+ if (hasJsonLines && lines.length > 1) {
849
+ // Parse each line, extract assistant messages
850
+ const assistantMsgs = [];
851
+ for (const line of lines) {
852
+ try {
853
+ const obj = JSON.parse(line.trim());
854
+ if (obj.role === "assistant" && obj.content) {
855
+ assistantMsgs.push(obj.content);
856
+ }
857
+ } catch { /* skip unparseable lines */ }
858
+ }
859
+ if (assistantMsgs.length) return assistantMsgs.join("\n\n");
860
+ // No assistant messages — try extracting any content field
861
+ const allContent = [];
862
+ for (const line of lines) {
863
+ try {
864
+ const obj = JSON.parse(line.trim());
865
+ if (obj.content) allContent.push(obj.content);
866
+ } catch { /* skip */ }
867
+ }
868
+ if (allContent.length) return allContent.join("\n\n");
869
+ }
870
+
871
+ // Try single JSON object
872
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
873
+ try {
874
+ const obj = JSON.parse(trimmed);
875
+ const text =
876
+ obj.result || obj.response || obj.content || obj.message ||
877
+ obj.text || obj.output || obj.answer ||
878
+ obj.choices?.[0]?.message?.content ||
879
+ obj.choices?.[0]?.text;
880
+ if (typeof text === "string") return text;
881
+ if (Array.isArray(obj)) {
882
+ const msgs = obj.map((m) => m.content || m.text || "").filter(Boolean);
883
+ if (msgs.length) return msgs.join("\n\n");
884
+ }
885
+ } catch { /* not valid JSON, fall through */ }
886
+ }
887
+
888
+ return raw;
889
+ }
890
+
891
+ // Streaming agent runner — returns a live state object + promise
892
+ function spawnAgent(agent, prompt, agentDir) {
893
+ const [bin, args] = buildAgentCommand(agent, prompt);
894
+ const state = { stdout: "", stderr: "", done: false, ok: false };
895
+
896
+ const proc = spawn(bin, args, { cwd: agentDir, env: { ...process.env } });
897
+ proc.stdout.setEncoding("utf-8");
898
+ proc.stderr.setEncoding("utf-8");
899
+ proc.stdout.on("data", (chunk) => { state.stdout += chunk; });
900
+ proc.stderr.on("data", (chunk) => { state.stderr += chunk; });
901
+
902
+ const timer = setTimeout(() => {
903
+ proc.kill();
904
+ state.stderr += `\n[Timeout after ${AGENT_TIMEOUT_LABEL}]`;
905
+ }, AGENT_TIMEOUT);
906
+
907
+ state.promise = new Promise((resolve) => {
908
+ proc.on("close", (code) => {
909
+ clearTimeout(timer);
910
+ state.done = true;
911
+ state.ok = code === 0;
912
+ resolve(state);
913
+ });
914
+ proc.on("error", (err) => {
915
+ clearTimeout(timer);
916
+ state.done = true;
917
+ state.ok = false;
918
+ state.stderr += err.message;
919
+ resolve(state);
920
+ });
921
+ });
922
+
923
+ return state;
924
+ }
925
+
926
+ // Blocking agent runner — used for followups (shorter, less need for streaming)
927
+ async function runAgent(agent, prompt, agentDir) {
928
+ const [bin, args] = buildAgentCommand(agent, prompt);
929
+ try {
930
+ const { stdout, stderr } = await execFileAsync(bin, args, {
931
+ cwd: agentDir,
932
+ timeout: AGENT_TIMEOUT,
933
+ encoding: "utf-8",
934
+ maxBuffer: 10 * 1024 * 1024,
935
+ });
936
+ return { ok: true, stdout, stderr };
937
+ } catch (err) {
938
+ const partialOut = err.stdout || "";
939
+ const partialErr = err.stderr || "";
940
+ const prefix = err.killed ? `[Timeout after ${AGENT_TIMEOUT_LABEL}]\n` : "";
941
+ return {
942
+ ok: false,
943
+ stdout: partialOut,
944
+ stderr: prefix + (partialErr || err.message),
945
+ };
946
+ }
947
+ }
948
+
949
+ function rebuildPrompt(rounds, followup) {
950
+ const parts = [];
951
+ for (const r of rounds) {
952
+ parts.push(`User: ${r.prompt}`);
953
+ parts.push(`Assistant: ${r.stdout}`);
954
+ }
955
+ parts.push(`User: ${followup}`);
956
+ return parts.join("\n\n");
957
+ }
958
+
959
+ async function runFollowup(agent, followup, agentDir, rounds) {
960
+ let bin = agent.bin, args;
961
+
962
+ switch (agent.followupStyle) {
963
+ case "continue":
964
+ args = ["-p", followup, ...agent.followupArgs];
965
+ break;
966
+ case "resume":
967
+ args = ["exec", ...agent.followupArgs, "resume", "--last", followup];
968
+ break;
969
+ case "replay": {
970
+ const full = rebuildPrompt(rounds, followup);
971
+ args = ["-p", full, ...agent.followupArgs];
972
+ break;
973
+ }
974
+ default:
975
+ throw new Error(`Unknown followupStyle "${agent.followupStyle}" for ${agent.id}`);
976
+ }
977
+
978
+ try {
979
+ const { stdout, stderr } = await execFileAsync(bin, args, {
980
+ cwd: agentDir,
981
+ timeout: AGENT_TIMEOUT,
982
+ encoding: "utf-8",
983
+ maxBuffer: 10 * 1024 * 1024,
984
+ });
985
+ return { ok: true, stdout, stderr };
986
+ } catch (err) {
987
+ const partialOut = err.stdout || "";
988
+ const partialErr = err.stderr || "";
989
+ const prefix = err.killed ? `[Timeout after ${AGENT_TIMEOUT_LABEL}]\n` : "";
990
+ return {
991
+ ok: false,
992
+ stdout: partialOut,
993
+ stderr: prefix + (partialErr || err.message),
994
+ };
995
+ }
996
+ }
997
+
998
+ // ---------------------------------------------------------------------------
999
+ // Prompt construction
1000
+ // ---------------------------------------------------------------------------
1001
+
1002
+ function buildPrompt(userPrompt, repoContext = "") {
1003
+ const parts = [SYSTEM_PREFIX];
1004
+ if (repoContext) parts.push(`Repository context:\n${repoContext}`);
1005
+ parts.push(userPrompt);
1006
+ return parts.join("\n\n");
1007
+ }
1008
+
1009
+ function stripContext(prompt) {
1010
+ const marker = "\n\n";
1011
+ // Find the last section which is the user query
1012
+ // The prompt format is: SYSTEM_PREFIX + \n\n + [repo context + \n\n] + user query
1013
+ // We strip SYSTEM_PREFIX and optional repo context
1014
+ let rest = prompt;
1015
+ if (rest.startsWith(SYSTEM_PREFIX)) {
1016
+ rest = rest.slice(SYSTEM_PREFIX.length);
1017
+ if (rest.startsWith("\n\n")) rest = rest.slice(2);
1018
+ }
1019
+ if (rest.startsWith("Repository context:\n")) {
1020
+ const idx = rest.indexOf("\n\n", "Repository context:\n".length);
1021
+ if (idx >= 0) rest = rest.slice(idx + 2);
1022
+ }
1023
+ return rest;
1024
+ }
1025
+
1026
+ // ---------------------------------------------------------------------------
1027
+ // Git operations (clone, checkout, diff)
1028
+ // ---------------------------------------------------------------------------
1029
+
1030
+ function cloneRepo(url, agentDir) {
1031
+ const { hostname, segments } = parseUrlPath(url);
1032
+ if (!hostname) return false;
1033
+
1034
+ let parsedInfo = null;
1035
+ let cloneUrl = null;
1036
+
1037
+ if (hostname.includes("github.com")) {
1038
+ parsedInfo = classifyGithubUrl(segments);
1039
+ if (!parsedInfo) return false;
1040
+ cloneUrl = `https://github.com/${parsedInfo.owner}/${parsedInfo.repo}.git`;
1041
+ } else if (hostname.includes("gitlab.com")) {
1042
+ parsedInfo = classifyGitlabUrl(segments);
1043
+ if (!parsedInfo) return false;
1044
+ cloneUrl = `https://gitlab.com/${parsedInfo.projectPath}.git`;
1045
+ } else if (hostname.includes("huggingface.co")) {
1046
+ parsedInfo = classifyHuggingfaceUrl(segments);
1047
+ if (!parsedInfo) return false;
1048
+ const prefix = parsedInfo.repoType ? `${parsedInfo.repoType}s/` : "";
1049
+ cloneUrl = `https://huggingface.co/${prefix}${parsedInfo.repoId}`;
1050
+ } else {
1051
+ return false;
1052
+ }
1053
+
1054
+ try {
1055
+ execFileSync("git", ["clone", "--depth=1", cloneUrl, "."], {
1056
+ cwd: agentDir,
1057
+ timeout: 120_000,
1058
+ stdio: "pipe",
1059
+ });
1060
+ checkoutRef(parsedInfo, agentDir);
1061
+ return true;
1062
+ } catch {
1063
+ return false;
1064
+ }
1065
+ }
1066
+
1067
+ function checkoutRef(parsedInfo, agentDir) {
1068
+ const resource = parsedInfo.resource;
1069
+ const run = (args) => {
1070
+ try {
1071
+ execFileSync("git", args, { cwd: agentDir, timeout: 60_000, stdio: "pipe" });
1072
+ } catch {}
1073
+ };
1074
+ try {
1075
+ if (resource === "pull" && parsedInfo.id) {
1076
+ run(["fetch", "origin", `pull/${parsedInfo.id}/head:pr`]);
1077
+ run(["checkout", "pr"]);
1078
+ } else if (resource === "merge_requests" && parsedInfo.id) {
1079
+ run(["fetch", "origin", `merge-requests/${parsedInfo.id}/head:mr`]);
1080
+ run(["checkout", "mr"]);
1081
+ } else if (resource === "commit" && parsedInfo.sha) {
1082
+ run(["fetch", "--depth=1", "origin", parsedInfo.sha]);
1083
+ run(["checkout", parsedInfo.sha]);
1084
+ } else if (
1085
+ (resource === "blob" || resource === "tree") &&
1086
+ parsedInfo.branch
1087
+ ) {
1088
+ run(["checkout", parsedInfo.branch]);
1089
+ } else if (
1090
+ (resource === "blob" || resource === "resolve" || resource === "tree") &&
1091
+ parsedInfo.revision
1092
+ ) {
1093
+ run(["checkout", parsedInfo.revision]);
1094
+ }
1095
+ } catch {} // best effort
1096
+ }
1097
+
1098
+ function captureDiff(agentDir) {
1099
+ try {
1100
+ execFileSync("git", ["add", "-A"], {
1101
+ cwd: agentDir,
1102
+ stdio: "pipe",
1103
+ });
1104
+ // Exclude CLI-specific config/state files so only the agent's
1105
+ // actual work appears in the diff.
1106
+ const result = execFileSync(
1107
+ "git",
1108
+ [
1109
+ "diff", "--cached", "--",
1110
+ ".",
1111
+ // Claude Code
1112
+ ":(exclude).claude",
1113
+ ":(exclude)CLAUDE.md",
1114
+ // Gemini CLI
1115
+ ":(exclude).gemini",
1116
+ // OpenAI Codex
1117
+ ":(exclude).codex",
1118
+ ":(exclude)codex.json",
1119
+ // Grok CLI
1120
+ ":(exclude).grok",
1121
+ // Common IDE / tool artifacts
1122
+ ":(exclude).vscode",
1123
+ ":(exclude)settings.json",
1124
+ ],
1125
+ {
1126
+ cwd: agentDir,
1127
+ encoding: "utf-8",
1128
+ maxBuffer: 10 * 1024 * 1024,
1129
+ }
1130
+ );
1131
+ return result.slice(0, 100_000);
1132
+ } catch (err) {
1133
+ console.error(`captureDiff failed: ${err.message}`);
1134
+ return "";
1135
+ }
1136
+ }
1137
+
1138
+ // ---------------------------------------------------------------------------
1139
+ // HF data I/O
1140
+ // ---------------------------------------------------------------------------
1141
+
1142
+ async function saveContentToHf(data, repoName, fileName, token) {
1143
+ const json = JSON.stringify(data, null, 2);
1144
+ const content = new Blob([json]);
1145
+ if (!token) token = process.env.HF_TOKEN;
1146
+ if (!token) throw new Error("No HF token available for upload.");
1147
+
1148
+ await uploadFile({
1149
+ repo: { type: "dataset", name: repoName },
1150
+ file: { content, path: `${fileName}.json` },
1151
+ credentials: { accessToken: token },
1152
+ });
1153
+ }
1154
+
1155
+ function isFileWithinTimeFrame(filePath, days) {
1156
+ try {
1157
+ const timestampStr = filePath.split("/").pop().replace(".json", "");
1158
+ // Format: YYYYMMDD_HHMMSS
1159
+ const m = timestampStr.match(
1160
+ /(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/
1161
+ );
1162
+ if (!m) return false;
1163
+ const fileDate = new Date(
1164
+ `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`
1165
+ );
1166
+ const diffDays = (Date.now() - fileDate.getTime()) / (1000 * 60 * 60 * 24);
1167
+ return diffDays <= days;
1168
+ } catch {
1169
+ return false;
1170
+ }
1171
+ }
1172
+
1173
+ async function loadContentFromHf(repoName, filePrefix) {
1174
+ const data = [];
1175
+ const token = process.env.HF_TOKEN;
1176
+ const credentials = token ? { accessToken: token } : undefined;
1177
+ const repo = { type: "dataset", name: repoName };
1178
+
1179
+ try {
1180
+ for await (const file of listFiles({ repo, credentials })) {
1181
+ if (!file.path.startsWith(`${filePrefix}/`)) continue;
1182
+ if (!file.path.endsWith(".json")) continue;
1183
+ if (
1184
+ !isFileWithinTimeFrame(file.path, LEADERBOARD_UPDATE_TIME_FRAME_DAYS)
1185
+ )
1186
+ continue;
1187
+
1188
+ const resp = await downloadFile({ repo, path: file.path, credentials });
1189
+ if (resp) {
1190
+ const entry = JSON.parse(await resp.text());
1191
+ entry.timestamp = file.path.split("/").pop().replace(".json", "");
1192
+ data.push(entry);
1193
+ }
1194
+ }
1195
+ return data;
1196
+ } catch (err) {
1197
+ console.error(`Error loading data from HF: ${err.message}`);
1198
+ throw err;
1199
+ }
1200
+ }
1201
+
1202
+ // ---------------------------------------------------------------------------
1203
+ // Leaderboard computation (custom JS — no evalica)
1204
+ // ---------------------------------------------------------------------------
1205
+
1206
+ function round2(n) {
1207
+ return Math.round(n * 100) / 100;
1208
+ }
1209
+
1210
+ const WINNER_MAP = {
1211
+ left: "X",
1212
+ right: "Y",
1213
+ tie: "draw",
1214
+ both_bad: "draw",
1215
+ };
1216
+
1217
+ function computeElo(votes) {
1218
+ const K = 32;
1219
+ const INITIAL = 1000;
1220
+ const scores = {};
1221
+
1222
+ for (const v of votes) {
1223
+ scores[v.left] ??= INITIAL;
1224
+ scores[v.right] ??= INITIAL;
1225
+
1226
+ const w = WINNER_MAP[v.winner];
1227
+ if (w === "draw") continue; // tieWeight = 0
1228
+
1229
+ const rA = scores[v.left];
1230
+ const rB = scores[v.right];
1231
+ const eA = 1 / (1 + 10 ** ((rB - rA) / 400));
1232
+ const eB = 1 - eA;
1233
+
1234
+ const sA = w === "X" ? 1 : 0;
1235
+ const sB = w === "Y" ? 1 : 0;
1236
+
1237
+ scores[v.left] += K * (sA - eA);
1238
+ scores[v.right] += K * (sB - eB);
1239
+ }
1240
+ return scores;
1241
+ }
1242
+
1243
+ function computeAvgWinRate(votes) {
1244
+ const wins = {};
1245
+ const losses = {};
1246
+
1247
+ for (const v of votes) {
1248
+ wins[v.left] ??= 0;
1249
+ wins[v.right] ??= 0;
1250
+ losses[v.left] ??= 0;
1251
+ losses[v.right] ??= 0;
1252
+
1253
+ const w = WINNER_MAP[v.winner];
1254
+ if (w === "draw") continue;
1255
+
1256
+ if (w === "X") {
1257
+ wins[v.left]++;
1258
+ losses[v.right]++;
1259
+ } else {
1260
+ wins[v.right]++;
1261
+ losses[v.left]++;
1262
+ }
1263
+ }
1264
+
1265
+ const result = {};
1266
+ for (const name of Object.keys(wins)) {
1267
+ const total = wins[name] + losses[name];
1268
+ result[name] = total > 0 ? wins[name] / total : 0;
1269
+ }
1270
+ return result;
1271
+ }
1272
+
1273
+ function computeBradleyTerry(votes, iterations = 100) {
1274
+ // Collect agents and win counts
1275
+ const agentSet = new Set();
1276
+ for (const v of votes) {
1277
+ agentSet.add(v.left);
1278
+ agentSet.add(v.right);
1279
+ }
1280
+ const agentList = [...agentSet];
1281
+ const n = agentList.length;
1282
+ const idx = Object.fromEntries(agentList.map((a, i) => [a, i]));
1283
+
1284
+ // Win matrix
1285
+ const W = Array.from({ length: n }, () => new Float64Array(n));
1286
+ for (const v of votes) {
1287
+ const w = WINNER_MAP[v.winner];
1288
+ if (w === "draw") continue;
1289
+ const i = idx[v.left];
1290
+ const j = idx[v.right];
1291
+ if (w === "X") W[i][j]++;
1292
+ else W[j][i]++;
1293
+ }
1294
+
1295
+ // Iterative MLE
1296
+ const p = new Float64Array(n).fill(1 / n);
1297
+
1298
+ for (let iter = 0; iter < iterations; iter++) {
1299
+ const pNew = new Float64Array(n);
1300
+ for (let i = 0; i < n; i++) {
1301
+ let num = 0;
1302
+ let den = 0;
1303
+ for (let j = 0; j < n; j++) {
1304
+ if (i === j) continue;
1305
+ num += W[i][j];
1306
+ const totalGames = W[i][j] + W[j][i];
1307
+ if (totalGames > 0) den += totalGames / (p[i] + p[j]);
1308
+ }
1309
+ pNew[i] = den > 0 ? num / den : 0;
1310
+ }
1311
+ // Normalize
1312
+ const sum = pNew.reduce((a, b) => a + b, 0);
1313
+ if (sum > 0) for (let i = 0; i < n; i++) pNew[i] /= sum;
1314
+ for (let i = 0; i < n; i++) p[i] = pNew[i];
1315
+ }
1316
+
1317
+ const result = {};
1318
+ for (let i = 0; i < n; i++) result[agentList[i]] = p[i];
1319
+ return result;
1320
+ }
1321
+
1322
+ function computePageRank(votes, damping = 0.85, iterations = 100) {
1323
+ const agentSet = new Set();
1324
+ for (const v of votes) {
1325
+ agentSet.add(v.left);
1326
+ agentSet.add(v.right);
1327
+ }
1328
+ const agentList = [...agentSet];
1329
+ const n = agentList.length;
1330
+ const idx = Object.fromEntries(agentList.map((a, i) => [a, i]));
1331
+
1332
+ // Adjacency: edge from loser to winner
1333
+ const outLinks = Array.from({ length: n }, () => new Float64Array(n));
1334
+ const outDegree = new Float64Array(n);
1335
+
1336
+ for (const v of votes) {
1337
+ const w = WINNER_MAP[v.winner];
1338
+ if (w === "draw") continue;
1339
+ const winner = w === "X" ? idx[v.left] : idx[v.right];
1340
+ const loser = w === "X" ? idx[v.right] : idx[v.left];
1341
+ outLinks[loser][winner]++;
1342
+ outDegree[loser]++;
1343
+ }
1344
+
1345
+ let pr = new Float64Array(n).fill(1 / n);
1346
+
1347
+ for (let iter = 0; iter < iterations; iter++) {
1348
+ const prNew = new Float64Array(n).fill((1 - damping) / n);
1349
+ for (let j = 0; j < n; j++) {
1350
+ if (outDegree[j] === 0) {
1351
+ // Dangling node: distribute evenly
1352
+ for (let i = 0; i < n; i++) prNew[i] += damping * pr[j] / n;
1353
+ } else {
1354
+ for (let i = 0; i < n; i++) {
1355
+ if (outLinks[j][i] > 0) {
1356
+ prNew[i] += damping * pr[j] * (outLinks[j][i] / outDegree[j]);
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ pr = prNew;
1362
+ }
1363
+
1364
+ const result = {};
1365
+ for (let i = 0; i < n; i++) result[agentList[i]] = pr[i];
1366
+ return result;
1367
+ }
1368
+
1369
+ function computeEigen(votes, iterations = 100) {
1370
+ const agentSet = new Set();
1371
+ for (const v of votes) {
1372
+ agentSet.add(v.left);
1373
+ agentSet.add(v.right);
1374
+ }
1375
+ const agentList = [...agentSet];
1376
+ const n = agentList.length;
1377
+ const idx = Object.fromEntries(agentList.map((a, i) => [a, i]));
1378
+
1379
+ // Adjacency matrix: wins
1380
+ const A = Array.from({ length: n }, () => new Float64Array(n));
1381
+ for (const v of votes) {
1382
+ const w = WINNER_MAP[v.winner];
1383
+ if (w === "draw") continue;
1384
+ const i = idx[v.left];
1385
+ const j = idx[v.right];
1386
+ if (w === "X") A[i][j]++;
1387
+ else A[j][i]++;
1388
+ }
1389
+
1390
+ // Power iteration for dominant eigenvector
1391
+ let vec = new Float64Array(n).fill(1 / Math.sqrt(n));
1392
+
1393
+ for (let iter = 0; iter < iterations; iter++) {
1394
+ const newVec = new Float64Array(n);
1395
+ for (let i = 0; i < n; i++) {
1396
+ for (let j = 0; j < n; j++) {
1397
+ newVec[i] += A[i][j] * vec[j];
1398
+ }
1399
+ }
1400
+ // Normalize
1401
+ const norm = Math.sqrt(newVec.reduce((s, v) => s + v * v, 0));
1402
+ if (norm > 0) for (let i = 0; i < n; i++) newVec[i] /= norm;
1403
+ vec = newVec;
1404
+ }
1405
+
1406
+ const result = {};
1407
+ for (let i = 0; i < n; i++) result[agentList[i]] = vec[i];
1408
+ return result;
1409
+ }
1410
+
1411
+ function computeNewman(votes) {
1412
+ // Simplified Newman modularity on win-graph
1413
+ const agentSet = new Set();
1414
+ for (const v of votes) {
1415
+ agentSet.add(v.left);
1416
+ agentSet.add(v.right);
1417
+ }
1418
+ const agentList = [...agentSet];
1419
+ const n = agentList.length;
1420
+ const idx = Object.fromEntries(agentList.map((a, i) => [a, i]));
1421
+
1422
+ const A = Array.from({ length: n }, () => new Float64Array(n));
1423
+ let totalEdges = 0;
1424
+ const degree = new Float64Array(n);
1425
+
1426
+ for (const v of votes) {
1427
+ const w = WINNER_MAP[v.winner];
1428
+ if (w === "draw") continue;
1429
+ const i = idx[v.left];
1430
+ const j = idx[v.right];
1431
+ if (w === "X") {
1432
+ A[i][j]++;
1433
+ A[j][i]++;
1434
+ } else {
1435
+ A[j][i]++;
1436
+ A[i][j]++;
1437
+ }
1438
+ degree[i]++;
1439
+ degree[j]++;
1440
+ totalEdges++;
1441
+ }
1442
+
1443
+ if (totalEdges === 0) {
1444
+ const result = {};
1445
+ for (const a of agentList) result[a] = 0;
1446
+ return result;
1447
+ }
1448
+
1449
+ // Each node in its own community -> modularity contribution
1450
+ const result = {};
1451
+ for (let i = 0; i < n; i++) {
1452
+ const qi =
1453
+ (A[i][i] || 0) / (2 * totalEdges) -
1454
+ (degree[i] / (2 * totalEdges)) ** 2;
1455
+ result[agentList[i]] = qi;
1456
+ }
1457
+ return result;
1458
+ }
1459
+
1460
+ function computeCeiMcs(votes, conversations) {
1461
+ const convMap = new Map();
1462
+ for (const c of conversations) {
1463
+ convMap.set(`${c.timestamp}|${c.left}|${c.right}`, c);
1464
+ }
1465
+
1466
+ const stats = {};
1467
+
1468
+ for (const vote of votes) {
1469
+ const conv = convMap.get(
1470
+ `${vote.timestamp}|${vote.left}|${vote.right}`
1471
+ );
1472
+
1473
+ for (const m of [vote.left, vote.right]) {
1474
+ stats[m] ??= { ceiSum: 0, ceiMax: 0, selfMatches: 0, selfDraws: 0 };
1475
+ }
1476
+
1477
+ if (vote.left === vote.right) {
1478
+ stats[vote.left].selfMatches++;
1479
+ if (vote.winner === "tie" || vote.winner === "both_bad") {
1480
+ stats[vote.left].selfDraws++;
1481
+ }
1482
+ continue;
1483
+ }
1484
+
1485
+ let leftScore, rightScore;
1486
+ switch (vote.winner) {
1487
+ case "left":
1488
+ leftScore = 1;
1489
+ rightScore = -1;
1490
+ break;
1491
+ case "right":
1492
+ leftScore = -1;
1493
+ rightScore = 1;
1494
+ break;
1495
+ case "tie":
1496
+ leftScore = 0.3;
1497
+ rightScore = 0.3;
1498
+ break;
1499
+ case "both_bad":
1500
+ leftScore = -0.3;
1501
+ rightScore = -0.3;
1502
+ break;
1503
+ default:
1504
+ continue;
1505
+ }
1506
+
1507
+ // CEI: use conversation rounds if available, default to 1
1508
+ const leftRounds = conv?.left_rounds?.length || 1;
1509
+ const rightRounds = conv?.right_rounds?.length || 1;
1510
+
1511
+ stats[vote.left].ceiMax += 1 / leftRounds;
1512
+ stats[vote.right].ceiMax += 1 / rightRounds;
1513
+ stats[vote.left].ceiSum += leftScore / leftRounds;
1514
+ stats[vote.right].ceiSum += rightScore / rightRounds;
1515
+ }
1516
+
1517
+ const cei = {};
1518
+ const mcs = {};
1519
+ for (const [agent, s] of Object.entries(stats)) {
1520
+ cei[agent] = s.ceiMax > 0 ? round2(s.ceiSum / s.ceiMax) : null;
1521
+ mcs[agent] = s.selfMatches > 0 ? round2(s.selfDraws / s.selfMatches) : null;
1522
+ }
1523
+ return { cei, mcs };
1524
+ }
1525
+
1526
+ async function getLeaderboardData({ voteEntry = null, convEntry = null, useCache = true } = {}) {
1527
+ // Return in-memory cache if available and no new vote to incorporate
1528
+ if (useCache && leaderboardCache && !voteEntry) return leaderboardCache;
1529
+
1530
+ const token = process.env.HF_TOKEN;
1531
+ const credentials = token ? { accessToken: token } : undefined;
1532
+
1533
+ if (useCache && !leaderboardCache) {
1534
+ try {
1535
+ const resp = await downloadFile({
1536
+ repo: { type: "dataset", name: LEADERBOARD_REPO },
1537
+ path: `${LEADERBOARD_FILE}.json`,
1538
+ credentials,
1539
+ });
1540
+ if (resp) {
1541
+ leaderboardCache = JSON.parse(await resp.text());
1542
+ return leaderboardCache;
1543
+ }
1544
+ } catch {
1545
+ console.log("No cached leaderboard found, computing from votes...");
1546
+ }
1547
+ }
1548
+
1549
+ const votes = await loadContentFromHf(VOTE_REPO, LEADERBOARD_FILE);
1550
+ if (voteEntry) votes.push(voteEntry);
1551
+ if (votes.length === 0) return [];
1552
+
1553
+ const conversations = await loadContentFromHf(
1554
+ CONVERSATION_REPO,
1555
+ LEADERBOARD_FILE
1556
+ );
1557
+ if (convEntry) conversations.push(convEntry);
1558
+
1559
+ const eloScores = computeElo(votes);
1560
+ const winRates = computeAvgWinRate(votes);
1561
+ const btScores = computeBradleyTerry(votes);
1562
+ const pagerankScr = computePageRank(votes);
1563
+ const eigenScores = computeEigen(votes);
1564
+ const newmanScores = computeNewman(votes);
1565
+ const { cei, mcs } = computeCeiMcs(votes, conversations);
1566
+
1567
+ const agentNames = Object.keys(eloScores);
1568
+ const rows = agentNames.map((name) => ({
1569
+ Agent: name,
1570
+ Provider: (agentByName[name] || agentById[name])?.provider || "",
1571
+ "Elo Score": round2(eloScores[name] ?? 0),
1572
+ "Win Rate": round2(winRates[name] ?? 0),
1573
+ "Conversation Efficiency Index": cei[name] ?? null,
1574
+ "Consistency Score": mcs[name] ?? null,
1575
+ "Bradley-Terry Coefficient": round2(btScores[name] ?? 0),
1576
+ "Eigenvector Centrality Value": round2(eigenScores[name] ?? 0),
1577
+ "Newman Modularity Score": round2(newmanScores[name] ?? 0),
1578
+ "PageRank Score": round2(pagerankScr[name] ?? 0),
1579
+ }));
1580
+
1581
+ rows.sort((a, b) => b["Elo Score"] - a["Elo Score"]);
1582
+ rows.forEach((row, i) => {
1583
+ row.Rank = i + 1;
1584
+ });
1585
+
1586
+ leaderboardCache = rows;
1587
+
1588
+ if (voteEntry && token) {
1589
+ saveContentToHf(rows, LEADERBOARD_REPO, LEADERBOARD_FILE, token).catch(
1590
+ (err) => console.error(`Failed to save leaderboard cache: ${err.message}`)
1591
+ );
1592
+ }
1593
+
1594
+ return rows;
1595
+ }
1596
+
1597
+ // ---------------------------------------------------------------------------
1598
+ // Guardrail
1599
+ // ---------------------------------------------------------------------------
1600
+
1601
+ async function guardrailCheckSeRelevance(userInput) {
1602
+ try {
1603
+ const response = await openaiClient.chat.completions.create({
1604
+ model: "openai/gpt-oss-safeguard-20b",
1605
+ messages: [
1606
+ {
1607
+ role: "system",
1608
+ content:
1609
+ "You are a classifier that decides if a user's question is relevant to software engineering. " +
1610
+ "If the question is about software engineering concepts, tools, processes, or code, respond with 'Yes'. " +
1611
+ "Otherwise, respond with 'No'.",
1612
+ },
1613
+ { role: "user", content: userInput },
1614
+ ],
1615
+ });
1616
+ const classification = response.choices[0].message.content
1617
+ .trim()
1618
+ .toLowerCase();
1619
+ return classification.startsWith("yes");
1620
+ } catch (err) {
1621
+ console.error(`Guardrail check failed: ${err.message}`);
1622
+ return true; // fail open
1623
+ }
1624
+ }
1625
+
1626
+ // ---------------------------------------------------------------------------
1627
+ // Express app
1628
+ // ---------------------------------------------------------------------------
1629
+
1630
+ const app = express();
1631
+ app.use(express.json({ limit: "10mb" }));
1632
+ app.use(express.static("public"));
1633
+ app.use(
1634
+ cookieSession({
1635
+ name: "session",
1636
+ keys: [process.env.SESSION_SECRET || randomUUID()],
1637
+ maxAge: 24 * 60 * 60 * 1000,
1638
+ })
1639
+ );
1640
+
1641
+ // In-memory battle state: battleId -> battle object
1642
+ const battles = new Map();
1643
+
1644
+ // ---------------------------------------------------------------------------
1645
+ // Auth routes (HF OAuth)
1646
+ // ---------------------------------------------------------------------------
1647
+
1648
+ app.get("/auth/login", (req, res) => {
1649
+ const clientId = process.env.OAUTH_CLIENT_ID;
1650
+ if (!clientId) return res.status(500).json({ error: "OAuth not configured" });
1651
+
1652
+ const redirectUri = `${req.protocol}://${req.get("host")}/auth/callback`;
1653
+ const params = new URLSearchParams({
1654
+ client_id: clientId,
1655
+ redirect_uri: redirectUri,
1656
+ response_type: "code",
1657
+ scope: process.env.OAUTH_SCOPES || "openid profile",
1658
+ state: randomUUID(),
1659
+ });
1660
+ res.redirect(`https://huggingface.co/oauth/authorize?${params}`);
1661
+ });
1662
+
1663
+ app.get("/auth/callback", async (req, res) => {
1664
+ const { code } = req.query;
1665
+ try {
1666
+ const tokenResp = await fetch("https://huggingface.co/oauth/token", {
1667
+ method: "POST",
1668
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1669
+ body: new URLSearchParams({
1670
+ grant_type: "authorization_code",
1671
+ code,
1672
+ redirect_uri: `${req.protocol}://${req.get("host")}/auth/callback`,
1673
+ client_id: process.env.OAUTH_CLIENT_ID,
1674
+ client_secret: process.env.OAUTH_CLIENT_SECRET,
1675
+ }),
1676
+ });
1677
+ const data = await tokenResp.json();
1678
+ req.session.hfToken = data.access_token;
1679
+ res.redirect("/");
1680
+ } catch (err) {
1681
+ console.error(`OAuth callback error: ${err.message}`);
1682
+ res.redirect("/");
1683
+ }
1684
+ });
1685
+
1686
+ app.get("/auth/status", (req, res) => {
1687
+ const token = req.session?.hfToken || process.env.HF_TOKEN;
1688
+ res.json({
1689
+ authenticated: !!token,
1690
+ hint: SHOW_HINT_STRING ? HINT_STRING : "",
1691
+ });
1692
+ });
1693
+
1694
+ // ---------------------------------------------------------------------------
1695
+ // API routes
1696
+ // ---------------------------------------------------------------------------
1697
+
1698
+ app.get("/api/config", (_req, res) => {
1699
+ res.json({ agentTimeoutMin: AGENT_TIMEOUT / 60_000 });
1700
+ });
1701
+
1702
+ app.get("/api/leaderboard", async (req, res) => {
1703
+ try {
1704
+ const data = await getLeaderboardData({ useCache: true });
1705
+ res.json(data);
1706
+ } catch (err) {
1707
+ console.error(`Leaderboard error: ${err.message}`);
1708
+ res.status(500).json({ error: err.message });
1709
+ }
1710
+ });
1711
+
1712
+ app.post("/api/battle/start", async (req, res) => {
1713
+ const { prompt, repoUrl } = req.body;
1714
+ if (!prompt || !prompt.trim()) {
1715
+ return res.status(400).json({ error: "Prompt is required." });
1716
+ }
1717
+
1718
+ // Guardrail (skip if URL provided)
1719
+ if (!repoUrl) {
1720
+ const isRelevant = await guardrailCheckSeRelevance(prompt);
1721
+ if (!isRelevant) {
1722
+ return res.status(400).json({
1723
+ error:
1724
+ "Oops! Try asking something about software engineering. Thanks!",
1725
+ });
1726
+ }
1727
+ }
1728
+
1729
+ const available = availableAgents();
1730
+ if (available.length < 2) {
1731
+ return res
1732
+ .status(500)
1733
+ .json({ error: "Not enough agents available for a battle." });
1734
+ }
1735
+
1736
+ // Pick 2 random agents independently (may be the same — needed for MCS self-match metric)
1737
+ const pick = () => available[Math.floor(Math.random() * available.length)];
1738
+ const agentA = pick();
1739
+ const agentB = pick();
1740
+
1741
+ // Create temp dirs
1742
+ const leftDir = mkdtempSync(join(tmpdir(), "agent_left_"));
1743
+ const rightDir = mkdtempSync(join(tmpdir(), "agent_right_"));
1744
+
1745
+ try {
1746
+ // Git init or clone
1747
+ for (const d of [leftDir, rightDir]) {
1748
+ if (repoUrl && repoUrl.trim()) {
1749
+ cloneRepo(repoUrl, d);
1750
+ } else {
1751
+ execFileSync("git", ["init"], { cwd: d, stdio: "pipe" });
1752
+ }
1753
+ }
1754
+
1755
+ // Fetch context & build prompt
1756
+ const repoContext = await fetchUrlContent(repoUrl || "");
1757
+ const fullPrompt = buildPrompt(prompt, repoContext);
1758
+
1759
+ // Spawn both agents (non-blocking — returns immediately with live state)
1760
+ const leftState = spawnAgent(agentA, fullPrompt, leftDir);
1761
+ const rightState = spawnAgent(agentB, fullPrompt, rightDir);
1762
+
1763
+ // Build battle state
1764
+ const battleId = randomUUID();
1765
+ battles.set(battleId, {
1766
+ id: battleId,
1767
+ left: agentA.name,
1768
+ right: agentB.name,
1769
+ leftAgent: agentA,
1770
+ rightAgent: agentB,
1771
+ url: repoUrl || "",
1772
+ leftDir,
1773
+ rightDir,
1774
+ fullPrompt,
1775
+ leftState,
1776
+ rightState,
1777
+ leftDiff: null,
1778
+ rightDiff: null,
1779
+ leftRounds: [],
1780
+ rightRounds: [],
1781
+ });
1782
+
1783
+ // Background: when each agent finishes, capture its diff and build its
1784
+ // initial round immediately so follow-ups can be sent independently.
1785
+ leftState.promise.then(() => {
1786
+ const b = battles.get(battleId);
1787
+ if (!b) return;
1788
+ b.leftDiff = captureDiff(leftDir);
1789
+ b.leftRounds = [{
1790
+ prompt: fullPrompt,
1791
+ stdout: leftState.stdout || leftState.stderr || "",
1792
+ stderr: leftState.stderr || "",
1793
+ diff: b.leftDiff || "",
1794
+ }];
1795
+ }).catch((err) => {
1796
+ console.error(`Left agent post-process error: ${err.message}`);
1797
+ const b = battles.get(battleId);
1798
+ if (!b) return;
1799
+ b.leftRounds = [{
1800
+ prompt: fullPrompt,
1801
+ stdout: leftState.stdout || leftState.stderr || "",
1802
+ stderr: leftState.stderr || "",
1803
+ diff: "",
1804
+ }];
1805
+ });
1806
+ rightState.promise.then(() => {
1807
+ const b = battles.get(battleId);
1808
+ if (!b) return;
1809
+ b.rightDiff = captureDiff(rightDir);
1810
+ b.rightRounds = [{
1811
+ prompt: fullPrompt,
1812
+ stdout: rightState.stdout || rightState.stderr || "",
1813
+ stderr: rightState.stderr || "",
1814
+ diff: b.rightDiff || "",
1815
+ }];
1816
+ }).catch((err) => {
1817
+ console.error(`Right agent post-process error: ${err.message}`);
1818
+ const b = battles.get(battleId);
1819
+ if (!b) return;
1820
+ b.rightRounds = [{
1821
+ prompt: fullPrompt,
1822
+ stdout: rightState.stdout || rightState.stderr || "",
1823
+ stderr: rightState.stderr || "",
1824
+ diff: "",
1825
+ }];
1826
+ });
1827
+
1828
+ // Return immediately — frontend will poll /api/battle/status
1829
+ res.json({ battleId });
1830
+ } catch (err) {
1831
+ rmSync(leftDir, { recursive: true, force: true });
1832
+ rmSync(rightDir, { recursive: true, force: true });
1833
+ console.error(`Battle start error: ${err.message}`);
1834
+ res.status(500).json({ error: err.message });
1835
+ }
1836
+ });
1837
+
1838
+ // Poll for live agent output
1839
+ app.get("/api/battle/status/:id", (req, res) => {
1840
+ const battle = battles.get(req.params.id);
1841
+ if (!battle) {
1842
+ return res.status(404).json({ error: "Battle not found (session expired)." });
1843
+ }
1844
+
1845
+ const { leftState, rightState } = battle;
1846
+
1847
+ const formatOutput = (state) => {
1848
+ const out = state.done ? parseAgentOutput(state.stdout) : state.stdout;
1849
+ if (state.done && !state.ok) {
1850
+ const prefix = out ? out + "\n\n" : "";
1851
+ return `${prefix}**Agent error:** ${state.stderr}`;
1852
+ }
1853
+ // Agent exited 0 but stderr has warnings/errors — append them
1854
+ if (state.done && state.stderr) {
1855
+ return `${out}\n\n**Agent warnings:** ${state.stderr}`;
1856
+ }
1857
+ return out;
1858
+ };
1859
+
1860
+ res.json({
1861
+ leftStatus: leftState.done ? "done" : "running",
1862
+ rightStatus: rightState.done ? "done" : "running",
1863
+ leftOutput: formatOutput(leftState),
1864
+ rightOutput: formatOutput(rightState),
1865
+ leftDiff: battle.leftDiff,
1866
+ rightDiff: battle.rightDiff,
1867
+ });
1868
+ });
1869
+
1870
+ app.post("/api/battle/followup", async (req, res) => {
1871
+ const { battleId, side, prompt } = req.body;
1872
+ const battle = battles.get(battleId);
1873
+ if (!battle)
1874
+ return res.status(404).json({ error: "Battle not found (session expired)." });
1875
+ if (!prompt || !prompt.trim())
1876
+ return res.status(400).json({ error: "Prompt is required." });
1877
+ if (side !== "left" && side !== "right")
1878
+ return res.status(400).json({ error: 'Side must be "left" or "right".' });
1879
+
1880
+ const state = side === "left" ? battle.leftState : battle.rightState;
1881
+ if (!state.done)
1882
+ return res.status(400).json({ error: "Agent is still running. Please wait for it to finish." });
1883
+
1884
+ const agent = side === "left" ? battle.leftAgent : battle.rightAgent;
1885
+ const agentDir = side === "left" ? battle.leftDir : battle.rightDir;
1886
+ const rounds = side === "left" ? battle.leftRounds : battle.rightRounds;
1887
+
1888
+ try {
1889
+ const result = await runFollowup(agent, prompt, agentDir, rounds);
1890
+ const diff = captureDiff(agentDir);
1891
+
1892
+ rounds.push({
1893
+ prompt,
1894
+ stdout: result.stdout || result.stderr || "",
1895
+ stderr: result.stderr || "",
1896
+ diff,
1897
+ });
1898
+
1899
+ res.json({
1900
+ output: result.ok
1901
+ ? parseAgentOutput(result.stdout)
1902
+ : `**Agent error:** ${result.stderr}`,
1903
+ diff,
1904
+ ok: result.ok,
1905
+ });
1906
+ } catch (err) {
1907
+ console.error(`Followup error: ${err.message}`);
1908
+ res.status(500).json({ error: err.message });
1909
+ }
1910
+ });
1911
+
1912
+ app.post("/api/battle/vote", async (req, res) => {
1913
+ const { battleId, winner } = req.body;
1914
+ const battle = battles.get(battleId);
1915
+ if (!battle)
1916
+ return res.status(404).json({ error: "Battle not found (session expired)." });
1917
+
1918
+ const validWinners = ["left", "right", "tie", "both_bad"];
1919
+ if (!validWinners.includes(winner))
1920
+ return res.status(400).json({ error: "Invalid winner value." });
1921
+
1922
+ const token = req.session?.hfToken || process.env.HF_TOKEN;
1923
+ const timestamp = new Date()
1924
+ .toISOString()
1925
+ .replace(/[-:T]/g, (c) => (c === "T" ? "_" : ""))
1926
+ .replace(/\.\d+Z$/, "");
1927
+ const fileName = `${LEADERBOARD_FILE}/${timestamp}`;
1928
+
1929
+ const voteEntry = {
1930
+ left: battle.left,
1931
+ right: battle.right,
1932
+ winner,
1933
+ timestamp,
1934
+ };
1935
+
1936
+ // Strip context from first round prompts before saving
1937
+ const leftRoundsClean = battle.leftRounds.map((r, i) => ({
1938
+ ...r,
1939
+ prompt: i === 0 ? stripContext(r.prompt) : r.prompt,
1940
+ }));
1941
+ const rightRoundsClean = battle.rightRounds.map((r, i) => ({
1942
+ ...r,
1943
+ prompt: i === 0 ? stripContext(r.prompt) : r.prompt,
1944
+ }));
1945
+
1946
+ const convData = {
1947
+ left: battle.left,
1948
+ right: battle.right,
1949
+ url: battle.url,
1950
+ left_rounds: leftRoundsClean,
1951
+ right_rounds: rightRoundsClean,
1952
+ winner,
1953
+ timestamp,
1954
+ };
1955
+
1956
+ // Save to HF (fire and forget)
1957
+ try {
1958
+ await Promise.all([
1959
+ saveContentToHf(voteEntry, VOTE_REPO, fileName, token),
1960
+ saveContentToHf(convData, CONVERSATION_REPO, fileName, token),
1961
+ ]);
1962
+ } catch (err) {
1963
+ console.error(`HF upload error: ${err.message}`);
1964
+ }
1965
+
1966
+ // Clean up
1967
+ rmSync(battle.leftDir, { recursive: true, force: true });
1968
+ rmSync(battle.rightDir, { recursive: true, force: true });
1969
+ battles.delete(battleId);
1970
+
1971
+ // Recompute leaderboard
1972
+ try {
1973
+ const leaderboard = await getLeaderboardData({
1974
+ voteEntry,
1975
+ convEntry: convData,
1976
+ useCache: false,
1977
+ });
1978
+ res.json({ leaderboard, agentA: battle.left, agentB: battle.right });
1979
+ } catch (err) {
1980
+ console.error(`Leaderboard recompute error: ${err.message}`);
1981
+ res.json({ leaderboard: [], agentA: battle.left, agentB: battle.right });
1982
+ }
1983
+ });
1984
+
1985
+ // ---------------------------------------------------------------------------
1986
+ // Start server
1987
+ // ---------------------------------------------------------------------------
1988
+
1989
+ process.on("uncaughtException", (err) => {
1990
+ console.error("Uncaught exception:", err);
1991
+ });
1992
+ process.on("unhandledRejection", (reason) => {
1993
+ console.error("Unhandled rejection:", reason);
1994
+ });
1995
+
1996
+ const PORT = process.env.PORT || 7860;
1997
+
1998
+ (async () => {
1999
+ // Load agent CLI metadata from HF before accepting requests
2000
+ try {
2001
+ await loadAgentsFromHf();
2002
+ } catch (err) {
2003
+ console.error(`Failed to load agents from HF: ${err.message}`);
2004
+ process.exit(1);
2005
+ }
2006
+
2007
+ const available = availableAgents();
2008
+ console.log(
2009
+ `Available agents: ${available.map((a) => a.name).join(", ") || "(none)"}`
2010
+ );
2011
+
2012
+ // Preload leaderboard
2013
+ try {
2014
+ const data = await getLeaderboardData({ useCache: true });
2015
+ console.log(`Leaderboard preloaded: ${data.length} entries.`);
2016
+ } catch (err) {
2017
+ console.error(`Failed to preload leaderboard: ${err.message}`);
2018
+ }
2019
+
2020
+ const server = app.listen(PORT, () => {
2021
+ console.log(`SWE-Agent-Arena running on http://localhost:${PORT}`);
2022
+ });
2023
+ server.on("error", (err) => {
2024
+ console.error("Server error:", err);
2025
+ });
2026
+ })();
package-lock.json ADDED
@@ -0,0 +1,1668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "swe-agent-arena",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "swe-agent-arena",
8
+ "dependencies": {
9
+ "@gitbeaker/rest": "^40.x",
10
+ "@huggingface/hub": "^1.x",
11
+ "@octokit/rest": "^21.x",
12
+ "cookie-session": "^2.x",
13
+ "diff2html": "^3.x",
14
+ "dotenv": "^16.x",
15
+ "express": "^5.x",
16
+ "openai": "^4.x",
17
+ "which": "^4.x"
18
+ }
19
+ },
20
+ "node_modules/@gitbeaker/core": {
21
+ "version": "40.6.0",
22
+ "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-40.6.0.tgz",
23
+ "integrity": "sha512-tVVm8ZPrS9YCHEcuPV8vD1IcEf9POpdygWo+kPvkK7LcC36EERVcXagb8snEaGgGLfUaVQh8qP4iDZgPnP3YBQ==",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@gitbeaker/requester-utils": "^40.6.0",
27
+ "qs": "^6.12.2",
28
+ "xcase": "^2.0.1"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.20.0"
32
+ }
33
+ },
34
+ "node_modules/@gitbeaker/requester-utils": {
35
+ "version": "40.6.0",
36
+ "resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-40.6.0.tgz",
37
+ "integrity": "sha512-DQu2l3iXtB+8e1Ye2ekeUHABt4mGMRTLtuVWtFqf74sqJnerHNOxVOjPn19qu/nKdvKR3ZLwSRTtPzEsxgcShg==",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "picomatch-browser": "^2.2.6",
41
+ "qs": "^6.12.2",
42
+ "rate-limiter-flexible": "^4.0.1",
43
+ "xcase": "^2.0.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.20.0"
47
+ }
48
+ },
49
+ "node_modules/@gitbeaker/rest": {
50
+ "version": "40.6.0",
51
+ "resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-40.6.0.tgz",
52
+ "integrity": "sha512-sAwYJclU3NlB/gdxqhH6Hnmy5LWzvW7D3W33eShQEnxMhM0VjnFHPHcgJLQCIux3hMiub1uGtTw1hBJTxDc2mQ==",
53
+ "license": "MIT",
54
+ "dependencies": {
55
+ "@gitbeaker/core": "^40.6.0",
56
+ "@gitbeaker/requester-utils": "^40.6.0"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.20.0"
60
+ }
61
+ },
62
+ "node_modules/@huggingface/hub": {
63
+ "version": "1.1.2",
64
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-1.1.2.tgz",
65
+ "integrity": "sha512-Jf4GhvVj9ABDw4Itb3BV1T7f22iewuZva476qTicQ4kOTbosuUuFDhsVH7ZH6rVNgg20Ll9kaNBF5CXjySIT+w==",
66
+ "license": "MIT",
67
+ "dependencies": {
68
+ "@huggingface/tasks": "^0.18.2"
69
+ },
70
+ "engines": {
71
+ "node": ">=18"
72
+ }
73
+ },
74
+ "node_modules/@huggingface/tasks": {
75
+ "version": "0.18.12",
76
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.18.12.tgz",
77
+ "integrity": "sha512-hUEYhXJzJCUhLSBNglW+Sji5fuzMnpBimp3kbdcGor+MaN9xb+qrcLnMC7X8aOcZaZdCErwYYIqz2oSP2EM4Bg==",
78
+ "license": "MIT"
79
+ },
80
+ "node_modules/@octokit/auth-token": {
81
+ "version": "5.1.2",
82
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
83
+ "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
84
+ "license": "MIT",
85
+ "engines": {
86
+ "node": ">= 18"
87
+ }
88
+ },
89
+ "node_modules/@octokit/core": {
90
+ "version": "6.1.6",
91
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz",
92
+ "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==",
93
+ "license": "MIT",
94
+ "dependencies": {
95
+ "@octokit/auth-token": "^5.0.0",
96
+ "@octokit/graphql": "^8.2.2",
97
+ "@octokit/request": "^9.2.3",
98
+ "@octokit/request-error": "^6.1.8",
99
+ "@octokit/types": "^14.0.0",
100
+ "before-after-hook": "^3.0.2",
101
+ "universal-user-agent": "^7.0.0"
102
+ },
103
+ "engines": {
104
+ "node": ">= 18"
105
+ }
106
+ },
107
+ "node_modules/@octokit/endpoint": {
108
+ "version": "10.1.4",
109
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
110
+ "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
111
+ "license": "MIT",
112
+ "dependencies": {
113
+ "@octokit/types": "^14.0.0",
114
+ "universal-user-agent": "^7.0.2"
115
+ },
116
+ "engines": {
117
+ "node": ">= 18"
118
+ }
119
+ },
120
+ "node_modules/@octokit/graphql": {
121
+ "version": "8.2.2",
122
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
123
+ "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
124
+ "license": "MIT",
125
+ "dependencies": {
126
+ "@octokit/request": "^9.2.3",
127
+ "@octokit/types": "^14.0.0",
128
+ "universal-user-agent": "^7.0.0"
129
+ },
130
+ "engines": {
131
+ "node": ">= 18"
132
+ }
133
+ },
134
+ "node_modules/@octokit/openapi-types": {
135
+ "version": "25.1.0",
136
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
137
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
138
+ "license": "MIT"
139
+ },
140
+ "node_modules/@octokit/plugin-paginate-rest": {
141
+ "version": "11.6.0",
142
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
143
+ "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
144
+ "license": "MIT",
145
+ "dependencies": {
146
+ "@octokit/types": "^13.10.0"
147
+ },
148
+ "engines": {
149
+ "node": ">= 18"
150
+ },
151
+ "peerDependencies": {
152
+ "@octokit/core": ">=6"
153
+ }
154
+ },
155
+ "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
156
+ "version": "24.2.0",
157
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
158
+ "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
159
+ "license": "MIT"
160
+ },
161
+ "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
162
+ "version": "13.10.0",
163
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
164
+ "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
165
+ "license": "MIT",
166
+ "dependencies": {
167
+ "@octokit/openapi-types": "^24.2.0"
168
+ }
169
+ },
170
+ "node_modules/@octokit/plugin-request-log": {
171
+ "version": "5.3.1",
172
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz",
173
+ "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==",
174
+ "license": "MIT",
175
+ "engines": {
176
+ "node": ">= 18"
177
+ },
178
+ "peerDependencies": {
179
+ "@octokit/core": ">=6"
180
+ }
181
+ },
182
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
183
+ "version": "13.5.0",
184
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz",
185
+ "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==",
186
+ "license": "MIT",
187
+ "dependencies": {
188
+ "@octokit/types": "^13.10.0"
189
+ },
190
+ "engines": {
191
+ "node": ">= 18"
192
+ },
193
+ "peerDependencies": {
194
+ "@octokit/core": ">=6"
195
+ }
196
+ },
197
+ "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
198
+ "version": "24.2.0",
199
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
200
+ "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
201
+ "license": "MIT"
202
+ },
203
+ "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
204
+ "version": "13.10.0",
205
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
206
+ "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
207
+ "license": "MIT",
208
+ "dependencies": {
209
+ "@octokit/openapi-types": "^24.2.0"
210
+ }
211
+ },
212
+ "node_modules/@octokit/request": {
213
+ "version": "9.2.4",
214
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz",
215
+ "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==",
216
+ "license": "MIT",
217
+ "dependencies": {
218
+ "@octokit/endpoint": "^10.1.4",
219
+ "@octokit/request-error": "^6.1.8",
220
+ "@octokit/types": "^14.0.0",
221
+ "fast-content-type-parse": "^2.0.0",
222
+ "universal-user-agent": "^7.0.2"
223
+ },
224
+ "engines": {
225
+ "node": ">= 18"
226
+ }
227
+ },
228
+ "node_modules/@octokit/request-error": {
229
+ "version": "6.1.8",
230
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
231
+ "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
232
+ "license": "MIT",
233
+ "dependencies": {
234
+ "@octokit/types": "^14.0.0"
235
+ },
236
+ "engines": {
237
+ "node": ">= 18"
238
+ }
239
+ },
240
+ "node_modules/@octokit/rest": {
241
+ "version": "21.1.1",
242
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
243
+ "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
244
+ "license": "MIT",
245
+ "dependencies": {
246
+ "@octokit/core": "^6.1.4",
247
+ "@octokit/plugin-paginate-rest": "^11.4.2",
248
+ "@octokit/plugin-request-log": "^5.3.1",
249
+ "@octokit/plugin-rest-endpoint-methods": "^13.3.0"
250
+ },
251
+ "engines": {
252
+ "node": ">= 18"
253
+ }
254
+ },
255
+ "node_modules/@octokit/types": {
256
+ "version": "14.1.0",
257
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
258
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
259
+ "license": "MIT",
260
+ "dependencies": {
261
+ "@octokit/openapi-types": "^25.1.0"
262
+ }
263
+ },
264
+ "node_modules/@profoundlogic/hogan": {
265
+ "version": "3.0.4",
266
+ "resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz",
267
+ "integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==",
268
+ "license": "Apache-2.0",
269
+ "dependencies": {
270
+ "nopt": "1.0.10"
271
+ },
272
+ "bin": {
273
+ "hulk": "bin/hulk"
274
+ }
275
+ },
276
+ "node_modules/@types/node": {
277
+ "version": "18.19.130",
278
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
279
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
280
+ "license": "MIT",
281
+ "dependencies": {
282
+ "undici-types": "~5.26.4"
283
+ }
284
+ },
285
+ "node_modules/@types/node-fetch": {
286
+ "version": "2.6.13",
287
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
288
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
289
+ "license": "MIT",
290
+ "dependencies": {
291
+ "@types/node": "*",
292
+ "form-data": "^4.0.4"
293
+ }
294
+ },
295
+ "node_modules/abbrev": {
296
+ "version": "1.1.1",
297
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
298
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
299
+ "license": "ISC"
300
+ },
301
+ "node_modules/abort-controller": {
302
+ "version": "3.0.0",
303
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
304
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
305
+ "license": "MIT",
306
+ "dependencies": {
307
+ "event-target-shim": "^5.0.0"
308
+ },
309
+ "engines": {
310
+ "node": ">=6.5"
311
+ }
312
+ },
313
+ "node_modules/accepts": {
314
+ "version": "2.0.0",
315
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
316
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
317
+ "license": "MIT",
318
+ "dependencies": {
319
+ "mime-types": "^3.0.0",
320
+ "negotiator": "^1.0.0"
321
+ },
322
+ "engines": {
323
+ "node": ">= 0.6"
324
+ }
325
+ },
326
+ "node_modules/agentkeepalive": {
327
+ "version": "4.6.0",
328
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
329
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
330
+ "license": "MIT",
331
+ "dependencies": {
332
+ "humanize-ms": "^1.2.1"
333
+ },
334
+ "engines": {
335
+ "node": ">= 8.0.0"
336
+ }
337
+ },
338
+ "node_modules/asynckit": {
339
+ "version": "0.4.0",
340
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
341
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
342
+ "license": "MIT"
343
+ },
344
+ "node_modules/before-after-hook": {
345
+ "version": "3.0.2",
346
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
347
+ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
348
+ "license": "Apache-2.0"
349
+ },
350
+ "node_modules/body-parser": {
351
+ "version": "2.2.2",
352
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
353
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
354
+ "license": "MIT",
355
+ "dependencies": {
356
+ "bytes": "^3.1.2",
357
+ "content-type": "^1.0.5",
358
+ "debug": "^4.4.3",
359
+ "http-errors": "^2.0.0",
360
+ "iconv-lite": "^0.7.0",
361
+ "on-finished": "^2.4.1",
362
+ "qs": "^6.14.1",
363
+ "raw-body": "^3.0.1",
364
+ "type-is": "^2.0.1"
365
+ },
366
+ "engines": {
367
+ "node": ">=18"
368
+ },
369
+ "funding": {
370
+ "type": "opencollective",
371
+ "url": "https://opencollective.com/express"
372
+ }
373
+ },
374
+ "node_modules/body-parser/node_modules/debug": {
375
+ "version": "4.4.3",
376
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
377
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
378
+ "license": "MIT",
379
+ "dependencies": {
380
+ "ms": "^2.1.3"
381
+ },
382
+ "engines": {
383
+ "node": ">=6.0"
384
+ },
385
+ "peerDependenciesMeta": {
386
+ "supports-color": {
387
+ "optional": true
388
+ }
389
+ }
390
+ },
391
+ "node_modules/bytes": {
392
+ "version": "3.1.2",
393
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
394
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
395
+ "license": "MIT",
396
+ "engines": {
397
+ "node": ">= 0.8"
398
+ }
399
+ },
400
+ "node_modules/call-bind-apply-helpers": {
401
+ "version": "1.0.2",
402
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
403
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
404
+ "license": "MIT",
405
+ "dependencies": {
406
+ "es-errors": "^1.3.0",
407
+ "function-bind": "^1.1.2"
408
+ },
409
+ "engines": {
410
+ "node": ">= 0.4"
411
+ }
412
+ },
413
+ "node_modules/call-bound": {
414
+ "version": "1.0.4",
415
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
416
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
417
+ "license": "MIT",
418
+ "dependencies": {
419
+ "call-bind-apply-helpers": "^1.0.2",
420
+ "get-intrinsic": "^1.3.0"
421
+ },
422
+ "engines": {
423
+ "node": ">= 0.4"
424
+ },
425
+ "funding": {
426
+ "url": "https://github.com/sponsors/ljharb"
427
+ }
428
+ },
429
+ "node_modules/combined-stream": {
430
+ "version": "1.0.8",
431
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
432
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
433
+ "license": "MIT",
434
+ "dependencies": {
435
+ "delayed-stream": "~1.0.0"
436
+ },
437
+ "engines": {
438
+ "node": ">= 0.8"
439
+ }
440
+ },
441
+ "node_modules/content-disposition": {
442
+ "version": "1.0.1",
443
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
444
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
445
+ "license": "MIT",
446
+ "engines": {
447
+ "node": ">=18"
448
+ },
449
+ "funding": {
450
+ "type": "opencollective",
451
+ "url": "https://opencollective.com/express"
452
+ }
453
+ },
454
+ "node_modules/content-type": {
455
+ "version": "1.0.5",
456
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
457
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
458
+ "license": "MIT",
459
+ "engines": {
460
+ "node": ">= 0.6"
461
+ }
462
+ },
463
+ "node_modules/cookie": {
464
+ "version": "0.7.2",
465
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
466
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
467
+ "license": "MIT",
468
+ "engines": {
469
+ "node": ">= 0.6"
470
+ }
471
+ },
472
+ "node_modules/cookie-session": {
473
+ "version": "2.1.1",
474
+ "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz",
475
+ "integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==",
476
+ "license": "MIT",
477
+ "dependencies": {
478
+ "cookies": "0.9.1",
479
+ "debug": "3.2.7",
480
+ "on-headers": "~1.1.0",
481
+ "safe-buffer": "5.2.1"
482
+ },
483
+ "engines": {
484
+ "node": ">= 0.10"
485
+ }
486
+ },
487
+ "node_modules/cookie-signature": {
488
+ "version": "1.2.2",
489
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
490
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
491
+ "license": "MIT",
492
+ "engines": {
493
+ "node": ">=6.6.0"
494
+ }
495
+ },
496
+ "node_modules/cookies": {
497
+ "version": "0.9.1",
498
+ "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
499
+ "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
500
+ "license": "MIT",
501
+ "dependencies": {
502
+ "depd": "~2.0.0",
503
+ "keygrip": "~1.1.0"
504
+ },
505
+ "engines": {
506
+ "node": ">= 0.8"
507
+ }
508
+ },
509
+ "node_modules/debug": {
510
+ "version": "3.2.7",
511
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
512
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
513
+ "license": "MIT",
514
+ "dependencies": {
515
+ "ms": "^2.1.1"
516
+ }
517
+ },
518
+ "node_modules/delayed-stream": {
519
+ "version": "1.0.0",
520
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
521
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
522
+ "license": "MIT",
523
+ "engines": {
524
+ "node": ">=0.4.0"
525
+ }
526
+ },
527
+ "node_modules/depd": {
528
+ "version": "2.0.0",
529
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
530
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
531
+ "license": "MIT",
532
+ "engines": {
533
+ "node": ">= 0.8"
534
+ }
535
+ },
536
+ "node_modules/diff": {
537
+ "version": "8.0.3",
538
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
539
+ "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
540
+ "license": "BSD-3-Clause",
541
+ "engines": {
542
+ "node": ">=0.3.1"
543
+ }
544
+ },
545
+ "node_modules/diff2html": {
546
+ "version": "3.4.56",
547
+ "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz",
548
+ "integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==",
549
+ "license": "MIT",
550
+ "dependencies": {
551
+ "@profoundlogic/hogan": "^3.0.4",
552
+ "diff": "^8.0.3"
553
+ },
554
+ "engines": {
555
+ "node": ">=12"
556
+ },
557
+ "optionalDependencies": {
558
+ "highlight.js": "11.11.1"
559
+ }
560
+ },
561
+ "node_modules/dotenv": {
562
+ "version": "16.6.1",
563
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
564
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
565
+ "license": "BSD-2-Clause",
566
+ "engines": {
567
+ "node": ">=12"
568
+ },
569
+ "funding": {
570
+ "url": "https://dotenvx.com"
571
+ }
572
+ },
573
+ "node_modules/dunder-proto": {
574
+ "version": "1.0.1",
575
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
576
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
577
+ "license": "MIT",
578
+ "dependencies": {
579
+ "call-bind-apply-helpers": "^1.0.1",
580
+ "es-errors": "^1.3.0",
581
+ "gopd": "^1.2.0"
582
+ },
583
+ "engines": {
584
+ "node": ">= 0.4"
585
+ }
586
+ },
587
+ "node_modules/ee-first": {
588
+ "version": "1.1.1",
589
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
590
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
591
+ "license": "MIT"
592
+ },
593
+ "node_modules/encodeurl": {
594
+ "version": "2.0.0",
595
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
596
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
597
+ "license": "MIT",
598
+ "engines": {
599
+ "node": ">= 0.8"
600
+ }
601
+ },
602
+ "node_modules/es-define-property": {
603
+ "version": "1.0.1",
604
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
605
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
606
+ "license": "MIT",
607
+ "engines": {
608
+ "node": ">= 0.4"
609
+ }
610
+ },
611
+ "node_modules/es-errors": {
612
+ "version": "1.3.0",
613
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
614
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
615
+ "license": "MIT",
616
+ "engines": {
617
+ "node": ">= 0.4"
618
+ }
619
+ },
620
+ "node_modules/es-object-atoms": {
621
+ "version": "1.1.1",
622
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
623
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
624
+ "license": "MIT",
625
+ "dependencies": {
626
+ "es-errors": "^1.3.0"
627
+ },
628
+ "engines": {
629
+ "node": ">= 0.4"
630
+ }
631
+ },
632
+ "node_modules/es-set-tostringtag": {
633
+ "version": "2.1.0",
634
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
635
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
636
+ "license": "MIT",
637
+ "dependencies": {
638
+ "es-errors": "^1.3.0",
639
+ "get-intrinsic": "^1.2.6",
640
+ "has-tostringtag": "^1.0.2",
641
+ "hasown": "^2.0.2"
642
+ },
643
+ "engines": {
644
+ "node": ">= 0.4"
645
+ }
646
+ },
647
+ "node_modules/escape-html": {
648
+ "version": "1.0.3",
649
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
650
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
651
+ "license": "MIT"
652
+ },
653
+ "node_modules/etag": {
654
+ "version": "1.8.1",
655
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
656
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
657
+ "license": "MIT",
658
+ "engines": {
659
+ "node": ">= 0.6"
660
+ }
661
+ },
662
+ "node_modules/event-target-shim": {
663
+ "version": "5.0.1",
664
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
665
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
666
+ "license": "MIT",
667
+ "engines": {
668
+ "node": ">=6"
669
+ }
670
+ },
671
+ "node_modules/express": {
672
+ "version": "5.2.1",
673
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
674
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
675
+ "license": "MIT",
676
+ "dependencies": {
677
+ "accepts": "^2.0.0",
678
+ "body-parser": "^2.2.1",
679
+ "content-disposition": "^1.0.0",
680
+ "content-type": "^1.0.5",
681
+ "cookie": "^0.7.1",
682
+ "cookie-signature": "^1.2.1",
683
+ "debug": "^4.4.0",
684
+ "depd": "^2.0.0",
685
+ "encodeurl": "^2.0.0",
686
+ "escape-html": "^1.0.3",
687
+ "etag": "^1.8.1",
688
+ "finalhandler": "^2.1.0",
689
+ "fresh": "^2.0.0",
690
+ "http-errors": "^2.0.0",
691
+ "merge-descriptors": "^2.0.0",
692
+ "mime-types": "^3.0.0",
693
+ "on-finished": "^2.4.1",
694
+ "once": "^1.4.0",
695
+ "parseurl": "^1.3.3",
696
+ "proxy-addr": "^2.0.7",
697
+ "qs": "^6.14.0",
698
+ "range-parser": "^1.2.1",
699
+ "router": "^2.2.0",
700
+ "send": "^1.1.0",
701
+ "serve-static": "^2.2.0",
702
+ "statuses": "^2.0.1",
703
+ "type-is": "^2.0.1",
704
+ "vary": "^1.1.2"
705
+ },
706
+ "engines": {
707
+ "node": ">= 18"
708
+ },
709
+ "funding": {
710
+ "type": "opencollective",
711
+ "url": "https://opencollective.com/express"
712
+ }
713
+ },
714
+ "node_modules/express/node_modules/debug": {
715
+ "version": "4.4.3",
716
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
717
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
718
+ "license": "MIT",
719
+ "dependencies": {
720
+ "ms": "^2.1.3"
721
+ },
722
+ "engines": {
723
+ "node": ">=6.0"
724
+ },
725
+ "peerDependenciesMeta": {
726
+ "supports-color": {
727
+ "optional": true
728
+ }
729
+ }
730
+ },
731
+ "node_modules/fast-content-type-parse": {
732
+ "version": "2.0.1",
733
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
734
+ "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
735
+ "funding": [
736
+ {
737
+ "type": "github",
738
+ "url": "https://github.com/sponsors/fastify"
739
+ },
740
+ {
741
+ "type": "opencollective",
742
+ "url": "https://opencollective.com/fastify"
743
+ }
744
+ ],
745
+ "license": "MIT"
746
+ },
747
+ "node_modules/finalhandler": {
748
+ "version": "2.1.1",
749
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
750
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
751
+ "license": "MIT",
752
+ "dependencies": {
753
+ "debug": "^4.4.0",
754
+ "encodeurl": "^2.0.0",
755
+ "escape-html": "^1.0.3",
756
+ "on-finished": "^2.4.1",
757
+ "parseurl": "^1.3.3",
758
+ "statuses": "^2.0.1"
759
+ },
760
+ "engines": {
761
+ "node": ">= 18.0.0"
762
+ },
763
+ "funding": {
764
+ "type": "opencollective",
765
+ "url": "https://opencollective.com/express"
766
+ }
767
+ },
768
+ "node_modules/finalhandler/node_modules/debug": {
769
+ "version": "4.4.3",
770
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
771
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
772
+ "license": "MIT",
773
+ "dependencies": {
774
+ "ms": "^2.1.3"
775
+ },
776
+ "engines": {
777
+ "node": ">=6.0"
778
+ },
779
+ "peerDependenciesMeta": {
780
+ "supports-color": {
781
+ "optional": true
782
+ }
783
+ }
784
+ },
785
+ "node_modules/form-data": {
786
+ "version": "4.0.5",
787
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
788
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
789
+ "license": "MIT",
790
+ "dependencies": {
791
+ "asynckit": "^0.4.0",
792
+ "combined-stream": "^1.0.8",
793
+ "es-set-tostringtag": "^2.1.0",
794
+ "hasown": "^2.0.2",
795
+ "mime-types": "^2.1.12"
796
+ },
797
+ "engines": {
798
+ "node": ">= 6"
799
+ }
800
+ },
801
+ "node_modules/form-data-encoder": {
802
+ "version": "1.7.2",
803
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
804
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
805
+ "license": "MIT"
806
+ },
807
+ "node_modules/form-data/node_modules/mime-db": {
808
+ "version": "1.52.0",
809
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
810
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
811
+ "license": "MIT",
812
+ "engines": {
813
+ "node": ">= 0.6"
814
+ }
815
+ },
816
+ "node_modules/form-data/node_modules/mime-types": {
817
+ "version": "2.1.35",
818
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
819
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
820
+ "license": "MIT",
821
+ "dependencies": {
822
+ "mime-db": "1.52.0"
823
+ },
824
+ "engines": {
825
+ "node": ">= 0.6"
826
+ }
827
+ },
828
+ "node_modules/formdata-node": {
829
+ "version": "4.4.1",
830
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
831
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
832
+ "license": "MIT",
833
+ "dependencies": {
834
+ "node-domexception": "1.0.0",
835
+ "web-streams-polyfill": "4.0.0-beta.3"
836
+ },
837
+ "engines": {
838
+ "node": ">= 12.20"
839
+ }
840
+ },
841
+ "node_modules/forwarded": {
842
+ "version": "0.2.0",
843
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
844
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
845
+ "license": "MIT",
846
+ "engines": {
847
+ "node": ">= 0.6"
848
+ }
849
+ },
850
+ "node_modules/fresh": {
851
+ "version": "2.0.0",
852
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
853
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
854
+ "license": "MIT",
855
+ "engines": {
856
+ "node": ">= 0.8"
857
+ }
858
+ },
859
+ "node_modules/function-bind": {
860
+ "version": "1.1.2",
861
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
862
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
863
+ "license": "MIT",
864
+ "funding": {
865
+ "url": "https://github.com/sponsors/ljharb"
866
+ }
867
+ },
868
+ "node_modules/get-intrinsic": {
869
+ "version": "1.3.0",
870
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
871
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
872
+ "license": "MIT",
873
+ "dependencies": {
874
+ "call-bind-apply-helpers": "^1.0.2",
875
+ "es-define-property": "^1.0.1",
876
+ "es-errors": "^1.3.0",
877
+ "es-object-atoms": "^1.1.1",
878
+ "function-bind": "^1.1.2",
879
+ "get-proto": "^1.0.1",
880
+ "gopd": "^1.2.0",
881
+ "has-symbols": "^1.1.0",
882
+ "hasown": "^2.0.2",
883
+ "math-intrinsics": "^1.1.0"
884
+ },
885
+ "engines": {
886
+ "node": ">= 0.4"
887
+ },
888
+ "funding": {
889
+ "url": "https://github.com/sponsors/ljharb"
890
+ }
891
+ },
892
+ "node_modules/get-proto": {
893
+ "version": "1.0.1",
894
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
895
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
896
+ "license": "MIT",
897
+ "dependencies": {
898
+ "dunder-proto": "^1.0.1",
899
+ "es-object-atoms": "^1.0.0"
900
+ },
901
+ "engines": {
902
+ "node": ">= 0.4"
903
+ }
904
+ },
905
+ "node_modules/gopd": {
906
+ "version": "1.2.0",
907
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
908
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
909
+ "license": "MIT",
910
+ "engines": {
911
+ "node": ">= 0.4"
912
+ },
913
+ "funding": {
914
+ "url": "https://github.com/sponsors/ljharb"
915
+ }
916
+ },
917
+ "node_modules/has-symbols": {
918
+ "version": "1.1.0",
919
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
920
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
921
+ "license": "MIT",
922
+ "engines": {
923
+ "node": ">= 0.4"
924
+ },
925
+ "funding": {
926
+ "url": "https://github.com/sponsors/ljharb"
927
+ }
928
+ },
929
+ "node_modules/has-tostringtag": {
930
+ "version": "1.0.2",
931
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
932
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
933
+ "license": "MIT",
934
+ "dependencies": {
935
+ "has-symbols": "^1.0.3"
936
+ },
937
+ "engines": {
938
+ "node": ">= 0.4"
939
+ },
940
+ "funding": {
941
+ "url": "https://github.com/sponsors/ljharb"
942
+ }
943
+ },
944
+ "node_modules/hasown": {
945
+ "version": "2.0.2",
946
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
947
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
948
+ "license": "MIT",
949
+ "dependencies": {
950
+ "function-bind": "^1.1.2"
951
+ },
952
+ "engines": {
953
+ "node": ">= 0.4"
954
+ }
955
+ },
956
+ "node_modules/highlight.js": {
957
+ "version": "11.11.1",
958
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
959
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
960
+ "license": "BSD-3-Clause",
961
+ "optional": true,
962
+ "engines": {
963
+ "node": ">=12.0.0"
964
+ }
965
+ },
966
+ "node_modules/http-errors": {
967
+ "version": "2.0.1",
968
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
969
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
970
+ "license": "MIT",
971
+ "dependencies": {
972
+ "depd": "~2.0.0",
973
+ "inherits": "~2.0.4",
974
+ "setprototypeof": "~1.2.0",
975
+ "statuses": "~2.0.2",
976
+ "toidentifier": "~1.0.1"
977
+ },
978
+ "engines": {
979
+ "node": ">= 0.8"
980
+ },
981
+ "funding": {
982
+ "type": "opencollective",
983
+ "url": "https://opencollective.com/express"
984
+ }
985
+ },
986
+ "node_modules/humanize-ms": {
987
+ "version": "1.2.1",
988
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
989
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
990
+ "license": "MIT",
991
+ "dependencies": {
992
+ "ms": "^2.0.0"
993
+ }
994
+ },
995
+ "node_modules/iconv-lite": {
996
+ "version": "0.7.2",
997
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
998
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
999
+ "license": "MIT",
1000
+ "dependencies": {
1001
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
1002
+ },
1003
+ "engines": {
1004
+ "node": ">=0.10.0"
1005
+ },
1006
+ "funding": {
1007
+ "type": "opencollective",
1008
+ "url": "https://opencollective.com/express"
1009
+ }
1010
+ },
1011
+ "node_modules/inherits": {
1012
+ "version": "2.0.4",
1013
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1014
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1015
+ "license": "ISC"
1016
+ },
1017
+ "node_modules/ipaddr.js": {
1018
+ "version": "1.9.1",
1019
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1020
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1021
+ "license": "MIT",
1022
+ "engines": {
1023
+ "node": ">= 0.10"
1024
+ }
1025
+ },
1026
+ "node_modules/is-promise": {
1027
+ "version": "4.0.0",
1028
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1029
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1030
+ "license": "MIT"
1031
+ },
1032
+ "node_modules/isexe": {
1033
+ "version": "3.1.5",
1034
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
1035
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
1036
+ "license": "BlueOak-1.0.0",
1037
+ "engines": {
1038
+ "node": ">=18"
1039
+ }
1040
+ },
1041
+ "node_modules/keygrip": {
1042
+ "version": "1.1.0",
1043
+ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
1044
+ "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
1045
+ "license": "MIT",
1046
+ "dependencies": {
1047
+ "tsscmp": "1.0.6"
1048
+ },
1049
+ "engines": {
1050
+ "node": ">= 0.6"
1051
+ }
1052
+ },
1053
+ "node_modules/math-intrinsics": {
1054
+ "version": "1.1.0",
1055
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1056
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1057
+ "license": "MIT",
1058
+ "engines": {
1059
+ "node": ">= 0.4"
1060
+ }
1061
+ },
1062
+ "node_modules/media-typer": {
1063
+ "version": "1.1.0",
1064
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
1065
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
1066
+ "license": "MIT",
1067
+ "engines": {
1068
+ "node": ">= 0.8"
1069
+ }
1070
+ },
1071
+ "node_modules/merge-descriptors": {
1072
+ "version": "2.0.0",
1073
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1074
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1075
+ "license": "MIT",
1076
+ "engines": {
1077
+ "node": ">=18"
1078
+ },
1079
+ "funding": {
1080
+ "url": "https://github.com/sponsors/sindresorhus"
1081
+ }
1082
+ },
1083
+ "node_modules/mime-db": {
1084
+ "version": "1.54.0",
1085
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
1086
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
1087
+ "license": "MIT",
1088
+ "engines": {
1089
+ "node": ">= 0.6"
1090
+ }
1091
+ },
1092
+ "node_modules/mime-types": {
1093
+ "version": "3.0.2",
1094
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
1095
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
1096
+ "license": "MIT",
1097
+ "dependencies": {
1098
+ "mime-db": "^1.54.0"
1099
+ },
1100
+ "engines": {
1101
+ "node": ">=18"
1102
+ },
1103
+ "funding": {
1104
+ "type": "opencollective",
1105
+ "url": "https://opencollective.com/express"
1106
+ }
1107
+ },
1108
+ "node_modules/ms": {
1109
+ "version": "2.1.3",
1110
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1111
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1112
+ "license": "MIT"
1113
+ },
1114
+ "node_modules/negotiator": {
1115
+ "version": "1.0.0",
1116
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
1117
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
1118
+ "license": "MIT",
1119
+ "engines": {
1120
+ "node": ">= 0.6"
1121
+ }
1122
+ },
1123
+ "node_modules/node-domexception": {
1124
+ "version": "1.0.0",
1125
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
1126
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
1127
+ "deprecated": "Use your platform's native DOMException instead",
1128
+ "funding": [
1129
+ {
1130
+ "type": "github",
1131
+ "url": "https://github.com/sponsors/jimmywarting"
1132
+ },
1133
+ {
1134
+ "type": "github",
1135
+ "url": "https://paypal.me/jimmywarting"
1136
+ }
1137
+ ],
1138
+ "license": "MIT",
1139
+ "engines": {
1140
+ "node": ">=10.5.0"
1141
+ }
1142
+ },
1143
+ "node_modules/node-fetch": {
1144
+ "version": "2.7.0",
1145
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
1146
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
1147
+ "license": "MIT",
1148
+ "dependencies": {
1149
+ "whatwg-url": "^5.0.0"
1150
+ },
1151
+ "engines": {
1152
+ "node": "4.x || >=6.0.0"
1153
+ },
1154
+ "peerDependencies": {
1155
+ "encoding": "^0.1.0"
1156
+ },
1157
+ "peerDependenciesMeta": {
1158
+ "encoding": {
1159
+ "optional": true
1160
+ }
1161
+ }
1162
+ },
1163
+ "node_modules/nopt": {
1164
+ "version": "1.0.10",
1165
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
1166
+ "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
1167
+ "license": "MIT",
1168
+ "dependencies": {
1169
+ "abbrev": "1"
1170
+ },
1171
+ "bin": {
1172
+ "nopt": "bin/nopt.js"
1173
+ },
1174
+ "engines": {
1175
+ "node": "*"
1176
+ }
1177
+ },
1178
+ "node_modules/object-inspect": {
1179
+ "version": "1.13.4",
1180
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1181
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1182
+ "license": "MIT",
1183
+ "engines": {
1184
+ "node": ">= 0.4"
1185
+ },
1186
+ "funding": {
1187
+ "url": "https://github.com/sponsors/ljharb"
1188
+ }
1189
+ },
1190
+ "node_modules/on-finished": {
1191
+ "version": "2.4.1",
1192
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1193
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1194
+ "license": "MIT",
1195
+ "dependencies": {
1196
+ "ee-first": "1.1.1"
1197
+ },
1198
+ "engines": {
1199
+ "node": ">= 0.8"
1200
+ }
1201
+ },
1202
+ "node_modules/on-headers": {
1203
+ "version": "1.1.0",
1204
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
1205
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
1206
+ "license": "MIT",
1207
+ "engines": {
1208
+ "node": ">= 0.8"
1209
+ }
1210
+ },
1211
+ "node_modules/once": {
1212
+ "version": "1.4.0",
1213
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1214
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1215
+ "license": "ISC",
1216
+ "dependencies": {
1217
+ "wrappy": "1"
1218
+ }
1219
+ },
1220
+ "node_modules/openai": {
1221
+ "version": "4.104.0",
1222
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
1223
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
1224
+ "license": "Apache-2.0",
1225
+ "dependencies": {
1226
+ "@types/node": "^18.11.18",
1227
+ "@types/node-fetch": "^2.6.4",
1228
+ "abort-controller": "^3.0.0",
1229
+ "agentkeepalive": "^4.2.1",
1230
+ "form-data-encoder": "1.7.2",
1231
+ "formdata-node": "^4.3.2",
1232
+ "node-fetch": "^2.6.7"
1233
+ },
1234
+ "bin": {
1235
+ "openai": "bin/cli"
1236
+ },
1237
+ "peerDependencies": {
1238
+ "ws": "^8.18.0",
1239
+ "zod": "^3.23.8"
1240
+ },
1241
+ "peerDependenciesMeta": {
1242
+ "ws": {
1243
+ "optional": true
1244
+ },
1245
+ "zod": {
1246
+ "optional": true
1247
+ }
1248
+ }
1249
+ },
1250
+ "node_modules/parseurl": {
1251
+ "version": "1.3.3",
1252
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1253
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1254
+ "license": "MIT",
1255
+ "engines": {
1256
+ "node": ">= 0.8"
1257
+ }
1258
+ },
1259
+ "node_modules/path-to-regexp": {
1260
+ "version": "8.3.0",
1261
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
1262
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
1263
+ "license": "MIT",
1264
+ "funding": {
1265
+ "type": "opencollective",
1266
+ "url": "https://opencollective.com/express"
1267
+ }
1268
+ },
1269
+ "node_modules/picomatch-browser": {
1270
+ "version": "2.2.6",
1271
+ "resolved": "https://registry.npmjs.org/picomatch-browser/-/picomatch-browser-2.2.6.tgz",
1272
+ "integrity": "sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==",
1273
+ "license": "MIT",
1274
+ "engines": {
1275
+ "node": ">=8.6"
1276
+ },
1277
+ "funding": {
1278
+ "url": "https://github.com/sponsors/jonschlinkert"
1279
+ }
1280
+ },
1281
+ "node_modules/proxy-addr": {
1282
+ "version": "2.0.7",
1283
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1284
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1285
+ "license": "MIT",
1286
+ "dependencies": {
1287
+ "forwarded": "0.2.0",
1288
+ "ipaddr.js": "1.9.1"
1289
+ },
1290
+ "engines": {
1291
+ "node": ">= 0.10"
1292
+ }
1293
+ },
1294
+ "node_modules/qs": {
1295
+ "version": "6.15.0",
1296
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
1297
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
1298
+ "license": "BSD-3-Clause",
1299
+ "dependencies": {
1300
+ "side-channel": "^1.1.0"
1301
+ },
1302
+ "engines": {
1303
+ "node": ">=0.6"
1304
+ },
1305
+ "funding": {
1306
+ "url": "https://github.com/sponsors/ljharb"
1307
+ }
1308
+ },
1309
+ "node_modules/range-parser": {
1310
+ "version": "1.2.1",
1311
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1312
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1313
+ "license": "MIT",
1314
+ "engines": {
1315
+ "node": ">= 0.6"
1316
+ }
1317
+ },
1318
+ "node_modules/rate-limiter-flexible": {
1319
+ "version": "4.0.1",
1320
+ "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz",
1321
+ "integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==",
1322
+ "license": "ISC"
1323
+ },
1324
+ "node_modules/raw-body": {
1325
+ "version": "3.0.2",
1326
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
1327
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
1328
+ "license": "MIT",
1329
+ "dependencies": {
1330
+ "bytes": "~3.1.2",
1331
+ "http-errors": "~2.0.1",
1332
+ "iconv-lite": "~0.7.0",
1333
+ "unpipe": "~1.0.0"
1334
+ },
1335
+ "engines": {
1336
+ "node": ">= 0.10"
1337
+ }
1338
+ },
1339
+ "node_modules/router": {
1340
+ "version": "2.2.0",
1341
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
1342
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
1343
+ "license": "MIT",
1344
+ "dependencies": {
1345
+ "debug": "^4.4.0",
1346
+ "depd": "^2.0.0",
1347
+ "is-promise": "^4.0.0",
1348
+ "parseurl": "^1.3.3",
1349
+ "path-to-regexp": "^8.0.0"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">= 18"
1353
+ }
1354
+ },
1355
+ "node_modules/router/node_modules/debug": {
1356
+ "version": "4.4.3",
1357
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1358
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1359
+ "license": "MIT",
1360
+ "dependencies": {
1361
+ "ms": "^2.1.3"
1362
+ },
1363
+ "engines": {
1364
+ "node": ">=6.0"
1365
+ },
1366
+ "peerDependenciesMeta": {
1367
+ "supports-color": {
1368
+ "optional": true
1369
+ }
1370
+ }
1371
+ },
1372
+ "node_modules/safe-buffer": {
1373
+ "version": "5.2.1",
1374
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1375
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1376
+ "funding": [
1377
+ {
1378
+ "type": "github",
1379
+ "url": "https://github.com/sponsors/feross"
1380
+ },
1381
+ {
1382
+ "type": "patreon",
1383
+ "url": "https://www.patreon.com/feross"
1384
+ },
1385
+ {
1386
+ "type": "consulting",
1387
+ "url": "https://feross.org/support"
1388
+ }
1389
+ ],
1390
+ "license": "MIT"
1391
+ },
1392
+ "node_modules/safer-buffer": {
1393
+ "version": "2.1.2",
1394
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1395
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1396
+ "license": "MIT"
1397
+ },
1398
+ "node_modules/send": {
1399
+ "version": "1.2.1",
1400
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
1401
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
1402
+ "license": "MIT",
1403
+ "dependencies": {
1404
+ "debug": "^4.4.3",
1405
+ "encodeurl": "^2.0.0",
1406
+ "escape-html": "^1.0.3",
1407
+ "etag": "^1.8.1",
1408
+ "fresh": "^2.0.0",
1409
+ "http-errors": "^2.0.1",
1410
+ "mime-types": "^3.0.2",
1411
+ "ms": "^2.1.3",
1412
+ "on-finished": "^2.4.1",
1413
+ "range-parser": "^1.2.1",
1414
+ "statuses": "^2.0.2"
1415
+ },
1416
+ "engines": {
1417
+ "node": ">= 18"
1418
+ },
1419
+ "funding": {
1420
+ "type": "opencollective",
1421
+ "url": "https://opencollective.com/express"
1422
+ }
1423
+ },
1424
+ "node_modules/send/node_modules/debug": {
1425
+ "version": "4.4.3",
1426
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1427
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1428
+ "license": "MIT",
1429
+ "dependencies": {
1430
+ "ms": "^2.1.3"
1431
+ },
1432
+ "engines": {
1433
+ "node": ">=6.0"
1434
+ },
1435
+ "peerDependenciesMeta": {
1436
+ "supports-color": {
1437
+ "optional": true
1438
+ }
1439
+ }
1440
+ },
1441
+ "node_modules/serve-static": {
1442
+ "version": "2.2.1",
1443
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
1444
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
1445
+ "license": "MIT",
1446
+ "dependencies": {
1447
+ "encodeurl": "^2.0.0",
1448
+ "escape-html": "^1.0.3",
1449
+ "parseurl": "^1.3.3",
1450
+ "send": "^1.2.0"
1451
+ },
1452
+ "engines": {
1453
+ "node": ">= 18"
1454
+ },
1455
+ "funding": {
1456
+ "type": "opencollective",
1457
+ "url": "https://opencollective.com/express"
1458
+ }
1459
+ },
1460
+ "node_modules/setprototypeof": {
1461
+ "version": "1.2.0",
1462
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1463
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1464
+ "license": "ISC"
1465
+ },
1466
+ "node_modules/side-channel": {
1467
+ "version": "1.1.0",
1468
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1469
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1470
+ "license": "MIT",
1471
+ "dependencies": {
1472
+ "es-errors": "^1.3.0",
1473
+ "object-inspect": "^1.13.3",
1474
+ "side-channel-list": "^1.0.0",
1475
+ "side-channel-map": "^1.0.1",
1476
+ "side-channel-weakmap": "^1.0.2"
1477
+ },
1478
+ "engines": {
1479
+ "node": ">= 0.4"
1480
+ },
1481
+ "funding": {
1482
+ "url": "https://github.com/sponsors/ljharb"
1483
+ }
1484
+ },
1485
+ "node_modules/side-channel-list": {
1486
+ "version": "1.0.0",
1487
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1488
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1489
+ "license": "MIT",
1490
+ "dependencies": {
1491
+ "es-errors": "^1.3.0",
1492
+ "object-inspect": "^1.13.3"
1493
+ },
1494
+ "engines": {
1495
+ "node": ">= 0.4"
1496
+ },
1497
+ "funding": {
1498
+ "url": "https://github.com/sponsors/ljharb"
1499
+ }
1500
+ },
1501
+ "node_modules/side-channel-map": {
1502
+ "version": "1.0.1",
1503
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1504
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1505
+ "license": "MIT",
1506
+ "dependencies": {
1507
+ "call-bound": "^1.0.2",
1508
+ "es-errors": "^1.3.0",
1509
+ "get-intrinsic": "^1.2.5",
1510
+ "object-inspect": "^1.13.3"
1511
+ },
1512
+ "engines": {
1513
+ "node": ">= 0.4"
1514
+ },
1515
+ "funding": {
1516
+ "url": "https://github.com/sponsors/ljharb"
1517
+ }
1518
+ },
1519
+ "node_modules/side-channel-weakmap": {
1520
+ "version": "1.0.2",
1521
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1522
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1523
+ "license": "MIT",
1524
+ "dependencies": {
1525
+ "call-bound": "^1.0.2",
1526
+ "es-errors": "^1.3.0",
1527
+ "get-intrinsic": "^1.2.5",
1528
+ "object-inspect": "^1.13.3",
1529
+ "side-channel-map": "^1.0.1"
1530
+ },
1531
+ "engines": {
1532
+ "node": ">= 0.4"
1533
+ },
1534
+ "funding": {
1535
+ "url": "https://github.com/sponsors/ljharb"
1536
+ }
1537
+ },
1538
+ "node_modules/statuses": {
1539
+ "version": "2.0.2",
1540
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1541
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1542
+ "license": "MIT",
1543
+ "engines": {
1544
+ "node": ">= 0.8"
1545
+ }
1546
+ },
1547
+ "node_modules/toidentifier": {
1548
+ "version": "1.0.1",
1549
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1550
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1551
+ "license": "MIT",
1552
+ "engines": {
1553
+ "node": ">=0.6"
1554
+ }
1555
+ },
1556
+ "node_modules/tr46": {
1557
+ "version": "0.0.3",
1558
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1559
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
1560
+ "license": "MIT"
1561
+ },
1562
+ "node_modules/tsscmp": {
1563
+ "version": "1.0.6",
1564
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
1565
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
1566
+ "license": "MIT",
1567
+ "engines": {
1568
+ "node": ">=0.6.x"
1569
+ }
1570
+ },
1571
+ "node_modules/type-is": {
1572
+ "version": "2.0.1",
1573
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1574
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1575
+ "license": "MIT",
1576
+ "dependencies": {
1577
+ "content-type": "^1.0.5",
1578
+ "media-typer": "^1.1.0",
1579
+ "mime-types": "^3.0.0"
1580
+ },
1581
+ "engines": {
1582
+ "node": ">= 0.6"
1583
+ }
1584
+ },
1585
+ "node_modules/undici-types": {
1586
+ "version": "5.26.5",
1587
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1588
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
1589
+ "license": "MIT"
1590
+ },
1591
+ "node_modules/universal-user-agent": {
1592
+ "version": "7.0.3",
1593
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
1594
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
1595
+ "license": "ISC"
1596
+ },
1597
+ "node_modules/unpipe": {
1598
+ "version": "1.0.0",
1599
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1600
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1601
+ "license": "MIT",
1602
+ "engines": {
1603
+ "node": ">= 0.8"
1604
+ }
1605
+ },
1606
+ "node_modules/vary": {
1607
+ "version": "1.1.2",
1608
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1609
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1610
+ "license": "MIT",
1611
+ "engines": {
1612
+ "node": ">= 0.8"
1613
+ }
1614
+ },
1615
+ "node_modules/web-streams-polyfill": {
1616
+ "version": "4.0.0-beta.3",
1617
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
1618
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
1619
+ "license": "MIT",
1620
+ "engines": {
1621
+ "node": ">= 14"
1622
+ }
1623
+ },
1624
+ "node_modules/webidl-conversions": {
1625
+ "version": "3.0.1",
1626
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1627
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
1628
+ "license": "BSD-2-Clause"
1629
+ },
1630
+ "node_modules/whatwg-url": {
1631
+ "version": "5.0.0",
1632
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1633
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1634
+ "license": "MIT",
1635
+ "dependencies": {
1636
+ "tr46": "~0.0.3",
1637
+ "webidl-conversions": "^3.0.0"
1638
+ }
1639
+ },
1640
+ "node_modules/which": {
1641
+ "version": "4.0.0",
1642
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
1643
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
1644
+ "license": "ISC",
1645
+ "dependencies": {
1646
+ "isexe": "^3.1.1"
1647
+ },
1648
+ "bin": {
1649
+ "node-which": "bin/which.js"
1650
+ },
1651
+ "engines": {
1652
+ "node": "^16.13.0 || >=18.0.0"
1653
+ }
1654
+ },
1655
+ "node_modules/wrappy": {
1656
+ "version": "1.0.2",
1657
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1658
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1659
+ "license": "ISC"
1660
+ },
1661
+ "node_modules/xcase": {
1662
+ "version": "2.0.1",
1663
+ "resolved": "https://registry.npmjs.org/xcase/-/xcase-2.0.1.tgz",
1664
+ "integrity": "sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==",
1665
+ "license": "MIT"
1666
+ }
1667
+ }
1668
+ }
package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "swe-agent-arena",
3
+ "type": "module",
4
+ "scripts": {
5
+ "start": "node app.js"
6
+ },
7
+ "dependencies": {
8
+ "@huggingface/hub": "^1.x",
9
+ "@octokit/rest": "^21.x",
10
+ "@gitbeaker/rest": "^40.x",
11
+ "cookie-session": "^2.x",
12
+ "diff2html": "^3.x",
13
+ "dotenv": "^16.x",
14
+ "express": "^5.x",
15
+ "openai": "^4.x",
16
+ "which": "^4.x"
17
+ }
18
+ }
public/index.html ADDED
@@ -0,0 +1,684 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>SWE-Agent-Arena</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
8
+ <style>
9
+ :root {
10
+ --bg: #f8f9fa;
11
+ --card: #fff;
12
+ --border: #dee2e6;
13
+ --primary: #4f46e5;
14
+ --primary-hover: #4338ca;
15
+ --text: #212529;
16
+ --muted: #6c757d;
17
+ --success: #198754;
18
+ --danger: #dc3545;
19
+ }
20
+ * { box-sizing: border-box; margin: 0; padding: 0; }
21
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
22
+ .container { max-width: 1400px; margin: 0 auto; padding: 0 20px; }
23
+ header { background: var(--primary); color: #fff; padding: 16px 0; margin-bottom: 24px; }
24
+ header h1 { font-size: 1.5rem; }
25
+ .tabs { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: 24px; }
26
+ .tab { padding: 10px 24px; cursor: pointer; border: none; background: none; font-size: 1rem; color: var(--muted); border-bottom: 2px solid transparent; margin-bottom: -2px; }
27
+ .tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
28
+ .tab-content { display: none; }
29
+ .tab-content.active { display: block; }
30
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 16px; }
31
+ h2 { margin-bottom: 12px; }
32
+ h3 { margin-bottom: 8px; }
33
+ .row { display: flex; gap: 16px; }
34
+ .col { flex: 1; min-width: 0; }
35
+ input[type="text"], textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 0.95rem; font-family: inherit; }
36
+ textarea { resize: vertical; min-height: 60px; }
37
+ button { padding: 10px 20px; border: none; border-radius: 6px; font-size: 0.95rem; cursor: pointer; font-family: inherit; }
38
+ .btn-primary { background: var(--primary); color: #fff; }
39
+ .btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
40
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
41
+ .btn-outline { background: none; border: 1px solid var(--border); color: var(--text); }
42
+ .btn-outline:hover { background: var(--bg); }
43
+ .hidden { display: none !important; }
44
+ .agent-panel { border: 1px solid var(--border); border-radius: 8px; padding: 16px; background: var(--card); }
45
+ .agent-panel h3 { margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
46
+ .agent-output { white-space: pre-wrap; word-wrap: break-word; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.85rem; margin-bottom: 16px; max-height: 400px; overflow-y: auto; }
47
+ .diff-container { margin-top: 12px; max-height: 500px; overflow: auto; position: relative; border: 1px solid var(--border); border-radius: 6px; }
48
+ .diff-container table { width: max-content; min-width: 100%; }
49
+ .vote-panel { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
50
+ .vote-btn { padding: 10px 24px; font-size: 1rem; }
51
+ .vote-btn.selected { outline: 3px solid var(--primary); outline-offset: 2px; }
52
+ table { width: 100%; border-collapse: collapse; }
53
+ th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); }
54
+ th { background: var(--bg); font-weight: 600; position: sticky; top: 0; cursor: pointer; user-select: none; }
55
+ th:hover { background: #e9ecef; }
56
+ tr:hover td { background: #f8f9fa; }
57
+ .leaderboard-wrapper { max-height: 600px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; }
58
+ .search-input { max-width: 300px; margin-bottom: 12px; }
59
+ .hint { color: var(--muted); font-style: italic; margin-bottom: 12px; }
60
+ .thanks { color: var(--success); font-size: 1.2rem; font-weight: 600; padding: 16px; text-align: center; }
61
+ .error-msg { color: var(--danger); padding: 12px; background: #fff5f5; border: 1px solid #fecdd3; border-radius: 6px; margin-bottom: 12px; }
62
+ .loading { text-align: center; padding: 40px; color: var(--muted); }
63
+ .spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; vertical-align: middle; }
64
+ @keyframes spin { to { transform: rotate(360deg); } }
65
+ .citation { background: #f1f5f9; padding: 16px; border-radius: 6px; font-family: monospace; font-size: 0.8rem; white-space: pre-wrap; margin-top: 16px; }
66
+ .user-query { color: #0066cc; background: #f0f7ff; padding: 10px; border-radius: 5px; margin-bottom: 12px; }
67
+ .followup-row { display: flex; gap: 8px; align-items: flex-end; }
68
+ .followup-row input { flex: 1; }
69
+ .tos { color: var(--muted); font-size: 0.85rem; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border); }
70
+ @media (max-width: 768px) { .row { flex-direction: column; } }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <header>
75
+ <div class="container">
76
+ <h1>&#9876;&#65039; SWE-Agent-Arena</h1>
77
+ </div>
78
+ </header>
79
+
80
+ <div class="container">
81
+ <div class="tabs">
82
+ <button class="tab active" data-tab="leaderboard">&#127942; Leaderboard</button>
83
+ <button class="tab" data-tab="arena">&#9876;&#65039; Arena</button>
84
+ </div>
85
+
86
+ <!-- ============ LEADERBOARD TAB ============ -->
87
+ <div id="tab-leaderboard" class="tab-content active">
88
+ <h2>&#127942; LLM4ASE Leaderboard</h2>
89
+ <p>Community-driven evaluation of LLMs on real agentic coding tasks.</p>
90
+ <p class="hint">
91
+ SWE-Agent-Arena pits coding agents pairwise in blind agentic coding battles.
92
+ Each agent uses its own CLI tool and default model, working in identical sandboxed environments.
93
+ Community votes determine the rankings.
94
+ </p>
95
+
96
+ <input type="text" class="search-input" id="leaderboard-search" placeholder="Search agents..." />
97
+
98
+ <div class="leaderboard-wrapper">
99
+ <table id="leaderboard-table">
100
+ <thead>
101
+ <tr>
102
+ <th data-col="Rank">Rank</th>
103
+ <th data-col="Agent">Agent</th>
104
+ <th data-col="Provider">Provider</th>
105
+ <th data-col="Elo Score">Elo Score</th>
106
+ <th data-col="Win Rate">Win Rate</th>
107
+ <th data-col="Conversation Efficiency Index">CEI</th>
108
+ <th data-col="Consistency Score">MCS</th>
109
+ <th data-col="Bradley-Terry Coefficient">Bradley-Terry</th>
110
+ <th data-col="PageRank Score">PageRank</th>
111
+ </tr>
112
+ </thead>
113
+ <tbody id="leaderboard-body"></tbody>
114
+ </table>
115
+ </div>
116
+
117
+ <div class="citation">
118
+ Made with &#10084;&#65039; for SWE-Agent-Arena. If this work is useful to you, please consider citing:
119
+
120
+ @inproceedings{zhao2025se,
121
+ title={SE Arena: An Interactive Platform for Evaluating Foundation Models in Software Engineering},
122
+ author={Zhao, Zhimin},
123
+ booktitle={2025 IEEE/ACM Second International Conference on AI Foundation Models and Software Engineering (Forge)},
124
+ pages={78--81},
125
+ year={2025},
126
+ organization={IEEE}
127
+ }
128
+ </div>
129
+ </div>
130
+
131
+ <!-- ============ ARENA TAB ============ -->
132
+ <div id="tab-arena" class="tab-content">
133
+ <h2>&#9876;&#65039; SWE-Agent-Arena</h2>
134
+ <p>Blind pairwise agentic coding battles &mdash; same scaffold, different CLI agent</p>
135
+
136
+ <div class="card">
137
+ <h3>&#128220; How It Works</h3>
138
+ <ul style="margin-left: 20px; margin-top: 8px;">
139
+ <li><strong>Blind Comparison</strong>: Submit a coding task to two anonymous agents, each a different CLI tool with its own default model.</li>
140
+ <li><strong>Same Scaffold, Different Brain</strong>: Both agents run in identical sandboxed temp directories. Only the CLI agent differs.</li>
141
+ <li><strong>Real Diffs</strong>: Each agent works in its own isolated git repo. You see actual code changes, not just chat.</li>
142
+ <li><strong>Multi-round &amp; Vote</strong>: Send follow-ups, then vote for the better one.</li>
143
+ </ul>
144
+ <p class="hint" style="margin-top: 8px;" id="timeout-hint">Note: Agent sessions that take longer than 10 minutes will be terminated.</p>
145
+ </div>
146
+
147
+ <!-- Auth -->
148
+ <div id="auth-section" class="card">
149
+ <div id="auth-hint">
150
+ <p><strong>Please sign in to vote!</strong></p>
151
+ <p class="hint" id="hint-text"></p>
152
+ </div>
153
+ <button class="btn-primary" id="login-btn" onclick="window.location.href='/auth/login'">Sign in with Hugging Face</button>
154
+ <span id="auth-status" class="hidden" style="color: var(--success); font-weight: 600;">&#10003; Signed in</span>
155
+ </div>
156
+
157
+ <!-- Error -->
158
+ <div id="error-msg" class="error-msg hidden"></div>
159
+
160
+ <!-- Input -->
161
+ <div id="input-section" class="card">
162
+ <input type="text" id="repo-url" placeholder="Optional: Enter any GitHub, GitLab, or Hugging Face URL." style="margin-bottom: 10px;" />
163
+ <textarea id="prompt-input" placeholder="Enter your task for both agents here." rows="3"></textarea>
164
+ <button class="btn-primary" id="battle-btn" disabled style="margin-top: 10px;">Battle!</button>
165
+ </div>
166
+
167
+ <!-- Loading -->
168
+ <div id="loading" class="card hidden">
169
+ <div class="loading">
170
+ <span class="spinner"></span> Running agents... This may take a few minutes.
171
+ </div>
172
+ </div>
173
+
174
+ <!-- User query display -->
175
+ <div id="query-display" class="hidden">
176
+ <div class="user-query" id="query-text"></div>
177
+ </div>
178
+
179
+ <!-- Agent outputs -->
180
+ <div id="results-section" class="hidden">
181
+ <div class="row">
182
+ <div class="col">
183
+ <div class="agent-panel">
184
+ <h3>Agent A</h3>
185
+ <div class="agent-output" id="output-a"></div>
186
+ <div class="diff-container" id="diff-a"></div>
187
+ </div>
188
+ </div>
189
+ <div class="col">
190
+ <div class="agent-panel">
191
+ <h3>Agent B</h3>
192
+ <div class="agent-output" id="output-b"></div>
193
+ <div class="diff-container" id="diff-b"></div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Follow-up inputs -->
199
+ <div class="card" id="followup-section" style="margin-top: 16px;">
200
+ <div class="row">
201
+ <div class="col">
202
+ <div class="followup-row">
203
+ <input type="text" id="followup-a" placeholder="Send follow-up to Agent A..." />
204
+ <button class="btn-outline" id="send-a-btn" disabled>Send to A</button>
205
+ </div>
206
+ </div>
207
+ <div class="col">
208
+ <div class="followup-row">
209
+ <input type="text" id="followup-b" placeholder="Send follow-up to Agent B..." />
210
+ <button class="btn-outline" id="send-b-btn" disabled>Send to B</button>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ <!-- Vote -->
217
+ <div class="card" id="vote-section" style="margin-top: 16px;">
218
+ <h3>Which agent do you prefer?</h3>
219
+ <div class="vote-panel" style="margin-top: 12px;">
220
+ <button class="btn-outline vote-btn" data-vote="left">&#128077; Agent A</button>
221
+ <button class="btn-outline vote-btn" data-vote="right">&#128077; Agent B</button>
222
+ <button class="btn-outline vote-btn" data-vote="tie">&#129309; Tie</button>
223
+ <button class="btn-outline vote-btn" data-vote="both_bad">&#128078; Tie (Both Bad)</button>
224
+ <button class="btn-primary vote-btn" id="submit-vote-btn" disabled>Submit Vote</button>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ <!-- Thanks -->
230
+ <div id="thanks-msg" class="thanks hidden">Thanks for your vote! Identities revealed above.</div>
231
+
232
+ <!-- Reveal -->
233
+ <div id="reveal-section" class="card hidden">
234
+ <p><strong>Agent A:</strong> <span id="reveal-a"></span></p>
235
+ <p><strong>Agent B:</strong> <span id="reveal-b"></span></p>
236
+ </div>
237
+
238
+ <div class="tos">
239
+ <h3>Terms of Service</h3>
240
+ <p><em>Users are required to agree to the following terms before using the service:</em></p>
241
+ <ul style="margin-left: 20px; margin-top: 8px;">
242
+ <li>The service is a <strong>research preview</strong>. It only provides limited safety measures and may generate offensive content.</li>
243
+ <li>It must not be used for any <strong>illegal, harmful, violent, racist, or sexual</strong> purposes.</li>
244
+ <li>Please do not upload any <strong>private</strong> information.</li>
245
+ <li>The service collects user dialogue data and reserves the right to distribute it under a <strong>Creative Commons Attribution (CC-BY)</strong> or similar license.</li>
246
+ </ul>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script>
252
+ <script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
253
+ <script>
254
+ // ---------------------------------------------------------------------------
255
+ // State
256
+ // ---------------------------------------------------------------------------
257
+ let currentBattleId = null;
258
+ let selectedVote = null;
259
+ let isAuthenticated = false;
260
+ let leaderboardData = [];
261
+ let sortCol = "Elo Score";
262
+ let sortAsc = false;
263
+ let followupSetup = false;
264
+ let followupInFlight = { left: false, right: false };
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Tabs
268
+ // ---------------------------------------------------------------------------
269
+ document.querySelectorAll(".tab").forEach((tab) => {
270
+ tab.addEventListener("click", () => {
271
+ document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
272
+ document.querySelectorAll(".tab-content").forEach((c) => c.classList.remove("active"));
273
+ tab.classList.add("active");
274
+ document.getElementById(`tab-${tab.dataset.tab}`).classList.add("active");
275
+ });
276
+ });
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Auth
280
+ // ---------------------------------------------------------------------------
281
+ async function checkAuth() {
282
+ try {
283
+ const resp = await fetch("/auth/status");
284
+ const data = await resp.json();
285
+ isAuthenticated = data.authenticated;
286
+ if (isAuthenticated) {
287
+ document.getElementById("login-btn").classList.add("hidden");
288
+ document.getElementById("auth-hint").classList.add("hidden");
289
+ document.getElementById("auth-status").classList.remove("hidden");
290
+ } else if (data.hint) {
291
+ document.getElementById("hint-text").textContent = data.hint;
292
+ }
293
+ } catch (err) {
294
+ console.error("Auth check failed:", err);
295
+ // Fail open — allow usage without auth check
296
+ }
297
+ }
298
+ checkAuth();
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Config (timeout hint)
302
+ // ---------------------------------------------------------------------------
303
+ fetch("/api/config").then(r => r.json()).then(cfg => {
304
+ const hint = document.getElementById("timeout-hint");
305
+ if (hint) hint.textContent = `Note: Agent sessions that take longer than ${cfg.agentTimeoutMin} minutes will be terminated.`;
306
+ }).catch(() => {});
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Leaderboard
310
+ // ---------------------------------------------------------------------------
311
+ async function loadLeaderboard() {
312
+ try {
313
+ const resp = await fetch("/api/leaderboard");
314
+ leaderboardData = await resp.json();
315
+ renderLeaderboard();
316
+ } catch (err) {
317
+ console.error("Failed to load leaderboard:", err);
318
+ }
319
+ }
320
+
321
+ function renderLeaderboard(filter = "") {
322
+ const tbody = document.getElementById("leaderboard-body");
323
+ const filtered = filter
324
+ ? leaderboardData.filter((r) =>
325
+ (r.Agent || "").toLowerCase().includes(filter.toLowerCase())
326
+ )
327
+ : leaderboardData;
328
+
329
+ // Sort
330
+ const sorted = [...filtered].sort((a, b) => {
331
+ const va = a[sortCol] ?? 0;
332
+ const vb = b[sortCol] ?? 0;
333
+ if (typeof va === "string") return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
334
+ return sortAsc ? va - vb : vb - va;
335
+ });
336
+
337
+ tbody.innerHTML = sorted
338
+ .map(
339
+ (r) => `<tr>
340
+ <td>${r.Rank ?? ""}</td>
341
+ <td>${r.Agent ?? ""}</td>
342
+ <td>${r.Provider ?? ""}</td>
343
+ <td>${r["Elo Score"] ?? ""}</td>
344
+ <td>${r["Win Rate"] != null ? r["Win Rate"].toFixed(2) : ""}</td>
345
+ <td>${r["Conversation Efficiency Index"] != null ? r["Conversation Efficiency Index"] : ""}</td>
346
+ <td>${r["Consistency Score"] != null ? r["Consistency Score"] : ""}</td>
347
+ <td>${r["Bradley-Terry Coefficient"] != null ? Number(r["Bradley-Terry Coefficient"]).toFixed(4) : ""}</td>
348
+ <td>${r["PageRank Score"] != null ? Number(r["PageRank Score"]).toFixed(4) : ""}</td>
349
+ </tr>`
350
+ )
351
+ .join("");
352
+ }
353
+
354
+ document.getElementById("leaderboard-search").addEventListener("input", (e) => {
355
+ renderLeaderboard(e.target.value);
356
+ });
357
+
358
+ document.querySelectorAll("#leaderboard-table th").forEach((th) => {
359
+ th.addEventListener("click", () => {
360
+ const col = th.dataset.col;
361
+ if (sortCol === col) sortAsc = !sortAsc;
362
+ else { sortCol = col; sortAsc = false; }
363
+ renderLeaderboard(document.getElementById("leaderboard-search").value);
364
+ });
365
+ });
366
+
367
+ loadLeaderboard();
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Battle
371
+ // ---------------------------------------------------------------------------
372
+ const promptInput = document.getElementById("prompt-input");
373
+ const battleBtn = document.getElementById("battle-btn");
374
+
375
+ promptInput.addEventListener("input", () => {
376
+ battleBtn.disabled = !promptInput.value.trim();
377
+ });
378
+
379
+ battleBtn.addEventListener("click", startBattle);
380
+
381
+ let pollTimer = null;
382
+
383
+ async function startBattle() {
384
+ const prompt = promptInput.value.trim();
385
+ if (!prompt) return;
386
+ const repoUrl = document.getElementById("repo-url").value.trim();
387
+
388
+ // Reset UI
389
+ hideEl("error-msg");
390
+ hideEl("results-section");
391
+ hideEl("thanks-msg");
392
+ hideEl("reveal-section");
393
+ hideEl("query-display");
394
+ showEl("loading");
395
+ battleBtn.disabled = true;
396
+ promptInput.disabled = true;
397
+ document.getElementById("repo-url").disabled = true;
398
+ currentBattleId = null;
399
+ selectedVote = null;
400
+ followupSetup = false;
401
+ followupInFlight = { left: false, right: false };
402
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
403
+
404
+ try {
405
+ const resp = await fetch("/api/battle/start", {
406
+ method: "POST",
407
+ headers: { "Content-Type": "application/json" },
408
+ body: JSON.stringify({ prompt, repoUrl }),
409
+ });
410
+ const data = await resp.json();
411
+
412
+ if (!resp.ok) {
413
+ showError(data.error || "Battle failed.");
414
+ resetInputs();
415
+ hideEl("loading");
416
+ return;
417
+ }
418
+
419
+ currentBattleId = data.battleId;
420
+
421
+ // Display query
422
+ document.getElementById("query-text").innerHTML =
423
+ `<strong>Your Query:</strong> ${escapeHtml(prompt)}` +
424
+ (repoUrl ? `<br/><strong>URL:</strong> ${escapeHtml(repoUrl)}` : "");
425
+ showEl("query-display");
426
+
427
+ // Show panels immediately with "working" placeholders
428
+ document.getElementById("output-a").textContent = "Agent is working...";
429
+ document.getElementById("output-b").textContent = "Agent is working...";
430
+ document.getElementById("diff-a").innerHTML = "";
431
+ document.getElementById("diff-b").innerHTML = "";
432
+ showEl("results-section");
433
+ hideEl("followup-section");
434
+ hideEl("vote-section");
435
+ hideEl("input-section");
436
+ hideEl("loading");
437
+
438
+ // Start polling for live output
439
+ pollBattleStatus();
440
+ } catch (err) {
441
+ showError(err.message);
442
+ resetInputs();
443
+ hideEl("loading");
444
+ }
445
+ }
446
+
447
+ function pollBattleStatus() {
448
+ const outputA = document.getElementById("output-a");
449
+ const outputB = document.getElementById("output-b");
450
+ let leftDone = false, rightDone = false;
451
+ let lastLeftDiff = null, lastRightDiff = null;
452
+
453
+ pollTimer = setInterval(async () => {
454
+ if (!currentBattleId) { clearInterval(pollTimer); return; }
455
+ try {
456
+ const resp = await fetch(`/api/battle/status/${currentBattleId}`);
457
+ if (!resp.ok) return;
458
+ const data = await resp.json();
459
+
460
+ // Update output panels with live text
461
+ outputA.textContent = data.leftOutput || (data.leftStatus === "running" ? "Agent is working..." : "(no output)");
462
+ outputB.textContent = data.rightOutput || (data.rightStatus === "running" ? "Agent is working..." : "(no output)");
463
+
464
+ // Auto-scroll output panels
465
+ outputA.scrollTop = outputA.scrollHeight;
466
+ outputB.scrollTop = outputB.scrollHeight;
467
+
468
+ // Render diff when a side finishes
469
+ if (data.leftStatus === "done" && data.leftDiff !== lastLeftDiff) {
470
+ lastLeftDiff = data.leftDiff;
471
+ renderDiff("diff-a", data.leftDiff);
472
+ }
473
+ if (data.rightStatus === "done" && data.rightDiff !== lastRightDiff) {
474
+ lastRightDiff = data.rightDiff;
475
+ renderDiff("diff-b", data.rightDiff);
476
+ }
477
+
478
+ // Show followup as soon as either side finishes
479
+ if (data.leftStatus === "done" || data.rightStatus === "done") {
480
+ showEl("followup-section");
481
+ if (!followupSetup) {
482
+ setupFollowup();
483
+ followupSetup = true;
484
+ }
485
+ // Enable/disable each side independently, respecting in-flight state
486
+ if (!followupInFlight.left) {
487
+ document.getElementById("followup-a").disabled = data.leftStatus !== "done";
488
+ document.getElementById("send-a-btn").disabled = data.leftStatus !== "done" || !document.getElementById("followup-a").value.trim();
489
+ }
490
+ if (!followupInFlight.right) {
491
+ document.getElementById("followup-b").disabled = data.rightStatus !== "done";
492
+ document.getElementById("send-b-btn").disabled = data.rightStatus !== "done" || !document.getElementById("followup-b").value.trim();
493
+ }
494
+ }
495
+
496
+ // Both done — stop polling, show voting
497
+ if (data.leftStatus === "done" && data.rightStatus === "done") {
498
+ clearInterval(pollTimer);
499
+ pollTimer = null;
500
+ showEl("vote-section");
501
+ document.querySelectorAll(".vote-btn").forEach((b) => b.classList.remove("selected"));
502
+ document.getElementById("submit-vote-btn").disabled = true;
503
+ }
504
+ } catch {
505
+ // ignore transient polling errors
506
+ }
507
+ }, 1000);
508
+ }
509
+
510
+ // ---------------------------------------------------------------------------
511
+ // Followup
512
+ // ---------------------------------------------------------------------------
513
+ function setupFollowup() {
514
+ const followupA = document.getElementById("followup-a");
515
+ const followupB = document.getElementById("followup-b");
516
+ const sendABtn = document.getElementById("send-a-btn");
517
+ const sendBBtn = document.getElementById("send-b-btn");
518
+
519
+ followupA.value = "";
520
+ followupB.value = "";
521
+
522
+ followupA.oninput = () => { sendABtn.disabled = !followupA.value.trim(); };
523
+ followupB.oninput = () => { sendBBtn.disabled = !followupB.value.trim(); };
524
+
525
+ sendABtn.onclick = () => sendFollowup("left", followupA, sendABtn, "output-a", "diff-a");
526
+ sendBBtn.onclick = () => sendFollowup("right", followupB, sendBBtn, "output-b", "diff-b");
527
+ }
528
+
529
+ async function sendFollowup(side, input, btn, outputId, diffId) {
530
+ const prompt = input.value.trim();
531
+ if (!prompt || !currentBattleId) return;
532
+
533
+ followupInFlight[side] = true;
534
+ input.disabled = true;
535
+ btn.disabled = true;
536
+ btn.textContent = "Processing...";
537
+
538
+ try {
539
+ const resp = await fetch("/api/battle/followup", {
540
+ method: "POST",
541
+ headers: { "Content-Type": "application/json" },
542
+ body: JSON.stringify({ battleId: currentBattleId, side, prompt }),
543
+ });
544
+ const data = await resp.json();
545
+
546
+ if (!resp.ok) {
547
+ showError(data.error || "Followup failed.");
548
+ return;
549
+ }
550
+
551
+ // Append output
552
+ const outputEl = document.getElementById(outputId);
553
+ outputEl.textContent += `\n\n--- Follow-up ---\nUser: ${prompt}\n\nAgent: ${data.output || "(no output)"}`;
554
+ renderDiff(diffId, data.diff);
555
+ } catch (err) {
556
+ showError(err.message);
557
+ } finally {
558
+ followupInFlight[side] = false;
559
+ input.value = "";
560
+ input.disabled = false;
561
+ btn.disabled = true;
562
+ btn.textContent = side === "left" ? "Send to A" : "Send to B";
563
+ }
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // Voting
568
+ // ---------------------------------------------------------------------------
569
+ document.querySelectorAll(".vote-btn[data-vote]").forEach((btn) => {
570
+ btn.addEventListener("click", () => {
571
+ document.querySelectorAll(".vote-btn[data-vote]").forEach((b) => b.classList.remove("selected"));
572
+ btn.classList.add("selected");
573
+ selectedVote = btn.dataset.vote;
574
+ document.getElementById("submit-vote-btn").disabled = false;
575
+ });
576
+ });
577
+
578
+ document.getElementById("submit-vote-btn").addEventListener("click", submitVote);
579
+
580
+ async function submitVote() {
581
+ if (!selectedVote || !currentBattleId) return;
582
+
583
+ document.getElementById("submit-vote-btn").disabled = true;
584
+
585
+ try {
586
+ const resp = await fetch("/api/battle/vote", {
587
+ method: "POST",
588
+ headers: { "Content-Type": "application/json" },
589
+ body: JSON.stringify({ battleId: currentBattleId, winner: selectedVote }),
590
+ });
591
+ const data = await resp.json();
592
+
593
+ if (!resp.ok) {
594
+ showError(data.error || "Vote failed.");
595
+ return;
596
+ }
597
+
598
+ // Reveal agents
599
+ document.getElementById("reveal-a").textContent = data.agentA || "Unknown";
600
+ document.getElementById("reveal-b").textContent = data.agentB || "Unknown";
601
+ showEl("reveal-section");
602
+ showEl("thanks-msg");
603
+
604
+ // Hide vote + followup
605
+ hideEl("vote-section");
606
+ hideEl("followup-section");
607
+
608
+ // Update leaderboard
609
+ if (data.leaderboard && data.leaderboard.length) {
610
+ leaderboardData = data.leaderboard;
611
+ renderLeaderboard(document.getElementById("leaderboard-search").value);
612
+ }
613
+
614
+ currentBattleId = null;
615
+
616
+ // Re-enable for next battle after a delay
617
+ setTimeout(() => {
618
+ hideEl("results-section");
619
+ hideEl("thanks-msg");
620
+ hideEl("reveal-section");
621
+ hideEl("query-display");
622
+ // Reset internal visibility of nested sections for next battle
623
+ document.getElementById("vote-section").classList.remove("hidden");
624
+ document.getElementById("followup-section").classList.remove("hidden");
625
+ // Show input and reset fields
626
+ showEl("input-section");
627
+ resetInputs();
628
+ }, 5000);
629
+ } catch (err) {
630
+ showError(err.message);
631
+ document.getElementById("submit-vote-btn").disabled = false;
632
+ }
633
+ }
634
+
635
+ // ---------------------------------------------------------------------------
636
+ // Helpers
637
+ // ---------------------------------------------------------------------------
638
+ function showEl(id) { document.getElementById(id).classList.remove("hidden"); }
639
+ function hideEl(id) { document.getElementById(id).classList.add("hidden"); }
640
+
641
+ function showError(msg) {
642
+ const el = document.getElementById("error-msg");
643
+ el.textContent = msg;
644
+ el.classList.remove("hidden");
645
+ setTimeout(() => el.classList.add("hidden"), 8000);
646
+ }
647
+
648
+ function resetInputs() {
649
+ promptInput.disabled = false;
650
+ promptInput.value = "";
651
+ document.getElementById("repo-url").disabled = false;
652
+ battleBtn.disabled = true;
653
+ showEl("input-section");
654
+ }
655
+
656
+ function escapeHtml(str) {
657
+ const div = document.createElement("div");
658
+ div.textContent = str;
659
+ return div.innerHTML;
660
+ }
661
+
662
+ function renderDiff(containerId, diffStr) {
663
+ const container = document.getElementById(containerId);
664
+ if (!diffStr || !diffStr.trim()) {
665
+ container.innerHTML = "<em>No diff</em>";
666
+ return;
667
+ }
668
+ try {
669
+ const targetEl = document.createElement("div");
670
+ container.innerHTML = "";
671
+ container.appendChild(targetEl);
672
+ const diff2htmlUi = new Diff2HtmlUI(targetEl, diffStr, {
673
+ drawFileList: false,
674
+ matching: "lines",
675
+ outputFormat: "side-by-side",
676
+ });
677
+ diff2htmlUi.draw();
678
+ } catch {
679
+ container.innerHTML = `<pre>${escapeHtml(diffStr)}</pre>`;
680
+ }
681
+ }
682
+ </script>
683
+ </body>
684
+ </html>