+{defaultValue}-+{customValue}diff --git a/apps/web-roo-code/.env.example b/apps/web-roo-code/.env.example index 734a9a2a08..01258bc500 100644 --- a/apps/web-roo-code/.env.example +++ b/apps/web-roo-code/.env.example @@ -6,3 +6,7 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # Basin Form Endpoint for Static Form Submissions # Replace this with your actual Basin form endpoint (e.g., https://usebasin.com/f/your-form-id) NEXT_PUBLIC_BASIN_ENDPOINT=https://usebasin.com/f/your-form-id-here + +# Site URL Configuration +# Used for generating absolute URLs in sitemap and other contexts +NEXT_PUBLIC_SITE_URL=https://roocode.com diff --git a/apps/web-roo-code/.gitignore b/apps/web-roo-code/.gitignore index 7b8da95f5e..6f31e99c00 100644 --- a/apps/web-roo-code/.gitignore +++ b/apps/web-roo-code/.gitignore @@ -40,3 +40,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# generated files +/public/sitemap*.xml +/public/robots.txt diff --git a/apps/web-roo-code/.npmrc b/apps/web-roo-code/.npmrc new file mode 100644 index 0000000000..b7425b9ee8 --- /dev/null +++ b/apps/web-roo-code/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts=true \ No newline at end of file diff --git a/apps/web-roo-code/next-sitemap.config.cjs b/apps/web-roo-code/next-sitemap.config.cjs new file mode 100644 index 0000000000..e9b0ca3c47 --- /dev/null +++ b/apps/web-roo-code/next-sitemap.config.cjs @@ -0,0 +1,73 @@ +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://roocode.com', + generateRobotsTxt: true, + generateIndexSitemap: false, // We don't need index sitemap for a small site + changefreq: 'monthly', + priority: 0.7, + sitemapSize: 5000, + exclude: [ + '/api/*', + '/server-sitemap-index.xml', + '/404', + '/500', + '/_not-found', + ], + robotsTxtOptions: { + policies: [ + { + userAgent: '*', + allow: '/', + }, + ], + additionalSitemaps: [ + // Add any additional sitemaps here if needed in the future + ], + }, + // Custom transform function to set specific priorities and change frequencies + transform: async (config, path) => { + // Set custom priority for specific pages + let priority = config.priority; + let changefreq = config.changefreq; + + if (path === '/') { + priority = 1.0; + changefreq = 'yearly'; + } else if (path === '/enterprise' || path === '/evals') { + priority = 0.8; + changefreq = 'monthly'; + } else if (path === '/privacy' || path === '/terms') { + priority = 0.5; + changefreq = 'yearly'; + } + + return { + loc: path, + changefreq, + priority, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + alternateRefs: config.alternateRefs ?? [], + }; + }, + additionalPaths: async (config) => { + // Add any additional paths that might not be automatically discovered + // This is useful for dynamic routes or API-generated pages + // Add the /evals page since it's a dynamic route + return [{ + loc: '/evals', + changefreq: 'monthly', + priority: 0.8, + lastmod: new Date().toISOString(), + }]; + + // Add the /evals page since it's a dynamic route + result.push({ + loc: '/evals', + changefreq: 'monthly', + priority: 0.8, + lastmod: new Date().toISOString(), + }); + + return result; + }, +}; \ No newline at end of file diff --git a/apps/web-roo-code/package.json b/apps/web-roo-code/package.json index 02812dc471..fa59cacccb 100644 --- a/apps/web-roo-code/package.json +++ b/apps/web-roo-code/package.json @@ -7,7 +7,9 @@ "check-types": "tsc --noEmit", "dev": "next dev", "build": "next build", - "start": "next start" + "postbuild": "next-sitemap --config next-sitemap.config.cjs", + "start": "next start", + "clean": "rimraf .next .turbo" }, "dependencies": { "@radix-ui/react-dialog": "^1.1.14", @@ -41,6 +43,7 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "autoprefixer": "^10.4.21", + "next-sitemap": "^4.2.3", "postcss": "^8.5.4", "tailwindcss": "^3.4.17" } diff --git a/apps/web-roo-code/public/RooCode-Badge-blk.svg b/apps/web-roo-code/public/RooCode-Badge-blk.svg new file mode 100644 index 0000000000..0ee7987cbb --- /dev/null +++ b/apps/web-roo-code/public/RooCode-Badge-blk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web-roo-code/public/RooCode-Badge-white.svg b/apps/web-roo-code/public/RooCode-Badge-white.svg new file mode 100644 index 0000000000..8e406be8cd --- /dev/null +++ b/apps/web-roo-code/public/RooCode-Badge-white.svg @@ -0,0 +1,6180 @@ + + \ No newline at end of file diff --git a/apps/web-roo-code/src/app/enterprise/page.tsx b/apps/web-roo-code/src/app/enterprise/page.tsx index d2c38fba05..7a1d36f068 100644 --- a/apps/web-roo-code/src/app/enterprise/page.tsx +++ b/apps/web-roo-code/src/app/enterprise/page.tsx @@ -1,16 +1,64 @@ -import { Code, CheckCircle, Shield, Users, Zap, Workflow, Lock } from "lucide-react" +import { Code, CheckCircle, Shield, Zap, Workflow, Lock, ArrowRight, DollarSign, Search, Network } from "lucide-react" import { Button } from "@/components/ui" import { AnimatedText } from "@/components/animated-text" import { AnimatedBackground } from "@/components/homepage" import { ContactForm } from "@/components/enterprise/contact-form" import { EXTERNAL_LINKS } from "@/lib/constants" +import type { Metadata } from "next" +import { SEO } from "@/lib/seo" + +const TITLE = "Enterprise Solution" +const DESCRIPTION = + "The control-plane for AI-powered software development. Gain visibility, governance, and control over your AI coding initiatives." +const PATH = "/enterprise" +const OG_IMAGE = SEO.ogImage + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: OG_IMAGE.url, + width: OG_IMAGE.width, + height: OG_IMAGE.height, + alt: OG_IMAGE.alt, + }, + ], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [OG_IMAGE.url], + }, + keywords: [ + ...SEO.keywords, + "Enterprise AI", + "AI governance", + "AI control-plane", + "developer productivity", + "SAML", + "SCIM", + "cost management", + ], +} export default async function Enterprise() { return ( <> {/* Hero Section */} -+ @@ -34,26 +82,19 @@ export default async function Enterprise() {-@@ -110,9 +151,11 @@ export default async function Enterprise() {{/* Card 1 */} ---+ +{/* Card 2 */} -+++ Centralized AI Management Hub
@@ -136,9 +179,11 @@ export default async function Enterprise() {
--+ +{/* Card 3 */} -+++ Real-Time Usage Visibility
@@ -161,9 +206,11 @@ export default async function Enterprise() {
--+ +{/* Card 4 */} -+++ Enterprise-Grade Governance
@@ -187,9 +234,11 @@ export default async function Enterprise() {
--+ +{/* Card 5 */} -+++ 5-Minute Control-Plane Setup
@@ -213,9 +262,11 @@ export default async function Enterprise() {
--+ +{/* Card 6 */} -+++ Manage AI Development Costs
@@ -238,9 +289,11 @@ export default async function Enterprise() {
--+ ++++ Zero Friction for Developers
@@ -392,8 +445,10 @@ export default async function Enterprise() {
-@@ -444,30 +510,44 @@ export default async function Enterprise() { {/* CTA Section */} ---+ +++ Enterprise-Grade Security
@@ -423,20 +478,31 @@ export default async function Enterprise() {
+--+ ++++ Security-First Design
-+
Every feature built with enterprise security requirements in mind
++ + View Security Details + + + - - View Security Details - - - - --- Ready to Transform Your Development Process? -
-- Join our early access program and be among the first to experience the power of Roo Code - Cloud for Enterprise. -
---Become an Early Access Partner
-- Collaborate in shaping Roo Code's enterprise solution. +
+ + ++diff --git a/apps/web-roo-code/src/app/evals/page.tsx b/apps/web-roo-code/src/app/evals/page.tsx index b97b025348..a6af30d70e 100644 --- a/apps/web-roo-code/src/app/evals/page.tsx +++ b/apps/web-roo-code/src/app/evals/page.tsx @@ -1,25 +1,45 @@ import type { Metadata } from "next" import { getEvalRuns } from "@/actions/evals" +import { SEO } from "@/lib/seo" import { Evals } from "./evals" export const revalidate = 300 export const dynamic = "force-dynamic" +const TITLE = "Evals" +const DESCRIPTION = "Explore quantitative evals of LLM coding skills across tasks and providers." +const PATH = "/evals" +const IMAGE = { + url: "https://i.imgur.com/ijP7aZm.png", + width: 1954, + height: 1088, + alt: "Roo Code Evals – LLM coding benchmarks", +} + export const metadata: Metadata = { - title: "Roo Code Evals", - openGraph: { - title: "Roo Code Evals", - description: "Quantitative evals of LLM coding skills.", - url: "https://roocode.com/evals", - siteName: "Roo Code", - images: { - url: "https://i.imgur.com/ijP7aZm.png", - width: 1954, - height: 1088, - }, + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [IMAGE], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [IMAGE.url], + }, + keywords: [...SEO.keywords, "benchmarks", "LLM evals", "coding evaluations", "model comparison"], } export default async function Page() { diff --git a/apps/web-roo-code/src/app/layout.tsx b/apps/web-roo-code/src/app/layout.tsx index 132a8b31d7..ac33c8920f 100644 --- a/apps/web-roo-code/src/app/layout.tsx +++ b/apps/web-roo-code/src/app/layout.tsx @@ -2,6 +2,7 @@ import React from "react" import type { Metadata } from "next" import { Inter } from "next/font/google" import Script from "next/script" +import { SEO } from "@/lib/seo" import { Providers } from "@/components/providers" @@ -12,11 +13,14 @@ import "./globals.css" const inter = Inter({ subsets: ["latin"] }) export const metadata: Metadata = { - title: "Roo Code – Your AI-Powered Dev Team in VS Code", - description: - "Roo Code puts an entire AI dev team right in your editor, outpacing closed tools with deep project-wide context, multi-step agentic coding, and unmatched developer-centric flexibility.", + metadataBase: new URL(SEO.url), + title: { + template: "%s | Roo Code", + default: SEO.title, + }, + description: SEO.description, alternates: { - canonical: "https://roocode.com", + canonical: SEO.url, }, icons: { icon: [ @@ -40,6 +44,42 @@ export const metadata: Metadata = { }, ], }, + openGraph: { + title: SEO.title, + description: SEO.description, + url: SEO.url, + siteName: SEO.name, + images: [ + { + url: SEO.ogImage.url, + width: SEO.ogImage.width, + height: SEO.ogImage.height, + alt: SEO.ogImage.alt, + }, + ], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: SEO.title, + description: SEO.description, + images: [SEO.ogImage.url], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-snippet": -1, + "max-image-preview": "large", + "max-video-preview": -1, + }, + }, + keywords: [...SEO.keywords], + applicationName: SEO.name, + category: SEO.category, } export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -64,8 +104,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) `}+ ++-+ Ready to Transform Your Development Process? +
++ Join our early access program and be among the first to experience the power of Roo + Code Cloud for Enterprise.
-- -Request a Demo
-- See Roo Code's enterprise capabilities in action. -
-+ +++Become an Early Access Partner
++ Collaborate in shaping Roo Code's enterprise solution. +
++ ++Request a Demo
++ See Roo Code's enterprise capabilities in action. +
++ - - + +{children} diff --git a/apps/web-roo-code/src/app/page.tsx b/apps/web-roo-code/src/app/page.tsx index 971332b105..a7fd810a66 100644 --- a/apps/web-roo-code/src/app/page.tsx +++ b/apps/web-roo-code/src/app/page.tsx @@ -21,7 +21,7 @@ export default async function Home() { return ( <> -+ @@ -64,12 +64,15 @@ export default async function Home() { -diff --git a/apps/web-roo-code/src/app/privacy/page.tsx b/apps/web-roo-code/src/app/privacy/page.tsx index ea12094ebb..b0efdbc444 100644 --- a/apps/web-roo-code/src/app/privacy/page.tsx +++ b/apps/web-roo-code/src/app/privacy/page.tsx @@ -1,9 +1,41 @@ -import { Metadata } from "next" +import type { Metadata } from "next" +import { SEO } from "@/lib/seo" + +const TITLE = "Privacy Policy" +const DESCRIPTION = + "Privacy policy for Roo Code Cloud and marketing website. Learn how we handle your data and protect your privacy." +const PATH = "/privacy" +const OG_IMAGE = SEO.ogImage export const metadata: Metadata = { - title: "Privacy Policy - Roo Code", - description: - "Privacy policy for Roo Code Cloud and marketing website. Learn how we handle your data and protect your privacy.", + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: OG_IMAGE.url, + width: OG_IMAGE.width, + height: OG_IMAGE.height, + alt: OG_IMAGE.alt, + }, + ], + locale: SEO.locale, + type: "article", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [OG_IMAGE.url], + }, + keywords: [...SEO.keywords, "privacy", "data protection", "GDPR", "security"], } export default function Privacy() { @@ -14,7 +46,7 @@ export default function Privacy() {+ - View Documentation + For Enterprise Roo Code Cloud Privacy Policy
-Last Updated: June 19, 2025
+Last Updated: August 20, 2025
This Privacy Policy explains how Roo Code, Inc. ("Roo Code," "we," @@ -50,9 +82,14 @@ export default function Privacy() {
Quick Summary
- - Your source code never transits Roo Code servers. It stays on your device - and is sent directly—via a client‑to‑provider TLS connection—to the - third‑party AI model you select. Roo Code never stores, inspects, or trains on your code. + + Your source code does not transit Roo Code servers unless you explicitly choose Roo Code + as a model provider (proxy mode). + {" "} + When Roo Code Cloud is your model provider, your code briefly transits Roo Code servers only + to forward it to the upstream model, is not stored, and is deleted immediately after + forwarding. Otherwise, your code is sent directly—via client‑to‑provider + TLS—to the model you select. Roo Code never stores, inspects, or trains on your code.
- Prompts and chat snippets are collected by default in Roo Code Cloud so you @@ -168,10 +205,12 @@ export default function Privacy() { Code & files you work on
- Your chosen model provider (direct client → provider TLS) + Your chosen model provider (direct client → provider TLS), or Roo Code (proxy + mode; transit‑only) when you select Roo Code as the provider - Roo Code servers; ad networks; model‑training pipelines + Roo Code servers (except proxy mode; transit‑only, no storage); ad networks; + model‑training pipelines diff --git a/apps/web-roo-code/src/app/robots.ts b/apps/web-roo-code/src/app/robots.ts new file mode 100644 index 0000000000..fcdda5031e --- /dev/null +++ b/apps/web-roo-code/src/app/robots.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next" +import { SEO } from "@/lib/seo" + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: `${SEO.url}/sitemap.xml`, + host: SEO.url, + } +} diff --git a/apps/web-roo-code/src/app/sitemap.xml b/apps/web-roo-code/src/app/sitemap.xml deleted file mode 100644 index 43ac973306..0000000000 --- a/apps/web-roo-code/src/app/sitemap.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/web-roo-code/src/app/terms/page.tsx b/apps/web-roo-code/src/app/terms/page.tsx index 545e577457..f5aea3d663 100644 --- a/apps/web-roo-code/src/app/terms/page.tsx +++ b/apps/web-roo-code/src/app/terms/page.tsx @@ -1,9 +1,41 @@ -import { Metadata } from "next" +import type { Metadata } from "next" +import { SEO } from "@/lib/seo" + +const TITLE = "Terms of Service" +const DESCRIPTION = + "Terms of Service for Roo Code Cloud. Learn about our service terms, commercial conditions, and legal framework." +const PATH = "/terms" +const OG_IMAGE = SEO.ogImage export const metadata: Metadata = { - title: "Terms of Service - Roo Code", - description: - "Terms of Service for Roo Code Cloud. Learn about our service terms, commercial conditions, and legal framework.", + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: OG_IMAGE.url, + width: OG_IMAGE.width, + height: OG_IMAGE.height, + alt: OG_IMAGE.alt, + }, + ], + locale: SEO.locale, + type: "article", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [OG_IMAGE.url], + }, + keywords: [...SEO.keywords, "terms of service", "legal", "agreement", "subscription"], } export default function Terms() { diff --git a/apps/web-roo-code/src/components/chromes/footer.tsx b/apps/web-roo-code/src/components/chromes/footer.tsx index 4c2b036190..b6a17cebe5 100644 --- a/apps/web-roo-code/src/components/chromes/footer.tsx +++ b/apps/web-roo-code/src/components/chromes/footer.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from "react" import Link from "next/link" import Image from "next/image" import { ChevronDown } from "lucide-react" -import { FaBluesky, FaDiscord, FaGithub, FaLinkedin, FaReddit, FaTiktok, FaXTwitter, FaYoutube } from "react-icons/fa6" +import { useTheme } from "next-themes" import { EXTERNAL_LINKS, INTERNAL_LINKS } from "@/lib/constants" import { useLogoSrc } from "@/lib/hooks/use-logo-src" @@ -14,6 +14,7 @@ export function Footer() { const [privacyDropdownOpen, setPrivacyDropdownOpen] = useState(false) const dropdownRef = useRef- -https://roocode.com/ -2025-03-13T22:26:09Z -yearly -1.0 -(null) const logoSrc = useLogoSrc() + const { resolvedTheme } = useTheme() // Close dropdown when clicking outside useEffect(() => { @@ -39,72 +40,21 @@ export function Footer() { Empowering developers to build better software faster with AI-powered tools and insights.
-- -+ + {/* Made with Roo Code */} + +- GitHub - - - - Discord - - - - Reddit - - - - X - - - - LinkedIn - - - - Bluesky - - - - TikTok - - - - YouTube - - + @@ -126,6 +76,15 @@ export function Footer() { Enterprise +- -- + + Evals + +
- -
- -
- Testimonials - -- -
Resources
+
- + + FAQ + +
- - Documentation + Docs
- @@ -183,40 +144,6 @@ export function Footer() { Tutorials
- - - Community - -
-- - - Discord - -
-- - - Reddit - -
- -- -diff --git a/apps/web-roo-code/src/components/chromes/nav-bar.tsx b/apps/web-roo-code/src/components/chromes/nav-bar.tsx index 336c6236e1..ca6a4d4b4f 100644 --- a/apps/web-roo-code/src/components/chromes/nav-bar.tsx +++ b/apps/web-roo-code/src/components/chromes/nav-bar.tsx @@ -46,11 +46,6 @@ export function NavBar({ stars, downloads }: NavBarProps) { className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground max-lg:hidden"> Testimonials -++++Company
- @@ -263,6 +194,24 @@ export function Footer() { Careers
+- + + Blog + +
+- + + Testimonials + +
+Connect
++
+- + + GitHub + +
+- + + Discord + +
+- + + Reddit + +
+- + + X / Twitter + +
+- + + LinkedIn + +
+- + + Bluesky + +
+- + + TikTok + +
+- + + YouTube + +
+- FAQ - @@ -68,10 +63,11 @@ export function NavBar({ stars, downloads }: NavBarProps) { Docs - Careers + Communitydiff --git a/apps/web-roo-code/src/components/homepage/features-mobile.tsx b/apps/web-roo-code/src/components/homepage/features-mobile.tsx index 7e623ecfd4..e924afd4bf 100644 --- a/apps/web-roo-code/src/components/homepage/features-mobile.tsx +++ b/apps/web-roo-code/src/components/homepage/features-mobile.tsx @@ -60,10 +60,10 @@ export function FeaturesMobile() {@@ -102,7 +98,7 @@ export function NavBar({ stars, downloads }: NavBarProps) { + className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-all duration-200 hover:bg-primary/80 hover:shadow-lg hover:scale-105 md:flex">Install · @@ -150,12 +146,6 @@ export function NavBar({ stars, downloads }: NavBarProps) { onClick={() => setIsMenuOpen(false)}> Testimonials - setIsMenuOpen(false)}> - FAQ - setIsMenuOpen(false)}> - Careers + Community
diff --git a/apps/web-roo-code/src/components/enterprise/contact-form.tsx b/apps/web-roo-code/src/components/enterprise/contact-form.tsx index b90435e1f3..00909f9fef 100644 --- a/apps/web-roo-code/src/components/enterprise/contact-form.tsx +++ b/apps/web-roo-code/src/components/enterprise/contact-form.tsx @@ -149,7 +149,7 @@ export function ContactForm({ formType, buttonText, buttonClassName }: ContactFo return ({features.map((feature, index) => (--++-{feature.icon}+{feature.icon}{feature.title}
diff --git a/apps/web-roo-code/src/components/homepage/features.tsx b/apps/web-roo-code/src/components/homepage/features.tsx index ce5534ab6e..4c71946d80 100644 --- a/apps/web-roo-code/src/components/homepage/features.tsx +++ b/apps/web-roo-code/src/components/homepage/features.tsx @@ -1,7 +1,7 @@ "use client" import { motion } from "framer-motion" -import { FaRobot, FaCode, FaBrain, FaTools, FaTerminal, FaPuzzlePiece, FaGlobe } from "react-icons/fa" +import { Bot, Code, Brain, Wrench, Terminal, Puzzle, Globe, Shield, Zap } from "lucide-react" import { FeaturesMobile } from "./features-mobile" import { ReactNode } from "react" @@ -10,58 +10,62 @@ export interface Feature { icon: ReactNode title: string description: string - size: "small" | "large" } export const features: Feature[] = [ { - icon:, + icon: , title: "Your AI Dev Team in VS Code", description: "Roo Code puts a team of agentic AI assistants directly in your editor, with the power to plan, write, and fix code across multiple files.", - size: "large", }, { - icon: , + icon: , title: "Multiple Specialized Modes", description: "From coding to debugging to architecture, Roo Code has a mode for every dev scenario—just switch on the fly.", - size: "small", }, { - icon:, + icon: , title: "Deep Project-wide Context", description: "Roo Code reads your entire codebase, preserving valid code through diff-based edits for seamless multi-file refactors.", - size: "small", }, { - icon: , + icon: , title: "Open-Source and Model-Agnostic", description: "Bring your own model or use local AI—no vendor lock-in. Roo Code is free, open, and adaptable to your needs.", - size: "large", }, { - icon: , + icon: , title: "Guarded Command Execution", description: "Approve or deny commands as needed. Roo Code automates your dev workflow while keeping oversight firmly in your hands.", - size: "small", }, { - icon: , + icon: , title: "Fully Customizable", description: - "Create or tweak modes, define usage rules, and shape Roo Code’s behavior precisely—your code, your way.", - size: "small", + "Create or tweak modes, define usage rules, and shape Roo Code's behavior precisely—your code, your way.", }, { - icon: , + icon: , title: "Automated Browser Actions", description: "Seamlessly test and verify your web app directly from VS Code—Roo Code can open a browser, run checks, and more.", - size: "small", + }, + { + icon: , + title: "Secure by Design", + description: + "Security-first from the ground up, Roo Code meets rigorous standards without slowing you down. Monitoring and strict policies keep your code safe at scale.", + }, + { + icon: , + title: "Seamless Setup and Workflows", + description: + "Get started in minutes—no heavy configs. Roo Code fits alongside your existing tools and dev flow, while supercharging your productivity.", }, ] @@ -127,7 +131,7 @@ export function Features() { duration: 0.6, ease: [0.21, 0.45, 0.27, 0.9], }}> - +
Powerful features for modern developers.
@@ -148,15 +152,12 @@ export function Features() { viewport={{ once: true }}>
{features.map((feature, index) => ( -- - -++ + +-{feature.icon}+ {feature.icon}{feature.title}
diff --git a/apps/web-roo-code/src/components/homepage/install-section.tsx b/apps/web-roo-code/src/components/homepage/install-section.tsx index 224a83cee3..5da3a7d4ae 100644 --- a/apps/web-roo-code/src/components/homepage/install-section.tsx +++ b/apps/web-roo-code/src/components/homepage/install-section.tsx @@ -23,59 +23,73 @@ export function InstallSection({ downloads }: InstallSectionProps) { } return ( -+ + {/* Enhanced background with better contrast */} + +- +--- Install Roo Code — Open & Flexible -
-- Roo Code is open-source, model-agnostic, and developer-focused. Install from the VS Code - Marketplace or the CLI in minutes, then bring your own AI model. -
-- - --- -- - VSCode Marketplace - {downloads !== null && ( - <> - - · - - {downloads} Downloads - > - )} - - - ----Install via CLI---+- code --install-extension RooVeterinaryInc.roo-cline --+ {/* Enhanced container with better visual separation */} ++ {/* Subtle gradient overlay */} + + ++ {/* Updated h2 to match other sections */} +diff --git a/apps/web-roo-code/src/components/homepage/testimonials-mobile.tsx b/apps/web-roo-code/src/components/homepage/testimonials-mobile.tsx index e4b3b6863a..8b90d27b5a 100644 --- a/apps/web-roo-code/src/components/homepage/testimonials-mobile.tsx +++ b/apps/web-roo-code/src/components/homepage/testimonials-mobile.tsx @@ -18,23 +18,37 @@ export function TestimonialsMobile() {+ Install Roo Code — Open & Flexible +
++ Roo Code is open-source, model-agnostic, and developer-focused. Install from the VS Code + Marketplace or the CLI in minutes, then bring your own AI model. +
+ ++ {/* Enhanced VSCode Marketplace button */} + + +++ + + {/* Enhanced CLI install section */} ++ + VSCode Marketplace + {downloads !== null && ( + <> + · + {downloads} Downloads + > + )} + + + ++++Install via CLI++++++ code --install-extension RooVeterinaryInc.roo-cline ++{testimonials.map((testimonial) => (-+--+
+"{testimonial.quote}"
diff --git a/apps/web-roo-code/src/components/homepage/testimonials.tsx b/apps/web-roo-code/src/components/homepage/testimonials.tsx index 8ffed444cc..4df5849d46 100644 --- a/apps/web-roo-code/src/components/homepage/testimonials.tsx +++ b/apps/web-roo-code/src/components/homepage/testimonials.tsx @@ -109,7 +109,7 @@ export function Testimonials() { duration: 0.6, ease: [0.21, 0.45, 0.27, 0.9], }}> -+
Empowering developers worldwide.
@@ -135,10 +135,10 @@ export function Testimonials() { key={testimonial.id} variants={itemVariants} className={`group relative ${index % 2 === 0 ? "md:translate-y-4" : "md:translate-y-12"}`}> -
-+ +{testimonial.image && ( -+)} - -- +++-+ ++ ++ {testimonial.quote} +
- {testimonial.quote} -
- -- -{testimonial.name}
-+
+ +diff --git a/apps/web-roo-code/src/lib/constants.ts b/apps/web-roo-code/src/lib/constants.ts index 9f769e2967..50898978ce 100644 --- a/apps/web-roo-code/src/lib/constants.ts +++ b/apps/web-roo-code/src/lib/constants.ts @@ -1,5 +1,6 @@ export const EXTERNAL_LINKS = { GITHUB: "https://github.com/RooCodeInc/Roo-Code", + GITHUB_DISCUSSIONS: "https://github.com/RooCodeInc/Roo-Code/discussions", DISCORD: "https://discord.gg/roocode", REDDIT: "https://reddit.com/r/RooCode", X: "https://x.com/roo_code", @@ -18,6 +19,11 @@ export const EXTERNAL_LINKS = { TUTORIALS: "https://docs.roocode.com/tutorial-videos", MARKETPLACE: "https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline", SECURITY: "https://trust.roocode.com", + EVALS: "https://roocode.com/evals", + BLOG: "https://blog.roocode.com", + OFFICE_HOURS_PODCAST: "https://www.youtube.com/@RooCodeYT/podcasts", + FAQ: "https://roocode.com/#faq", + TESTIMONIALS: "https://roocode.com/#testimonials", } export const INTERNAL_LINKS = { diff --git a/apps/web-roo-code/src/lib/seo.ts b/apps/web-roo-code/src/lib/seo.ts new file mode 100644 index 0000000000..962662bb22 --- /dev/null +++ b/apps/web-roo-code/src/lib/seo.ts @@ -0,0 +1,30 @@ +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://roocode.com" + +export const SEO = { + url: SITE_URL, + name: "Roo Code", + title: "Roo Code – Your AI-Powered Dev Team in VS Code", + description: + "Roo Code puts an entire AI dev team right in your editor, outpacing closed tools with deep project-wide context, multi-step agentic coding, and unmatched developer-centric flexibility.", + locale: "en_US", + ogImage: { + url: "/android-chrome-512x512.png", + width: 512, + height: 512, + alt: "Roo Code Logo", + }, + keywords: [ + "Roo Code", + "AI coding agent", + "VS Code extension", + "AI pair programmer", + "software development", + "agentic coding", + "code refactoring", + "debugging", + ], + category: "technology", + twitterCard: "summary_large_image" as const, +} as const + +export type SeoConfig = typeof SEO diff --git a/knip.json b/knip.json index 7ca8cfad7b..a847111981 100644 --- a/knip.json +++ b/knip.json @@ -7,7 +7,8 @@ "src/activate/**", "src/workers/countTokens.ts", "src/extension.ts", - "scripts/**" + "scripts/**", + "apps/web-roo-code/next-sitemap.config.cjs" ], "workspaces": { "src": { @@ -16,7 +17,7 @@ }, "webview-ui": { "entry": ["src/index.tsx"], - "project": ["src/**/*.{ts,tsx}"] + "project": ["src/**/*.{ts,tsx}", "../src/shared/*.ts"] }, "packages/{build,cloud,evals,ipc,telemetry,types}": { "project": ["src/**/*.ts"] diff --git a/locales/ca/README.md b/locales/ca/README.md index e0cefbc39b..3a156e83a6 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -182,45 +182,47 @@ Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code! -|+ {testimonial.name} +
+{testimonial.role} at {testimonial.company}
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/de/README.md b/locales/de/README.md index 36056d88bb..4856e9255b 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -182,45 +182,47 @@ Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/es/README.md b/locales/es/README.md index 8101c1d7b5..4a2298009f 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -182,45 +182,47 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/fr/README.md b/locales/fr/README.md index 852e0169a6..7ea8ed8bfa 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -182,45 +182,47 @@ Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/hi/README.md b/locales/hi/README.md index e3538597d9..8e174bce24 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -182,45 +182,47 @@ Roo Code को बेहतर बनाने में मदद करने -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/id/README.md b/locales/id/README.md index c97c557054..ebdc117626 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -176,45 +176,47 @@ Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/it/README.md b/locales/it/README.md index 63f4bdb408..70c034677b 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -182,45 +182,47 @@ Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/ja/README.md b/locales/ja/README.md index 2deed765e4..15cbde889e 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -182,45 +182,47 @@ Roo Codeの改善に貢献してくれたすべての貢献者に感謝します -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/ko/README.md b/locales/ko/README.md index 4c3df318a8..9453cdd389 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -182,45 +182,47 @@ Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사 -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/nl/README.md b/locales/nl/README.md index ae4551e931..ec4e34f69e 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -182,45 +182,47 @@ Dank aan alle bijdragers die Roo Code beter hebben gemaakt! -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/pl/README.md b/locales/pl/README.md index 4d4cf2ee56..4424ef18ad 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -182,45 +182,47 @@ Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 6335fad2ae..a8ab2c3890 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -182,45 +182,47 @@ Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melho -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/ru/README.md b/locales/ru/README.md index 2ae9f0ce1e..84f6830e52 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -182,45 +182,47 @@ code --install-extension bin/roo-cline-.vsix -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/tr/README.md b/locales/tr/README.md index 122799bf72..579b7da4fc 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -182,45 +182,47 @@ Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara te -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/vi/README.md b/locales/vi/README.md index da26fdc6b5..b6a133b5c7 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -182,45 +182,47 @@ Cảm ơn tất cả những người đóng góp đã giúp cải thiện Roo C -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 39f913feac..dd2e654dbe 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -182,45 +182,47 @@ code --install-extension bin/roo-cline-.vsix -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 6491f5695d..85bcf647bb 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -183,45 +183,47 @@ code --install-extension bin/roo-cline-.vsix -|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | -|
joemanley201 |
System233 |
jr |
nissa-seru |
jquanton |
roomote-agent | -|
NyxJae |
d-oit |
elianiva |
qdaxb |
punkpeye |
wkordalski | -|
SannidhyaSah |
xyOz-dev |
chrarnoldus |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | -|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | -|
lloydchang |
dtrugman |
Szpadel |
lupuletic |
kiwina |
liwilliam2021 | -|
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie |
hassoncs |
ChuKhaLi | -|
PeterDaveHello |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner |
afshawnlotfi | -|
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex |
emshvac | -|
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical |
dlab-anton |
arthurauffray | -|
upamune |
NamesMT |
taylorwilsdon |
sammcj |
p12tic |
gtaylor | -|
brunobergher |
aitoroses |
ross |
mr-ryan-james |
heyseth |
taisukeoe | -|
avtc |
eonghk |
GOODBOY008 |
kcwhite |
ronyblum |
teddyOOXX | -|
vincentsong |
yongjer |
zeozeozeo |
ashktn |
franekp |
yt3trees | -|
seedlord |
bramburn |
anton-otee |
benzntech |
axkirillov |
olearycrew | -|
catrielmuller |
devxpain |
snoyiatk |
GitlyHallows |
jcbdev |
Chenjiayuan195 | -|
julionav |
KanTakahiro |
SplittyDev |
mdp |
napter |
philfung | -|
bbenshalom |
chris-garrett |
dairui1 |
dqroid |
janaki-sasidhar |
forestyoo | -|
hatsu38 |
hongzio |
im47cn |
shoopapa |
jwcraig |
kinandan | -|
bannzai |
axmo |
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao | -|
zxdvd |
s97712 |
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 | -|
TGlide |
Githubguy132010 |
nevermorec |
PretzelVector |
zetaloop |
cdlliuy | -|
user202729 |
thill2323 |
takakoutso |
student20880 |
shubhamgupta731 |
shohei-ihaya | -|
shivamd1810 |
shaybc |
sensei-woo |
samir-nimbly |
robertheadley |
refactorthis | -|
qingyuan1109 |
pokutuna |
philipnext |
village-way |
oprstchn |
nobu007 | -|
mosleyit |
moqimoqidea |
mlopezr |
mecab |
olup |
lightrabbit | -|
lhish |
kohii |
tgfjt |
DeXtroTip |
pfitz |
ExactDoug | -|
celestial-vault |
linegel |
edwin-truthsearch-io |
EamonNerbonne |
dbasclpy |
dflatline | -|
Deon588 |
dleen |
CW-B-W |
chadgauth |
thecolorblue |
bogdan0083 | -|
benashby |
Atlogit |
atlasgong |
andrewshu2000 |
andreastempsch |
alasano | -|
QuinsZouls |
HadesArchitect |
alarno |
nexon33 |
adilhafeez |
adamwlarson | -|
adamhill |
AMHesch |
maekawataiki |
AlexandruSmirnov |
samsilveira |
01Rian | -|
RSO |
RandalSchwartz |
SECKainersdorfer |
R-omk |
Sarke |
PaperBoardOfficial | -|
OlegOAndreev |
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code | -|
markijbema |
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior | -|
kevinvandijk |
KevinZhao |
ksze |
Juice10 |
Fovty |
Jdo300 | -|
hesara | | | | | | +|
mrubens |
saoudrizwan |
cte |
daniel-lxs |
samhvw8 |
hannesrudolph | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
KJ7LNW |
a8trejo |
MuriloFP |
ColemanRoo |
canrobins13 |
stea9499 | +|
jr |
joemanley201 |
System233 |
nissa-seru |
jquanton |
roomote-agent | +|
NyxJae |
d-oit |
elianiva |
chrarnoldus |
qdaxb |
xyOz-dev | +|
punkpeye |
wkordalski |
SannidhyaSah |
sachasayan |
Smartsheet-JB-Brown |
monotykamary | +|
cannuri |
feifei325 |
zhangtony239 |
shariqriazz |
vigneshsubbiah16 |
pugazhendhi-m | +|
lloydchang |
liwilliam2021 |
dtrugman |
hassoncs |
PeterDaveHello |
Szpadel | +|
lupuletic |
kiwina |
Premshay |
psv2522 |
olweraltuve |
diarmidmackenzie | +|
ChuKhaLi |
NaccOll |
aheizi |
nbihan-mediware |
noritaka1166 |
RaySinner | +|
afshawnlotfi |
dleffel |
StevenTCramer |
Ruakij |
pdecat |
kyle-apex | +|
emshvac |
brunobergher |
Lunchb0ne |
SmartManoj |
vagadiya |
slytechnical | +|
dlab-anton |
arthurauffray |
upamune |
NamesMT |
taylorwilsdon |
sammcj | +|
p12tic |
gtaylor |
aitoroses |
benzntech |
ross |
mr-ryan-james | +|
heyseth |
taisukeoe |
avtc |
eonghk |
GOODBOY008 |
kcwhite | +|
ronyblum |
teddyOOXX |
thill2323 |
vincentsong |
yongjer |
zeozeozeo | +|
ashktn |
franekp |
yt3trees |
seedlord |
QuinsZouls |
anton-otee | +|
axkirillov |
bramburn |
olearycrew |
catrielmuller |
devxpain |
snoyiatk | +|
GitlyHallows |
jcbdev |
Chenjiayuan195 |
julionav |
KanTakahiro |
kevint-cerebras | +|
SplittyDev |
mdp |
napter |
philfung |
dqroid |
dairui1 | +|
chris-garrett |
bbenshalom |
bannzai |
ershang-fireworks |
f14XuanLv |
janaki-sasidhar | +|
forestyoo |
hatsu38 |
hongzio |
im47cn |
shoopapa |
axmo | +|
asychin |
amittell |
Yoshino-Yukitaro |
Yikai-Liao |
zxdvd |
s97712 | +|
vladstudio |
vivekfyi |
HahaBill |
tmsjngx0 |
TGlide |
Githubguy132010 | +|
tgfjt |
maekawataiki |
DeXtroTip |
qingyuan1109 |
refactorthis |
robertheadley | +|
samir-nimbly |
sensei-woo |
shaybc |
shivamd1810 |
shohei-ihaya |
shubhamgupta731 | +|
student20880 |
takakoutso |
user202729 |
cdlliuy |
zetaloop |
PretzelVector | +|
nevermorec |
jues |
jwcraig |
kinandan |
kohii |
lhish | +|
lightrabbit |
olup |
mecab |
mlopezr |
moqimoqidea |
mosleyit | +|
nobu007 |
oprstchn |
village-way |
philipnext |
pokutuna |
pfitz | +|
ExactDoug |
celestial-vault |
linegel |
ertan2002 |
edwin-truthsearch-io |
EamonNerbonne | +|
dbasclpy |
dflatline |
Deon588 |
dleen |
CW-B-W |
chadgauth | +|
thecolorblue |
bogdan0083 |
benashby |
Atlogit |
atlasgong |
AntiMoron | +|
andrewshu2000 |
andreastempsch |
alasano |
HadesArchitect |
alarno |
nexon33 | +|
adilhafeez |
adamwlarson |
adamhill |
AMHesch |
adambrand |
abumalick | +|
AlexandruSmirnov |
samsilveira |
01Rian |
RSO |
RandalSchwartz |
SECKainersdorfer | +|
R-omk |
pwilkin |
Sarke |
PaperBoardOfficial |
OlegOAndreev |
niteshbalusu11 | +|
Naam |
kvokka |
ecmasx |
mollux |
marvijo-code |
markijbema | +|
mamertofabian |
monkeyDluffy6017 |
libertyteeth |
shtse8 |
Rexarrior |
kevinvandijk | +|
KevinZhao |
ksze |
AyazKaan |
Juice10 |
snova-jorgep |
Fovty | +|
Jdo300 |
hesara | | | | | diff --git a/package.json b/package.json index cc917ab7ca..cdc8a1abca 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,21 @@ "changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .", "knip": "knip --include files", "update-contributors": "node scripts/update-contributors.js", - "evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0" + "evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0", + "npm:publish:types": "pnpm --filter @roo-code/types npm:publish", + "link-workspace-packages": "tsx scripts/link-packages.ts", + "unlink-workspace-packages": "tsx scripts/link-packages.ts --unlink" }, "devDependencies": { "@changesets/cli": "^2.27.10", "@dotenvx/dotenvx": "^1.34.0", + "@roo-code/config-typescript": "workspace:^", + "@types/glob": "^9.0.0", + "@types/node": "^24.1.0", "@vscode/vsce": "3.3.2", "esbuild": "^0.25.0", "eslint": "^9.27.0", + "glob": "^11.0.3", "husky": "^9.1.7", "knip": "^5.44.4", "lint-staged": "^16.0.0", @@ -39,6 +46,7 @@ "ovsx": "0.10.4", "prettier": "^3.4.2", "rimraf": "^6.0.1", + "tsx": "^4.19.3", "turbo": "^2.5.3", "typescript": "^5.4.5" }, @@ -53,7 +61,8 @@ "esbuild": ">=0.25.0", "undici": ">=5.29.0", "brace-expansion": ">=2.0.2", - "form-data": ">=4.0.4" + "form-data": ">=4.0.4", + "bluebird": ">=3.7.2" } } } diff --git a/packages/cloud/eslint.config.mjs b/packages/cloud/eslint.config.mjs deleted file mode 100644 index 694bf73664..0000000000 --- a/packages/cloud/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { config } from "@roo-code/config-eslint/base" - -/** @type {import("eslint").Linter.Config} */ -export default [...config] diff --git a/packages/cloud/package.json b/packages/cloud/package.json deleted file mode 100644 index d67b5ae7eb..0000000000 --- a/packages/cloud/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@roo-code/cloud", - "description": "Roo Code Cloud VSCode integration.", - "version": "0.0.0", - "type": "module", - "exports": "./src/index.ts", - "scripts": { - "lint": "eslint src --ext=ts --max-warnings=0", - "check-types": "tsc --noEmit", - "test": "vitest run", - "clean": "rimraf dist .turbo" - }, - "dependencies": { - "@roo-code/telemetry": "workspace:^", - "@roo-code/types": "workspace:^", - "zod": "^3.25.61" - }, - "devDependencies": { - "@roo-code/config-eslint": "workspace:^", - "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.x", - "@types/vscode": "^1.84.0", - "vitest": "^3.2.3" - } -} diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts deleted file mode 100644 index 9a32a16fcb..0000000000 --- a/packages/cloud/src/CloudService.ts +++ /dev/null @@ -1,274 +0,0 @@ -import * as vscode from "vscode" - -import type { - CloudUserInfo, - TelemetryEvent, - OrganizationAllowList, - OrganizationSettings, - ClineMessage, - ShareVisibility, -} from "@roo-code/types" -import { TelemetryService } from "@roo-code/telemetry" - -import { CloudServiceCallbacks } from "./types" -import type { AuthService } from "./auth" -import { WebAuthService, StaticTokenAuthService } from "./auth" -import type { SettingsService } from "./SettingsService" -import { CloudSettingsService } from "./CloudSettingsService" -import { StaticSettingsService } from "./StaticSettingsService" -import { TelemetryClient } from "./TelemetryClient" -import { ShareService, TaskNotFoundError } from "./ShareService" - -export class CloudService { - private static _instance: CloudService | null = null - - private context: vscode.ExtensionContext - private callbacks: CloudServiceCallbacks - private authListener: () => void - private authService: AuthService | null = null - private settingsService: SettingsService | null = null - private telemetryClient: TelemetryClient | null = null - private shareService: ShareService | null = null - private isInitialized = false - private log: (...args: unknown[]) => void - - private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) { - this.context = context - this.callbacks = callbacks - this.log = callbacks.log || console.log - this.authListener = () => { - this.callbacks.stateChanged?.() - } - } - - public async initialize(): Promise{ - if (this.isInitialized) { - return - } - - try { - const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN - if (cloudToken && cloudToken.length > 0) { - this.authService = new StaticTokenAuthService(this.context, cloudToken, this.log) - } else { - this.authService = new WebAuthService(this.context, this.log) - } - - await this.authService.initialize() - - this.authService.on("attempting-session", this.authListener) - this.authService.on("inactive-session", this.authListener) - this.authService.on("active-session", this.authListener) - this.authService.on("logged-out", this.authListener) - this.authService.on("user-info", this.authListener) - - // Check for static settings environment variable - const staticOrgSettings = process.env.ROO_CODE_CLOUD_ORG_SETTINGS - if (staticOrgSettings && staticOrgSettings.length > 0) { - this.settingsService = new StaticSettingsService(staticOrgSettings, this.log) - } else { - const cloudSettingsService = new CloudSettingsService( - this.context, - this.authService, - () => this.callbacks.stateChanged?.(), - this.log, - ) - cloudSettingsService.initialize() - this.settingsService = cloudSettingsService - } - - this.telemetryClient = new TelemetryClient(this.authService, this.settingsService) - - this.shareService = new ShareService(this.authService, this.settingsService, this.log) - - try { - TelemetryService.instance.register(this.telemetryClient) - } catch (error) { - this.log("[CloudService] Failed to register TelemetryClient:", error) - } - - this.isInitialized = true - } catch (error) { - this.log("[CloudService] Failed to initialize:", error) - throw new Error(`Failed to initialize CloudService: ${error}`) - } - } - - // AuthService - - public async login(): Promise { - this.ensureInitialized() - return this.authService!.login() - } - - public async logout(): Promise { - this.ensureInitialized() - return this.authService!.logout() - } - - public isAuthenticated(): boolean { - this.ensureInitialized() - return this.authService!.isAuthenticated() - } - - public hasActiveSession(): boolean { - this.ensureInitialized() - return this.authService!.hasActiveSession() - } - - public hasOrIsAcquiringActiveSession(): boolean { - this.ensureInitialized() - return this.authService!.hasOrIsAcquiringActiveSession() - } - - public getUserInfo(): CloudUserInfo | null { - this.ensureInitialized() - return this.authService!.getUserInfo() - } - - public getOrganizationId(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationId || null - } - - public getOrganizationName(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationName || null - } - - public getOrganizationRole(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationRole || null - } - - public hasStoredOrganizationId(): boolean { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() !== null - } - - public getStoredOrganizationId(): string | null { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() - } - - public getAuthState(): string { - this.ensureInitialized() - return this.authService!.getState() - } - - public async handleAuthCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { - this.ensureInitialized() - return this.authService!.handleCallback(code, state, organizationId) - } - - // SettingsService - - public getAllowList(): OrganizationAllowList { - this.ensureInitialized() - return this.settingsService!.getAllowList() - } - - public getOrganizationSettings(): OrganizationSettings | undefined { - this.ensureInitialized() - return this.settingsService!.getSettings() - } - - // TelemetryClient - - public captureEvent(event: TelemetryEvent): void { - this.ensureInitialized() - this.telemetryClient!.capture(event) - } - - // ShareService - - public async shareTask( - taskId: string, - visibility: ShareVisibility = "organization", - clineMessages?: ClineMessage[], - ) { - this.ensureInitialized() - - try { - return await this.shareService!.shareTask(taskId, visibility) - } catch (error) { - if (error instanceof TaskNotFoundError && clineMessages) { - // Backfill messages and retry - await this.telemetryClient!.backfillMessages(clineMessages, taskId) - return await this.shareService!.shareTask(taskId, visibility) - } - throw error - } - } - - public async canShareTask(): Promise { - this.ensureInitialized() - return this.shareService!.canShareTask() - } - - // Lifecycle - - public dispose(): void { - if (this.authService) { - this.authService.off("attempting-session", this.authListener) - this.authService.off("inactive-session", this.authListener) - this.authService.off("active-session", this.authListener) - this.authService.off("logged-out", this.authListener) - this.authService.off("user-info", this.authListener) - } - if (this.settingsService) { - this.settingsService.dispose() - } - - this.isInitialized = false - } - - private ensureInitialized(): void { - if (!this.isInitialized) { - throw new Error("CloudService not initialized.") - } - } - - static get instance(): CloudService { - if (!this._instance) { - throw new Error("CloudService not initialized") - } - - return this._instance - } - - static async createInstance( - context: vscode.ExtensionContext, - callbacks: CloudServiceCallbacks = {}, - ): Promise { - if (this._instance) { - throw new Error("CloudService instance already created") - } - - this._instance = new CloudService(context, callbacks) - await this._instance.initialize() - return this._instance - } - - static hasInstance(): boolean { - return this._instance !== null && this._instance.isInitialized - } - - static resetInstance(): void { - if (this._instance) { - this._instance.dispose() - this._instance = null - } - } - - static isEnabled(): boolean { - return !!this._instance?.isAuthenticated() - } -} diff --git a/packages/cloud/src/CloudSettingsService.ts b/packages/cloud/src/CloudSettingsService.ts deleted file mode 100644 index 6692d8141d..0000000000 --- a/packages/cloud/src/CloudSettingsService.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as vscode from "vscode" - -import { - ORGANIZATION_ALLOW_ALL, - OrganizationAllowList, - OrganizationSettings, - organizationSettingsSchema, -} from "@roo-code/types" - -import { getRooCodeApiUrl } from "./Config" -import type { AuthService } from "./auth" -import { RefreshTimer } from "./RefreshTimer" -import type { SettingsService } from "./SettingsService" - -const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings" - -export class CloudSettingsService implements SettingsService { - private context: vscode.ExtensionContext - private authService: AuthService - private settings: OrganizationSettings | undefined = undefined - private timer: RefreshTimer - private log: (...args: unknown[]) => void - - constructor( - context: vscode.ExtensionContext, - authService: AuthService, - callback: () => void, - log?: (...args: unknown[]) => void, - ) { - this.context = context - this.authService = authService - this.log = log || console.log - - this.timer = new RefreshTimer({ - callback: async () => { - return await this.fetchSettings(callback) - }, - successInterval: 30000, - initialBackoffMs: 1000, - maxBackoffMs: 30000, - }) - } - - public initialize(): void { - this.loadCachedSettings() - - // Clear cached settings if we have missed a log out. - if (this.authService.getState() == "logged-out" && this.settings) { - this.removeSettings() - } - - this.authService.on("active-session", () => { - this.timer.start() - }) - - this.authService.on("logged-out", () => { - this.timer.stop() - this.removeSettings() - }) - - if (this.authService.hasActiveSession()) { - this.timer.start() - } - } - - private async fetchSettings(callback: () => void): Promise { - const token = this.authService.getSessionToken() - - if (!token) { - return false - } - - try { - const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - if (!response.ok) { - this.log( - "[cloud-settings] Failed to fetch organization settings:", - response.status, - response.statusText, - ) - return false - } - - const data = await response.json() - const result = organizationSettingsSchema.safeParse(data) - - if (!result.success) { - this.log("[cloud-settings] Invalid organization settings format:", result.error) - return false - } - - const newSettings = result.data - - if (!this.settings || this.settings.version !== newSettings.version) { - this.settings = newSettings - await this.cacheSettings() - callback() - } - - return true - } catch (error) { - this.log("[cloud-settings] Error fetching organization settings:", error) - return false - } - } - - private async cacheSettings(): Promise { - await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings) - } - - private loadCachedSettings(): void { - this.settings = this.context.globalState.get (ORGANIZATION_SETTINGS_CACHE_KEY) - } - - public getAllowList(): OrganizationAllowList { - return this.settings?.allowList || ORGANIZATION_ALLOW_ALL - } - - public getSettings(): OrganizationSettings | undefined { - return this.settings - } - - private async removeSettings(): Promise { - this.settings = undefined - await this.cacheSettings() - } - - public dispose(): void { - this.timer.stop() - } -} diff --git a/packages/cloud/src/Config.ts b/packages/cloud/src/Config.ts deleted file mode 100644 index 08b0cc7a18..0000000000 --- a/packages/cloud/src/Config.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Production constants -export const PRODUCTION_CLERK_BASE_URL = "https://clerk.roocode.com" -export const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com" - -// Functions with environment variable fallbacks -export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL -export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_URL diff --git a/packages/cloud/src/RefreshTimer.ts b/packages/cloud/src/RefreshTimer.ts deleted file mode 100644 index e7294222d7..0000000000 --- a/packages/cloud/src/RefreshTimer.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * RefreshTimer - A utility for executing a callback with configurable retry behavior - * - * This timer executes a callback function and schedules the next execution based on the result: - * - If the callback succeeds (returns true), it schedules the next attempt after a fixed interval - * - If the callback fails (returns false), it uses exponential backoff up to a maximum interval - */ - -/** - * Configuration options for the RefreshTimer - */ -export interface RefreshTimerOptions { - /** - * The callback function to execute - * Should return a Promise that resolves to a boolean indicating success (true) or failure (false) - */ - callback: () => Promise - - /** - * Time in milliseconds to wait before next attempt after success - * @default 50000 (50 seconds) - */ - successInterval?: number - - /** - * Initial backoff time in milliseconds for the first failure - * @default 1000 (1 second) - */ - initialBackoffMs?: number - - /** - * Maximum backoff time in milliseconds - * @default 300000 (5 minutes) - */ - maxBackoffMs?: number -} - -/** - * A timer utility that executes a callback with configurable retry behavior - */ -export class RefreshTimer { - private callback: () => Promise - private successInterval: number - private initialBackoffMs: number - private maxBackoffMs: number - private currentBackoffMs: number - private attemptCount: number - private timerId: NodeJS.Timeout | null - private isRunning: boolean - - /** - * Creates a new RefreshTimer - * - * @param options Configuration options for the timer - */ - constructor(options: RefreshTimerOptions) { - this.callback = options.callback - this.successInterval = options.successInterval ?? 50000 // 50 seconds - this.initialBackoffMs = options.initialBackoffMs ?? 1000 // 1 second - this.maxBackoffMs = options.maxBackoffMs ?? 300000 // 5 minutes - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - this.timerId = null - this.isRunning = false - } - - /** - * Starts the timer and executes the callback immediately - */ - public start(): void { - if (this.isRunning) { - return - } - - this.isRunning = true - - // Execute the callback immediately - this.executeCallback() - } - - /** - * Stops the timer and cancels any pending execution - */ - public stop(): void { - if (!this.isRunning) { - return - } - - if (this.timerId) { - clearTimeout(this.timerId) - this.timerId = null - } - - this.isRunning = false - } - - /** - * Resets the backoff state and attempt count - * Does not affect whether the timer is running - */ - public reset(): void { - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - } - - /** - * Schedules the next attempt based on the success/failure of the current attempt - * - * @param wasSuccessful Whether the current attempt was successful - */ - private scheduleNextAttempt(wasSuccessful: boolean): void { - if (!this.isRunning) { - return - } - - if (wasSuccessful) { - // Reset backoff on success - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - - this.timerId = setTimeout(() => this.executeCallback(), this.successInterval) - } else { - // Increment attempt count - this.attemptCount++ - - // Calculate backoff time with exponential increase - // Formula: initialBackoff * 2^(attemptCount - 1) - this.currentBackoffMs = Math.min( - this.initialBackoffMs * Math.pow(2, this.attemptCount - 1), - this.maxBackoffMs, - ) - - this.timerId = setTimeout(() => this.executeCallback(), this.currentBackoffMs) - } - } - - /** - * Executes the callback and handles the result - */ - private async executeCallback(): Promise { - if (!this.isRunning) { - return - } - - try { - const result = await this.callback() - - this.scheduleNextAttempt(result) - } catch (_error) { - // Treat errors as failed attempts - this.scheduleNextAttempt(false) - } - } -} diff --git a/packages/cloud/src/SettingsService.ts b/packages/cloud/src/SettingsService.ts deleted file mode 100644 index c1027dc25c..0000000000 --- a/packages/cloud/src/SettingsService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { OrganizationAllowList, OrganizationSettings } from "@roo-code/types" - -/** - * Interface for settings services that provide organization settings - */ -export interface SettingsService { - /** - * Get the organization allow list - * @returns The organization allow list or default if none available - */ - getAllowList(): OrganizationAllowList - - /** - * Get the current organization settings - * @returns The organization settings or undefined if none available - */ - getSettings(): OrganizationSettings | undefined - - /** - * Dispose of the settings service and clean up resources - */ - dispose(): void -} diff --git a/packages/cloud/src/ShareService.ts b/packages/cloud/src/ShareService.ts deleted file mode 100644 index 5dcc7cae3f..0000000000 --- a/packages/cloud/src/ShareService.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as vscode from "vscode" - -import { shareResponseSchema } from "@roo-code/types" -import { getRooCodeApiUrl } from "./Config" -import type { AuthService } from "./auth" -import type { SettingsService } from "./SettingsService" -import { getUserAgent } from "./utils" - -export type ShareVisibility = "organization" | "public" - -export class TaskNotFoundError extends Error { - constructor(taskId?: string) { - super(taskId ? `Task '${taskId}' not found` : "Task not found") - Object.setPrototypeOf(this, TaskNotFoundError.prototype) - } -} - -export class ShareService { - private authService: AuthService - private settingsService: SettingsService - private log: (...args: unknown[]) => void - - constructor(authService: AuthService, settingsService: SettingsService, log?: (...args: unknown[]) => void) { - this.authService = authService - this.settingsService = settingsService - this.log = log || console.log - } - - /** - * Share a task with specified visibility - * Returns the share response data - */ - async shareTask(taskId: string, visibility: ShareVisibility = "organization") { - try { - const sessionToken = this.authService.getSessionToken() - if (!sessionToken) { - throw new Error("Authentication required") - } - - const response = await fetch(`${getRooCodeApiUrl()}/api/extension/share`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - "User-Agent": getUserAgent(), - }, - body: JSON.stringify({ taskId, visibility }), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - if (response.status === 404) { - throw new TaskNotFoundError(taskId) - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data = shareResponseSchema.parse(await response.json()) - this.log("[share] Share link created successfully:", data) - - if (data.success && data.shareUrl) { - // Copy to clipboard - await vscode.env.clipboard.writeText(data.shareUrl) - } - - return data - } catch (error) { - this.log("[share] Error sharing task:", error) - throw error - } - } - - /** - * Check if sharing is available - */ - async canShareTask(): Promise { - try { - if (!this.authService.isAuthenticated()) { - return false - } - - return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing - } catch (error) { - this.log("[share] Error checking if task can be shared:", error) - return false - } - } -} diff --git a/packages/cloud/src/StaticSettingsService.ts b/packages/cloud/src/StaticSettingsService.ts deleted file mode 100644 index 3aac37bda5..0000000000 --- a/packages/cloud/src/StaticSettingsService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - ORGANIZATION_ALLOW_ALL, - OrganizationAllowList, - OrganizationSettings, - organizationSettingsSchema, -} from "@roo-code/types" - -import type { SettingsService } from "./SettingsService" - -export class StaticSettingsService implements SettingsService { - private settings: OrganizationSettings - private log: (...args: unknown[]) => void - - constructor(envValue: string, log?: (...args: unknown[]) => void) { - this.log = log || console.log - this.settings = this.parseEnvironmentSettings(envValue) - } - - private parseEnvironmentSettings(envValue: string): OrganizationSettings { - try { - const decodedValue = Buffer.from(envValue, "base64").toString("utf-8") - const parsedJson = JSON.parse(decodedValue) - return organizationSettingsSchema.parse(parsedJson) - } catch (error) { - this.log(`[StaticSettingsService] failed to parse static settings: ${error.message}`, error) - throw new Error("Failed to parse static settings", { cause: error }) - } - } - - public getAllowList(): OrganizationAllowList { - return this.settings?.allowList || ORGANIZATION_ALLOW_ALL - } - - public getSettings(): OrganizationSettings | undefined { - return this.settings - } - - public dispose(): void { - // No resources to clean up for static settings - } -} diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts deleted file mode 100644 index e33843a30c..0000000000 --- a/packages/cloud/src/TelemetryClient.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { - TelemetryEventName, - type TelemetryEvent, - rooCodeTelemetryEventSchema, - type ClineMessage, -} from "@roo-code/types" -import { BaseTelemetryClient } from "@roo-code/telemetry" - -import { getRooCodeApiUrl } from "./Config" -import type { AuthService } from "./auth" -import type { SettingsService } from "./SettingsService" - -export class TelemetryClient extends BaseTelemetryClient { - constructor( - private authService: AuthService, - private settingsService: SettingsService, - debug = false, - ) { - super( - { - type: "exclude", - events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE], - }, - debug, - ) - } - - private async fetch(path: string, options: RequestInit) { - if (!this.authService.isAuthenticated()) { - return - } - - const token = this.authService.getSessionToken() - - if (!token) { - console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`) - return - } - - const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, { - ...options, - headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - }) - - if (!response.ok) { - console.error( - `[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`, - ) - } - } - - public override async capture(event: TelemetryEvent) { - if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) { - if (this.debug) { - console.info(`[TelemetryClient#capture] Skipping event: ${event.event}`) - } - - return - } - - const payload = { - type: event.event, - properties: await this.getEventProperties(event), - } - - if (this.debug) { - console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`) - } - - const result = rooCodeTelemetryEventSchema.safeParse(payload) - - if (!result.success) { - console.error( - `[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`, - ) - - return - } - - try { - await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) }) - } catch (error) { - console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`) - } - } - - public async backfillMessages(messages: ClineMessage[], taskId: string): Promise { - if (!this.authService.isAuthenticated()) { - if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`) - } - return - } - - const token = this.authService.getSessionToken() - - if (!token) { - console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`) - return - } - - try { - const mergedProperties = await this.getEventProperties({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { taskId }, - }) - - const formData = new FormData() - formData.append("taskId", taskId) - formData.append("properties", JSON.stringify(mergedProperties)) - - formData.append( - "file", - new File([JSON.stringify(messages)], "task.json", { - type: "application/json", - }), - ) - - if (this.debug) { - console.info( - `[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`, - ) - } - - // Custom fetch for multipart - don't set Content-Type header (let browser set it) - const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - // Note: No Content-Type header - browser will set multipart/form-data with boundary - }, - body: formData, - }) - - if (!response.ok) { - console.error( - `[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`, - ) - } else if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`) - } - } catch (error) { - console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`) - } - } - - public override updateTelemetryState(_didUserOptIn: boolean) {} - - public override isTelemetryEnabled(): boolean { - return true - } - - protected override isEventCapturable(eventName: TelemetryEventName): boolean { - // Ensure that this event type is supported by the telemetry client - if (!super.isEventCapturable(eventName)) { - return false - } - - // Only record message telemetry if a cloud account is present and explicitly configured to record messages - if (eventName === TelemetryEventName.TASK_MESSAGE) { - return this.settingsService.getSettings()?.cloudSettings?.recordTaskMessages || false - } - - // Other telemetry types are capturable at this point - return true - } - - public override async shutdown() {} -} diff --git a/packages/cloud/src/__mocks__/vscode.ts b/packages/cloud/src/__mocks__/vscode.ts deleted file mode 100644 index ac9082375e..0000000000 --- a/packages/cloud/src/__mocks__/vscode.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export const window = { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), -} - -export const env = { - openExternal: vi.fn(), -} - -export const Uri = { - parse: vi.fn((uri: string) => ({ toString: () => uri })), -} - -export interface ExtensionContext { - secrets: { - get: (key: string) => Promise - store: (key: string, value: string) => Promise - delete: (key: string) => Promise - onDidChange: (listener: (e: { key: string }) => void) => { dispose: () => void } - } - globalState: { - get: (key: string) => T | undefined - update: (key: string, value: any) => Promise - } - subscriptions: any[] - extension?: { - packageJSON?: { - version?: string - publisher?: string - name?: string - } - } -} - -// Mock implementation for tests -export const mockExtensionContext: ExtensionContext = { - secrets: { - get: vi.fn().mockResolvedValue(undefined), - store: vi.fn().mockResolvedValue(undefined), - delete: vi.fn().mockResolvedValue(undefined), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn().mockReturnValue(undefined), - update: vi.fn().mockResolvedValue(undefined), - }, - subscriptions: [], - extension: { - packageJSON: { - version: "1.0.0", - publisher: "RooVeterinaryInc", - name: "roo-cline", - }, - }, -} diff --git a/packages/cloud/src/__tests__/CloudService.integration.test.ts b/packages/cloud/src/__tests__/CloudService.integration.test.ts deleted file mode 100644 index f3cef27718..0000000000 --- a/packages/cloud/src/__tests__/CloudService.integration.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -// npx vitest run src/__tests__/CloudService.integration.test.ts - -import * as vscode from "vscode" -import { CloudService } from "../CloudService" -import { StaticSettingsService } from "../StaticSettingsService" -import { CloudSettingsService } from "../CloudSettingsService" - -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, -})) - -describe("CloudService Integration - Settings Service Selection", () => { - let mockContext: vscode.ExtensionContext - - beforeEach(() => { - CloudService.resetInstance() - - mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - } as unknown as vscode.ExtensionContext - }) - - afterEach(() => { - CloudService.resetInstance() - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - }) - - it("should use CloudSettingsService when no environment variable is set", async () => { - // Ensure no environment variables are set - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(CloudSettingsService) - }) - - it("should use StaticSettingsService when ROO_CODE_CLOUD_ORG_SETTINGS is set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set the environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Verify the settings are correctly loaded - expect(cloudService.getAllowList()).toEqual(validSettings.allowList) - }) - - it("should throw error when ROO_CODE_CLOUD_ORG_SETTINGS contains invalid data", async () => { - // Set invalid environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = "invalid-base64-data" - - await expect(CloudService.createInstance(mockContext)).rejects.toThrow("Failed to initialize CloudService") - }) - - it("should prioritize static token auth when both environment variables are set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set both environment variables - process.env.ROO_CODE_CLOUD_TOKEN = "test-token" - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Should use StaticSettingsService for settings - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Should use StaticTokenAuthService for auth (from the existing logic) - expect(cloudService.isAuthenticated()).toBe(true) - expect(cloudService.hasActiveSession()).toBe(true) - }) -}) diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts deleted file mode 100644 index 1384b6de6b..0000000000 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -// npx vitest run src/__tests__/CloudService.test.ts - -import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" - -import { CloudService } from "../CloudService" -import { WebAuthService } from "../auth/WebAuthService" -import { CloudSettingsService } from "../CloudSettingsService" -import { ShareService, TaskNotFoundError } from "../ShareService" -import { TelemetryClient } from "../TelemetryClient" -import { TelemetryService } from "@roo-code/telemetry" -import { CloudServiceCallbacks } from "../types" - -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, -})) - -vi.mock("@roo-code/telemetry") - -vi.mock("../auth/WebAuthService") - -vi.mock("../CloudSettingsService") - -vi.mock("../ShareService") - -vi.mock("../TelemetryClient") - -describe("CloudService", () => { - let mockContext: vscode.ExtensionContext - let mockAuthService: { - initialize: ReturnType - login: ReturnType - logout: ReturnType - isAuthenticated: ReturnType - hasActiveSession: ReturnType - hasOrIsAcquiringActiveSession: ReturnType - getUserInfo: ReturnType - getState: ReturnType - getSessionToken: ReturnType - handleCallback: ReturnType - getStoredOrganizationId: ReturnType - on: ReturnType - off: ReturnType - once: ReturnType - emit: ReturnType - } - let mockSettingsService: { - initialize: ReturnType - getSettings: ReturnType - getAllowList: ReturnType - dispose: ReturnType - } - let mockShareService: { - shareTask: ReturnType - canShareTask: ReturnType - } - let mockTelemetryClient: { - backfillMessages: ReturnType - } - let mockTelemetryService: { - hasInstance: ReturnType - instance: { - register: ReturnType - } - } - - beforeEach(() => { - CloudService.resetInstance() - - mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - } as unknown as vscode.ExtensionContext - - mockAuthService = { - initialize: vi.fn().mockResolvedValue(undefined), - login: vi.fn(), - logout: vi.fn(), - isAuthenticated: vi.fn().mockReturnValue(false), - hasActiveSession: vi.fn().mockReturnValue(false), - hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false), - getUserInfo: vi.fn(), - getState: vi.fn().mockReturnValue("logged-out"), - getSessionToken: vi.fn(), - handleCallback: vi.fn(), - getStoredOrganizationId: vi.fn().mockReturnValue(null), - on: vi.fn(), - off: vi.fn(), - once: vi.fn(), - emit: vi.fn(), - } - - mockSettingsService = { - initialize: vi.fn(), - getSettings: vi.fn(), - getAllowList: vi.fn(), - dispose: vi.fn(), - } - - mockShareService = { - shareTask: vi.fn(), - canShareTask: vi.fn().mockResolvedValue(true), - } - - mockTelemetryClient = { - backfillMessages: vi.fn().mockResolvedValue(undefined), - } - - mockTelemetryService = { - hasInstance: vi.fn().mockReturnValue(true), - instance: { - register: vi.fn(), - }, - } - - vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService) - vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService) - vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService) - vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) - - vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) - Object.defineProperty(TelemetryService, "instance", { - get: () => mockTelemetryService.instance, - configurable: true, - }) - }) - - afterEach(() => { - vi.clearAllMocks() - CloudService.resetInstance() - }) - - describe("createInstance", () => { - it("should create and initialize CloudService instance", async () => { - const callbacks = { - stateChanged: vi.fn(), - } - - const cloudService = await CloudService.createInstance(mockContext, callbacks) - - expect(cloudService).toBeInstanceOf(CloudService) - expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) - expect(CloudSettingsService).toHaveBeenCalledWith( - mockContext, - mockAuthService, - expect.any(Function), - expect.any(Function), - ) - }) - - it("should throw error if instance already exists", async () => { - await CloudService.createInstance(mockContext) - - await expect(CloudService.createInstance(mockContext)).rejects.toThrow( - "CloudService instance already created", - ) - }) - }) - - describe("authentication methods", () => { - let cloudService: CloudService - let callbacks: CloudServiceCallbacks - - beforeEach(async () => { - callbacks = { stateChanged: vi.fn() } - cloudService = await CloudService.createInstance(mockContext, callbacks) - }) - - it("should delegate login to AuthService", async () => { - await cloudService.login() - expect(mockAuthService.login).toHaveBeenCalled() - }) - - it("should delegate logout to AuthService", async () => { - await cloudService.logout() - expect(mockAuthService.logout).toHaveBeenCalled() - }) - - it("should delegate isAuthenticated to AuthService", () => { - const result = cloudService.isAuthenticated() - expect(mockAuthService.isAuthenticated).toHaveBeenCalled() - expect(result).toBe(false) - }) - - it("should delegate hasActiveSession to AuthService", () => { - const result = cloudService.hasActiveSession() - expect(mockAuthService.hasActiveSession).toHaveBeenCalled() - expect(result).toBe(false) - }) - - it("should delegate getUserInfo to AuthService", async () => { - await cloudService.getUserInfo() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - }) - - it("should return organization ID from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationId() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("org_123") - }) - - it("should return null when no organization ID available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationId() - expect(result).toBe(null) - }) - - it("should return organization name from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationName() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("Test Org") - }) - - it("should return null when no organization name available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationName() - expect(result).toBe(null) - }) - - it("should return organization role from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationRole() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("admin") - }) - - it("should return null when no organization role available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationRole() - expect(result).toBe(null) - }) - - it("should delegate getAuthState to AuthService", () => { - const result = cloudService.getAuthState() - expect(mockAuthService.getState).toHaveBeenCalled() - expect(result).toBe("logged-out") - }) - - it("should delegate handleAuthCallback to AuthService", async () => { - await cloudService.handleAuthCallback("code", "state") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined) - }) - - it("should delegate handleAuthCallback with organizationId to AuthService", async () => { - await cloudService.handleAuthCallback("code", "state", "org_123") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123") - }) - - it("should return stored organization ID from AuthService", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_456") - - const result = cloudService.getStoredOrganizationId() - expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled() - expect(result).toBe("org_456") - }) - - it("should return null when no stored organization ID available", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.getStoredOrganizationId() - expect(result).toBe(null) - }) - - it("should return true when stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_789") - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(true) - }) - - it("should return false when no stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(false) - }) - }) - - describe("organization settings methods", () => { - let cloudService: CloudService - - beforeEach(async () => { - cloudService = await CloudService.createInstance(mockContext) - }) - - it("should delegate getAllowList to SettingsService", () => { - cloudService.getAllowList() - expect(mockSettingsService.getAllowList).toHaveBeenCalled() - }) - }) - - describe("error handling", () => { - it("should throw error when accessing methods before initialization", () => { - expect(() => CloudService.instance.login()).toThrow("CloudService not initialized") - }) - - it("should throw error when accessing instance before creation", () => { - expect(() => CloudService.instance).toThrow("CloudService not initialized") - }) - }) - - describe("hasInstance", () => { - it("should return false when no instance exists", () => { - expect(CloudService.hasInstance()).toBe(false) - }) - - it("should return true when instance exists and is initialized", async () => { - await CloudService.createInstance(mockContext) - expect(CloudService.hasInstance()).toBe(true) - }) - }) - - describe("dispose", () => { - it("should dispose of all services and clean up", async () => { - const cloudService = await CloudService.createInstance(mockContext) - cloudService.dispose() - - expect(mockSettingsService.dispose).toHaveBeenCalled() - }) - }) - - describe("shareTask with ClineMessage retry logic", () => { - let cloudService: CloudService - - beforeEach(async () => { - // Reset mocks for shareTask tests - vi.clearAllMocks() - - // Reset authentication state for shareTask tests - mockAuthService.isAuthenticated.mockReturnValue(true) - mockAuthService.hasActiveSession.mockReturnValue(true) - mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true) - mockAuthService.getState.mockReturnValue("active") - - cloudService = await CloudService.createInstance(mockContext, {}) - }) - - it("should call shareTask without retry when successful", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - expect(result).toEqual(expectedResult) - }) - - it("should retry with backfill when TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - - // First call throws TaskNotFoundError, second call succeeds - mockShareService.shareTask - .mockRejectedValueOnce(new TaskNotFoundError(taskId)) - .mockResolvedValueOnce(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(2) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId) - expect(result).toEqual(expectedResult) - }) - - it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => { - const taskId = "test-task-id" - const visibility = "organization" - - const taskNotFoundError = new TaskNotFoundError(taskId) - mockShareService.shareTask.mockRejectedValue(taskNotFoundError) - - await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should not retry when non-TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const genericError = new Error("Some other error") - mockShareService.shareTask.mockRejectedValue(genericError) - - await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should work with default parameters", async () => { - const taskId = "test-task-id" - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization") - expect(result).toEqual(expectedResult) - }) - }) -}) diff --git a/packages/cloud/src/__tests__/RefreshTimer.test.ts b/packages/cloud/src/__tests__/RefreshTimer.test.ts deleted file mode 100644 index 2f87488568..0000000000 --- a/packages/cloud/src/__tests__/RefreshTimer.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// npx vitest run src/__tests__/RefreshTimer.test.ts - -import type { Mock } from "vitest" - -import { RefreshTimer } from "../RefreshTimer" - -vi.useFakeTimers() - -describe("RefreshTimer", () => { - let mockCallback: Mock - let refreshTimer: RefreshTimer - - beforeEach(() => { - mockCallback = vi.fn() - mockCallback.mockResolvedValue(true) - }) - - afterEach(() => { - if (refreshTimer) { - refreshTimer.stop() - } - - vi.clearAllTimers() - vi.clearAllMocks() - }) - - it("should execute callback immediately when started", () => { - refreshTimer = new RefreshTimer({ - callback: mockCallback, - }) - - refreshTimer.start() - - expect(mockCallback).toHaveBeenCalledTimes(1) - }) - - it("should schedule next attempt after success interval when callback succeeds", async () => { - mockCallback.mockResolvedValue(true) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - successInterval: 50000, // 50 seconds - }) - - refreshTimer.start() - - // Fast-forward to execute the first callback - await Promise.resolve() - - expect(mockCallback).toHaveBeenCalledTimes(1) - - // Fast-forward 50 seconds - vi.advanceTimersByTime(50000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(2) - }) - - it("should use exponential backoff when callback fails", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, // 1 second - }) - - refreshTimer.start() - - // Fast-forward to execute the first callback - await Promise.resolve() - - expect(mockCallback).toHaveBeenCalledTimes(1) - - // Fast-forward 1 second - vi.advanceTimersByTime(1000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(2) - - // Fast-forward to execute the second callback - await Promise.resolve() - - // Fast-forward 2 seconds - vi.advanceTimersByTime(2000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(3) - - // Fast-forward to execute the third callback - await Promise.resolve() - }) - - it("should not exceed maximum backoff interval", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, // 1 second - maxBackoffMs: 5000, // 5 seconds - }) - - refreshTimer.start() - - // Fast-forward through multiple failures to reach max backoff - await Promise.resolve() // First attempt - vi.advanceTimersByTime(1000) - - await Promise.resolve() // Second attempt (backoff = 2000ms) - vi.advanceTimersByTime(2000) - - await Promise.resolve() // Third attempt (backoff = 4000ms) - vi.advanceTimersByTime(4000) - - await Promise.resolve() // Fourth attempt (backoff would be 8000ms but max is 5000ms) - - // Should be capped at maxBackoffMs (no way to verify without logger) - }) - - it("should reset backoff after a successful attempt", async () => { - // First call fails, second succeeds, third fails - mockCallback.mockResolvedValueOnce(false).mockResolvedValueOnce(true).mockResolvedValueOnce(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - successInterval: 5000, - }) - - refreshTimer.start() - - // First attempt (fails) - await Promise.resolve() - - // Fast-forward 1 second - vi.advanceTimersByTime(1000) - - // Second attempt (succeeds) - await Promise.resolve() - - // Fast-forward 5 seconds - vi.advanceTimersByTime(5000) - - // Third attempt (fails) - await Promise.resolve() - - // Backoff should be reset to initial value (no way to verify without logger) - }) - - it("should handle errors in callback as failures", async () => { - mockCallback.mockRejectedValue(new Error("Test error")) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - }) - - refreshTimer.start() - - // Fast-forward to execute the callback - await Promise.resolve() - - // Error should be treated as a failure (no way to verify without logger) - }) - - it("should stop the timer and cancel pending executions", () => { - refreshTimer = new RefreshTimer({ - callback: mockCallback, - }) - - refreshTimer.start() - - // Stop the timer - refreshTimer.stop() - - // Fast-forward a long time - vi.advanceTimersByTime(1000000) - - // Callback should only have been called once (the initial call) - expect(mockCallback).toHaveBeenCalledTimes(1) - }) - - it("should reset the backoff state", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - }) - - refreshTimer.start() - - // Fast-forward through a few failures - await Promise.resolve() - vi.advanceTimersByTime(1000) - - await Promise.resolve() - vi.advanceTimersByTime(2000) - - // Reset the timer - refreshTimer.reset() - - // Stop and restart to trigger a new execution - refreshTimer.stop() - refreshTimer.start() - - await Promise.resolve() - - // Backoff should be back to initial value (no way to verify without logger) - }) -}) diff --git a/packages/cloud/src/__tests__/ShareService.test.ts b/packages/cloud/src/__tests__/ShareService.test.ts deleted file mode 100644 index dd5b669603..0000000000 --- a/packages/cloud/src/__tests__/ShareService.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { MockedFunction } from "vitest" -import * as vscode from "vscode" - -import { ShareService, TaskNotFoundError } from "../ShareService" -import type { AuthService } from "../auth" -import type { SettingsService } from "../SettingsService" - -// Mock fetch -const mockFetch = vi.fn() -global.fetch = mockFetch as any - -// Mock vscode -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - showQuickPick: vi.fn(), - }, - env: { - clipboard: { - writeText: vi.fn(), - }, - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, - extensions: { - getExtension: vi.fn(() => ({ - packageJSON: { version: "1.0.0" }, - })), - }, -})) - -// Mock config -vi.mock("../Config", () => ({ - getRooCodeApiUrl: () => "https://app.roocode.com", -})) - -// Mock utils -vi.mock("../utils", () => ({ - getUserAgent: () => "Roo-Code 1.0.0", -})) - -describe("ShareService", () => { - let shareService: ShareService - let mockAuthService: AuthService - let mockSettingsService: SettingsService - let mockLog: MockedFunction<(...args: unknown[]) => void> - - beforeEach(() => { - vi.clearAllMocks() - mockFetch.mockClear() - - mockLog = vi.fn() - mockAuthService = { - hasActiveSession: vi.fn(), - getSessionToken: vi.fn(), - isAuthenticated: vi.fn(), - } as any - - mockSettingsService = { - getSettings: vi.fn(), - } as any - - shareService = new ShareService(mockAuthService, mockSettingsService, mockLog) - }) - - describe("shareTask", () => { - it("should share task with organization visibility and copy to clipboard", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "organization") - - expect(result.success).toBe(true) - expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123") - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) - expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123") - }) - - it("should share task with public visibility", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "public") - - expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "public" }), - signal: expect.any(AbortSignal), - }) - }) - - it("should default to organization visibility when not specified", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123") - - expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) - }) - - it("should handle API error response", async () => { - const mockResponseData = { - success: false, - error: "Task not found", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "organization") - - expect(result.success).toBe(false) - expect(result.error).toBe("Task not found") - }) - - it("should handle authentication errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue(null) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required") - }) - - it("should handle unexpected errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockRejectedValue(new Error("Network error")) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error") - }) - - it("should throw TaskNotFoundError for 404 responses", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError) - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "Task 'task-123' not found", - ) - }) - - it("should throw generic Error for non-404 HTTP errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "HTTP 500: Internal Server Error", - ) - await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError) - }) - - it("should create TaskNotFoundError with correct properties", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - try { - await shareService.shareTask("task-123", "organization") - expect.fail("Expected TaskNotFoundError to be thrown") - } catch (error) { - expect(error).toBeInstanceOf(TaskNotFoundError) - expect(error).toBeInstanceOf(Error) - expect((error as TaskNotFoundError).message).toBe("Task 'task-123' not found") - } - }) - }) - - describe("canShareTask", () => { - it("should return true when authenticated and sharing is enabled", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: { - enableTaskSharing: true, - }, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(true) - }) - - it("should return false when authenticated but sharing is disabled", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: { - enableTaskSharing: false, - }, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when authenticated and sharing setting is undefined (default)", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: {}, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when authenticated and no settings available (default)", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue(undefined) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when not authenticated", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(false) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should handle errors gracefully", async () => { - ;(mockAuthService.isAuthenticated as any).mockImplementation(() => { - throw new Error("Auth error") - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - }) -}) diff --git a/packages/cloud/src/__tests__/StaticSettingsService.test.ts b/packages/cloud/src/__tests__/StaticSettingsService.test.ts deleted file mode 100644 index 26c0ada9cd..0000000000 --- a/packages/cloud/src/__tests__/StaticSettingsService.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// npx vitest run src/__tests__/StaticSettingsService.test.ts - -import { StaticSettingsService } from "../StaticSettingsService" - -describe("StaticSettingsService", () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - maxOpenTabsContext: 10, - }, - allowList: { - allowAll: false, - providers: { - anthropic: { - allowAll: true, - }, - }, - }, - } - - const validBase64 = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - describe("constructor", () => { - it("should parse valid base64 encoded JSON settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getSettings()).toEqual(validSettings) - }) - - it("should throw error for invalid base64", () => { - expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow("Failed to parse static settings") - }) - - it("should throw error for invalid JSON", () => { - const invalidJson = Buffer.from("{ invalid json }").toString("base64") - expect(() => new StaticSettingsService(invalidJson)).toThrow("Failed to parse static settings") - }) - - it("should throw error for invalid schema", () => { - const invalidSettings = { invalid: "schema" } - const invalidBase64 = Buffer.from(JSON.stringify(invalidSettings)).toString("base64") - expect(() => new StaticSettingsService(invalidBase64)).toThrow("Failed to parse static settings") - }) - }) - - describe("getAllowList", () => { - it("should return the allow list from settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getAllowList()).toEqual(validSettings.allowList) - }) - }) - - describe("getSettings", () => { - it("should return the parsed settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getSettings()).toEqual(validSettings) - }) - }) - - describe("dispose", () => { - it("should be a no-op for static settings", () => { - const service = new StaticSettingsService(validBase64) - expect(() => service.dispose()).not.toThrow() - }) - }) - - describe("logging", () => { - it("should use provided logger for errors", () => { - const mockLog = vi.fn() - expect(() => new StaticSettingsService("invalid-base64!@#", mockLog)).toThrow() - - expect(mockLog).toHaveBeenCalledWith( - expect.stringContaining("[StaticSettingsService] failed to parse static settings:"), - expect.any(Error), - ) - }) - - it("should use console.log as default logger for errors", () => { - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}) - expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow() - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("[StaticSettingsService] failed to parse static settings:"), - expect.any(Error), - ) - - consoleSpy.mockRestore() - }) - - it("should not log anything for successful parsing", () => { - const mockLog = vi.fn() - new StaticSettingsService(validBase64, mockLog) - - expect(mockLog).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/__tests__/TelemetryClient.test.ts b/packages/cloud/src/__tests__/TelemetryClient.test.ts deleted file mode 100644 index e4c62b1e4e..0000000000 --- a/packages/cloud/src/__tests__/TelemetryClient.test.ts +++ /dev/null @@ -1,738 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// npx vitest run src/__tests__/TelemetryClient.test.ts - -import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types" - -import { TelemetryClient } from "../TelemetryClient" - -const mockFetch = vi.fn() -global.fetch = mockFetch as any - -describe("TelemetryClient", () => { - const getPrivateProperty = (instance: any, propertyName: string): T => { - return instance[propertyName] - } - - let mockAuthService: any - let mockSettingsService: any - - beforeEach(() => { - vi.clearAllMocks() - - // Create a mock AuthService instead of using the singleton - mockAuthService = { - getSessionToken: vi.fn().mockReturnValue("mock-token"), - getState: vi.fn().mockReturnValue("active-session"), - isAuthenticated: vi.fn().mockReturnValue(true), - hasActiveSession: vi.fn().mockReturnValue(true), - } - - // Create a mock SettingsService - mockSettingsService = { - getSettings: vi.fn().mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }), - } - - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue({}), - }) - - vi.spyOn(console, "info").mockImplementation(() => {}) - vi.spyOn(console, "error").mockImplementation(() => {}) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe("isEventCapturable", () => { - it("should return true for events not in exclude list", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true) - expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true) - expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true) - expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true) - }) - - it("should return false for events in exclude list", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false) - }) - - it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true) - }) - - it("should return false for TASK_MESSAGE events when recordTaskMessages is false", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: false, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when recordTaskMessages is undefined", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: {}, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when cloudSettings is undefined", () => { - mockSettingsService.getSettings.mockReturnValue({}) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when getSettings returns undefined", () => { - mockSettingsService.getSettings.mockReturnValue(undefined) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - }) - - describe("getEventProperties", () => { - it("should merge provider properties with event properties", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - }), - } - - client.setProvider(mockProvider) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise > - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { - customProp: "value", - mode: "override", // This should override the provider's mode. - }, - }) - - expect(result).toEqual({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "override", // Event property takes precedence. - customProp: "value", - }) - - expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1) - }) - - it("should handle errors from provider gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), - } - - const consoleErrorSpy = vi.spyOn(console, "error") - - client.setProvider(mockProvider) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise > - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { customProp: "value" }, - }) - - expect(result).toEqual({ customProp: "value" }) - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining("Error getting telemetry properties: Provider error"), - ) - }) - - it("should return event properties when no provider is set", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise > - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { customProp: "value" }, - }) - - expect(result).toEqual({ customProp: "value" }) - }) - }) - - describe("capture", () => { - it("should not capture events that are not capturable", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list. - properties: { test: "value" }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: false, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: {}, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not send request when schema validation fails", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: { test: "value" }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event")) - }) - - it("should send request when event is capturable and validation passes", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const providerProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - } - - const eventProperties = { - taskId: "test-task-id", - } - - const mockValidatedData = { - type: TelemetryEventName.TASK_CREATED, - properties: { - ...providerProperties, - taskId: "test-task-id", - }, - } - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), - } - - client.setProvider(mockProvider) - - await client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: eventProperties, - }) - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events", - expect.objectContaining({ - method: "POST", - body: JSON.stringify(mockValidatedData), - }), - ) - }) - - it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }) - - const eventProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - } - - const mockValidatedData = { - type: TelemetryEventName.TASK_MESSAGE, - properties: eventProperties, - } - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: eventProperties, - }) - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events", - expect.objectContaining({ - method: "POST", - body: JSON.stringify(mockValidatedData), - }), - ) - }) - - it("should handle fetch errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockRejectedValue(new Error("Network error")) - - await expect( - client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: { test: "value" }, - }), - ).resolves.not.toThrow() - }) - }) - - describe("telemetry state methods", () => { - it("should always return true for isTelemetryEnabled", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - expect(client.isTelemetryEnabled()).toBe(true) - }) - - it("should have empty implementations for updateTelemetryState and shutdown", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - client.updateTelemetryState(true) - await client.shutdown() - }) - }) - - describe("backfillMessages", () => { - it("should not send request when not authenticated", async () => { - mockAuthService.isAuthenticated.mockReturnValue(false) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not send request when no session token available", async () => { - mockAuthService.getSessionToken.mockReturnValue(null) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Unauthorized: No session token available.", - ) - }) - - it("should send FormData request with correct structure when authenticated", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const providerProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - } - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message 1", - }, - { - ts: 2, - type: "ask" as const, - ask: "followup" as const, - text: "test question", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - - // Parse and compare properties as objects since JSON.stringify order can vary - const propertiesJson = formData.get("properties") as string - const parsedProperties = JSON.parse(propertiesJson) - expect(parsedProperties).toEqual({ - taskId: "test-task-id", - ...providerProperties, - }) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle provider errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should still work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should work without provider set", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle fetch errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockRejectedValue(new Error("Network error")) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow() - - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining( - "[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error", - ), - ) - }) - - it("should handle HTTP error responses", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found", - ) - }) - - it("should log debug information when debug is enabled", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService, true) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id", - ) - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id", - ) - }) - - it("should handle empty messages array", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.backfillMessages([], "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the empty messages array - const fileContent = await fileField.text() - expect(fileContent).toBe("[]") - }) - }) -}) diff --git a/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts b/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts deleted file mode 100644 index cbf3a7b998..0000000000 --- a/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest" -import * as vscode from "vscode" - -import { StaticTokenAuthService } from "../../auth/StaticTokenAuthService" - -// Mock vscode -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - uriScheme: "vscode", - }, - Uri: { - parse: vi.fn(), - }, -})) - -describe("StaticTokenAuthService", () => { - let authService: StaticTokenAuthService - let mockContext: vscode.ExtensionContext - let mockLog: (...args: unknown[]) => void - const testToken = "test-static-token" - - beforeEach(() => { - mockLog = vi.fn() - - // Create a minimal mock that satisfies the constructor requirements - const mockContextPartial = { - extension: { - packageJSON: { - publisher: "TestPublisher", - name: "test-extension", - }, - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn(), - }, - subscriptions: [], - } - - // Use type assertion for test mocking - mockContext = mockContextPartial as unknown as vscode.ExtensionContext - - authService = new StaticTokenAuthService(mockContext, testToken, mockLog) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("constructor", () => { - it("should create instance and log static token mode", () => { - expect(authService).toBeInstanceOf(StaticTokenAuthService) - expect(mockLog).toHaveBeenCalledWith("[auth] Using static token authentication mode") - }) - - it("should use console.log as default logger", () => { - const serviceWithoutLog = new StaticTokenAuthService( - mockContext as unknown as vscode.ExtensionContext, - testToken, - ) - // Can't directly test console.log usage, but constructor should not throw - expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService) - }) - }) - - describe("initialize", () => { - it("should start in active-session state", async () => { - await authService.initialize() - expect(authService.getState()).toBe("active-session") - }) - - it("should emit active-session event on initialize", async () => { - const spy = vi.fn() - authService.on("active-session", spy) - - await authService.initialize() - - expect(spy).toHaveBeenCalledWith({ previousState: "initializing" }) - }) - - it("should log successful initialization", async () => { - await authService.initialize() - expect(mockLog).toHaveBeenCalledWith("[auth] Static token auth service initialized in active-session state") - }) - }) - - describe("getSessionToken", () => { - it("should return the provided token", () => { - expect(authService.getSessionToken()).toBe(testToken) - }) - - it("should return different token when constructed with different token", () => { - const differentToken = "different-token" - const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog) - expect(differentService.getSessionToken()).toBe(differentToken) - }) - }) - - describe("getUserInfo", () => { - it("should return empty object", () => { - expect(authService.getUserInfo()).toEqual({}) - }) - }) - - describe("getStoredOrganizationId", () => { - it("should return null", () => { - expect(authService.getStoredOrganizationId()).toBeNull() - }) - }) - - describe("authentication state methods", () => { - it("should always return true for isAuthenticated", () => { - expect(authService.isAuthenticated()).toBe(true) - }) - - it("should always return true for hasActiveSession", () => { - expect(authService.hasActiveSession()).toBe(true) - }) - - it("should always return true for hasOrIsAcquiringActiveSession", () => { - expect(authService.hasOrIsAcquiringActiveSession()).toBe(true) - }) - - it("should return active-session for getState", () => { - expect(authService.getState()).toBe("active-session") - }) - }) - - describe("disabled authentication methods", () => { - const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService" - - it("should throw error for login", async () => { - await expect(authService.login()).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for logout", async () => { - await expect(authService.logout()).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for handleCallback", async () => { - await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for handleCallback with organization", async () => { - await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage) - }) - }) - - describe("event emission", () => { - it("should be able to register and emit events", async () => { - const activeSessionSpy = vi.fn() - const userInfoSpy = vi.fn() - - authService.on("active-session", activeSessionSpy) - authService.on("user-info", userInfoSpy) - - await authService.initialize() - - expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" }) - // user-info event is not emitted in static token mode - expect(userInfoSpy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts deleted file mode 100644 index 0e6681c20b..0000000000 --- a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts +++ /dev/null @@ -1,1095 +0,0 @@ -// npx vitest run src/__tests__/AuthService.spec.ts - -import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest" -import crypto from "crypto" -import * as vscode from "vscode" - -import { WebAuthService } from "../../auth/WebAuthService" -import { RefreshTimer } from "../../RefreshTimer" -import * as Config from "../../Config" -import * as utils from "../../utils" - -// Mock external dependencies -vi.mock("../../RefreshTimer") -vi.mock("../../Config") -vi.mock("../../utils") -vi.mock("crypto") - -// Mock fetch globally -const mockFetch = vi.fn() -global.fetch = mockFetch - -// Mock vscode module -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - uriScheme: "vscode", - }, - Uri: { - parse: vi.fn((uri: string) => ({ toString: () => uri })), - }, -})) - -describe("WebAuthService", () => { - let authService: WebAuthService - let mockTimer: { - start: Mock - stop: Mock - reset: Mock - } - let mockLog: Mock - let mockContext: { - subscriptions: { push: Mock } - secrets: { - get: Mock - store: Mock - delete: Mock - onDidChange: Mock - } - globalState: { - get: Mock - update: Mock - } - extension: { - packageJSON: { - version: string - publisher: string - name: string - } - } - } - - beforeEach(() => { - // Reset all mocks - vi.clearAllMocks() - - // Setup mock context with proper subscriptions array - mockContext = { - subscriptions: { - push: vi.fn(), - }, - secrets: { - get: vi.fn().mockResolvedValue(undefined), - store: vi.fn().mockResolvedValue(undefined), - delete: vi.fn().mockResolvedValue(undefined), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn().mockReturnValue(undefined), - update: vi.fn().mockResolvedValue(undefined), - }, - extension: { - packageJSON: { - version: "1.0.0", - publisher: "RooVeterinaryInc", - name: "roo-cline", - }, - }, - } - - // Setup timer mock - mockTimer = { - start: vi.fn(), - stop: vi.fn(), - reset: vi.fn(), - } - const MockedRefreshTimer = vi.mocked(RefreshTimer) - MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer) - - // Setup config mocks - use production URL by default to maintain existing test behavior - vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - vi.mocked(Config.getRooCodeApiUrl).mockReturnValue("https://api.test.com") - - // Setup utils mock - vi.mocked(utils.getUserAgent).mockReturnValue("Roo-Code 1.0.0") - - // Setup crypto mock - vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never) - - // Setup log mock - mockLog = vi.fn() - - authService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("constructor", () => { - it("should initialize with correct default values", () => { - expect(authService.getState()).toBe("initializing") - expect(authService.isAuthenticated()).toBe(false) - expect(authService.hasActiveSession()).toBe(false) - expect(authService.getSessionToken()).toBeUndefined() - expect(authService.getUserInfo()).toBeNull() - }) - - it("should create RefreshTimer with correct configuration", () => { - expect(RefreshTimer).toHaveBeenCalledWith({ - callback: expect.any(Function), - successInterval: 50_000, - initialBackoffMs: 1_000, - maxBackoffMs: 300_000, - }) - }) - - it("should use console.log as default logger", () => { - const serviceWithoutLog = new WebAuthService(mockContext as unknown as vscode.ExtensionContext) - // Can't directly test console.log usage, but constructor should not throw - expect(serviceWithoutLog).toBeInstanceOf(WebAuthService) - }) - }) - - describe("initialize", () => { - it("should handle credentials change and setup event listener", async () => { - await authService.initialize() - - expect(mockContext.subscriptions.push).toHaveBeenCalled() - expect(mockContext.secrets.onDidChange).toHaveBeenCalled() - }) - - it("should not initialize twice", async () => { - await authService.initialize() - const firstCallCount = vi.mocked(mockContext.secrets.onDidChange).mock.calls.length - - await authService.initialize() - expect(mockContext.secrets.onDidChange).toHaveBeenCalledTimes(firstCallCount) - expect(mockLog).toHaveBeenCalledWith("[auth] initialize() called after already initialized") - }) - - it("should transition to logged-out when no credentials exist", async () => { - mockContext.secrets.get.mockResolvedValue(undefined) - - const loggedOutSpy = vi.fn() - authService.on("logged-out", loggedOutSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "initializing" }) - }) - - it("should transition to attempting-session when valid credentials exist", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const attemptingSessionSpy = vi.fn() - authService.on("attempting-session", attemptingSessionSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("attempting-session") - expect(attemptingSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" }) - expect(mockTimer.start).toHaveBeenCalled() - }) - - it("should handle invalid credentials gracefully", async () => { - mockContext.secrets.get.mockResolvedValue("invalid-json") - - const loggedOutSpy = vi.fn() - authService.on("logged-out", loggedOutSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error)) - }) - - it("should handle credentials change events", async () => { - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - await authService.initialize() - - // Simulate credentials change event - const newCredentials = { clientToken: "new-token", sessionId: "new-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials)) - - const attemptingSessionSpy = vi.fn() - authService.on("attempting-session", attemptingSessionSpy) - - onDidChangeCallback!({ key: "clerk-auth-credentials" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(attemptingSessionSpy).toHaveBeenCalled() - }) - }) - - describe("login", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should generate state and open external URL", async () => { - const mockOpenExternal = vi.fn() - const vscode = await import("vscode") - vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal) - - await authService.login() - - expect(crypto.randomBytes).toHaveBeenCalledWith(16) - expect(mockContext.globalState.update).toHaveBeenCalledWith( - "clerk-auth-state", - "746573742d72616e646f6d2d6279746573", - ) - expect(mockOpenExternal).toHaveBeenCalledWith( - expect.objectContaining({ - toString: expect.any(Function), - }), - ) - }) - - it("should use package.json values for redirect URI", async () => { - const mockOpenExternal = vi.fn() - const vscode = await import("vscode") - vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal) - - await authService.login() - - const expectedUrl = - "https://api.test.com/extension/sign-in?state=746573742d72616e646f6d2d6279746573&auth_redirect=vscode%3A%2F%2FRooVeterinaryInc.roo-cline" - expect(mockOpenExternal).toHaveBeenCalledWith( - expect.objectContaining({ - toString: expect.any(Function), - }), - ) - - // Verify the actual URL - const calledUri = mockOpenExternal.mock.calls[0][0] - expect(calledUri.toString()).toBe(expectedUrl) - }) - - it("should handle errors during login", async () => { - vi.mocked(crypto.randomBytes).mockImplementation(() => { - throw new Error("Crypto error") - }) - - await expect(authService.login()).rejects.toThrow("Failed to initiate Roo Code Cloud authentication") - expect(mockLog).toHaveBeenCalledWith("[auth] Error initiating Roo Code Cloud auth: Error: Crypto error") - }) - }) - - describe("handleCallback", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should handle invalid parameters", async () => { - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.handleCallback(null, "state") - expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url") - - await authService.handleCallback("code", null) - expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url") - }) - - it("should validate state parameter", async () => { - mockContext.globalState.get.mockReturnValue("stored-state") - - await expect(authService.handleCallback("code", "different-state")).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - expect(mockLog).toHaveBeenCalledWith("[auth] State mismatch in callback") - }) - - it("should successfully handle valid callback", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - // Mock successful Clerk sign-in response - const mockResponse = { - ok: true, - json: () => - Promise.resolve({ - response: { created_session_id: "session-123" }, - }), - headers: { - get: (header: string) => (header === "authorization" ? "Bearer token-123" : null), - }, - } - mockFetch.mockResolvedValue(mockResponse) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.handleCallback("auth-code", storedState) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - "clerk-auth-credentials", - JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123", organizationId: null }), - ) - expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud") - }) - - it("should handle Clerk API errors", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - mockFetch.mockResolvedValue({ - ok: false, - status: 400, - statusText: "Bad Request", - }) - - const loggedOutSpy = vi.fn() - authService.on("logged-out", loggedOutSpy) - - await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - expect(loggedOutSpy).toHaveBeenCalled() - }) - }) - - describe("logout", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should clear credentials and call Clerk logout", async () => { - // Set up credentials first by simulating a login state - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - // Manually set the credentials in the service - authService["credentials"] = credentials - - // Mock successful logout response - mockFetch.mockResolvedValue({ ok: true }) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockContext.globalState.update).toHaveBeenCalledWith("clerk-auth-state", undefined) - expect(mockFetch).toHaveBeenCalledWith( - "https://clerk.roocode.com/v1/client/sessions/test-session/remove", - expect.objectContaining({ - method: "POST", - headers: expect.objectContaining({ - Authorization: "Bearer test-token", - }), - }), - ) - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - - it("should handle logout without credentials", async () => { - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockContext.secrets.delete).toHaveBeenCalled() - expect(mockFetch).not.toHaveBeenCalled() - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - - it("should handle Clerk logout errors gracefully", async () => { - // Set up credentials first by simulating a login state - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - // Manually set the credentials in the service - authService["credentials"] = credentials - - // Mock failed logout response - mockFetch.mockRejectedValue(new Error("Network error")) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockLog).toHaveBeenCalledWith("[auth] Error calling clerkLogout:", expect.any(Error)) - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - }) - - describe("state management", () => { - it("should return correct state", () => { - expect(authService.getState()).toBe("initializing") - }) - - it("should return correct authentication status", async () => { - await authService.initialize() - expect(authService.isAuthenticated()).toBe(false) - - // Create a new service instance with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const authenticatedService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await authenticatedService.initialize() - - expect(authenticatedService.isAuthenticated()).toBe(true) - expect(authenticatedService.hasActiveSession()).toBe(false) - }) - - it("should return session token only for active sessions", () => { - expect(authService.getSessionToken()).toBeUndefined() - - // Manually set state to active-session for testing - // This would normally happen through refreshSession - authService["state"] = "active-session" - authService["sessionToken"] = "test-jwt" - - expect(authService.getSessionToken()).toBe("test-jwt") - }) - - it("should return correct values for new methods", async () => { - await authService.initialize() - expect(authService.hasOrIsAcquiringActiveSession()).toBe(false) - - // Create a new service instance with credentials (attempting-session) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const attemptingService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await attemptingService.initialize() - - expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true) - expect(attemptingService.hasActiveSession()).toBe(false) - - // Manually set state to active-session for testing - attemptingService["state"] = "active-session" - expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true) - expect(attemptingService.hasActiveSession()).toBe(true) - }) - }) - - describe("session refresh", () => { - beforeEach(async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - }) - - it("should refresh session successfully", async () => { - // Mock successful token creation and user info fetch - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "new-jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "John", - last_name: "Doe", - image_url: "https://example.com/avatar.jpg", - primary_email_address_id: "email-1", - email_addresses: [{ id: "email-1", email_address: "john@example.com" }], - }, - }), - }) - - const activeSessionSpy = vi.fn() - const userInfoSpy = vi.fn() - authService.on("active-session", activeSessionSpy) - authService.on("user-info", userInfoSpy) - - // Trigger refresh by calling the timer callback - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(authService.getState()).toBe("active-session") - expect(authService.hasActiveSession()).toBe(true) - expect(authService.getSessionToken()).toBe("new-jwt-token") - expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" }) - expect(userInfoSpy).toHaveBeenCalledWith({ - userInfo: { - name: "John Doe", - email: "john@example.com", - picture: "https://example.com/avatar.jpg", - }, - }) - }) - - it("should handle invalid client token error", async () => { - // Mock 401 response (invalid token) - mockFetch.mockResolvedValue({ - ok: false, - status: 401, - statusText: "Unauthorized", - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow() - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials") - }) - - it("should handle network errors during refresh", async () => { - mockFetch.mockRejectedValue(new Error("Network error")) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow("Network error") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to refresh session", expect.any(Error)) - }) - - it("should transition to inactive-session on first attempt failure", async () => { - // Mock failed token creation response - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - const inactiveSessionSpy = vi.fn() - authService.on("inactive-session", inactiveSessionSpy) - - // Verify we start in attempting-session state - expect(authService.getState()).toBe("attempting-session") - expect(authService["isFirstRefreshAttempt"]).toBe(true) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow() - - // Should transition to inactive-session after first failure - expect(authService.getState()).toBe("inactive-session") - expect(authService["isFirstRefreshAttempt"]).toBe(false) - expect(inactiveSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" }) - }) - - it("should not transition to inactive-session on subsequent failures", async () => { - // First, transition to inactive-session by failing the first attempt - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await expect(timerCallback()).rejects.toThrow() - - // Verify we're now in inactive-session - expect(authService.getState()).toBe("inactive-session") - expect(authService["isFirstRefreshAttempt"]).toBe(false) - - const inactiveSessionSpy = vi.fn() - authService.on("inactive-session", inactiveSessionSpy) - - // Subsequent failure should not trigger another transition - await expect(timerCallback()).rejects.toThrow() - - expect(authService.getState()).toBe("inactive-session") - expect(inactiveSessionSpy).not.toHaveBeenCalled() - }) - - it("should clear credentials on 401 during first refresh attempt (bug fix)", async () => { - // Mock 401 response during first refresh attempt - mockFetch.mockResolvedValue({ - ok: false, - status: 401, - statusText: "Unauthorized", - }) - - const loggedOutSpy = vi.fn() - authService.on("logged-out", loggedOutSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await expect(timerCallback()).rejects.toThrow() - - // Should clear credentials (not just transition to inactive-session) - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials") - - // Simulate credentials cleared event - mockContext.secrets.get.mockResolvedValue(undefined) - await authService["handleCredentialsChange"]() - - expect(authService.getState()).toBe("logged-out") - expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "attempting-session" }) - }) - }) - - describe("user info", () => { - it("should return null initially", () => { - expect(authService.getUserInfo()).toBeNull() - }) - - it("should parse user info correctly for personal accounts", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock successful responses - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Jane", - last_name: "Smith", - image_url: "https://example.com/jane.jpg", - primary_email_address_id: "email-2", - email_addresses: [ - { id: "email-1", email_address: "jane.old@example.com" }, - { id: "email-2", email_address: "jane@example.com" }, - ], - }, - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "Jane Smith", - email: "jane@example.com", - picture: "https://example.com/jane.jpg", - }) - }) - - it("should parse user info correctly for organization accounts", async () => { - // Set up with credentials for organization account - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: "org_1" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock successful responses - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Jane", - last_name: "Smith", - image_url: "https://example.com/jane.jpg", - primary_email_address_id: "email-2", - email_addresses: [ - { id: "email-1", email_address: "jane.old@example.com" }, - { id: "email-2", email_address: "jane@example.com" }, - ], - }, - }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: [ - { - id: "org_member_id_1", - role: "member", - organization: { - id: "org_1", - name: "Org 1", - }, - }, - ], - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "Jane Smith", - email: "jane@example.com", - picture: "https://example.com/jane.jpg", - organizationId: "org_1", - organizationName: "Org 1", - organizationRole: "member", - }) - }) - - it("should handle missing user info fields", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock responses with minimal data - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "John", - last_name: "Doe", - // Missing other fields - }, - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "John Doe", - email: undefined, - picture: undefined, - }) - }) - }) - - describe("event emissions", () => { - it("should emit logged-out event", async () => { - const loggedOutSpy = vi.fn() - authService.on("logged-out", loggedOutSpy) - - await authService.initialize() - - expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "initializing" }) - }) - - it("should emit attempting-session event", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const attemptingSessionSpy = vi.fn() - authService.on("attempting-session", attemptingSessionSpy) - - await authService.initialize() - - expect(attemptingSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" }) - }) - - it("should emit active-session event", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock both the token creation and user info fetch - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Test", - last_name: "User", - }, - }), - }) - - const activeSessionSpy = vi.fn() - authService.on("active-session", activeSessionSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" }) - }) - - it("should emit user-info event", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Test", - last_name: "User", - }, - }), - }) - - const userInfoSpy = vi.fn() - authService.on("user-info", userInfoSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(userInfoSpy).toHaveBeenCalledWith({ - userInfo: { - name: "Test User", - email: undefined, - picture: undefined, - }, - }) - }) - }) - - describe("error handling", () => { - it("should handle credentials change errors", async () => { - mockContext.secrets.get.mockRejectedValue(new Error("Storage error")) - - await authService.initialize() - - expect(mockLog).toHaveBeenCalledWith("[auth] Error handling credentials change:", expect.any(Error)) - }) - - it("should handle malformed JSON in credentials", async () => { - mockContext.secrets.get.mockResolvedValue("invalid-json{") - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error)) - }) - - it("should handle invalid credentials schema", async () => { - mockContext.secrets.get.mockResolvedValue(JSON.stringify({ invalid: "data" })) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid credentials format:", expect.any(Array)) - }) - - it("should handle missing authorization header in sign-in response", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - mockFetch.mockResolvedValue({ - ok: true, - json: () => - Promise.resolve({ - response: { created_session_id: "session-123" }, - }), - headers: { - get: () => null, // No authorization header - }, - }) - - await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - }) - }) - - describe("timer integration", () => { - it("should stop timer on logged-out transition", async () => { - await authService.initialize() - - expect(mockTimer.stop).toHaveBeenCalled() - }) - - it("should start timer on attempting-session transition", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - await authService.initialize() - - expect(mockTimer.start).toHaveBeenCalled() - }) - }) - - describe("auth credentials key scoping", () => { - it("should use default key when getClerkBaseUrl returns production URL", async () => { - // Mock getClerkBaseUrl to return production URL - vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - await service.initialize() - await service["storeCredentials"](credentials) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - "clerk-auth-credentials", - JSON.stringify(credentials), - ) - }) - - it("should use scoped key when getClerkBaseUrl returns custom URL", async () => { - const customUrl = "https://custom.clerk.com" - // Mock getClerkBaseUrl to return custom URL - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - await service.initialize() - await service["storeCredentials"](credentials) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - `clerk-auth-credentials-${customUrl}`, - JSON.stringify(credentials), - ) - }) - - it("should load credentials using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - await service.initialize() - const loadedCredentials = await service["loadCredentials"]() - - expect(mockContext.secrets.get).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`) - expect(loadedCredentials).toEqual(credentials) - }) - - it("should clear credentials using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - - await service.initialize() - await service["clearCredentials"]() - - expect(mockContext.secrets.delete).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`) - }) - - it("should listen for changes on scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - // Simulate credentials change event with scoped key - const newCredentials = { clientToken: "new-token", sessionId: "new-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials)) - - const attemptingSessionSpy = vi.fn() - service.on("attempting-session", attemptingSessionSpy) - - onDidChangeCallback!({ key: `clerk-auth-credentials-${customUrl}` }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(attemptingSessionSpy).toHaveBeenCalled() - }) - - it("should not respond to changes on different scoped keys", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - const inactiveSessionSpy = vi.fn() - service.on("inactive-session", inactiveSessionSpy) - - // Simulate credentials change event with different scoped key - onDidChangeCallback!({ key: "clerk-auth-credentials-https://other.clerk.com" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(inactiveSessionSpy).not.toHaveBeenCalled() - }) - - it("should not respond to changes on default key when using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - const inactiveSessionSpy = vi.fn() - service.on("inactive-session", inactiveSessionSpy) - - // Simulate credentials change event with default key - onDidChangeCallback!({ key: "clerk-auth-credentials" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(inactiveSessionSpy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/auth/AuthService.ts b/packages/cloud/src/auth/AuthService.ts deleted file mode 100644 index 11ed5161ed..0000000000 --- a/packages/cloud/src/auth/AuthService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import EventEmitter from "events" -import type { CloudUserInfo } from "@roo-code/types" - -export interface AuthServiceEvents { - "attempting-session": [data: { previousState: AuthState }] - "inactive-session": [data: { previousState: AuthState }] - "active-session": [data: { previousState: AuthState }] - "logged-out": [data: { previousState: AuthState }] - "user-info": [data: { userInfo: CloudUserInfo }] -} - -export type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session" - -export interface AuthService extends EventEmitter { - // Lifecycle - initialize(): Promise - - // Authentication methods - login(): Promise - logout(): Promise - handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise - - // State methods - getState(): AuthState - isAuthenticated(): boolean - hasActiveSession(): boolean - hasOrIsAcquiringActiveSession(): boolean - - // Token and user info - getSessionToken(): string | undefined - getUserInfo(): CloudUserInfo | null - getStoredOrganizationId(): string | null -} diff --git a/packages/cloud/src/auth/StaticTokenAuthService.ts b/packages/cloud/src/auth/StaticTokenAuthService.ts deleted file mode 100644 index 11fc18d3fb..0000000000 --- a/packages/cloud/src/auth/StaticTokenAuthService.ts +++ /dev/null @@ -1,68 +0,0 @@ -import EventEmitter from "events" -import * as vscode from "vscode" -import type { CloudUserInfo } from "@roo-code/types" -import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" - -export class StaticTokenAuthService extends EventEmitter implements AuthService { - private state: AuthState = "active-session" - private token: string - private log: (...args: unknown[]) => void - - constructor(context: vscode.ExtensionContext, token: string, log?: (...args: unknown[]) => void) { - super() - this.token = token - this.log = log || console.log - this.log("[auth] Using static token authentication mode") - } - - public async initialize(): Promise { - const previousState: AuthState = "initializing" - this.state = "active-session" - this.emit("active-session", { previousState }) - this.log("[auth] Static token auth service initialized in active-session state") - } - - public async login(): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public async logout(): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public async handleCallback( - _code: string | null, - _state: string | null, - _organizationId?: string | null, - ): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public getState(): AuthState { - return this.state - } - - public getSessionToken(): string | undefined { - return this.token - } - - public isAuthenticated(): boolean { - return true - } - - public hasActiveSession(): boolean { - return true - } - - public hasOrIsAcquiringActiveSession(): boolean { - return true - } - - public getUserInfo(): CloudUserInfo | null { - return {} - } - - public getStoredOrganizationId(): string | null { - return null - } -} diff --git a/packages/cloud/src/auth/WebAuthService.ts b/packages/cloud/src/auth/WebAuthService.ts deleted file mode 100644 index 82d3122426..0000000000 --- a/packages/cloud/src/auth/WebAuthService.ts +++ /dev/null @@ -1,641 +0,0 @@ -import crypto from "crypto" -import EventEmitter from "events" - -import * as vscode from "vscode" -import { z } from "zod" - -import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types" - -import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../Config" -import { RefreshTimer } from "../RefreshTimer" -import { getUserAgent } from "../utils" -import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" - -const authCredentialsSchema = z.object({ - clientToken: z.string().min(1, "Client token cannot be empty"), - sessionId: z.string().min(1, "Session ID cannot be empty"), - organizationId: z.string().nullable().optional(), -}) - -type AuthCredentials = z.infer - -const AUTH_STATE_KEY = "clerk-auth-state" - -const clerkSignInResponseSchema = z.object({ - response: z.object({ - created_session_id: z.string(), - }), -}) - -const clerkCreateSessionTokenResponseSchema = z.object({ - jwt: z.string(), -}) - -const clerkMeResponseSchema = z.object({ - response: z.object({ - first_name: z.string().optional().nullable(), - last_name: z.string().optional().nullable(), - image_url: z.string().optional(), - primary_email_address_id: z.string().optional(), - email_addresses: z - .array( - z.object({ - id: z.string(), - email_address: z.string(), - }), - ) - .optional(), - }), -}) - -const clerkOrganizationMembershipsSchema = z.object({ - response: z.array( - z.object({ - id: z.string(), - role: z.string(), - permissions: z.array(z.string()).optional(), - created_at: z.number().optional(), - updated_at: z.number().optional(), - organization: z.object({ - id: z.string(), - name: z.string(), - slug: z.string().optional(), - image_url: z.string().optional(), - has_image: z.boolean().optional(), - created_at: z.number().optional(), - updated_at: z.number().optional(), - }), - }), - ), -}) - -class InvalidClientTokenError extends Error { - constructor() { - super("Invalid/Expired client token") - Object.setPrototypeOf(this, InvalidClientTokenError.prototype) - } -} - -export class WebAuthService extends EventEmitter implements AuthService { - private context: vscode.ExtensionContext - private timer: RefreshTimer - private state: AuthState = "initializing" - private log: (...args: unknown[]) => void - private readonly authCredentialsKey: string - - private credentials: AuthCredentials | null = null - private sessionToken: string | null = null - private userInfo: CloudUserInfo | null = null - private isFirstRefreshAttempt: boolean = false - - constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) { - super() - - this.context = context - this.log = log || console.log - - // Calculate auth credentials key based on Clerk base URL - const clerkBaseUrl = getClerkBaseUrl() - if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) { - this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}` - } else { - this.authCredentialsKey = "clerk-auth-credentials" - } - - this.timer = new RefreshTimer({ - callback: async () => { - await this.refreshSession() - return true - }, - successInterval: 50_000, - initialBackoffMs: 1_000, - maxBackoffMs: 300_000, - }) - } - - private async handleCredentialsChange(): Promise { - try { - const credentials = await this.loadCredentials() - - if (credentials) { - if ( - this.credentials === null || - this.credentials.clientToken !== credentials.clientToken || - this.credentials.sessionId !== credentials.sessionId - ) { - this.transitionToAttemptingSession(credentials) - } - } else { - if (this.state !== "logged-out") { - this.transitionToLoggedOut() - } - } - } catch (error) { - this.log("[auth] Error handling credentials change:", error) - } - } - - private transitionToLoggedOut(): void { - this.timer.stop() - - const previousState = this.state - - this.credentials = null - this.sessionToken = null - this.userInfo = null - this.state = "logged-out" - - this.emit("logged-out", { previousState }) - - this.log("[auth] Transitioned to logged-out state") - } - - private transitionToAttemptingSession(credentials: AuthCredentials): void { - this.credentials = credentials - - const previousState = this.state - this.state = "attempting-session" - - this.sessionToken = null - this.userInfo = null - this.isFirstRefreshAttempt = true - - this.emit("attempting-session", { previousState }) - - this.timer.start() - - this.log("[auth] Transitioned to attempting-session state") - } - - private transitionToInactiveSession(): void { - const previousState = this.state - this.state = "inactive-session" - - this.sessionToken = null - this.userInfo = null - - this.emit("inactive-session", { previousState }) - - this.log("[auth] Transitioned to inactive-session state") - } - - /** - * Initialize the auth state - * - * This method loads tokens from storage and determines the current auth state. - * It also starts the refresh timer if we have an active session. - */ - public async initialize(): Promise { - if (this.state !== "initializing") { - this.log("[auth] initialize() called after already initialized") - return - } - - await this.handleCredentialsChange() - - this.context.subscriptions.push( - this.context.secrets.onDidChange((e) => { - if (e.key === this.authCredentialsKey) { - this.handleCredentialsChange() - } - }), - ) - } - - private async storeCredentials(credentials: AuthCredentials): Promise { - await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials)) - } - - private async loadCredentials(): Promise { - const credentialsJson = await this.context.secrets.get(this.authCredentialsKey) - if (!credentialsJson) return null - - try { - const parsedJson = JSON.parse(credentialsJson) - const credentials = authCredentialsSchema.parse(parsedJson) - - // Migration: If no organizationId but we have userInfo, add it - if (credentials.organizationId === undefined && this.userInfo?.organizationId) { - credentials.organizationId = this.userInfo.organizationId - await this.storeCredentials(credentials) - this.log("[auth] Migrated credentials with organizationId") - } - - return credentials - } catch (error) { - if (error instanceof z.ZodError) { - this.log("[auth] Invalid credentials format:", error.errors) - } else { - this.log("[auth] Failed to parse stored credentials:", error) - } - return null - } - } - - private async clearCredentials(): Promise { - await this.context.secrets.delete(this.authCredentialsKey) - } - - /** - * Start the login process - * - * This method initiates the authentication flow by generating a state parameter - * and opening the browser to the authorization URL. - */ - public async login(): Promise { - try { - // Generate a cryptographically random state parameter. - const state = crypto.randomBytes(16).toString("hex") - await this.context.globalState.update(AUTH_STATE_KEY, state) - const packageJSON = this.context.extension?.packageJSON - const publisher = packageJSON?.publisher ?? "RooVeterinaryInc" - const name = packageJSON?.name ?? "roo-cline" - const params = new URLSearchParams({ - state, - auth_redirect: `${vscode.env.uriScheme}://${publisher}.${name}`, - }) - const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}` - await vscode.env.openExternal(vscode.Uri.parse(url)) - } catch (error) { - this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`) - throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`) - } - } - - /** - * Handle the callback from Roo Code Cloud - * - * This method is called when the user is redirected back to the extension - * after authenticating with Roo Code Cloud. - * - * @param code The authorization code from the callback - * @param state The state parameter from the callback - * @param organizationId The organization ID from the callback (null for personal accounts) - */ - public async handleCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { - if (!code || !state) { - vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url") - return - } - - try { - // Validate state parameter to prevent CSRF attacks. - const storedState = this.context.globalState.get(AUTH_STATE_KEY) - - if (state !== storedState) { - this.log("[auth] State mismatch in callback") - throw new Error("Invalid state parameter. Authentication request may have been tampered with.") - } - - const credentials = await this.clerkSignIn(code) - - // Set organizationId (null for personal accounts) - credentials.organizationId = organizationId || null - - await this.storeCredentials(credentials) - - vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud") - this.log("[auth] Successfully authenticated with Roo Code Cloud") - } catch (error) { - this.log(`[auth] Error handling Roo Code Cloud callback: ${error}`) - const previousState = this.state - this.state = "logged-out" - this.emit("logged-out", { previousState }) - throw new Error(`Failed to handle Roo Code Cloud callback: ${error}`) - } - } - - /** - * Log out - * - * This method removes all stored tokens and stops the refresh timer. - */ - public async logout(): Promise { - const oldCredentials = this.credentials - - try { - // Clear credentials from storage - onDidChange will handle state transitions - await this.clearCredentials() - await this.context.globalState.update(AUTH_STATE_KEY, undefined) - - if (oldCredentials) { - try { - await this.clerkLogout(oldCredentials) - } catch (error) { - this.log("[auth] Error calling clerkLogout:", error) - } - } - - vscode.window.showInformationMessage("Logged out from Roo Code Cloud") - this.log("[auth] Logged out from Roo Code Cloud") - } catch (error) { - this.log(`[auth] Error logging out from Roo Code Cloud: ${error}`) - throw new Error(`Failed to log out from Roo Code Cloud: ${error}`) - } - } - - public getState(): AuthState { - return this.state - } - - public getSessionToken(): string | undefined { - if (this.state === "active-session" && this.sessionToken) { - return this.sessionToken - } - - return - } - - /** - * Check if the user is authenticated - * - * @returns True if the user is authenticated (has an active, attempting, or inactive session) - */ - public isAuthenticated(): boolean { - return ( - this.state === "active-session" || this.state === "attempting-session" || this.state === "inactive-session" - ) - } - - public hasActiveSession(): boolean { - return this.state === "active-session" - } - - /** - * Check if the user has an active session or is currently attempting to acquire one - * - * @returns True if the user has an active session or is attempting to get one - */ - public hasOrIsAcquiringActiveSession(): boolean { - return this.state === "active-session" || this.state === "attempting-session" - } - - /** - * Refresh the session - * - * This method refreshes the session token using the client token. - */ - private async refreshSession(): Promise { - if (!this.credentials) { - this.log("[auth] Cannot refresh session: missing credentials") - return - } - - try { - const previousState = this.state - this.sessionToken = await this.clerkCreateSessionToken() - this.state = "active-session" - - if (previousState !== "active-session") { - this.log("[auth] Transitioned to active-session state") - this.emit("active-session", { previousState }) - this.fetchUserInfo() - } - } catch (error) { - if (error instanceof InvalidClientTokenError) { - this.log("[auth] Invalid/Expired client token: clearing credentials") - this.clearCredentials() - } else if (this.isFirstRefreshAttempt && this.state === "attempting-session") { - this.isFirstRefreshAttempt = false - this.transitionToInactiveSession() - } - this.log("[auth] Failed to refresh session", error) - throw error - } - } - - private async fetchUserInfo(): Promise { - if (!this.credentials) { - return - } - - this.userInfo = await this.clerkMe() - this.emit("user-info", { userInfo: this.userInfo }) - } - - /** - * Extract user information from the ID token - * - * @returns User information from ID token claims or null if no ID token available - */ - public getUserInfo(): CloudUserInfo | null { - return this.userInfo - } - - /** - * Get the stored organization ID from credentials - * - * @returns The stored organization ID, null for personal accounts or if no credentials exist - */ - public getStoredOrganizationId(): string | null { - return this.credentials?.organizationId || null - } - - private async clerkSignIn(ticket: string): Promise { - const formData = new URLSearchParams() - formData.append("strategy", "ticket") - formData.append("ticket", ticket) - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sign_ins`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const { - response: { created_session_id: sessionId }, - } = clerkSignInResponseSchema.parse(await response.json()) - - // 3. Extract the client token from the Authorization header. - const clientToken = response.headers.get("authorization") - - if (!clientToken) { - throw new Error("No authorization header found in the response") - } - - return authCredentialsSchema.parse({ clientToken, sessionId }) - } - - private async clerkCreateSessionToken(): Promise { - const formData = new URLSearchParams() - formData.append("_is_native", "1") - - // Handle 3 cases for organization_id: - // 1. Have an org id: organization_id=THE_ORG_ID - // 2. Have a personal account: organization_id= (empty string) - // 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all - const organizationId = this.getStoredOrganizationId() - if (this.credentials?.organizationId !== undefined) { - // We have organization context info (either org id or personal account) - formData.append("organization_id", organizationId || "") - } - // If organizationId is undefined, don't send the param at all (old credentials) - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (response.status === 401 || response.status === 404) { - throw new InvalidClientTokenError() - } else if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data = clerkCreateSessionTokenResponseSchema.parse(await response.json()) - - return data.jwt - } - - private async clerkMe(): Promise { - const response = await fetch(`${getClerkBaseUrl()}/v1/me`, { - headers: { - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const { response: userData } = clerkMeResponseSchema.parse(await response.json()) - - const userInfo: CloudUserInfo = {} - - const names = [userData.first_name, userData.last_name].filter((name) => !!name) - userInfo.name = names.length > 0 ? names.join(" ") : undefined - const primaryEmailAddressId = userData.primary_email_address_id - const emailAddresses = userData.email_addresses - - if (primaryEmailAddressId && emailAddresses) { - userInfo.email = emailAddresses.find( - (email: { id: string }) => primaryEmailAddressId === email.id, - )?.email_address - } - - userInfo.picture = userData.image_url - - // Fetch organization info if user is in organization context - try { - const storedOrgId = this.getStoredOrganizationId() - - if (this.credentials?.organizationId !== undefined) { - // We have organization context info - if (storedOrgId !== null) { - // User is in organization context - fetch user's memberships and filter - const orgMemberships = await this.clerkGetOrganizationMemberships() - const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId) - - if (userMembership) { - this.setUserOrganizationInfo(userInfo, userMembership) - this.log("[auth] User in organization context:", { - id: userMembership.organization.id, - name: userMembership.organization.name, - role: userMembership.role, - }) - } else { - this.log("[auth] Warning: User not found in stored organization:", storedOrgId) - } - } else { - this.log("[auth] User in personal account context - not setting organization info") - } - } else { - // Old credentials without organization context - fetch organization info to determine context - const orgMemberships = await this.clerkGetOrganizationMemberships() - const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships) - - if (primaryOrgMembership) { - this.setUserOrganizationInfo(userInfo, primaryOrgMembership) - this.log("[auth] Legacy credentials: Found organization membership:", { - id: primaryOrgMembership.organization.id, - name: primaryOrgMembership.organization.name, - role: primaryOrgMembership.role, - }) - } else { - this.log("[auth] Legacy credentials: No organization memberships found") - } - } - } catch (error) { - this.log("[auth] Failed to fetch organization info:", error) - // Don't throw - organization info is optional - } - - return userInfo - } - - private findOrganizationMembership( - memberships: CloudOrganizationMembership[], - organizationId: string, - ): CloudOrganizationMembership | undefined { - return memberships?.find((membership) => membership.organization.id === organizationId) - } - - private findPrimaryOrganizationMembership( - memberships: CloudOrganizationMembership[], - ): CloudOrganizationMembership | undefined { - return memberships && memberships.length > 0 ? memberships[0] : undefined - } - - private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void { - userInfo.organizationId = membership.organization.id - userInfo.organizationName = membership.organization.name - userInfo.organizationRole = membership.role - userInfo.organizationImageUrl = membership.organization.image_url - } - - private async clerkGetOrganizationMemberships(): Promise { - const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { - headers: { - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - signal: AbortSignal.timeout(10000), - }) - - return clerkOrganizationMembershipsSchema.parse(await response.json()).response - } - - private async clerkLogout(credentials: AuthCredentials): Promise { - const formData = new URLSearchParams() - formData.append("_is_native", "1") - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${credentials.clientToken}`, - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - } - - private userAgent(): string { - return getUserAgent(this.context) - } -} diff --git a/packages/cloud/src/auth/index.ts b/packages/cloud/src/auth/index.ts deleted file mode 100644 index b04a805295..0000000000 --- a/packages/cloud/src/auth/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" -export { WebAuthService } from "./WebAuthService" -export { StaticTokenAuthService } from "./StaticTokenAuthService" diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts deleted file mode 100644 index 9770f349c6..0000000000 --- a/packages/cloud/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./CloudService" -export * from "./Config" diff --git a/packages/cloud/src/types.ts b/packages/cloud/src/types.ts deleted file mode 100644 index 0139bb78ec..0000000000 --- a/packages/cloud/src/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CloudServiceCallbacks { - stateChanged?: () => void - log?: (...args: unknown[]) => void -} diff --git a/packages/cloud/src/utils.ts b/packages/cloud/src/utils.ts deleted file mode 100644 index cf87aa5e28..0000000000 --- a/packages/cloud/src/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as vscode from "vscode" - -/** - * Get the User-Agent string for API requests - * @param context Optional extension context for more accurate version detection - * @returns User-Agent string in format "Roo-Code {version}" - */ -export function getUserAgent(context?: vscode.ExtensionContext): string { - return `Roo-Code ${context?.extension?.packageJSON?.version || "unknown"}` -} diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json deleted file mode 100644 index f599e2220d..0000000000 --- a/packages/cloud/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@roo-code/config-typescript/vscode-library.json", - "include": ["src"], - "exclude": ["node_modules"] -} diff --git a/packages/cloud/vitest.config.ts b/packages/cloud/vitest.config.ts deleted file mode 100644 index 569f167543..0000000000 --- a/packages/cloud/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "vitest/config" - -export default defineConfig({ - test: { - globals: true, - environment: "node", - watch: false, - }, - resolve: { - alias: { - vscode: new URL("./src/__mocks__/vscode.ts", import.meta.url).pathname, - }, - }, -}) diff --git a/packages/evals/Dockerfile.runner b/packages/evals/Dockerfile.runner index b718b9cd7b..ec6dc7a8a3 100644 --- a/packages/evals/Dockerfile.runner +++ b/packages/evals/Dockerfile.runner @@ -84,7 +84,6 @@ WORKDIR /roo/repo RUN mkdir -p \ scripts \ packages/build \ - packages/cloud \ packages/config-eslint \ packages/config-typescript \ packages/evals \ @@ -99,7 +98,6 @@ COPY ./pnpm-lock.yaml ./ COPY ./pnpm-workspace.yaml ./ COPY ./scripts/bootstrap.mjs ./scripts/ COPY ./packages/build/package.json ./packages/build/ -COPY ./packages/cloud/package.json ./packages/cloud/ COPY ./packages/config-eslint/package.json ./packages/config-eslint/ COPY ./packages/config-typescript/package.json ./packages/config-typescript/ COPY ./packages/evals/package.json ./packages/evals/ diff --git a/packages/ipc/README.md b/packages/ipc/README.md new file mode 100644 index 0000000000..7642af4958 --- /dev/null +++ b/packages/ipc/README.md @@ -0,0 +1,90 @@ +# IPC (Inter-Process Communication) + +This package provides IPC functionality for Roo Code, allowing external applications to communicate with the extension through a socket-based interface. + +## Available Commands + +The IPC interface supports the following task commands: + +### StartNewTask + +Starts a new task with optional configuration and initial message. + +**Parameters:** + +- `configuration`: RooCode settings object +- `text`: Initial task message (string) +- `images`: Array of image data URIs (optional) +- `newTab`: Whether to open in a new tab (boolean, optional) + +### CancelTask + +Cancels a running task. + +**Parameters:** + +- `data`: Task ID to cancel (string) + +### CloseTask + +Closes a task and performs cleanup. + +**Parameters:** + +- `data`: Task ID to close (string) + +### ResumeTask + +Resumes a task from history. + +**Parameters:** + +- `data`: Task ID to resume (string) + +**Error Handling:** + +- If the task ID is not found in history, the command will fail gracefully without crashing the IPC server +- Errors are logged for debugging purposes but do not propagate to the client + +## Usage Example + +```typescript +import { IpcClient } from "@roo-code/ipc" + +const client = new IpcClient("/path/to/socket") + +// Resume a task +client.sendCommand({ + commandName: "ResumeTask", + data: "task-123", +}) + +// Start a new task +client.sendCommand({ + commandName: "StartNewTask", + data: { + configuration: { + /* RooCode settings */ + }, + text: "Hello, world!", + images: [], + newTab: false, + }, +}) +``` + +## Events + +The IPC interface also emits task events that clients can listen to: + +- `TaskStarted`: When a task begins +- `TaskCompleted`: When a task finishes +- `TaskAborted`: When a task is cancelled +- `Message`: When a task sends a message + +## Socket Path + +The socket path is typically located in the system's temporary directory and follows the pattern: + +- Unix/Linux/macOS: `/tmp/roo-code-{id}.sock` +- Windows: `\\.\pipe\roo-code-{id}` diff --git a/packages/types/.gitignore b/packages/types/.gitignore new file mode 100644 index 0000000000..ddf5f39ede --- /dev/null +++ b/packages/types/.gitignore @@ -0,0 +1,2 @@ +dist +npm/package.json diff --git a/packages/types/README.md b/packages/types/README.md deleted file mode 100644 index 635c380139..0000000000 --- a/packages/types/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# @roo-code/types - -### Publish to NPM - -First authenticate with NPM: - -```sh -npm login -``` - -Next, manually bump the NPM package version: - -```sh -cd packages/types/npm && npm version minor && cd - -``` - -Finally, publish to NPM: - -```sh -pnpm --filter @roo-code/types npm:publish -``` - -Note that you'll be asked for an MFA code to complete the publish. diff --git a/packages/types/eslint.config.mjs b/packages/types/eslint.config.mjs index 694bf73664..c603a68f12 100644 --- a/packages/types/eslint.config.mjs +++ b/packages/types/eslint.config.mjs @@ -1,4 +1,20 @@ import { config } from "@roo-code/config-eslint/base" +import globals from "globals" /** @type {import("eslint").Linter.Config} */ -export default [...config] +export default [ + ...config, + { + files: ["**/*.cjs"], + languageOptions: { + globals: { + ...globals.node, + ...globals.commonjs, + }, + sourceType: "commonjs", + }, + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, +] diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json deleted file mode 100644 index 3ab21bda2c..0000000000 --- a/packages/types/npm/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "@roo-code/types", - "version": "1.40.0", - "description": "TypeScript type definitions for Roo Code.", - "publishConfig": { - "access": "public", - "name": "@roo-code/types" - }, - "author": "Roo Code Team", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/RooCodeInc/Roo-Code.git" - }, - "bugs": { - "url": "https://github.com/RooCodeInc/Roo-Code/issues" - }, - "homepage": "https://github.com/RooCodeInc/Roo-Code/tree/main/packages/types", - "keywords": [ - "roo", - "roo-code", - "ai" - ], - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - } - }, - "files": [ - "dist" - ] -} diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json new file mode 100644 index 0000000000..2b1aa701e2 --- /dev/null +++ b/packages/types/npm/package.metadata.json @@ -0,0 +1,22 @@ +{ + "name": "@roo-code/types", + "version": "1.60.0", + "description": "TypeScript type definitions for Roo Code.", + "publishConfig": { + "access": "public", + "name": "@roo-code/types", + "registry": "https://registry.npmjs.org/" + }, + "author": "Roo Code Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/RooCodeInc/Roo-Code.git" + }, + "bugs": { + "url": "https://github.com/RooCodeInc/Roo-Code/issues" + }, + "homepage": "https://roocode.com", + "keywords": ["roo", "roo-code", "ai"], + "files": ["dist"] +} diff --git a/packages/types/package.json b/packages/types/package.json index 341b98fe0d..44fde30b59 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -18,9 +18,9 @@ "check-types": "tsc --noEmit", "test": "vitest run", "build": "tsup", - "npm:publish:test": "tsup --outDir npm/dist && cd npm && npm publish --dry-run", - "npm:publish": "tsup --outDir npm/dist && cd npm && npm publish", - "clean": "rimraf dist npm/dist .turbo" + "build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'", + "npm:publish": "node scripts/publish-npm.cjs", + "clean": "rimraf dist .turbo" }, "dependencies": { "zod": "^3.25.61" @@ -28,7 +28,8 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.x", + "@types/node": "^24.1.0", + "globals": "^16.3.0", "tsup": "^8.3.5", "vitest": "^3.2.3" } diff --git a/packages/types/scripts/publish-npm.cjs b/packages/types/scripts/publish-npm.cjs new file mode 100644 index 0000000000..467ae3d506 --- /dev/null +++ b/packages/types/scripts/publish-npm.cjs @@ -0,0 +1,351 @@ +/* eslint-env node */ + +const fs = require("fs") +const path = require("path") +const { execSync } = require("child_process") +const readline = require("readline") + +const PACKAGE_NAME = "@roo-code/types" +const BRANCH_NAME = "roo-code-types-v" + +const rootDir = path.join(__dirname, "..") +const npmDir = path.join(rootDir, "npm") +const monorepoPackagePath = path.join(rootDir, "package.json") +const npmMetadataPath = path.join(npmDir, "package.metadata.json") +const npmPackagePath = path.join(npmDir, "package.json") + +const args = process.argv.slice(2) +const publishOnly = args.includes("--publish-only") + +async function confirmPublish() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + return new Promise((resolve) => { + rl.question("\n⚠️ Are you sure you want to publish to npm? (y/n): ", (answer) => { + rl.close() + resolve(answer.toLowerCase() === "y") + }) + }) +} + +function updatePackageVersion(filePath, version) { + try { + const packageContent = JSON.parse(fs.readFileSync(filePath, "utf8")) + const oldVersion = packageContent.version + packageContent.version = version + fs.writeFileSync(filePath, JSON.stringify(packageContent, null, 2) + "\n") + + try { + execSync(`npx prettier --write "${filePath}"`, { stdio: "pipe" }) + console.log(`✨ Formatted ${path.basename(filePath)} with prettier`) + } catch (prettierError) { + console.warn(`⚠️ Could not format with prettier:`, prettierError.message) + } + + const fileName = path.basename(filePath) + console.log(`✅ Updated ${fileName} version: ${oldVersion} → ${version}`) + return oldVersion + } catch (error) { + throw new Error(`Failed to update version in ${path.basename(filePath)}: ${error.message}`) + } +} + +function syncVersionToMetadata(version) { + console.log(" 📝 Syncing version to package.metadata.json...") + updatePackageVersion(npmMetadataPath, version) +} + +function commitVersionChanges(version) { + try { + console.log(" 📝 Committing version changes to git...") + + try { + const status = execSync("git status --porcelain", { encoding: "utf8" }) + const relevantChanges = status.split("\n").filter((line) => line.includes("packages/types/npm/package")) + + if (relevantChanges.length === 0) { + console.log(" ⚠️ No version changes to commit") + return + } + } catch (error) { + console.warn(" ⚠️ Could not check git status:", error.message) + } + + execSync("git add .", { stdio: "pipe" }) + const commitMessage = `chore: bump version to v${version}` + execSync(`git commit -m "${commitMessage}"`, { stdio: "pipe" }) + console.log(` ✅ Committed: ${commitMessage}`) + } catch (error) { + console.warn(" ⚠️ Could not commit version changes:", error.message) + console.log(" You may need to commit these changes manually.") + } +} + +function checkGitHubCLI() { + try { + execSync("gh --version", { stdio: "pipe" }) + execSync("gh auth status", { stdio: "pipe" }) + return true + } catch (_error) { + return false + } +} + +function createPullRequest(branchName, baseBranch, version) { + try { + console.log(` 🔄 Creating pull request...`) + + if (!checkGitHubCLI()) { + console.warn(" ⚠️ GitHub CLI not found or not authenticated") + console.log(" Install gh CLI and run: gh auth login") + console.log(" Then manually create PR with: gh pr create") + return + } + + const title = `Release: v${version}` + const body = `## 🚀 Release v${version} + +This PR contains the version bump for the SDK release v${version}. + +### Changes +- Bumped version from previous to v${version} +- Published to npm as ${PACKAGE_NAME}@${version} + +### Checklist +- [x] Version bumped +- [x] Package published to npm +- [ ] Changelog updated (if applicable) +- [ ] Documentation updated (if applicable) + +--- +*This PR was automatically created by the npm publish script.*` + + try { + // Create the pull request + const prUrl = execSync( + `gh pr create --base "${baseBranch}" --head "${branchName}" --title "${title}" --body "${body}"`, + { encoding: "utf8", stdio: "pipe" }, + ).trim() + + console.log(` ✅ Pull request created: ${prUrl}`) + } catch (error) { + if (error.message.includes("already exists")) { + console.log(" ℹ️ Pull request already exists for this branch") + } else { + throw error + } + } + } catch (error) { + console.error(" ❌ Failed to create pull request:", error.message) + console.log(" You can manually create a PR with:") + console.log(` gh pr create --base "${baseBranch}" --head "${branchName}"`) + } +} + +function createVersionBranchAndCommit(version) { + try { + const branchName = `${BRANCH_NAME}${version}` + console.log(` 🌿 Creating version branch: ${branchName}...`) + + let currentBranch + + try { + currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { + encoding: "utf8", + }).trim() + } catch (_error) { + console.warn(" ⚠️ Could not determine current branch") + currentBranch = "main" + } + + execSync(`git checkout -b ${branchName}`, { stdio: "pipe" }) + console.log(` ✅ Created branch: ${branchName}`) + commitVersionChanges(version) + execSync(`git push --set-upstream origin ${branchName}`, { stdio: "pipe" }) + console.log(` ✅ Pushed branch to origin with upstream tracking`) + createPullRequest(branchName, currentBranch, version) + + if (currentBranch) { + execSync(`git checkout ${currentBranch}`, { stdio: "pipe" }) + console.log(` ✅ Returned to branch: ${currentBranch}`) + } + + console.log(` 🎯 Version branch created with commits: ${branchName}`) + } catch (error) { + console.error(" ❌ Failed to create version branch:", error.message) + console.log(" You may need to create the branch manually.") + } +} + +function generateNpmPackage() { + try { + console.log(" 📖 Reading monorepo package.json...") + + const monorepoPackageContent = fs.readFileSync(monorepoPackagePath, "utf8") + const monorepoPackage = JSON.parse(monorepoPackageContent) + + console.log(" 📖 Reading npm package metadata...") + + const npmMetadataContent = fs.readFileSync(npmMetadataPath, "utf8") + const npmMetadata = JSON.parse(npmMetadataContent) + + console.log(" 🔨 Generating npm package.json...") + + const npmPackage = { + ...npmMetadata, + dependencies: monorepoPackage.dependencies || {}, + main: "./dist/index.cjs", + module: "./dist/index.js", + types: "./dist/index.d.ts", + exports: { + ".": { + types: "./dist/index.d.ts", + import: "./dist/index.js", + require: { + types: "./dist/index.d.cts", + default: "./dist/index.cjs", + }, + }, + }, + files: ["dist"], + } + + const outputContent = JSON.stringify(npmPackage, null, 2) + "\n" + fs.writeFileSync(npmPackagePath, outputContent) + + console.log(" ✅ npm/package.json generated successfully") + console.log(` 📦 Package name: ${npmPackage.name}`) + console.log(` 📌 Version: ${npmPackage.version}`) + console.log(` 📚 Dependencies: ${Object.keys(npmPackage.dependencies).length}`) + } catch (error) { + throw new Error(`Failed to generate npm package.json: ${error.message}`) + } +} + +async function publish() { + try { + console.log("\n🚀 NPM PUBLISH WORKFLOW") + if (publishOnly) { + console.log("📌 Mode: Publish only (no git operations)") + } + console.log("=".repeat(60)) + + console.log("\n📦 Step 1: Generating npm package.json...") + generateNpmPackage() + + const npmPackage = JSON.parse(fs.readFileSync(npmPackagePath, "utf8")) + const originalVersion = npmPackage.version // Save original version + console.log(`\n📌 Current version: ${npmPackage.version}`) + console.log(`📦 Package name: ${npmPackage.name}`) + + console.log("\n📈 Step 2: Bumping version (minor)...") + + try { + execSync("npm version minor --no-git-tag-version", { + cwd: npmDir, + stdio: "inherit", + }) + } catch (error) { + console.error("❌ Failed to bump version:", error.message) + throw error + } + + const updatedPackage = JSON.parse(fs.readFileSync(npmPackagePath, "utf8")) + console.log(`✅ New version: ${updatedPackage.version}`) + + console.log("\n🔨 Step 3: Building production bundle...") + console.log(" This may take a moment...") + + try { + execSync("NODE_ENV=production pnpm tsup --outDir npm/dist", { + cwd: rootDir, + stdio: "inherit", + }) + + console.log("✅ Production build complete") + } catch (error) { + console.error("❌ Build failed:", error.message) + throw error + } + + console.log("\n" + "=".repeat(60)) + console.log("📋 PUBLISH SUMMARY:") + console.log(` Package: ${updatedPackage.name}`) + console.log(` Version: ${updatedPackage.version}`) + console.log(` Registry: ${updatedPackage.publishConfig?.registry || "https://registry.npmjs.org/"}`) + console.log(` Access: ${updatedPackage.publishConfig?.access || "public"}`) + console.log("=".repeat(60)) + + const confirmed = await confirmPublish() + + if (!confirmed) { + console.log("\n❌ Publishing cancelled by user") + console.log("🔙 Reverting version change...") + + try { + updatePackageVersion(npmPackagePath, originalVersion) + } catch (revertError) { + console.error("⚠️ Could not revert version:", revertError.message) + console.log(` You may need to manually change version back to ${originalVersion}`) + } + + process.exit(0) + } + + console.log("\n💾 Step 4: Syncing version to metadata...") + syncVersionToMetadata(updatedPackage.version) + + console.log("\n🚀 Step 5: Publishing to npm...") + + try { + execSync("npm publish", { + cwd: npmDir, + stdio: "inherit", + }) + } catch (error) { + console.error("❌ Publish failed:", error.message) + console.error("💡 The package was built but not published.") + console.error(" You can try publishing manually from the npm directory.") + + throw error + } + + if (!publishOnly) { + console.log("\n🌿 Step 6: Creating version branch, committing, and opening PR...") + createVersionBranchAndCommit(updatedPackage.version) + } else { + console.log("\n📝 Step 6: Skipping version branch creation (--publish-only mode)") + } + + console.log("\n" + "=".repeat(60)) + console.log("✅ Successfully published to npm!") + console.log(`🎉 ${updatedPackage.name}@${updatedPackage.version} is now live`) + console.log(`📦 View at: https://www.npmjs.com/package/${updatedPackage.name}`) + + if (!publishOnly) { + console.log(`🌿 Version branch: ${BRANCH_NAME}${updatedPackage.version}`) + } + + console.log("=".repeat(60) + "\n") + } catch (error) { + console.error("\n❌ Error during publish process:", error.message) + console.error("\n💡 Troubleshooting tips:") + console.error(" 1. Ensure you are logged in to npm: npm whoami") + console.error(" 2. Check your npm permissions for this package") + console.error(" 3. Verify the package name is not already taken") + console.error(" 4. Make sure all dependencies are installed: pnpm install") + process.exit(1) + } +} + +async function main() { + await publish() +} + +main().catch((error) => { + console.error("Unexpected error:", error) + process.exit(1) +}) diff --git a/packages/types/src/__tests__/ipc.test.ts b/packages/types/src/__tests__/ipc.test.ts new file mode 100644 index 0000000000..a2b4429356 --- /dev/null +++ b/packages/types/src/__tests__/ipc.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest" +import { TaskCommandName, taskCommandSchema } from "../ipc.js" + +describe("IPC Types", () => { + describe("TaskCommandName", () => { + it("should include ResumeTask command", () => { + expect(TaskCommandName.ResumeTask).toBe("ResumeTask") + }) + + it("should have all expected task commands", () => { + const expectedCommands = ["StartNewTask", "CancelTask", "CloseTask", "ResumeTask"] + const actualCommands = Object.values(TaskCommandName) + + expectedCommands.forEach((command) => { + expect(actualCommands).toContain(command) + }) + }) + + describe("Error Handling", () => { + it("should handle ResumeTask command gracefully when task not found", () => { + // This test verifies the schema validation - the actual error handling + // for invalid task IDs is tested at the API level, not the schema level + const resumeTaskCommand = { + commandName: TaskCommandName.ResumeTask, + data: "non-existent-task-id", + } + + const result = taskCommandSchema.safeParse(resumeTaskCommand) + expect(result.success).toBe(true) + + if (result.success) { + expect(result.data.commandName).toBe("ResumeTask") + expect(result.data.data).toBe("non-existent-task-id") + } + }) + }) + }) + + describe("taskCommandSchema", () => { + it("should validate ResumeTask command with taskId", () => { + const resumeTaskCommand = { + commandName: TaskCommandName.ResumeTask, + data: "task-123", + } + + const result = taskCommandSchema.safeParse(resumeTaskCommand) + expect(result.success).toBe(true) + + if (result.success) { + expect(result.data.commandName).toBe("ResumeTask") + expect(result.data.data).toBe("task-123") + } + }) + + it("should reject ResumeTask command with invalid data", () => { + const invalidCommand = { + commandName: TaskCommandName.ResumeTask, + data: 123, // Should be string + } + + const result = taskCommandSchema.safeParse(invalidCommand) + expect(result.success).toBe(false) + }) + + it("should reject ResumeTask command without data", () => { + const invalidCommand = { + commandName: TaskCommandName.ResumeTask, + // Missing data field + } + + const result = taskCommandSchema.safeParse(invalidCommand) + expect(result.success).toBe(false) + }) + }) +}) diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 6fb181b573..e61e1e6106 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -1,27 +1,12 @@ import type { EventEmitter } from "events" import type { Socket } from "net" +import type { RooCodeEvents } from "./events.js" import type { RooCodeSettings } from "./global-settings.js" import type { ProviderSettingsEntry, ProviderSettings } from "./provider-settings.js" -import type { ClineMessage, TokenUsage } from "./message.js" -import type { ToolUsage, ToolName } from "./tool.js" -import type { IpcMessage, IpcServerEvents, IsSubtask } from "./ipc.js" +import type { IpcMessage, IpcServerEvents } from "./ipc.js" -// TODO: Make sure this matches `RooCodeEvents` from `@roo-code/types`. -export interface RooCodeAPIEvents { - message: [data: { taskId: string; action: "created" | "updated"; message: ClineMessage }] - taskCreated: [taskId: string] - taskStarted: [taskId: string] - taskModeSwitched: [taskId: string, mode: string] - taskPaused: [taskId: string] - taskUnpaused: [taskId: string] - taskAskResponded: [taskId: string] - taskAborted: [taskId: string] - taskSpawned: [parentTaskId: string, childTaskId: string] - taskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage, isSubtask: IsSubtask] - taskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage] - taskToolFailed: [taskId: string, toolName: ToolName, error: string] -} +export type RooCodeAPIEvents = RooCodeEvents export interface RooCodeAPI extends EventEmitter { /** diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts deleted file mode 100644 index 5ef90b6e5a..0000000000 --- a/packages/types/src/cloud.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { z } from "zod" - -import { globalSettingsSchema } from "./global-settings.js" -import { mcpMarketplaceItemSchema } from "./marketplace.js" - -/** - * CloudUserInfo - */ - -export interface CloudUserInfo { - name?: string - email?: string - picture?: string - organizationId?: string - organizationName?: string - organizationRole?: string - organizationImageUrl?: string -} - -/** - * CloudOrganization Types - */ - -export interface CloudOrganization { - id: string - name: string - slug?: string - image_url?: string - has_image?: boolean - created_at?: number - updated_at?: number -} - -export interface CloudOrganizationMembership { - id: string - organization: CloudOrganization - role: string - permissions?: string[] - created_at?: number - updated_at?: number -} - -/** - * OrganizationAllowList - */ - -export const organizationAllowListSchema = z.object({ - allowAll: z.boolean(), - providers: z.record( - z.object({ - allowAll: z.boolean(), - models: z.array(z.string()).optional(), - }), - ), -}) - -export type OrganizationAllowList = z.infer - -/** - * OrganizationDefaultSettings - */ - -export const organizationDefaultSettingsSchema = globalSettingsSchema - .pick({ - enableCheckpoints: true, - fuzzyMatchThreshold: true, - maxOpenTabsContext: true, - maxReadFileLine: true, - maxWorkspaceFiles: true, - showRooIgnoredFiles: true, - terminalCommandDelay: true, - terminalCompressProgressBar: true, - terminalOutputLineLimit: true, - terminalShellIntegrationDisabled: true, - terminalShellIntegrationTimeout: true, - terminalZshClearEolMark: true, - }) - // Add stronger validations for some fields. - .merge( - z.object({ - maxOpenTabsContext: z.number().int().nonnegative().optional(), - maxReadFileLine: z.number().int().gte(-1).optional(), - maxWorkspaceFiles: z.number().int().nonnegative().optional(), - terminalCommandDelay: z.number().int().nonnegative().optional(), - terminalOutputLineLimit: z.number().int().nonnegative().optional(), - terminalShellIntegrationTimeout: z.number().int().nonnegative().optional(), - }), - ) - -export type OrganizationDefaultSettings = z.infer - -/** - * OrganizationCloudSettings - */ - -export const organizationCloudSettingsSchema = z.object({ - recordTaskMessages: z.boolean().optional(), - enableTaskSharing: z.boolean().optional(), - taskShareExpirationDays: z.number().int().positive().optional(), - allowMembersViewAllTasks: z.boolean().optional(), -}) - -export type OrganizationCloudSettings = z.infer - -/** - * Organization Settings - */ - -export const organizationSettingsSchema = z.object({ - version: z.number(), - cloudSettings: organizationCloudSettingsSchema.optional(), - defaultSettings: organizationDefaultSettingsSchema, - allowList: organizationAllowListSchema, - hiddenMcps: z.array(z.string()).optional(), - hideMarketplaceMcps: z.boolean().optional(), - mcps: z.array(mcpMarketplaceItemSchema).optional(), -}) - -export type OrganizationSettings = z.infer - -/** - * Constants - */ - -export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = { - allowAll: true, - providers: {}, -} as const - -export const ORGANIZATION_DEFAULT: OrganizationSettings = { - version: 0, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - allowMembersViewAllTasks: true, - }, - defaultSettings: {}, - allowList: ORGANIZATION_ALLOW_ALL, -} as const - -/** - * Share Types - */ - -export const shareResponseSchema = z.object({ - success: z.boolean(), - shareUrl: z.string().optional(), - error: z.string().optional(), - isNewShare: z.boolean().optional(), - manageUrl: z.string().optional(), -}) - -export type ShareResponse = z.infer diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts new file mode 100644 index 0000000000..2b6b810c81 --- /dev/null +++ b/packages/types/src/events.ts @@ -0,0 +1,206 @@ +import { z } from "zod" + +import { clineMessageSchema, tokenUsageSchema } from "./message.js" +import { toolNamesSchema, toolUsageSchema } from "./tool.js" + +/** + * RooCodeEventName + */ + +export enum RooCodeEventName { + // Task Provider Lifecycle + TaskCreated = "taskCreated", + + // Task Lifecycle + TaskStarted = "taskStarted", + TaskCompleted = "taskCompleted", + TaskAborted = "taskAborted", + TaskFocused = "taskFocused", + TaskUnfocused = "taskUnfocused", + TaskActive = "taskActive", + TaskInteractive = "taskInteractive", + TaskResumable = "taskResumable", + TaskIdle = "taskIdle", + + // Subtask Lifecycle + TaskPaused = "taskPaused", + TaskUnpaused = "taskUnpaused", + TaskSpawned = "taskSpawned", + + // Task Execution + Message = "message", + TaskModeSwitched = "taskModeSwitched", + TaskAskResponded = "taskAskResponded", + + // Task Analytics + TaskTokenUsageUpdated = "taskTokenUsageUpdated", + TaskToolFailed = "taskToolFailed", + + // Evals + EvalPass = "evalPass", + EvalFail = "evalFail", +} + +/** + * RooCodeEvents + */ + +export const rooCodeEventsSchema = z.object({ + [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), + + [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), + [RooCodeEventName.TaskCompleted]: z.tuple([ + z.string(), + tokenUsageSchema, + toolUsageSchema, + z.object({ + isSubtask: z.boolean(), + }), + ]), + [RooCodeEventName.TaskAborted]: z.tuple([z.string()]), + [RooCodeEventName.TaskFocused]: z.tuple([z.string()]), + [RooCodeEventName.TaskUnfocused]: z.tuple([z.string()]), + [RooCodeEventName.TaskActive]: z.tuple([z.string()]), + [RooCodeEventName.TaskInteractive]: z.tuple([z.string()]), + [RooCodeEventName.TaskResumable]: z.tuple([z.string()]), + [RooCodeEventName.TaskIdle]: z.tuple([z.string()]), + + [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), + [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), + [RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]), + + [RooCodeEventName.Message]: z.tuple([ + z.object({ + taskId: z.string(), + action: z.union([z.literal("created"), z.literal("updated")]), + message: clineMessageSchema, + }), + ]), + [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), + [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), + + [RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]), + [RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]), +}) + +export type RooCodeEvents = z.infer + +/** + * TaskEvent + */ + +export const taskEventSchema = z.discriminatedUnion("eventName", [ + // Task Provider Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskCreated), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated], + taskId: z.number().optional(), + }), + + // Task Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskStarted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskCompleted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskAborted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskFocused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskFocused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskUnfocused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnfocused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskActive), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskActive], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskInteractive), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskInteractive], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskResumable), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskResumable], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskIdle), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskIdle], + taskId: z.number().optional(), + }), + + // Subtask Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskPaused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskUnpaused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskSpawned), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned], + taskId: z.number().optional(), + }), + + // Task Execution + z.object({ + eventName: z.literal(RooCodeEventName.Message), + payload: rooCodeEventsSchema.shape[RooCodeEventName.Message], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskModeSwitched), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskAskResponded), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded], + taskId: z.number().optional(), + }), + + // Task Analytics + z.object({ + eventName: z.literal(RooCodeEventName.TaskToolFailed), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskToolFailed], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated], + taskId: z.number().optional(), + }), + + // Evals + z.object({ + eventName: z.literal(RooCodeEventName.EvalPass), + payload: z.undefined(), + taskId: z.number(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.EvalFail), + payload: z.undefined(), + taskId: z.number(), + }), +]) + +export type TaskEvent = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 39dfb7fd93..2f311ea737 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -68,6 +68,7 @@ export const globalSettingsSchema = z.object({ commandTimeoutAllowlist: z.array(z.string()).optional(), preventCompletionWithOpenTodos: z.boolean().optional(), allowedMaxRequests: z.number().nullish(), + allowedMaxCost: z.number().nullish(), autoCondenseContext: z.boolean().optional(), autoCondenseContextPercent: z.number().optional(), maxConcurrentFileReads: z.number().optional(), @@ -133,12 +134,15 @@ export const globalSettingsSchema = z.object({ mcpEnabled: z.boolean().optional(), enableMcpServerCreation: z.boolean().optional(), + remoteControlEnabled: z.boolean().optional(), + mode: z.string().optional(), modeApiConfigs: z.record(z.string(), z.string()).optional(), customModes: z.array(modeConfigSchema).optional(), customModePrompts: customModePromptsSchema.optional(), customSupportPrompts: customSupportPromptsSchema.optional(), enhancementApiConfigId: z.string().optional(), + includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), profileThresholds: z.record(z.string(), z.number()).optional(), hasOpenedModeSelector: z.boolean().optional(), @@ -172,7 +176,9 @@ export const SECRET_STATE_KEYS = [ "openAiApiKey", "geminiApiKey", "openAiNativeApiKey", + "cerebrasApiKey", "deepSeekApiKey", + "doubaoApiKey", "moonshotApiKey", "mistralApiKey", "unboundApiKey", @@ -190,6 +196,11 @@ export const SECRET_STATE_KEYS = [ "watsonxApiKey", "codebaseIndexWatsonxApiKey", "codebaseIndexWatsonxProjectId", + "sambaNovaApiKey", + "zaiApiKey", + "fireworksApiKey", + "featherlessApiKey", + "ioIntelligenceApiKey", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick @@ -287,6 +298,8 @@ export const EVALS_SETTINGS: RooCodeSettings = { mcpEnabled: false, + remoteControlEnabled: false, + mode: "code", // "architect", customModes: [], diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 44937da235..b151067d1d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,6 @@ -export * from "./providers/index.js" - export * from "./api.js" export * from "./codebase-index.js" -export * from "./cloud.js" +export * from "./events.js" export * from "./experiment.js" export * from "./followup.js" export * from "./global-settings.js" @@ -14,10 +12,13 @@ export * from "./message.js" export * from "./mode.js" export * from "./model.js" export * from "./provider-settings.js" -export * from "./sharing.js" +export * from "./single-file-read-models.js" +export * from "./task.js" +export * from "./todo.js" export * from "./telemetry.js" export * from "./terminal.js" export * from "./tool.js" export * from "./type-fu.js" export * from "./vscode.js" -export * from "./todo.js" + +export * from "./providers/index.js" diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 28accde9de..ace39c3f2b 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -1,60 +1,28 @@ import { z } from "zod" -import { clineMessageSchema, tokenUsageSchema } from "./message.js" -import { toolNamesSchema, toolUsageSchema } from "./tool.js" +import { type TaskEvent, taskEventSchema } from "./events.js" import { rooCodeSettingsSchema } from "./global-settings.js" /** - * isSubtaskSchema - */ -export const isSubtaskSchema = z.object({ - isSubtask: z.boolean(), -}) -export type IsSubtask = z.infer - -/** - * RooCodeEvent + * IpcMessageType */ -export enum RooCodeEventName { - Message = "message", - TaskCreated = "taskCreated", - TaskStarted = "taskStarted", - TaskModeSwitched = "taskModeSwitched", - TaskPaused = "taskPaused", - TaskUnpaused = "taskUnpaused", - TaskAskResponded = "taskAskResponded", - TaskAborted = "taskAborted", - TaskSpawned = "taskSpawned", - TaskCompleted = "taskCompleted", - TaskTokenUsageUpdated = "taskTokenUsageUpdated", - TaskToolFailed = "taskToolFailed", - EvalPass = "evalPass", - EvalFail = "evalFail", +export enum IpcMessageType { + Connect = "Connect", + Disconnect = "Disconnect", + Ack = "Ack", + TaskCommand = "TaskCommand", + TaskEvent = "TaskEvent", } -export const rooCodeEventsSchema = z.object({ - [RooCodeEventName.Message]: z.tuple([ - z.object({ - taskId: z.string(), - action: z.union([z.literal("created"), z.literal("updated")]), - message: clineMessageSchema, - }), - ]), - [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), - [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), - [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), - [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), - [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), - [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), - [RooCodeEventName.TaskAborted]: z.tuple([z.string()]), - [RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]), - [RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema, isSubtaskSchema]), - [RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]), - [RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]), -}) +/** + * IpcOrigin + */ -export type RooCodeEvents = z.infer +export enum IpcOrigin { + Client = "client", + Server = "server", +} /** * Ack @@ -69,15 +37,20 @@ export const ackSchema = z.object({ export type Ack = z.infer /** - * TaskCommand + * TaskCommandName */ export enum TaskCommandName { StartNewTask = "StartNewTask", CancelTask = "CancelTask", CloseTask = "CloseTask", + ResumeTask = "ResumeTask", } +/** + * TaskCommand + */ + export const taskCommandSchema = z.discriminatedUnion("commandName", [ z.object({ commandName: z.literal(TaskCommandName.StartNewTask), @@ -96,106 +69,18 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ commandName: z.literal(TaskCommandName.CloseTask), data: z.string(), }), + z.object({ + commandName: z.literal(TaskCommandName.ResumeTask), + data: z.string(), + }), ]) export type TaskCommand = z.infer -/** - * TaskEvent - */ - -export const taskEventSchema = z.discriminatedUnion("eventName", [ - z.object({ - eventName: z.literal(RooCodeEventName.Message), - payload: rooCodeEventsSchema.shape[RooCodeEventName.Message], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskCreated), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskStarted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskModeSwitched), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskPaused), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskUnpaused), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskAskResponded), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskAborted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskSpawned), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskCompleted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskToolFailed), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskToolFailed], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.EvalPass), - payload: z.undefined(), - taskId: z.number(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.EvalFail), - payload: z.undefined(), - taskId: z.number(), - }), -]) - -export type TaskEvent = z.infer - /** * IpcMessage */ -export enum IpcMessageType { - Connect = "Connect", - Disconnect = "Disconnect", - Ack = "Ack", - TaskCommand = "TaskCommand", - TaskEvent = "TaskEvent", -} - -export enum IpcOrigin { - Client = "client", - Server = "server", -} - export const ipcMessageSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(IpcMessageType.Ack), @@ -219,7 +104,7 @@ export const ipcMessageSchema = z.discriminatedUnion("type", [ export type IpcMessage = z.infer /** - * Client + * IpcClientEvents */ export type IpcClientEvents = { @@ -231,7 +116,7 @@ export type IpcClientEvents = { } /** - * Server + * IpcServerEvents */ export type IpcServerEvents = { diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index eaec2ad886..5037370f24 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -44,6 +44,63 @@ export const clineAskSchema = z.enum(clineAsks) export type ClineAsk = z.infer +// Needs classification: +// - `followup` +// - `command_output + +/** + * IdleAsk + * + * Asks that put the task into an "idle" state. + */ + +export const idleAsks = [ + "completion_result", + "api_req_failed", + "resume_completed_task", + "mistake_limit_reached", + "auto_approval_max_req_reached", +] as const satisfies readonly ClineAsk[] + +export type IdleAsk = (typeof idleAsks)[number] + +export function isIdleAsk(ask: ClineAsk): ask is IdleAsk { + return (idleAsks as readonly ClineAsk[]).includes(ask) +} + +/** + * ResumableAsk + * + * Asks that put the task into an "resumable" state. + */ + +export const resumableAsks = ["resume_task"] as const satisfies readonly ClineAsk[] + +export type ResumableAsk = (typeof resumableAsks)[number] + +export function isResumableAsk(ask: ClineAsk): ask is ResumableAsk { + return (resumableAsks as readonly ClineAsk[]).includes(ask) +} + +/** + * InteractiveAsk + * + * Asks that put the task into an "user interaction required" state. + */ + +export const interactiveAsks = [ + "command", + "tool", + "browser_action_launch", + "use_mcp_server", +] as const satisfies readonly ClineAsk[] + +export type InteractiveAsk = (typeof interactiveAsks)[number] + +export function isInteractiveAsk(ask: ClineAsk): ask is InteractiveAsk { + return (interactiveAsks as readonly ClineAsk[]).includes(ask) +} + /** * ClineSay */ @@ -156,6 +213,17 @@ export const clineMessageSchema = z.object({ contextCondense: contextCondenseSchema.optional(), isProtected: z.boolean().optional(), apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(), + metadata: z + .object({ + gpt5: z + .object({ + previous_response_id: z.string().optional(), + instructions: z.string().optional(), + reasoning_summary: z.string().optional(), + }) + .optional(), + }) + .optional(), }) export type ClineMessage = z.infer diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 3bd66782cf..969a4caa96 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -10,6 +10,24 @@ export const reasoningEffortsSchema = z.enum(reasoningEfforts) export type ReasoningEffort = z.infer +/** + * ReasoningEffortWithMinimal + */ + +export const reasoningEffortWithMinimalSchema = z.union([reasoningEffortsSchema, z.literal("minimal")]) + +export type ReasoningEffortWithMinimal = z.infer + +/** + * Verbosity + */ + +export const verbosityLevels = ["low", "medium", "high"] as const + +export const verbosityLevelsSchema = z.enum(verbosityLevels) + +export type VerbosityLevel = z.infer + /** * ModelParameter */ @@ -34,6 +52,8 @@ export const modelInfoSchema = z.object({ supportsImages: z.boolean().optional(), supportsComputerUse: z.boolean().optional(), supportsPromptCache: z.boolean(), + // Capability flag to indicate whether the model supports an output verbosity parameter + supportsVerbosity: z.boolean().optional(), supportsReasoningBudget: z.boolean().optional(), requiredReasoningBudget: z.boolean().optional(), supportsReasoningEffort: z.boolean().optional(), diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4a5a431439..c783c2cc99 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -1,7 +1,31 @@ import { z } from "zod" -import { reasoningEffortsSchema, modelInfoSchema } from "./model.js" +import { modelInfoSchema, reasoningEffortWithMinimalSchema, verbosityLevelsSchema } from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" +import { + anthropicModels, + bedrockModels, + cerebrasModels, + chutesModels, + claudeCodeModels, + deepSeekModels, + doubaoModels, + featherlessModels, + fireworksModels, + geminiModels, + groqModels, + ioIntelligenceModels, + mistralModels, + moonshotModels, + openAiNativeModels, + qwenCodeModels, + rooModels, + sambaNovaModels, + vertexModels, + vscodeLlmModels, + xaiModels, + internationalZAiModels, +} from "./providers/index.js" /** * ProviderName @@ -24,6 +48,8 @@ export const providerNames = [ "mistral", "moonshot", "deepseek", + "doubao", + "qwen-code", "unbound", "requesty", "human-relay", @@ -34,6 +60,13 @@ export const providerNames = [ "litellm", "huggingface", "watsonx", + "cerebras", + "sambanova", + "zai", + "fireworks", + "featherless", + "io-intelligence", + "roo", ] as const export const providerNamesSchema = z.enum(providerNames) @@ -72,9 +105,12 @@ const baseProviderSettingsSchema = z.object({ // Model reasoning. enableReasoningEffort: z.boolean().optional(), - reasoningEffort: reasoningEffortsSchema.optional(), + reasoningEffort: reasoningEffortWithMinimalSchema.optional(), modelMaxTokens: z.number().optional(), modelMaxThinkingTokens: z.number().optional(), + + // Model verbosity. + verbosity: verbosityLevelsSchema.optional(), }) // Several of the providers share common model config properties. @@ -86,6 +122,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ apiKey: z.string().optional(), anthropicBaseUrl: z.string().optional(), anthropicUseAuthToken: z.boolean().optional(), + anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window }) const claudeCodeSchema = apiModelIdProviderModelSchema.extend({ @@ -121,6 +158,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({ awsModelContextWindow: z.number().optional(), awsBedrockEndpointEnabled: z.boolean().optional(), awsBedrockEndpoint: z.string().optional(), + awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window }) const vertexSchema = apiModelIdProviderModelSchema.extend({ @@ -128,6 +166,8 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({ vertexJsonCredentials: z.string().optional(), vertexProjectId: z.string().optional(), vertexRegion: z.string().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), }) const openAiSchema = baseProviderSettingsSchema.extend({ @@ -194,6 +234,11 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({ deepSeekApiKey: z.string().optional(), }) +const doubaoSchema = apiModelIdProviderModelSchema.extend({ + doubaoBaseUrl: z.string().optional(), + doubaoApiKey: z.string().optional(), +}) + const moonshotSchema = apiModelIdProviderModelSchema.extend({ moonshotBaseUrl: z .union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")]) @@ -207,6 +252,7 @@ const unboundSchema = baseProviderSettingsSchema.extend({ }) const requestySchema = baseProviderSettingsSchema.extend({ + requestyBaseUrl: z.string().optional(), requestyApiKey: z.string().optional(), requestyModelId: z.string().optional(), }) @@ -249,6 +295,40 @@ const watsonxSchema = baseProviderSettingsSchema.extend({ watsonxModelId: z.string().optional(), }) +const cerebrasSchema = apiModelIdProviderModelSchema.extend({ + cerebrasApiKey: z.string().optional(), +}) + +const sambaNovaSchema = apiModelIdProviderModelSchema.extend({ + sambaNovaApiKey: z.string().optional(), +}) + +const zaiSchema = apiModelIdProviderModelSchema.extend({ + zaiApiKey: z.string().optional(), + zaiApiLine: z.union([z.literal("china"), z.literal("international")]).optional(), +}) + +const fireworksSchema = apiModelIdProviderModelSchema.extend({ + fireworksApiKey: z.string().optional(), +}) + +const featherlessSchema = apiModelIdProviderModelSchema.extend({ + featherlessApiKey: z.string().optional(), +}) + +const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({ + ioIntelligenceModelId: z.string().optional(), + ioIntelligenceApiKey: z.string().optional(), +}) + +const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ + qwenCodeOauthPath: z.string().optional(), +}) + +const rooSchema = apiModelIdProviderModelSchema.extend({ + // No additional fields needed - uses cloud authentication +}) + const defaultSchema = z.object({ apiProvider: z.undefined(), }) @@ -269,6 +349,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })), mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), + doubaoSchema.merge(z.object({ apiProvider: z.literal("doubao") })), moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), @@ -280,6 +361,14 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv chutesSchema.merge(z.object({ apiProvider: z.literal("chutes") })), litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })), watsonxSchema.merge(z.object({ apiProvider: z.literal("watsonx") })), + cerebrasSchema.merge(z.object({ apiProvider: z.literal("cerebras") })), + sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), + zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), + fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), + featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), + ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })), + qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), + rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), defaultSchema, ]) @@ -300,6 +389,7 @@ export const providerSettingsSchema = z.object({ ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, + ...doubaoSchema.shape, ...moonshotSchema.shape, ...unboundSchema.shape, ...requestySchema.shape, @@ -310,11 +400,26 @@ export const providerSettingsSchema = z.object({ ...huggingFaceSchema.shape, ...chutesSchema.shape, ...litellmSchema.shape, + ...cerebrasSchema.shape, + ...sambaNovaSchema.shape, + ...zaiSchema.shape, + ...fireworksSchema.shape, + ...featherlessSchema.shape, + ...ioIntelligenceSchema.shape, + ...qwenCodeSchema.shape, + ...rooSchema.shape, ...codebaseIndexProviderSchema.shape, ...watsonxSchema.shape, }) export type ProviderSettings = z.infer + +export const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() }) +export const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and( + z.object({ id: z.string().optional() }), +) +export type ProviderSettingsWithId = z.infer + export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options export const MODEL_ID_KEYS: Partial [] = [ @@ -329,6 +434,7 @@ export const MODEL_ID_KEYS: Partial [] = [ "requestyModelId", "litellmModelId", "huggingFaceModelId", + "ioIntelligenceModelId", ] export const getModelId = (settings: ProviderSettings): string | undefined => { @@ -336,21 +442,127 @@ export const getModelId = (settings: ProviderSettings): string | undefined => { return modelIdKey ? (settings[modelIdKey] as string) : undefined } -// Providers that use Anthropic-style API protocol +// Providers that use Anthropic-style API protocol. export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"] -// Helper function to determine API protocol for a provider and model export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { - // First check if the provider is an Anthropic-style provider if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) { return "anthropic" } - // For vertex provider, check if the model ID contains "claude" (case-insensitive) if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) { return "anthropic" } - // Default to OpenAI protocol return "openai" } + +export const MODELS_BY_PROVIDER: Record< + Exclude , + { id: ProviderName; label: string; models: string[] } +> = { + anthropic: { + id: "anthropic", + label: "Anthropic", + models: Object.keys(anthropicModels), + }, + bedrock: { + id: "bedrock", + label: "Amazon Bedrock", + models: Object.keys(bedrockModels), + }, + cerebras: { + id: "cerebras", + label: "Cerebras", + models: Object.keys(cerebrasModels), + }, + chutes: { + id: "chutes", + label: "Chutes AI", + models: Object.keys(chutesModels), + }, + "claude-code": { id: "claude-code", label: "Claude Code", models: Object.keys(claudeCodeModels) }, + deepseek: { + id: "deepseek", + label: "DeepSeek", + models: Object.keys(deepSeekModels), + }, + doubao: { id: "doubao", label: "Doubao", models: Object.keys(doubaoModels) }, + featherless: { + id: "featherless", + label: "Featherless", + models: Object.keys(featherlessModels), + }, + fireworks: { + id: "fireworks", + label: "Fireworks", + models: Object.keys(fireworksModels), + }, + gemini: { + id: "gemini", + label: "Google Gemini", + models: Object.keys(geminiModels), + }, + groq: { id: "groq", label: "Groq", models: Object.keys(groqModels) }, + "io-intelligence": { + id: "io-intelligence", + label: "IO Intelligence", + models: Object.keys(ioIntelligenceModels), + }, + mistral: { + id: "mistral", + label: "Mistral", + models: Object.keys(mistralModels), + }, + moonshot: { + id: "moonshot", + label: "Moonshot", + models: Object.keys(moonshotModels), + }, + "openai-native": { + id: "openai-native", + label: "OpenAI", + models: Object.keys(openAiNativeModels), + }, + "qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) }, + roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) }, + sambanova: { + id: "sambanova", + label: "SambaNova", + models: Object.keys(sambaNovaModels), + }, + vertex: { + id: "vertex", + label: "GCP Vertex AI", + models: Object.keys(vertexModels), + }, + "vscode-lm": { + id: "vscode-lm", + label: "VS Code LM API", + models: Object.keys(vscodeLlmModels), + }, + xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) }, + zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) }, + + // Dynamic providers; models pulled from the respective APIs. + glama: { id: "glama", label: "Glama", models: [] }, + huggingface: { id: "huggingface", label: "Hugging Face", models: [] }, + litellm: { id: "litellm", label: "LiteLLM", models: [] }, + openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, + requesty: { id: "requesty", label: "Requesty", models: [] }, + unbound: { id: "unbound", label: "Unbound", models: [] }, +} + +export const dynamicProviders = [ + "glama", + "huggingface", + "litellm", + "openrouter", + "requesty", + "unbound", +] as const satisfies readonly ProviderName[] + +export type DynamicProvider = (typeof dynamicProviders)[number] + +export const isDynamicProvider = (key: string): key is DynamicProvider => + dynamicProviders.includes(key as DynamicProvider) diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index d0f1629ee9..2cb38537a4 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -8,14 +8,36 @@ export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-202505 export const anthropicModels = { "claude-sonnet-4-20250514": { maxTokens: 64_000, // Overridden to 8k if `enableReasoningEffort` is false. + contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07' + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million input tokens (≤200K context) + outputPrice: 15.0, // $15 per million output tokens (≤200K context) + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + supportsReasoningBudget: true, + // Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07') + tiers: [ + { + contextWindow: 1_000_000, // 1M tokens with beta flag + inputPrice: 6.0, // $6 per million input tokens (>200K context) + outputPrice: 22.5, // $22.50 per million output tokens (>200K context) + cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context) + cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context) + }, + ], + }, + "claude-opus-4-1-20250805": { + maxTokens: 8192, contextWindow: 200_000, supportsImages: true, supportsComputerUse: true, supportsPromptCache: true, - inputPrice: 3.0, // $3 per million input tokens - outputPrice: 15.0, // $15 per million output tokens - cacheWritesPrice: 3.75, // $3.75 per million tokens - cacheReadsPrice: 0.3, // $0.30 per million tokens + inputPrice: 15.0, // $15 per million input tokens + outputPrice: 75.0, // $75 per million output tokens + cacheWritesPrice: 18.75, // $18.75 per million tokens + cacheReadsPrice: 1.5, // $1.50 per million tokens supportsReasoningBudget: true, }, "claude-opus-4-20250514": { diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index 9c1f349334..67215e7796 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -82,6 +82,21 @@ export const bedrockModels = { maxCachePoints: 4, cachableFields: ["system", "messages", "tools"], }, + "anthropic.claude-opus-4-1-20250805-v1:0": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 15.0, + outputPrice: 75.0, + cacheWritesPrice: 18.75, + cacheReadsPrice: 1.5, + minTokensPerCachePoint: 1024, + maxCachePoints: 4, + cachableFields: ["system", "messages", "tools"], + }, "anthropic.claude-opus-4-20250514-v1:0": { maxTokens: 8192, contextWindow: 200_000, @@ -206,6 +221,26 @@ export const bedrockModels = { inputPrice: 1.35, outputPrice: 5.4, }, + "openai.gpt-oss-20b-1:0": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsComputerUse: false, + supportsPromptCache: false, + inputPrice: 0.5, + outputPrice: 1.5, + description: "GPT-OSS 20B - Optimized for low latency and local/specialized use cases", + }, + "openai.gpt-oss-120b-1:0": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsComputerUse: false, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 6.0, + description: "GPT-OSS 120B - Production-ready, general-purpose, high-reasoning model", + }, "meta.llama3-3-70b-instruct-v1:0": { maxTokens: 8192, contextWindow: 128_000, @@ -406,3 +441,5 @@ export const BEDROCK_REGIONS = [ { value: "us-gov-east-1", label: "us-gov-east-1" }, { value: "us-gov-west-1", label: "us-gov-west-1" }, ].sort((a, b) => a.value.localeCompare(b.value)) + +export const BEDROCK_CLAUDE_SONNET_4_MODEL_ID = "anthropic.claude-sonnet-4-20250514-v1:0" diff --git a/packages/types/src/providers/cerebras.ts b/packages/types/src/providers/cerebras.ts new file mode 100644 index 0000000000..4765302a4e --- /dev/null +++ b/packages/types/src/providers/cerebras.ts @@ -0,0 +1,76 @@ +import type { ModelInfo } from "../model.js" + +// https://inference-docs.cerebras.ai/api-reference/chat-completions +export type CerebrasModelId = keyof typeof cerebrasModels + +export const cerebrasDefaultModelId: CerebrasModelId = "qwen-3-coder-480b-free" + +export const cerebrasModels = { + "qwen-3-coder-480b-free": { + maxTokens: 40000, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "SOTA coding model with ~2000 tokens/s ($0 free tier)\n\n• Use this if you don't have a Cerebras subscription\n• 64K context window\n• Rate limits: 150K TPM, 1M TPH/TPD, 10 RPM, 100 RPH/RPD\n\nUpgrade for higher limits: [https://cloud.cerebras.ai/?utm=roocode](https://cloud.cerebras.ai/?utm=roocode)", + }, + "qwen-3-coder-480b": { + maxTokens: 40000, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "SOTA coding model with ~2000 tokens/s ($50/$250 paid tiers)\n\n• Use this if you have a Cerebras subscription\n• 131K context window with higher rate limits", + }, + "qwen-3-235b-a22b-instruct-2507": { + maxTokens: 64000, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Intelligent model with ~1400 tokens/s", + }, + "llama-3.3-70b": { + maxTokens: 64000, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Powerful model with ~2600 tokens/s", + }, + "qwen-3-32b": { + maxTokens: 64000, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "SOTA coding performance with ~2500 tokens/s", + }, + "qwen-3-235b-a22b-thinking-2507": { + maxTokens: 40000, + contextWindow: 65000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "SOTA performance with ~1500 tokens/s", + supportsReasoningEffort: true, + }, + "gpt-oss-120b": { + maxTokens: 8000, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "OpenAI GPT OSS model with ~2800 tokens/s\n\n• 64K context window\n• Excels at efficient reasoning across science, math, and coding", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/chutes.ts b/packages/types/src/providers/chutes.ts index 98a2f4f360..bdb4a6c8c0 100644 --- a/packages/types/src/providers/chutes.ts +++ b/packages/types/src/providers/chutes.ts @@ -5,6 +5,7 @@ export type ChutesModelId = | "deepseek-ai/DeepSeek-R1-0528" | "deepseek-ai/DeepSeek-R1" | "deepseek-ai/DeepSeek-V3" + | "deepseek-ai/DeepSeek-V3.1" | "unsloth/Llama-3.3-70B-Instruct" | "chutesai/Llama-4-Scout-17B-16E-Instruct" | "unsloth/Mistral-Nemo-Instruct-2407" @@ -23,9 +24,12 @@ export type ChutesModelId = | "Qwen/Qwen3-30B-A3B" | "Qwen/Qwen3-14B" | "Qwen/Qwen3-8B" + | "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8" | "microsoft/MAI-DS-R1-FP8" | "tngtech/DeepSeek-R1T-Chimera" | "zai-org/GLM-4.5-Air" + | "zai-org/GLM-4.5-FP8" + | "moonshotai/Kimi-K2-Instruct-75k" export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1-0528" @@ -57,6 +61,15 @@ export const chutesModels = { outputPrice: 0, description: "DeepSeek V3 model.", }, + "deepseek-ai/DeepSeek-V3.1": { + maxTokens: 32768, + contextWindow: 163840, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek V3.1 model.", + }, "unsloth/Llama-3.3-70B-Instruct": { maxTokens: 32768, // From Groq contextWindow: 131072, // From Groq @@ -247,4 +260,32 @@ export const chutesModels = { description: "GLM-4.5-Air model with 151,329 token context window and 106B total parameters with 12B activated.", }, + "zai-org/GLM-4.5-FP8": { + maxTokens: 32768, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "GLM-4.5-FP8 model with 128k token context window, optimized for agent-based applications with MoE architecture.", + }, + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { + maxTokens: 32768, + contextWindow: 262144, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.", + }, + "moonshotai/Kimi-K2-Instruct-75k": { + maxTokens: 32768, + contextWindow: 75000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1481, + outputPrice: 0.5926, + description: "Moonshot AI Kimi K2 Instruct model with 75k context window.", + }, } as const satisfies Record diff --git a/packages/types/src/providers/claude-code.ts b/packages/types/src/providers/claude-code.ts index 6f72baf008..d9b658319a 100644 --- a/packages/types/src/providers/claude-code.ts +++ b/packages/types/src/providers/claude-code.ts @@ -48,6 +48,14 @@ export const claudeCodeModels = { supportsReasoningBudget: false, requiredReasoningBudget: false, }, + "claude-opus-4-1-20250805": { + ...anthropicModels["claude-opus-4-1-20250805"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, "claude-opus-4-20250514": { ...anthropicModels["claude-opus-4-20250514"], supportsImages: false, diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index 5ef757ffdf..8ce5ef87b2 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -7,8 +7,8 @@ export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" export const deepSeekModels = { "deepseek-chat": { - maxTokens: 8192, - contextWindow: 64_000, + maxTokens: 8192, // 8K max output + contextWindow: 128_000, supportsImages: false, supportsPromptCache: true, inputPrice: 0.27, // $0.27 per million tokens (cache miss) @@ -18,15 +18,15 @@ export const deepSeekModels = { description: `DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.`, }, "deepseek-reasoner": { - maxTokens: 8192, - contextWindow: 64_000, + maxTokens: 65536, // 64K max output for reasoning mode + contextWindow: 128_000, supportsImages: false, supportsPromptCache: true, inputPrice: 0.55, // $0.55 per million tokens (cache miss) outputPrice: 2.19, // $2.19 per million tokens cacheWritesPrice: 0.55, // $0.55 per million tokens (cache miss) cacheReadsPrice: 0.14, // $0.14 per million tokens (cache hit) - description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 32K tokens.`, + description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 64K output tokens.`, }, } as const satisfies Record diff --git a/packages/types/src/providers/doubao.ts b/packages/types/src/providers/doubao.ts new file mode 100644 index 0000000000..f948450bc4 --- /dev/null +++ b/packages/types/src/providers/doubao.ts @@ -0,0 +1,44 @@ +import type { ModelInfo } from "../model.js" + +export const doubaoDefaultModelId = "doubao-seed-1-6-250615" + +export const doubaoModels = { + "doubao-seed-1-6-250615": { + maxTokens: 32_768, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.0001, // $0.0001 per million tokens (cache miss) + outputPrice: 0.0004, // $0.0004 per million tokens + cacheWritesPrice: 0.0001, // $0.0001 per million tokens (cache miss) + cacheReadsPrice: 0.00002, // $0.00002 per million tokens (cache hit) + description: `Doubao Seed 1.6 is a powerful model designed for high-performance tasks with extensive context handling.`, + }, + "doubao-seed-1-6-thinking-250715": { + maxTokens: 32_768, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.0002, // $0.0002 per million tokens + outputPrice: 0.0008, // $0.0008 per million tokens + cacheWritesPrice: 0.0002, // $0.0002 per million + cacheReadsPrice: 0.00004, // $0.00004 per million tokens (cache hit) + description: `Doubao Seed 1.6 Thinking is optimized for reasoning tasks, providing enhanced performance in complex problem-solving scenarios.`, + }, + "doubao-seed-1-6-flash-250715": { + maxTokens: 32_768, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.00015, // $0.00015 per million tokens + outputPrice: 0.0006, // $0.0006 per million tokens + cacheWritesPrice: 0.00015, // $0.00015 per million + cacheReadsPrice: 0.00003, // $0.00003 per million tokens (cache hit) + description: `Doubao Seed 1.6 Flash is tailored for speed and efficiency, making it ideal for applications requiring rapid responses.`, + }, +} as const satisfies Record + +export const doubaoDefaultModelInfo: ModelInfo = doubaoModels[doubaoDefaultModelId] + +export const DOUBAO_API_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3" +export const DOUBAO_API_CHAT_PATH = "/chat/completions" diff --git a/packages/types/src/providers/featherless.ts b/packages/types/src/providers/featherless.ts new file mode 100644 index 0000000000..d24f1fd882 --- /dev/null +++ b/packages/types/src/providers/featherless.ts @@ -0,0 +1,58 @@ +import type { ModelInfo } from "../model.js" + +export type FeatherlessModelId = + | "deepseek-ai/DeepSeek-V3-0324" + | "deepseek-ai/DeepSeek-R1-0528" + | "moonshotai/Kimi-K2-Instruct" + | "openai/gpt-oss-120b" + | "Qwen/Qwen3-Coder-480B-A35B-Instruct" + +export const featherlessModels = { + "deepseek-ai/DeepSeek-V3-0324": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek V3 0324 model.", + }, + "deepseek-ai/DeepSeek-R1-0528": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek R1 0528 model.", + }, + "moonshotai/Kimi-K2-Instruct": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Kimi K2 Instruct model.", + }, + "openai/gpt-oss-120b": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "GPT-OSS 120B model.", + }, + "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Qwen3 Coder 480B A35B Instruct model.", + }, +} as const satisfies Record + +export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" diff --git a/packages/types/src/providers/fireworks.ts b/packages/types/src/providers/fireworks.ts new file mode 100644 index 0000000000..45144b666f --- /dev/null +++ b/packages/types/src/providers/fireworks.ts @@ -0,0 +1,116 @@ +import type { ModelInfo } from "../model.js" + +export type FireworksModelId = + | "accounts/fireworks/models/kimi-k2-instruct" + | "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507" + | "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct" + | "accounts/fireworks/models/deepseek-r1-0528" + | "accounts/fireworks/models/deepseek-v3" + | "accounts/fireworks/models/deepseek-v3p1" + | "accounts/fireworks/models/glm-4p5" + | "accounts/fireworks/models/glm-4p5-air" + | "accounts/fireworks/models/gpt-oss-20b" + | "accounts/fireworks/models/gpt-oss-120b" + +export const fireworksDefaultModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct" + +export const fireworksModels = { + "accounts/fireworks/models/kimi-k2-instruct": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.6, + outputPrice: 2.5, + description: + "Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters. Trained with the Muon optimizer, Kimi K2 achieves exceptional performance across frontier knowledge, reasoning, and coding tasks while being meticulously optimized for agentic capabilities.", + }, + "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507": { + maxTokens: 32768, + contextWindow: 256000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.22, + outputPrice: 0.88, + description: "Latest Qwen3 thinking model, competitive against the best closed source models in Jul 2025.", + }, + "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct": { + maxTokens: 32768, + contextWindow: 256000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.45, + outputPrice: 1.8, + description: "Qwen3's most agentic code model to date.", + }, + "accounts/fireworks/models/deepseek-r1-0528": { + maxTokens: 20480, + contextWindow: 160000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 3, + outputPrice: 8, + description: + "05/28 updated checkpoint of Deepseek R1. Its overall performance is now approaching that of leading models, such as O3 and Gemini 2.5 Pro. Compared to the previous version, the upgraded model shows significant improvements in handling complex reasoning tasks, and this version also offers a reduced hallucination rate, enhanced support for function calling, and better experience for vibe coding. Note that fine-tuning for this model is only available through contacting fireworks at https://fireworks.ai/company/contact-us.", + }, + "accounts/fireworks/models/deepseek-v3": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.9, + outputPrice: 0.9, + description: + "A strong Mixture-of-Experts (MoE) language model with 671B total parameters with 37B activated for each token from Deepseek. Note that fine-tuning for this model is only available through contacting fireworks at https://fireworks.ai/company/contact-us.", + }, + "accounts/fireworks/models/deepseek-v3p1": { + maxTokens: 16384, + contextWindow: 163840, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.56, + outputPrice: 1.68, + description: + "DeepSeek v3.1 is an improved version of the v3 model with enhanced performance, better reasoning capabilities, and improved code generation. This Mixture-of-Experts (MoE) model maintains the same 671B total parameters with 37B activated per token.", + }, + "accounts/fireworks/models/glm-4p5": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.55, + outputPrice: 2.19, + description: + "Z.ai GLM-4.5 with 355B total parameters and 32B active parameters. Features unified reasoning, coding, and intelligent agent capabilities.", + }, + "accounts/fireworks/models/glm-4p5-air": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.55, + outputPrice: 2.19, + description: + "Z.ai GLM-4.5-Air with 106B total parameters and 12B active parameters. Features unified reasoning, coding, and intelligent agent capabilities.", + }, + "accounts/fireworks/models/gpt-oss-20b": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.07, + outputPrice: 0.3, + description: + "OpenAI gpt-oss-20b: Compact model for local/edge deployments. Optimized for low-latency and resource-constrained environments with chain-of-thought output, adjustable reasoning, and agentic workflows.", + }, + "accounts/fireworks/models/gpt-oss-120b": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + description: + "OpenAI gpt-oss-120b: Production-grade, general-purpose model that fits on a single H100 GPU. Features complex reasoning, configurable effort, full chain-of-thought transparency, and supports function calling, tool use, and structured outputs.", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/groq.ts b/packages/types/src/providers/groq.ts index 2eac1f954a..feb1777ce3 100644 --- a/packages/types/src/providers/groq.ts +++ b/packages/types/src/providers/groq.ts @@ -11,6 +11,8 @@ export type GroqModelId = | "qwen/qwen3-32b" | "deepseek-r1-distill-llama-70b" | "moonshotai/kimi-k2-instruct" + | "openai/gpt-oss-120b" + | "openai/gpt-oss-20b" export const groqDefaultModelId: GroqModelId = "llama-3.3-70b-versatile" // Defaulting to Llama3 70B Versatile @@ -92,9 +94,30 @@ export const groqModels = { maxTokens: 16384, contextWindow: 131072, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 1.0, outputPrice: 3.0, + cacheReadsPrice: 0.5, // 50% discount for cached input tokens description: "Moonshot AI Kimi K2 Instruct 1T model, 128K context.", }, + "openai/gpt-oss-120b": { + maxTokens: 32766, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.75, + description: + "GPT-OSS 120B is OpenAI's flagship open source model, built on a Mixture-of-Experts (MoE) architecture with 20 billion parameters and 128 experts.", + }, + "openai/gpt-oss-20b": { + maxTokens: 32768, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.5, + description: + "GPT-OSS 20B is OpenAI's flagship open source model, built on a Mixture-of-Experts (MoE) architecture with 20 billion parameters and 32 experts.", + }, } as const satisfies Record diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 55827bbb68..7ec8d28542 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -1,12 +1,17 @@ export * from "./anthropic.js" export * from "./bedrock.js" +export * from "./cerebras.js" export * from "./chutes.js" export * from "./claude-code.js" export * from "./deepseek.js" +export * from "./doubao.js" +export * from "./featherless.js" +export * from "./fireworks.js" export * from "./gemini.js" export * from "./glama.js" export * from "./groq.js" export * from "./huggingface.js" +export * from "./io-intelligence.js" export * from "./lite-llm.js" export * from "./lm-studio.js" export * from "./mistral.js" @@ -14,9 +19,13 @@ export * from "./moonshot.js" export * from "./ollama.js" export * from "./openai.js" export * from "./openrouter.js" +export * from "./qwen-code.js" export * from "./requesty.js" +export * from "./roo.js" +export * from "./sambanova.js" export * from "./unbound.js" export * from "./vertex.js" export * from "./vscode-llm.js" export * from "./xai.js" export * from "./watsonx.js" +export * from "./zai.js" diff --git a/packages/types/src/providers/io-intelligence.ts b/packages/types/src/providers/io-intelligence.ts new file mode 100644 index 0000000000..a9b845393f --- /dev/null +++ b/packages/types/src/providers/io-intelligence.ts @@ -0,0 +1,44 @@ +import type { ModelInfo } from "../model.js" + +export type IOIntelligenceModelId = + | "deepseek-ai/DeepSeek-R1-0528" + | "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" + | "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar" + | "openai/gpt-oss-120b" + +export const ioIntelligenceDefaultModelId: IOIntelligenceModelId = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" + +export const ioIntelligenceDefaultBaseUrl = "https://api.intelligence.io.solutions/api/v1" + +export const IO_INTELLIGENCE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour + +export const ioIntelligenceModels = { + "deepseek-ai/DeepSeek-R1-0528": { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + description: "DeepSeek R1 reasoning model", + }, + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + maxTokens: 8192, + contextWindow: 430000, + supportsImages: true, + supportsPromptCache: false, + description: "Llama 4 Maverick 17B model", + }, + "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { + maxTokens: 8192, + contextWindow: 106000, + supportsImages: false, + supportsPromptCache: false, + description: "Qwen3 Coder 480B specialized for coding", + }, + "openai/gpt-oss-120b": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + description: "OpenAI GPT-OSS 120B model", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/lite-llm.ts b/packages/types/src/providers/lite-llm.ts index 303aa2b298..fdfef95bc6 100644 --- a/packages/types/src/providers/lite-llm.ts +++ b/packages/types/src/providers/lite-llm.ts @@ -17,6 +17,7 @@ export const litellmDefaultModelInfo: ModelInfo = { export const LITELLM_COMPUTER_USE_MODELS = new Set([ "claude-3-5-sonnet-latest", + "claude-opus-4-1-20250805", "claude-opus-4-20250514", "claude-sonnet-4-20250514", "claude-3-7-sonnet-latest", @@ -26,22 +27,26 @@ export const LITELLM_COMPUTER_USE_MODELS = new Set([ "vertex_ai/claude-3-5-sonnet-v2", "vertex_ai/claude-3-5-sonnet-v2@20241022", "vertex_ai/claude-3-7-sonnet@20250219", + "vertex_ai/claude-opus-4-1@20250805", "vertex_ai/claude-opus-4@20250514", "vertex_ai/claude-sonnet-4@20250514", "openrouter/anthropic/claude-3.5-sonnet", "openrouter/anthropic/claude-3.5-sonnet:beta", "openrouter/anthropic/claude-3.7-sonnet", "openrouter/anthropic/claude-3.7-sonnet:beta", + "anthropic.claude-opus-4-1-20250805-v1:0", "anthropic.claude-opus-4-20250514-v1:0", "anthropic.claude-sonnet-4-20250514-v1:0", "anthropic.claude-3-7-sonnet-20250219-v1:0", "anthropic.claude-3-5-sonnet-20241022-v2:0", "us.anthropic.claude-3-5-sonnet-20241022-v2:0", "us.anthropic.claude-3-7-sonnet-20250219-v1:0", + "us.anthropic.claude-opus-4-1-20250805-v1:0", "us.anthropic.claude-opus-4-20250514-v1:0", "us.anthropic.claude-sonnet-4-20250514-v1:0", "eu.anthropic.claude-3-5-sonnet-20241022-v2:0", "eu.anthropic.claude-3-7-sonnet-20250219-v1:0", + "eu.anthropic.claude-opus-4-1-20250805-v1:0", "eu.anthropic.claude-opus-4-20250514-v1:0", "eu.anthropic.claude-sonnet-4-20250514-v1:0", "snowflake/claude-3-5-sonnet", diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 0afdd46feb..6409e67586 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -3,9 +3,61 @@ import type { ModelInfo } from "../model.js" // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels -export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1" +export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07" export const openAiNativeModels = { + "gpt-5-chat-latest": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: false, + inputPrice: 1.25, + outputPrice: 10.0, + cacheReadsPrice: 0.13, + description: "GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks", + supportsVerbosity: true, + }, + "gpt-5-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + reasoningEffort: "medium", + inputPrice: 1.25, + outputPrice: 10.0, + cacheReadsPrice: 0.13, + description: "GPT-5: The best model for coding and agentic tasks across domains", + // supportsVerbosity is a new capability; ensure ModelInfo includes it + supportsVerbosity: true, + }, + "gpt-5-mini-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + reasoningEffort: "medium", + inputPrice: 0.25, + outputPrice: 2.0, + cacheReadsPrice: 0.03, + description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks", + supportsVerbosity: true, + }, + "gpt-5-nano-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + reasoningEffort: "medium", + inputPrice: 0.05, + outputPrice: 0.4, + cacheReadsPrice: 0.01, + description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5", + supportsVerbosity: true, + }, "gpt-4.1": { maxTokens: 32_768, contextWindow: 1_047_576, @@ -153,15 +205,6 @@ export const openAiNativeModels = { outputPrice: 4.4, cacheReadsPrice: 0.55, }, - "gpt-4.5-preview": { - maxTokens: 16_384, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 75, - outputPrice: 150, - cacheReadsPrice: 37.5, - }, "gpt-4o": { maxTokens: 16_384, contextWindow: 128_000, @@ -180,6 +223,17 @@ export const openAiNativeModels = { outputPrice: 0.6, cacheReadsPrice: 0.075, }, + "codex-mini-latest": { + maxTokens: 16_384, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1.5, + outputPrice: 6, + cacheReadsPrice: 0, + description: + "Codex Mini: Cloud-based software engineering agent powered by codex-1, a version of o3 optimized for coding tasks. Trained with reinforcement learning to generate human-style code, adhere to instructions, and iteratively run tests.", + }, } as const satisfies Record export const openAiModelInfoSaneDefaults: ModelInfo = { @@ -196,5 +250,6 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { export const azureOpenAiDefaultApiVersion = "2024-08-01-preview" export const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0 +export const GPT5_DEFAULT_TEMPERATURE = 1.0 export const OPENAI_AZURE_AI_INFERENCE_PATH = "/models/chat/completions" diff --git a/packages/types/src/providers/openrouter.ts b/packages/types/src/providers/openrouter.ts index bbdbc7e732..51d096130b 100644 --- a/packages/types/src/providers/openrouter.ts +++ b/packages/types/src/providers/openrouter.ts @@ -39,6 +39,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-3.7-sonnet:thinking", "anthropic/claude-sonnet-4", "anthropic/claude-opus-4", + "anthropic/claude-opus-4.1", "google/gemini-2.5-flash-preview", "google/gemini-2.5-flash-preview:thinking", "google/gemini-2.5-flash-preview-05-20", @@ -59,6 +60,7 @@ export const OPEN_ROUTER_COMPUTER_USE_MODELS = new Set([ "anthropic/claude-3.7-sonnet:thinking", "anthropic/claude-sonnet-4", "anthropic/claude-opus-4", + "anthropic/claude-opus-4.1", ]) // When we first launched these models we didn't have support for @@ -77,6 +79,7 @@ export const OPEN_ROUTER_REQUIRED_REASONING_BUDGET_MODELS = new Set([ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([ "anthropic/claude-3.7-sonnet:beta", "anthropic/claude-opus-4", + "anthropic/claude-opus-4.1", "anthropic/claude-sonnet-4", "google/gemini-2.5-pro-preview", "google/gemini-2.5-pro", diff --git a/packages/types/src/providers/qwen-code.ts b/packages/types/src/providers/qwen-code.ts new file mode 100644 index 0000000000..0f51e4eacb --- /dev/null +++ b/packages/types/src/providers/qwen-code.ts @@ -0,0 +1,30 @@ +import type { ModelInfo } from "../model.js" + +export type QwenCodeModelId = "qwen3-coder-plus" | "qwen3-coder-flash" + +export const qwenCodeDefaultModelId: QwenCodeModelId = "qwen3-coder-plus" + +export const qwenCodeModels = { + "qwen3-coder-plus": { + maxTokens: 65_536, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + cacheWritesPrice: 0, + cacheReadsPrice: 0, + description: "Qwen3 Coder Plus - High-performance coding model with 1M context window for large codebases", + }, + "qwen3-coder-flash": { + maxTokens: 65_536, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + cacheWritesPrice: 0, + cacheReadsPrice: 0, + description: "Qwen3 Coder Flash - Fast coding model with 1M context window optimized for speed", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/roo.ts b/packages/types/src/providers/roo.ts new file mode 100644 index 0000000000..c95e500f54 --- /dev/null +++ b/packages/types/src/providers/roo.ts @@ -0,0 +1,19 @@ +import type { ModelInfo } from "../model.js" + +// Roo provider with single model +export type RooModelId = "roo/sonic" + +export const rooDefaultModelId: RooModelId = "roo/sonic" + +export const rooModels = { + "roo/sonic": { + maxTokens: 16_384, + contextWindow: 262_144, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + description: + "A stealth reasoning model that is blazing fast and excels at agentic coding, accessible for free through Roo Code Cloud for a limited time. (Note: prompts and completions are logged by the model creator and used to improve the model.)", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/sambanova.ts b/packages/types/src/providers/sambanova.ts new file mode 100644 index 0000000000..bed143f6e5 --- /dev/null +++ b/packages/types/src/providers/sambanova.ts @@ -0,0 +1,90 @@ +import type { ModelInfo } from "../model.js" + +// https://docs.sambanova.ai/cloud/docs/get-started/supported-models +export type SambaNovaModelId = + | "Meta-Llama-3.1-8B-Instruct" + | "Meta-Llama-3.3-70B-Instruct" + | "DeepSeek-R1" + | "DeepSeek-V3-0324" + | "DeepSeek-R1-Distill-Llama-70B" + | "Llama-4-Maverick-17B-128E-Instruct" + | "Llama-3.3-Swallow-70B-Instruct-v0.4" + | "Qwen3-32B" + +export const sambaNovaDefaultModelId: SambaNovaModelId = "Meta-Llama-3.3-70B-Instruct" + +export const sambaNovaModels = { + "Meta-Llama-3.1-8B-Instruct": { + maxTokens: 8192, + contextWindow: 16384, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.2, + description: "Meta Llama 3.1 8B Instruct model with 16K context window.", + }, + "Meta-Llama-3.3-70B-Instruct": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.6, + outputPrice: 1.2, + description: "Meta Llama 3.3 70B Instruct model with 128K context window.", + }, + "DeepSeek-R1": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + supportsReasoningBudget: true, + inputPrice: 5.0, + outputPrice: 7.0, + description: "DeepSeek R1 reasoning model with 32K context window.", + }, + "DeepSeek-V3-0324": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 4.5, + description: "DeepSeek V3 model with 32K context window.", + }, + "DeepSeek-R1-Distill-Llama-70B": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.7, + outputPrice: 1.4, + description: "DeepSeek R1 distilled Llama 70B model with 128K context window.", + }, + "Llama-4-Maverick-17B-128E-Instruct": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.63, + outputPrice: 1.8, + description: "Meta Llama 4 Maverick 17B 128E Instruct model with 128K context window.", + }, + "Llama-3.3-Swallow-70B-Instruct-v0.4": { + maxTokens: 8192, + contextWindow: 16384, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.6, + outputPrice: 1.2, + description: "Tokyotech Llama 3.3 Swallow 70B Instruct v0.4 model with 16K context window.", + }, + "Qwen3-32B": { + maxTokens: 8192, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.4, + outputPrice: 0.8, + description: "Alibaba Qwen 3 32B model with 8K context window.", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index a48ebacdfb..ee8a56ae2c 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -175,6 +175,18 @@ export const vertexModels = { cacheReadsPrice: 0.3, supportsReasoningBudget: true, }, + "claude-opus-4-1@20250805": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 15.0, + outputPrice: 75.0, + cacheWritesPrice: 18.75, + cacheReadsPrice: 1.5, + supportsReasoningBudget: true, + }, "claude-opus-4@20250514": { maxTokens: 8192, contextWindow: 200_000, diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index bf38cb814b..efe0691913 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -4,6 +4,7 @@ export type VscodeLlmModelId = keyof typeof vscodeLlmModels export const vscodeLlmDefaultModelId: VscodeLlmModelId = "claude-3.5-sonnet" +// https://docs.cline.bot/provider-config/vscode-language-model-api export const vscodeLlmModels = { "gpt-3.5-turbo": { contextWindow: 12114, @@ -101,6 +102,18 @@ export const vscodeLlmModels = { supportsToolCalling: true, maxInputTokens: 81638, }, + "claude-4-sonnet": { + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + family: "claude-sonnet-4", + version: "claude-sonnet-4", + name: "Claude Sonnet 4", + supportsToolCalling: true, + maxInputTokens: 111836, + }, "gemini-2.0-flash-001": { contextWindow: 127827, supportsImages: true, @@ -114,7 +127,7 @@ export const vscodeLlmModels = { maxInputTokens: 127827, }, "gemini-2.5-pro": { - contextWindow: 63830, + contextWindow: 128000, supportsImages: true, supportsPromptCache: false, inputPrice: 0, @@ -123,10 +136,10 @@ export const vscodeLlmModels = { version: "gemini-2.5-pro-preview-03-25", name: "Gemini 2.5 Pro (Preview)", supportsToolCalling: true, - maxInputTokens: 63830, + maxInputTokens: 108637, }, "o4-mini": { - contextWindow: 111446, + contextWindow: 128000, supportsImages: false, supportsPromptCache: false, inputPrice: 0, @@ -135,10 +148,10 @@ export const vscodeLlmModels = { version: "o4-mini-2025-04-16", name: "o4-mini (Preview)", supportsToolCalling: true, - maxInputTokens: 111446, + maxInputTokens: 111452, }, "gpt-4.1": { - contextWindow: 111446, + contextWindow: 128000, supportsImages: true, supportsPromptCache: false, inputPrice: 0, @@ -147,7 +160,31 @@ export const vscodeLlmModels = { version: "gpt-4.1-2025-04-14", name: "GPT-4.1 (Preview)", supportsToolCalling: true, - maxInputTokens: 111446, + maxInputTokens: 111452, + }, + "gpt-5-mini": { + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + family: "gpt-5-mini", + version: "gpt-5-mini", + name: "GPT-5 mini (Preview)", + supportsToolCalling: true, + maxInputTokens: 108637, + }, + "gpt-5": { + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + family: "gpt-5", + version: "gpt-5", + name: "GPT-5 (Preview)", + supportsToolCalling: true, + maxInputTokens: 108637, }, } as const satisfies Record< string, diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts new file mode 100644 index 0000000000..f724744827 --- /dev/null +++ b/packages/types/src/providers/zai.ts @@ -0,0 +1,105 @@ +import type { ModelInfo } from "../model.js" + +// Z AI +// https://docs.z.ai/guides/llm/glm-4.5 +// https://docs.z.ai/guides/overview/pricing + +export type InternationalZAiModelId = keyof typeof internationalZAiModels +export const internationalZAiDefaultModelId: InternationalZAiModelId = "glm-4.5" +export const internationalZAiModels = { + "glm-4.5": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.6, + outputPrice: 2.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.11, + description: + "GLM-4.5 is Zhipu's latest featured model. Its comprehensive capabilities in reasoning, coding, and agent reach the state-of-the-art (SOTA) level among open-source models, with a context length of up to 128k.", + }, + "glm-4.5-air": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.2, + outputPrice: 1.1, + cacheWritesPrice: 0, + cacheReadsPrice: 0.03, + description: + "GLM-4.5-Air is the lightweight version of GLM-4.5. It balances performance and cost-effectiveness, and can flexibly switch to hybrid thinking models.", + }, +} as const satisfies Record + +export type MainlandZAiModelId = keyof typeof mainlandZAiModels +export const mainlandZAiDefaultModelId: MainlandZAiModelId = "glm-4.5" +export const mainlandZAiModels = { + "glm-4.5": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.29, + outputPrice: 1.14, + cacheWritesPrice: 0, + cacheReadsPrice: 0.057, + description: + "GLM-4.5 is Zhipu's latest featured model. Its comprehensive capabilities in reasoning, coding, and agent reach the state-of-the-art (SOTA) level among open-source models, with a context length of up to 128k.", + tiers: [ + { + contextWindow: 32_000, + inputPrice: 0.21, + outputPrice: 1.0, + cacheReadsPrice: 0.043, + }, + { + contextWindow: 128_000, + inputPrice: 0.29, + outputPrice: 1.14, + cacheReadsPrice: 0.057, + }, + { + contextWindow: Infinity, + inputPrice: 0.29, + outputPrice: 1.14, + cacheReadsPrice: 0.057, + }, + ], + }, + "glm-4.5-air": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.1, + outputPrice: 0.6, + cacheWritesPrice: 0, + cacheReadsPrice: 0.02, + description: + "GLM-4.5-Air is the lightweight version of GLM-4.5. It balances performance and cost-effectiveness, and can flexibly switch to hybrid thinking models.", + tiers: [ + { + contextWindow: 32_000, + inputPrice: 0.07, + outputPrice: 0.4, + cacheReadsPrice: 0.014, + }, + { + contextWindow: 128_000, + inputPrice: 0.1, + outputPrice: 0.6, + cacheReadsPrice: 0.02, + }, + { + contextWindow: Infinity, + inputPrice: 0.1, + outputPrice: 0.6, + cacheReadsPrice: 0.02, + }, + ], + }, +} as const satisfies Record + +export const ZAI_DEFAULT_TEMPERATURE = 0 diff --git a/packages/types/src/sharing.ts b/packages/types/src/sharing.ts deleted file mode 100644 index f295798032..0000000000 --- a/packages/types/src/sharing.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Types related to task sharing functionality - */ - -/** - * Visibility options for sharing tasks - */ -export type ShareVisibility = "organization" | "public" diff --git a/packages/types/src/single-file-read-models.ts b/packages/types/src/single-file-read-models.ts new file mode 100644 index 0000000000..4e6f4e1f57 --- /dev/null +++ b/packages/types/src/single-file-read-models.ts @@ -0,0 +1,32 @@ +/** + * Configuration for models that should use simplified single-file read_file tool + * These models will use the simpler format + * instead of the more complex multi-file args format + */ + +// List of model IDs (or patterns) that should use single file reads only +export const SINGLE_FILE_READ_MODELS = new Set ... (["roo/sonic"]) + +/** + * Check if a model should use single file read format + * @param modelId The model ID to check + * @returns true if the model should use single file reads + */ +export function shouldUseSingleFileRead(modelId: string): boolean { + // Direct match + if (SINGLE_FILE_READ_MODELS.has(modelId)) { + return true + } + + // Pattern matching for model families + // Check if model ID starts with any configured pattern + // Using Array.from for compatibility with older TypeScript targets + const patterns = Array.from(SINGLE_FILE_READ_MODELS) + for (const pattern of patterns) { + if (pattern.endsWith("*") && modelId.startsWith(pattern.slice(0, -1))) { + return true + } + } + + return false +} diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts new file mode 100644 index 0000000000..3f741fc6dd --- /dev/null +++ b/packages/types/src/task.ts @@ -0,0 +1,123 @@ +import { z } from "zod" + +import { RooCodeEventName } from "./events.js" +import { type ClineMessage, type TokenUsage } from "./message.js" +import { type ToolUsage, type ToolName } from "./tool.js" +import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js" + +/** + * TaskProviderLike + */ + +export interface TaskProviderState { + mode?: string +} + +export interface TaskProviderLike { + readonly cwd: string + readonly appProperties: StaticAppProperties + readonly gitProperties: GitProperties | undefined + + getCurrentTask(): TaskLike | undefined + getCurrentTaskStack(): string[] + getRecentTasks(): string[] + + createTask(text?: string, images?: string[], parentTask?: TaskLike): Promise + cancelTask(): Promise + clearTask(): Promise + resumeTask(taskId: string): void + + getState(): Promise + postStateToWebview(): Promise + postMessageToWebview(message: unknown): Promise + + getTelemetryProperties(): Promise + + on ( + event: K, + listener: (...args: TaskProviderEvents[K]) => void | Promise , + ): this + + off ( + event: K, + listener: (...args: TaskProviderEvents[K]) => void | Promise , + ): this +} + +export type TaskProviderEvents = { + [RooCodeEventName.TaskCreated]: [task: TaskLike] + + // Proxied from the Task EventEmitter. + [RooCodeEventName.TaskStarted]: [taskId: string] + [RooCodeEventName.TaskCompleted]: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] + [RooCodeEventName.TaskAborted]: [taskId: string] + [RooCodeEventName.TaskFocused]: [taskId: string] + [RooCodeEventName.TaskUnfocused]: [taskId: string] + [RooCodeEventName.TaskActive]: [taskId: string] + [RooCodeEventName.TaskInteractive]: [taskId: string] + [RooCodeEventName.TaskResumable]: [taskId: string] + [RooCodeEventName.TaskIdle]: [taskId: string] +} + +/** + * TaskLike + */ + +export enum TaskStatus { + Running = "running", + Interactive = "interactive", + Resumable = "resumable", + Idle = "idle", + None = "none", +} + +export const taskMetadataSchema = z.object({ + task: z.string().optional(), + images: z.array(z.string()).optional(), +}) + +export type TaskMetadata = z.infer + +export interface TaskLike { + readonly taskId: string + readonly taskStatus: TaskStatus + readonly taskAsk: ClineMessage | undefined + readonly metadata: TaskMetadata + + readonly rootTask?: TaskLike + + on (event: K, listener: (...args: TaskEvents[K]) => void | Promise ): this + off (event: K, listener: (...args: TaskEvents[K]) => void | Promise ): this + + approveAsk(options?: { text?: string; images?: string[] }): void + denyAsk(options?: { text?: string; images?: string[] }): void + submitUserMessage(text: string, images?: string[]): void + abortTask(): void +} + +export type TaskEvents = { + // Task Lifecycle + [RooCodeEventName.TaskStarted]: [] + [RooCodeEventName.TaskCompleted]: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] + [RooCodeEventName.TaskAborted]: [] + [RooCodeEventName.TaskFocused]: [] + [RooCodeEventName.TaskUnfocused]: [] + [RooCodeEventName.TaskActive]: [taskId: string] + [RooCodeEventName.TaskInteractive]: [taskId: string] + [RooCodeEventName.TaskResumable]: [taskId: string] + [RooCodeEventName.TaskIdle]: [taskId: string] + + // Subtask Lifecycle + [RooCodeEventName.TaskPaused]: [] + [RooCodeEventName.TaskUnpaused]: [] + [RooCodeEventName.TaskSpawned]: [taskId: string] + + // Task Execution + [RooCodeEventName.Message]: [{ action: "created" | "updated"; message: ClineMessage }] + [RooCodeEventName.TaskModeSwitched]: [taskId: string, mode: string] + [RooCodeEventName.TaskAskResponded]: [] + + // Task Analytics + [RooCodeEventName.TaskToolFailed]: [taskId: string, tool: ToolName, error: string] + [RooCodeEventName.TaskTokenUsageUpdated]: [taskId: string, tokenUsage: TokenUsage] +} diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 223c39484c..d872329b93 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -72,17 +72,37 @@ export enum TelemetryEventName { * TelemetryProperties */ -export const appPropertiesSchema = z.object({ +export const staticAppPropertiesSchema = z.object({ appName: z.string(), appVersion: z.string(), vscodeVersion: z.string(), platform: z.string(), editorName: z.string(), +}) + +export type StaticAppProperties = z.infer + +export const dynamicAppPropertiesSchema = z.object({ language: z.string(), mode: z.string(), +}) + +export type DynamicAppProperties = z.infer + +export const cloudAppPropertiesSchema = z.object({ cloudIsAuthenticated: z.boolean().optional(), }) +export type CloudAppProperties = z.infer + +export const appPropertiesSchema = z.object({ + ...staticAppPropertiesSchema.shape, + ...dynamicAppPropertiesSchema.shape, + ...cloudAppPropertiesSchema.shape, +}) + +export type AppProperties = z.infer + export const taskPropertiesSchema = z.object({ taskId: z.string().optional(), apiProvider: z.enum(providerNames).optional(), @@ -99,12 +119,16 @@ export const taskPropertiesSchema = z.object({ .optional(), }) +export type TaskProperties = z.infer + export const gitPropertiesSchema = z.object({ repositoryUrl: z.string().optional(), repositoryName: z.string().optional(), defaultBranch: z.string().optional(), }) +export type GitProperties = z.infer + export const telemetryPropertiesSchema = z.object({ ...appPropertiesSchema.shape, ...taskPropertiesSchema.shape, @@ -112,7 +136,6 @@ export const telemetryPropertiesSchema = z.object({ }) export type TelemetryProperties = z.infer -export type GitProperties = z.infer /** * TelemetryEvent diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index a66434e570..2a73ee92bb 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -4,6 +4,6 @@ "types": ["vitest/globals"], "outDir": "dist" }, - "include": ["src"], + "include": ["src", "scripts", "*.config.ts"], "exclude": ["node_modules"] } diff --git a/packages/types/tsup.config.ts b/packages/types/tsup.config.ts index 9c96eb1901..38b458806a 100644 --- a/packages/types/tsup.config.ts +++ b/packages/types/tsup.config.ts @@ -4,8 +4,8 @@ export default defineConfig({ entry: ["src/index.ts"], format: ["cjs", "esm"], dts: true, - clean: false, splitting: false, sourcemap: true, + clean: true, outDir: "dist", }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25fe6bdd64..31aea6f423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,7 @@ overrides: undici: '>=5.29.0' brace-expansion: '>=2.0.2' form-data: '>=4.0.4' + bluebird: '>=3.7.2' importers: @@ -17,10 +18,19 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.27.10 - version: 2.29.4 + version: 2.29.5 '@dotenvx/dotenvx': specifier: ^1.34.0 version: 1.44.2 + '@roo-code/config-typescript': + specifier: workspace:^ + version: link:packages/config-typescript + '@types/glob': + specifier: ^9.0.0 + version: 9.0.0 + '@types/node': + specifier: ^24.1.0 + version: 24.2.1 '@vscode/vsce': specifier: 3.3.2 version: 3.3.2 @@ -30,15 +40,18 @@ importers: eslint: specifier: ^9.27.0 version: 9.28.0(jiti@2.4.2) + glob: + specifier: ^11.0.3 + version: 11.0.3 husky: specifier: ^9.1.7 version: 9.1.7 knip: specifier: ^5.44.4 - version: 5.60.2(@types/node@22.15.29)(typescript@5.8.3) + version: 5.60.2(@types/node@24.2.1)(typescript@5.8.3) lint-staged: specifier: ^16.0.0 - version: 16.1.0 + version: 16.1.2 mkdirp: specifier: ^3.0.1 version: 3.0.1 @@ -54,6 +67,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + tsx: + specifier: ^4.19.3 + version: 4.19.4 turbo: specifier: ^2.5.3 version: 2.5.4 @@ -234,7 +250,7 @@ importers: version: 4.1.6 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) apps/web-roo-code: dependencies: @@ -294,7 +310,7 @@ importers: version: 5.5.0(react@18.3.1) recharts: specifier: ^2.15.3 - version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^3.3.0 version: 3.3.0 @@ -326,6 +342,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.4) + next-sitemap: + specifier: ^4.2.3 + version: 4.2.3(next@15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) postcss: specifier: ^8.5.4 version: 8.5.4 @@ -352,34 +371,6 @@ importers: specifier: ^3.2.3 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - packages/cloud: - dependencies: - '@roo-code/telemetry': - specifier: workspace:^ - version: link:../telemetry - '@roo-code/types': - specifier: workspace:^ - version: link:../types - zod: - specifier: ^3.25.61 - version: 3.25.61 - devDependencies: - '@roo-code/config-eslint': - specifier: workspace:^ - version: link:../config-eslint - '@roo-code/config-typescript': - specifier: workspace:^ - version: link:../config-typescript - '@types/node': - specifier: 20.x - version: 20.17.57 - '@types/vscode': - specifier: ^1.84.0 - version: 1.100.0 - vitest: - specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - packages/config-eslint: devDependencies: '@eslint/js': @@ -474,7 +465,7 @@ importers: version: 1.1.6 drizzle-kit: specifier: ^0.31.1 - version: 0.31.1 + version: 0.31.4 tsx: specifier: ^4.19.3 version: 4.19.4 @@ -548,14 +539,17 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.x - version: 20.17.57 + specifier: ^24.1.0 + version: 24.2.1 + globals: + specifier: ^16.3.0 + version: 16.3.0 tsup: specifier: ^8.3.5 version: 8.5.0(jiti@2.4.2)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.0) vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) src: dependencies: @@ -584,8 +578,8 @@ importers: specifier: ^1.1.1 version: 1.2.0 '@mistralai/mistralai': - specifier: ^1.3.6 - version: 1.6.1(zod@3.25.61) + specifier: ^1.9.18 + version: 1.9.18(zod@3.25.61) '@modelcontextprotocol/sdk': specifier: ^1.9.0 version: 1.12.0 @@ -593,8 +587,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@roo-code/cloud': - specifier: workspace:^ - version: link:../packages/cloud + specifier: ^0.21.0 + version: 0.21.0 '@roo-code/ipc': specifier: workspace:^ version: link:../packages/ipc @@ -674,8 +668,8 @@ importers: specifier: ^4.0.8 version: 4.0.8 mammoth: - specifier: ^1.8.0 - version: 1.9.0 + specifier: ^1.9.1 + version: 1.9.1 monaco-vscode-textmate-theme-converter: specifier: ^0.1.7 version: 0.1.7(tslib@2.8.1) @@ -685,9 +679,12 @@ importers: node-ipc: specifier: ^12.0.0 version: 12.0.0 + ollama: + specifier: ^0.5.17 + version: 0.5.17 openai: specifier: ^5.0.0 - version: 5.5.1(ws@8.18.2)(zod@3.25.61) + version: 5.5.1(ws@8.18.3)(zod@3.25.61) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -733,6 +730,9 @@ importers: simple-git: specifier: ^3.27.0 version: 3.27.0 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 sound-play: specifier: ^1.1.0 version: 1.1.0 @@ -753,7 +753,7 @@ importers: version: 1.0.21 tmp: specifier: ^0.2.3 - version: 0.2.3 + version: 0.2.4 tree-sitter-wasms: specifier: ^0.1.12 version: 0.1.12 @@ -991,7 +991,7 @@ importers: version: 0.518.0(react@18.3.1) mermaid: specifier: ^11.4.1 - version: 11.6.0 + version: 11.10.0 posthog-js: specifier: ^1.227.2 version: 1.242.1 @@ -1452,6 +1452,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -1473,14 +1477,14 @@ packages: '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} - '@changesets/assemble-release-plan@6.0.8': - resolution: {integrity: sha512-y8+8LvZCkKJdbUlpXFuqcavpzJR80PN0OIfn8HZdwK7Sh6MgLXm4hKY5vu6/NDoKp8lAlM4ERZCqRMLxP4m+MQ==} + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.4': - resolution: {integrity: sha512-VW30x9oiFp/un/80+5jLeWgEU6Btj8IqOgI+X/zAYu4usVOWXjPIK5jSSlt5jsCU7/6Z7AxEkarxBxGUqkAmNg==} + '@changesets/cli@2.29.5': + resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==} hasBin: true '@changesets/config@3.1.1': @@ -1492,8 +1496,8 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.12': - resolution: {integrity: sha512-KukdEgaafnyGryUwpHG2kZ7xJquOmWWWk5mmoeQaSvZTWH1DC5D/Sw6ClgGFYtQnOMSQhgoEbDxAbpIIayKH1g==} + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -1540,6 +1544,9 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@corex/deepmerge@4.0.43': + resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1613,150 +1620,306 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.5': resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.5': resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.5': resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.5': resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.5': resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.5': resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.5': resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.5': resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.5': resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.5': resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.5': resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.5': resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.5': resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.5': resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.5': resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.5': resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.5': resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.5': resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.5': resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.5': resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.5': resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.5': resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.5': resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.5': resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1966,6 +2129,17 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.3.0': + resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2017,16 +2191,16 @@ packages: '@libsql/client@0.15.8': resolution: {integrity: sha512-TskygwF+ToZeWhPPT0WennyGrP3tmkKraaKopT2YwUjqD6DWDRm6SG5iy0VqnaO+HC9FNBCDX0oQPODU3gqqPQ==} - '@libsql/core@0.15.9': - resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==} + '@libsql/core@0.15.12': + resolution: {integrity: sha512-S3tF6885ZizVjfym7f8SevL2VId/+DzxiKmP5zFbrhA8oMLh2XH8bYXChmhab7o9qUSHx+XjK4jCFpUwR5g+Ig==} - '@libsql/darwin-arm64@0.5.13': - resolution: {integrity: sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==} + '@libsql/darwin-arm64@0.5.17': + resolution: {integrity: sha512-WTYG2skZsUnZmfZ2v7WFj7s3/5s2PfrYBZOWBKOnxHA8g4XCDc/4bFDaqob9Q2e88+GC7cWeJ8VNkVBFpD2Xxg==} cpu: [arm64] os: [darwin] - '@libsql/darwin-x64@0.5.13': - resolution: {integrity: sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==} + '@libsql/darwin-x64@0.5.17': + resolution: {integrity: sha512-ab0RlTR4KYrxgjNrZhAhY/10GibKoq6G0W4oi0kdm+eYiAv/Ip8GDMpSaZdAcoKA4T+iKR/ehczKHnMEB8MFxA==} cpu: [x64] os: [darwin] @@ -2040,38 +2214,38 @@ packages: '@libsql/isomorphic-ws@0.1.5': resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - '@libsql/linux-arm-gnueabihf@0.5.13': - resolution: {integrity: sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==} + '@libsql/linux-arm-gnueabihf@0.5.17': + resolution: {integrity: sha512-PcASh4k47RqC+kMWAbLUKf1y6Do0q8vnUGi0yhKY4ghJcimMExViBimjbjYRSa+WIb/zh3QxNoXOhQAXx3tiuw==} cpu: [arm] os: [linux] - '@libsql/linux-arm-musleabihf@0.5.13': - resolution: {integrity: sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==} + '@libsql/linux-arm-musleabihf@0.5.17': + resolution: {integrity: sha512-vxOkSLG9Wspit+SNle84nuIzMtr2G2qaxFzW7BhsZBjlZ8+kErf9RXcT2YJQdJYxmBYRbsOrc91gg0jLEQVCqg==} cpu: [arm] os: [linux] - '@libsql/linux-arm64-gnu@0.5.13': - resolution: {integrity: sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==} + '@libsql/linux-arm64-gnu@0.5.17': + resolution: {integrity: sha512-L8jnaN01TxjBJlDuDTX2W2BKzBkAOhcnKfCOf3xzvvygblxnDOK0whkYwIXeTfwtd/rr4jN/d6dZD/bcHiDxEQ==} cpu: [arm64] os: [linux] - '@libsql/linux-arm64-musl@0.5.13': - resolution: {integrity: sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==} + '@libsql/linux-arm64-musl@0.5.17': + resolution: {integrity: sha512-HfFD7TzQtmmTwyQsuiHhWZdMRtdNpKJ1p4tbMMTMRECk+971NFHrj69D64cc2ClVTAmn7fA9XibKPil7WN/Q7w==} cpu: [arm64] os: [linux] - '@libsql/linux-x64-gnu@0.5.13': - resolution: {integrity: sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==} + '@libsql/linux-x64-gnu@0.5.17': + resolution: {integrity: sha512-5l3XxWqUPVFrtX0xnZaXwqsXs0BFbP4w6ahRFTPSdXU50YBfUOajFznJRB6bJTMsCvraDSD0IkHhjSNfrE1CuQ==} cpu: [x64] os: [linux] - '@libsql/linux-x64-musl@0.5.13': - resolution: {integrity: sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==} + '@libsql/linux-x64-musl@0.5.17': + resolution: {integrity: sha512-FvSpWlwc+dIeYIFYlsSv+UdQ/NiZWr+SstwVji+QZ//8NnvzwWQU9cgP+Vpps6Qiq4jyYQm9chJhTYOVT9Y3BA==} cpu: [x64] os: [linux] - '@libsql/win32-x64-msvc@0.5.13': - resolution: {integrity: sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==} + '@libsql/win32-x64-msvc@0.5.17': + resolution: {integrity: sha512-f5bGH8+3A5sn6Lrqg8FsQ09a1pYXPnKGXGTFiAYlfQXVst1tUTxDTugnuWcJYKXyzDe/T7ccxyIZXeSmPOhq8A==} cpu: [x64] os: [win32] @@ -2091,8 +2265,8 @@ packages: resolution: {integrity: sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==} engines: {node: '>=12'} - '@mermaid-js/parser@0.4.0': - resolution: {integrity: sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==} + '@mermaid-js/parser@0.6.2': + resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==} '@microsoft/fast-element@1.14.0': resolution: {integrity: sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==} @@ -2108,8 +2282,8 @@ packages: '@microsoft/fast-web-utilities@5.4.1': resolution: {integrity: sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==} - '@mistralai/mistralai@1.6.1': - resolution: {integrity: sha512-NFAMamNFSAaLT4YhDrqEjhJALJXSheZdA5jXT6gG5ICCJRk9+WQx7vRQO1sIZNIRP+xpPyROpa7X6ZcufiucIA==} + '@mistralai/mistralai@1.9.18': + resolution: {integrity: sha512-D/vNAGEvWMsg95tzgLTg7pPnW9leOPyH+nh1Os05NwxVPbUykoYgMAwOEX7J46msahWdvZ4NQQuxUXIUV2P6dg==} peerDependencies: zod: '>= 3' @@ -2133,6 +2307,9 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@next/env@13.5.11': + resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==} + '@next/env@15.2.5': resolution: {integrity: sha512-uWkCf9C8wKTyQjqrNk+BA7eL3LOQdhL+xlmJUf2O85RM4lbzwBwot3Sqv2QGe/RGnc3zysIf1oJdtq9S00pkmQ==} @@ -3095,6 +3272,12 @@ packages: cpu: [x64] os: [win32] + '@roo-code/cloud@0.21.0': + resolution: {integrity: sha512-yNVybIjaS7Hy8GwDtGJc76N1WpCXGaCSlAEsW7VGjnojpxaIzV2GcJP1j1hg5q8HqLQnU4ixV0qXxOkxwhkEiA==} + + '@roo-code/types@1.60.0': + resolution: {integrity: sha512-tQO6njPr/ZDNBoSHQg1/dpxfVEYeUzpKcernUxgJzmttn1zJbS0sc3CfUyPYOfYKB331z6O3KFUpaiqYFje1wA==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -3438,6 +3621,9 @@ packages: resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} engines: {node: '>=18.0.0'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -3726,8 +3912,8 @@ packages: '@types/d3-delaunay@6.0.4': resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - '@types/d3-dispatch@3.0.6': - resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} '@types/d3-drag@3.0.7': resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} @@ -3825,6 +4011,10 @@ packages: '@types/glob@8.1.0': resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + '@types/glob@9.0.0': + resolution: {integrity: sha512-00UxlRaIUvYm4R4W9WYkN8/J+kV8fmOQ7okeH6YFtGWFMt3odD45tpG5yA5wnL7HE6lLgjaTW5n14ju2hl2NNA==} + deprecated: This is a stub types definition. glob provides its own type definitions, so you do not need this installed. + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -3895,11 +4085,8 @@ packages: '@types/node@20.17.57': resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==} - '@types/node@20.19.1': - resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} - - '@types/node@22.15.29': - resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + '@types/node@24.2.1': + resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -4412,8 +4599,8 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bluebird@3.4.7: - resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} @@ -4859,8 +5046,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.32.0: - resolution: {integrity: sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==} + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -5047,6 +5234,15 @@ packages: supports-color: optional: true + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -5123,6 +5319,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -5202,8 +5402,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.5: - resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} + dompurify@3.2.6: + resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5216,8 +5416,8 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} - drizzle-kit@0.31.1: - resolution: {integrity: sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==} + drizzle-kit@0.31.4: + resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==} hasBin: true drizzle-orm@0.44.1: @@ -5390,6 +5590,13 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -5462,6 +5669,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5671,8 +5883,8 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} - exsolve@1.0.5: - resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -5994,6 +6206,9 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-uri@6.0.4: resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} engines: {node: '>= 14'} @@ -6018,6 +6233,11 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -6038,6 +6258,10 @@ packages: resolution: {integrity: sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==} engines: {node: '>=18'} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -6308,6 +6532,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} + engines: {node: '>=12.22.0'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -6592,6 +6820,10 @@ packages: resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6624,8 +6856,8 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} @@ -6778,8 +7010,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.5.13: - resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==} + libsql@0.5.17: + resolution: {integrity: sha512-RRlj5XQI9+Wq+/5UY8EnugSWfRmHEw4hn3DKlPrkUgZONsge1PwTtHcpStP6MSNi8ohcbsRgEHJaymA33a8cBw==} cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] @@ -6924,8 +7156,8 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - lint-staged@16.1.0: - resolution: {integrity: sha512-HkpQh69XHxgCjObjejBT3s2ILwNjFx8M3nw+tJ/ssBauDlIpkx2RpqWSi1fBgkXLSSXnbR3iEq1NkVtpvV+FLQ==} + lint-staged@16.1.2: + resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==} engines: {node: '>=20.17'} hasBin: true @@ -6979,6 +7211,9 @@ packages: lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} @@ -7097,8 +7332,8 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - mammoth@1.9.0: - resolution: {integrity: sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==} + mammoth@1.9.1: + resolution: {integrity: sha512-4S2v1eP4Yo4so0zGNicJKcP93su3wDPcUk+xvkjSG75nlNjSkDJu8BhWQ+e54BROM0HfA6nPzJn12S6bq2Ko6w==} engines: {node: '>=12.0.0'} hasBin: true @@ -7112,9 +7347,9 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@15.0.11: - resolution: {integrity: sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==} - engines: {node: '>= 18'} + marked@16.2.0: + resolution: {integrity: sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg==} + engines: {node: '>= 20'} hasBin: true math-intrinsics@1.1.0: @@ -7209,8 +7444,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.6.0: - resolution: {integrity: sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==} + mermaid@11.10.0: + resolution: {integrity: sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7351,6 +7586,10 @@ packages: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -7459,6 +7698,13 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + next-sitemap@4.2.3: + resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + next: '*' + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -7603,6 +7849,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + ollama@0.5.17: + resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -7880,8 +8129,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.1.0: - resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -8114,8 +8363,8 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8309,8 +8558,8 @@ packages: recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - recharts@2.15.3: - resolution: {integrity: sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==} + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} engines: {node: '>=14'} peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -8324,6 +8573,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redis@5.5.5: resolution: {integrity: sha512-x7vpciikEY7nptGzQrE5I+/pvwFZJDadPk/uEoyGSg/pZ2m/CX2n5EhSgUh+S5T7Gz3uKM6YzWcXEu3ioAsdFQ==} engines: {node: '>= 18'} @@ -8669,6 +8926,14 @@ packages: resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} engines: {node: '>= 18'} + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -8708,6 +8973,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -8746,6 +9012,9 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -9074,6 +9343,10 @@ packages: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} + tmp@0.2.4: + resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -9294,8 +9567,8 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} @@ -9646,6 +9919,9 @@ packages: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -9732,6 +10008,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.2: resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} engines: {node: '>=10.0.0'} @@ -9744,6 +10032,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -9763,6 +10063,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -9855,6 +10159,9 @@ packages: zod@3.25.61: resolution: {integrity: sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -10609,6 +10916,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.3': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -10652,7 +10961,7 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.2 - '@changesets/assemble-release-plan@6.0.8': + '@changesets/assemble-release-plan@6.0.9': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 @@ -10665,15 +10974,15 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.4': + '@changesets/cli@2.29.5': dependencies: '@changesets/apply-release-plan': 7.0.12 - '@changesets/assemble-release-plan': 6.0.8 + '@changesets/assemble-release-plan': 6.0.9 '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.1 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.12 + '@changesets/get-release-plan': 4.0.13 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 @@ -10717,9 +11026,9 @@ snapshots: picocolors: 1.1.1 semver: 7.7.2 - '@changesets/get-release-plan@4.0.12': + '@changesets/get-release-plan@4.0.13': dependencies: - '@changesets/assemble-release-plan': 6.0.8 + '@changesets/assemble-release-plan': 6.0.9 '@changesets/config': 3.1.1 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.5 @@ -10795,6 +11104,8 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@corex/deepmerge@4.0.43': {} + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -10859,89 +11170,167 @@ snapshots: '@esbuild-kit/core-utils@3.3.2': dependencies: - esbuild: 0.25.5 + esbuild: 0.25.9 source-map-support: 0.5.21 '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.10.0 + get-tsconfig: 4.10.1 '@esbuild/aix-ppc64@0.25.5': optional: true + '@esbuild/aix-ppc64@0.25.9': + optional: true + '@esbuild/android-arm64@0.25.5': optional: true + '@esbuild/android-arm64@0.25.9': + optional: true + '@esbuild/android-arm@0.25.5': optional: true + '@esbuild/android-arm@0.25.9': + optional: true + '@esbuild/android-x64@0.25.5': optional: true + '@esbuild/android-x64@0.25.9': + optional: true + '@esbuild/darwin-arm64@0.25.5': optional: true + '@esbuild/darwin-arm64@0.25.9': + optional: true + '@esbuild/darwin-x64@0.25.5': optional: true + '@esbuild/darwin-x64@0.25.9': + optional: true + '@esbuild/freebsd-arm64@0.25.5': optional: true + '@esbuild/freebsd-arm64@0.25.9': + optional: true + '@esbuild/freebsd-x64@0.25.5': optional: true + '@esbuild/freebsd-x64@0.25.9': + optional: true + '@esbuild/linux-arm64@0.25.5': optional: true + '@esbuild/linux-arm64@0.25.9': + optional: true + '@esbuild/linux-arm@0.25.5': optional: true + '@esbuild/linux-arm@0.25.9': + optional: true + '@esbuild/linux-ia32@0.25.5': optional: true + '@esbuild/linux-ia32@0.25.9': + optional: true + '@esbuild/linux-loong64@0.25.5': optional: true + '@esbuild/linux-loong64@0.25.9': + optional: true + '@esbuild/linux-mips64el@0.25.5': optional: true + '@esbuild/linux-mips64el@0.25.9': + optional: true + '@esbuild/linux-ppc64@0.25.5': optional: true + '@esbuild/linux-ppc64@0.25.9': + optional: true + '@esbuild/linux-riscv64@0.25.5': optional: true + '@esbuild/linux-riscv64@0.25.9': + optional: true + '@esbuild/linux-s390x@0.25.5': optional: true + '@esbuild/linux-s390x@0.25.9': + optional: true + '@esbuild/linux-x64@0.25.5': optional: true + '@esbuild/linux-x64@0.25.9': + optional: true + '@esbuild/netbsd-arm64@0.25.5': optional: true + '@esbuild/netbsd-arm64@0.25.9': + optional: true + '@esbuild/netbsd-x64@0.25.5': optional: true + '@esbuild/netbsd-x64@0.25.9': + optional: true + '@esbuild/openbsd-arm64@0.25.5': optional: true + '@esbuild/openbsd-arm64@0.25.9': + optional: true + '@esbuild/openbsd-x64@0.25.5': optional: true + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + '@esbuild/sunos-x64@0.25.5': optional: true + '@esbuild/sunos-x64@0.25.9': + optional: true + '@esbuild/win32-arm64@0.25.5': optional: true + '@esbuild/win32-arm64@0.25.9': + optional: true + '@esbuild/win32-ia32@0.25.5': optional: true + '@esbuild/win32-ia32@0.25.9': + optional: true + '@esbuild/win32-x64@0.25.5': optional: true + '@esbuild/win32-x64@0.25.9': + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': dependencies: eslint: 9.27.0(jiti@2.4.2) @@ -11158,6 +11547,14 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@ioredis/commands@1.3.0': {} + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11186,7 +11583,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.1 + '@types/node': 24.2.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -11217,32 +11614,32 @@ snapshots: '@libsql/client@0.15.8': dependencies: - '@libsql/core': 0.15.9 + '@libsql/core': 0.15.12 '@libsql/hrana-client': 0.7.0 - js-base64: 3.7.7 - libsql: 0.5.13 + js-base64: 3.7.8 + libsql: 0.5.17 promise-limit: 2.7.0 transitivePeerDependencies: - bufferutil - utf-8-validate optional: true - '@libsql/core@0.15.9': + '@libsql/core@0.15.12': dependencies: - js-base64: 3.7.7 + js-base64: 3.7.8 optional: true - '@libsql/darwin-arm64@0.5.13': + '@libsql/darwin-arm64@0.5.17': optional: true - '@libsql/darwin-x64@0.5.13': + '@libsql/darwin-x64@0.5.17': optional: true '@libsql/hrana-client@0.7.0': dependencies: '@libsql/isomorphic-fetch': 0.3.1 '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.7 + js-base64: 3.7.8 node-fetch: 3.3.2 transitivePeerDependencies: - bufferutil @@ -11255,31 +11652,31 @@ snapshots: '@libsql/isomorphic-ws@0.1.5': dependencies: '@types/ws': 8.18.1 - ws: 8.18.2 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate optional: true - '@libsql/linux-arm-gnueabihf@0.5.13': + '@libsql/linux-arm-gnueabihf@0.5.17': optional: true - '@libsql/linux-arm-musleabihf@0.5.13': + '@libsql/linux-arm-musleabihf@0.5.17': optional: true - '@libsql/linux-arm64-gnu@0.5.13': + '@libsql/linux-arm64-gnu@0.5.17': optional: true - '@libsql/linux-arm64-musl@0.5.13': + '@libsql/linux-arm64-musl@0.5.17': optional: true - '@libsql/linux-x64-gnu@0.5.13': + '@libsql/linux-x64-gnu@0.5.17': optional: true - '@libsql/linux-x64-musl@0.5.13': + '@libsql/linux-x64-musl@0.5.17': optional: true - '@libsql/win32-x64-msvc@0.5.13': + '@libsql/win32-x64-msvc@0.5.17': optional: true '@lmstudio/lms-isomorphic@0.4.5': @@ -11302,14 +11699,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -11320,7 +11717,7 @@ snapshots: dependencies: unist-util-visit: 1.4.1 - '@mermaid-js/parser@0.4.0': + '@mermaid-js/parser@0.6.2': dependencies: langium: 3.3.1 @@ -11343,7 +11740,7 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@mistralai/mistralai@1.6.1(zod@3.25.61)': + '@mistralai/mistralai@1.9.18(zod@3.25.61)': dependencies: zod: 3.25.61 zod-to-json-schema: 3.24.5(zod@3.25.61) @@ -11392,6 +11789,8 @@ snapshots: '@neon-rs/load@0.0.4': optional: true + '@next/env@13.5.11': {} + '@next/env@15.2.5': {} '@next/eslint-plugin-next@15.3.2': @@ -12259,6 +12658,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true + '@roo-code/cloud@0.21.0': + dependencies: + '@roo-code/types': 1.60.0 + ioredis: 5.6.1 + p-wait-for: 5.0.2 + socket.io-client: 4.8.1 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@roo-code/types@1.60.0': + dependencies: + zod: 3.25.76 + '@sec-ant/readable-stream@0.4.1': {} '@sevinf/maybe@0.5.0': {} @@ -12782,6 +13197,8 @@ snapshots: '@smithy/util-buffer-from': 4.0.0 tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/utils@0.3.0': {} '@swc/counter@0.1.3': {} @@ -12958,7 +13375,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -13049,7 +13466,7 @@ snapshots: '@types/d3-delaunay@6.0.4': {} - '@types/d3-dispatch@3.0.6': {} + '@types/d3-dispatch@3.0.7': {} '@types/d3-drag@3.0.7': dependencies: @@ -13121,7 +13538,7 @@ snapshots: '@types/d3-color': 3.1.3 '@types/d3-contour': 3.0.6 '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.6 + '@types/d3-dispatch': 3.0.7 '@types/d3-drag': 3.0.7 '@types/d3-dsv': 3.0.7 '@types/d3-ease': 3.0.2 @@ -13168,7 +13585,11 @@ snapshots: '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.17.57 + '@types/node': 24.2.1 + + '@types/glob@9.0.0': + dependencies: + glob: 11.0.3 '@types/hast@3.0.4': dependencies: @@ -13221,12 +13642,12 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 20.17.57 + '@types/node': 24.2.1 form-data: 4.0.4 '@types/node-ipc@9.2.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 24.2.1 '@types/node@12.20.55': {} @@ -13244,13 +13665,9 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/node@20.19.1': + '@types/node@24.2.1': dependencies: - undici-types: 6.21.0 - - '@types/node@22.15.29': - dependencies: - undici-types: 6.21.0 + undici-types: 7.10.0 '@types/prop-types@15.7.14': {} @@ -13281,11 +13698,11 @@ snapshots: '@types/stream-chain@2.1.0': dependencies: - '@types/node': 20.19.1 + '@types/node': 24.2.1 '@types/stream-json@1.7.8': dependencies: - '@types/node': 20.19.1 + '@types/node': 24.2.1 '@types/stream-chain': 2.1.0 '@types/string-similarity@4.0.2': {} @@ -13313,7 +13730,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.1 + '@types/node': 24.2.1 optional: true '@types/yargs-parser@21.0.3': {} @@ -13324,7 +13741,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.19.1 + '@types/node': 24.2.1 optional: true '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': @@ -13449,13 +13866,13 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -13486,7 +13903,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -13567,7 +13984,7 @@ snapshots: cockatiel: 3.2.1 commander: 12.1.0 form-data: 4.0.4 - glob: 11.0.2 + glob: 11.0.3 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 leven: 3.1.0 @@ -13899,7 +14316,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bluebird@3.4.7: {} + bluebird@3.7.2: {} body-parser@2.2.0: dependencies: @@ -14129,7 +14546,7 @@ snapshots: dependencies: devtools-protocol: 0.0.1452169 mitt: 3.0.1 - zod: 3.25.61 + zod: 3.25.76 ci-info@2.0.0: {} @@ -14364,17 +14781,17 @@ snapshots: csstype@3.1.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.32.0): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): dependencies: cose-base: 1.0.3 - cytoscape: 3.32.0 + cytoscape: 3.33.1 - cytoscape-fcose@2.2.0(cytoscape@3.32.0): + cytoscape-fcose@2.2.0(cytoscape@3.33.1): dependencies: cose-base: 2.2.0 - cytoscape: 3.32.0 + cytoscape: 3.33.1 - cytoscape@3.32.0: {} + cytoscape@3.33.1: {} d3-array@2.12.1: dependencies: @@ -14586,6 +15003,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -14651,6 +15072,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -14698,7 +15121,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 csstype: 3.1.3 dom-serializer@2.0.0: @@ -14713,7 +15136,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.5: + dompurify@3.2.6: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -14727,12 +15150,12 @@ snapshots: dotenv@16.5.0: {} - drizzle-kit@0.31.1: + drizzle-kit@0.31.4: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.5 - esbuild-register: 3.6.0(esbuild@0.25.5) + esbuild: 0.25.9 + esbuild-register: 3.6.0(esbuild@0.25.9) transitivePeerDependencies: - supports-color @@ -14817,6 +15240,20 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -14940,10 +15377,10 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-register@3.6.0(esbuild@0.25.5): + esbuild-register@3.6.0(esbuild@0.25.9): dependencies: debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.5 + esbuild: 0.25.9 transitivePeerDependencies: - supports-color @@ -14975,6 +15412,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -15204,7 +15670,7 @@ snapshots: jszip: 3.10.1 readable-stream: 3.6.2 saxes: 5.0.1 - tmp: 0.2.3 + tmp: 0.2.4 unzipper: 0.10.14 uuid: 8.3.2 @@ -15313,7 +15779,7 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.5: {} + exsolve@1.0.7: {} extend-shallow@2.0.1: dependencies: @@ -15663,6 +16129,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-uri@6.0.4: dependencies: basic-ftp: 5.0.5 @@ -15700,6 +16170,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15717,6 +16196,8 @@ snapshots: globals@16.1.0: {} + globals@16.3.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -16060,6 +16541,20 @@ snapshots: internmap@2.0.3: {} + ioredis@5.6.1: + dependencies: + '@ioredis/commands': 1.3.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1(supports-color@8.1.1) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -16179,7 +16674,7 @@ snapshots: is-it-type@5.1.2: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 globalthis: 1.0.4 is-map@2.0.3: {} @@ -16314,6 +16809,10 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -16345,7 +16844,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.19.1 + '@types/node': 24.2.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -16357,7 +16856,7 @@ snapshots: joycon@3.1.1: {} - js-base64@3.7.7: + js-base64@3.7.8: optional: true js-cookie@2.2.1: {} @@ -16505,10 +17004,10 @@ snapshots: kind-of@6.0.3: {} - knip@5.60.2(@types/node@22.15.29)(typescript@5.8.3): + knip@5.60.2(@types/node@24.2.1)(typescript@5.8.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 22.15.29 + '@types/node': 24.2.1 fast-glob: 3.3.3 formatly: 0.2.4 jiti: 2.4.2 @@ -16552,20 +17051,20 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libsql@0.5.13: + libsql@0.5.17: dependencies: '@neon-rs/load': 0.0.4 detect-libc: 2.0.2 optionalDependencies: - '@libsql/darwin-arm64': 0.5.13 - '@libsql/darwin-x64': 0.5.13 - '@libsql/linux-arm-gnueabihf': 0.5.13 - '@libsql/linux-arm-musleabihf': 0.5.13 - '@libsql/linux-arm64-gnu': 0.5.13 - '@libsql/linux-arm64-musl': 0.5.13 - '@libsql/linux-x64-gnu': 0.5.13 - '@libsql/linux-x64-musl': 0.5.13 - '@libsql/win32-x64-msvc': 0.5.13 + '@libsql/darwin-arm64': 0.5.17 + '@libsql/darwin-x64': 0.5.17 + '@libsql/linux-arm-gnueabihf': 0.5.17 + '@libsql/linux-arm-musleabihf': 0.5.17 + '@libsql/linux-arm64-gnu': 0.5.17 + '@libsql/linux-arm64-musl': 0.5.17 + '@libsql/linux-x64-gnu': 0.5.17 + '@libsql/linux-x64-musl': 0.5.17 + '@libsql/win32-x64-msvc': 0.5.17 optional: true lie@3.3.0: @@ -16670,7 +17169,7 @@ snapshots: dependencies: uc.micro: 2.1.0 - lint-staged@16.1.0: + lint-staged@16.1.2: dependencies: chalk: 5.4.1 commander: 14.0.0 @@ -16701,8 +17200,8 @@ snapshots: local-pkg@1.1.1: dependencies: mlly: 1.7.4 - pkg-types: 2.1.0 - quansync: 0.2.10 + pkg-types: 2.2.0 + quansync: 0.2.11 locate-path@5.0.0: dependencies: @@ -16730,6 +17229,8 @@ snapshots: lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} lodash.isequal@4.5.0: {} @@ -16832,12 +17333,12 @@ snapshots: dependencies: semver: 7.7.2 - mammoth@1.9.0: + mammoth@1.9.1: dependencies: '@xmldom/xmldom': 0.8.10 argparse: 1.0.10 base64-js: 1.5.1 - bluebird: 3.4.7 + bluebird: 3.7.2 dingbat-to-unicode: 1.0.1 jszip: 3.10.1 lop: 0.4.2 @@ -16858,7 +17359,7 @@ snapshots: markdown-table@3.0.4: {} - marked@15.0.11: {} + marked@16.2.0: {} math-intrinsics@1.1.0: {} @@ -17070,24 +17571,24 @@ snapshots: merge2@1.4.1: {} - mermaid@11.6.0: + mermaid@11.10.0: dependencies: '@braintree/sanitize-url': 7.1.1 '@iconify/utils': 2.3.0 - '@mermaid-js/parser': 0.4.0 + '@mermaid-js/parser': 0.6.2 '@types/d3': 7.4.3 - cytoscape: 3.32.0 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.32.0) - cytoscape-fcose: 2.2.0(cytoscape@3.32.0) + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.11 dayjs: 1.11.13 - dompurify: 3.2.5 + dompurify: 3.2.6 katex: 0.16.22 khroma: 2.1.0 lodash-es: 4.17.21 - marked: 15.0.11 + marked: 16.2.0 roughjs: 4.6.6 stylis: 4.3.6 ts-dedent: 2.2.0 @@ -17337,6 +17838,10 @@ snapshots: dependencies: brace-expansion: 4.0.1 + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 4.0.1 @@ -17452,6 +17957,14 @@ snapshots: netmask@2.0.2: {} + next-sitemap@4.2.3(next@15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + dependencies: + '@corex/deepmerge': 4.0.43 + '@next/env': 13.5.11 + fast-glob: 3.3.3 + minimist: 1.2.8 + next: 15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -17605,6 +18118,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + ollama@0.5.17: + dependencies: + whatwg-fetch: 3.6.20 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -17646,9 +18163,9 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.5.1(ws@8.18.2)(zod@3.25.61): + openai@5.5.1(ws@8.18.3)(zod@3.25.61): optionalDependencies: - ws: 8.18.2 + ws: 8.18.3 zod: 3.25.61 option@0.2.4: {} @@ -17902,10 +18419,10 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - pkg-types@2.1.0: + pkg-types@2.2.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.5 + exsolve: 1.0.7 pathe: 2.0.3 points-on-curve@0.2.0: {} @@ -18160,7 +18677,7 @@ snapshots: quansync@0.2.10: {} - querystringify@2.2.0: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -18291,7 +18808,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -18400,7 +18917,7 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 @@ -18420,6 +18937,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redis@5.5.5: dependencies: '@redis/bloom': 5.5.5(@redis/client@5.5.5) @@ -18581,7 +19104,7 @@ snapshots: rimraf@6.0.1: dependencies: - glob: 11.0.2 + glob: 11.0.3 package-json-from-dist: 1.0.1 robust-predicates@3.0.2: {} @@ -18633,7 +19156,7 @@ snapshots: rtl-css-js@1.16.1: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 run-applescript@7.0.0: {} @@ -18902,6 +19425,24 @@ snapshots: smol-toml@1.3.4: {} + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 @@ -18979,6 +19520,8 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} std-env@3.9.0: {} @@ -19341,6 +19884,8 @@ snapshots: tmp@0.2.3: {} + tmp@0.2.4: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -19569,7 +20114,7 @@ snapshots: undici-types@6.19.8: {} - undici-types@6.21.0: {} + undici-types@7.10.0: {} undici@6.21.3: {} @@ -19673,7 +20218,7 @@ snapshots: dependencies: big-integer: 1.6.52 binary: 0.3.0 - bluebird: 3.4.7 + bluebird: 3.7.2 buffer-indexof-polyfill: 1.0.2 duplexer2: 0.1.4 fstream: 1.0.12 @@ -19851,13 +20396,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.4(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -19904,7 +20449,7 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.4(picomatch@4.0.2) @@ -19913,7 +20458,7 @@ snapshots: rollup: 4.40.2 tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.15.29 + '@types/node': 24.2.1 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 @@ -20008,11 +20553,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -20030,12 +20575,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.15.29 + '@types/node': 24.2.1 '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -20107,6 +20652,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@4.0.0: {} whatwg-url@14.2.0: @@ -20219,8 +20766,13 @@ snapshots: wrappy@1.0.2: {} + ws@8.17.1: {} + ws@8.18.2: {} + ws@8.18.3: + optional: true + xml-name-validator@5.0.0: {} xml2js@0.5.0: @@ -20234,6 +20786,8 @@ snapshots: xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -20321,4 +20875,6 @@ snapshots: zod@3.25.61: {} + zod@3.25.76: {} + zwitch@2.0.4: {} diff --git a/scripts/link-packages.ts b/scripts/link-packages.ts new file mode 100644 index 0000000000..97aeebd6b5 --- /dev/null +++ b/scripts/link-packages.ts @@ -0,0 +1,340 @@ +import { spawn, execSync, type ChildProcess } from "child_process" +import * as path from "path" +import * as fs from "fs" +import { fileURLToPath } from "url" +import { glob } from "glob" + +// @ts-expect-error - TS1470: We only run this script with tsx so it will never +// compile to CJS and it's safe to ignore this tsc error. +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +interface PackageConfig { + readonly name: string + readonly sourcePath: string + readonly targetPaths: readonly string[] + readonly replacePath?: string + readonly npmPath: string + readonly watchCommand?: string + readonly watchOutput?: { + readonly start: string[] + readonly stop: string[] + } +} + +interface Config { + readonly packages: readonly PackageConfig[] +} + +interface WatcherResult { + child: ChildProcess +} + +interface NpmPackage { + name?: string + version?: string + type: "module" + dependencies: Record + main: string + module: string + types: string + exports: { + ".": { + types: string + import: string + require: { + types: string + default: string + } + } + } + files: string[] +} + +const config: Config = { + packages: [ + { + name: "@roo-code/cloud", + sourcePath: "../Roo-Code-Cloud/packages/sdk", + targetPaths: ["src/node_modules/@roo-code/cloud"] as const, + replacePath: "node_modules/.pnpm/@roo-code+cloud*", + npmPath: "npm", + watchCommand: "pnpm build:development:watch", + watchOutput: { + start: ["CLI Building", "CLI Change detected"], + stop: ["DTS ⚡️ Build success"], + }, + }, + ], +} as const + +const args = process.argv.slice(2) +const packageName = args.find((arg) => !arg.startsWith("--")) +const watchMode = !args.includes("--no-watch") +const unlink = args.includes("--unlink") + +const packages: readonly PackageConfig[] = packageName + ? config.packages.filter((p) => p.name === packageName) + : config.packages + +if (!packages.length) { + console.error(`Package '${packageName}' not found`) + process.exit(1) +} + +function pathExists(filePath: string): boolean { + try { + fs.accessSync(filePath) + return true + } catch { + return false + } +} + +function copyRecursiveSync(src: string, dest: string): void { + const exists = pathExists(src) + + if (!exists) { + return + } + + const stats = fs.statSync(src) + const isDirectory = stats.isDirectory() + + if (isDirectory) { + if (!pathExists(dest)) { + fs.mkdirSync(dest, { recursive: true }) + } + + const children = fs.readdirSync(src) + + children.forEach((childItemName) => { + copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)) + }) + } else { + fs.copyFileSync(src, dest) + } +} + +function generateNpmPackageJson(sourcePath: string, npmPath: string): string { + const npmDir = path.join(sourcePath, npmPath) + const npmPackagePath = path.join(npmDir, "package.json") + const npmMetadataPath = path.join(npmDir, "package.metadata.json") + const monorepoPackagePath = path.join(sourcePath, "package.json") + + if (pathExists(npmPackagePath)) { + return npmPackagePath + } + + if (!pathExists(npmMetadataPath)) { + throw new Error(`No package.metadata.json found in ${npmDir}`) + } + + const monorepoPackageContent = fs.readFileSync(monorepoPackagePath, "utf8") + + const monorepoPackage = JSON.parse(monorepoPackageContent) as { + dependencies?: Record + } + + const npmMetadataContent = fs.readFileSync(npmMetadataPath, "utf8") + const npmMetadata = JSON.parse(npmMetadataContent) as Partial + + const npmPackage: NpmPackage = { + ...npmMetadata, + type: "module", + dependencies: monorepoPackage.dependencies || {}, + main: "./dist/index.cjs", + module: "./dist/index.js", + types: "./dist/index.d.ts", + exports: { + ".": { + types: "./dist/index.d.ts", + import: "./dist/index.js", + require: { + types: "./dist/index.d.cts", + default: "./dist/index.cjs", + }, + }, + }, + files: ["dist"], + } + + fs.writeFileSync(npmPackagePath, JSON.stringify(npmPackage, null, 2) + "\n") + + return npmPackagePath +} + +function linkPackage(pkg: PackageConfig): void { + const sourcePath = path.resolve(__dirname, "..", pkg.sourcePath) + + if (!pathExists(sourcePath)) { + console.error(`❌ Source not found: ${sourcePath}`) + process.exit(1) + } + + generateNpmPackageJson(sourcePath, pkg.npmPath) + + for (const currentTargetPath of pkg.targetPaths) { + const targetPath = path.resolve(__dirname, "..", currentTargetPath) + + if (pathExists(targetPath)) { + fs.rmSync(targetPath, { recursive: true, force: true }) + } + + const parentDir = path.dirname(targetPath) + fs.mkdirSync(parentDir, { recursive: true }) + + const linkSource = pkg.npmPath ? path.join(sourcePath, pkg.npmPath) : sourcePath + copyRecursiveSync(linkSource, targetPath) + } +} + +function unlinkPackage(pkg: PackageConfig): void { + for (const currentTargetPath of pkg.targetPaths) { + const targetPath = path.resolve(__dirname, "..", currentTargetPath) + + if (pathExists(targetPath)) { + fs.rmSync(targetPath, { recursive: true, force: true }) + console.log(`🗑️ Removed ${pkg.name} from ${currentTargetPath}`) + } + } +} + +function startWatch(pkg: PackageConfig): WatcherResult { + if (!pkg.watchCommand) { + throw new Error(`Package ${pkg.name} has no watch command configured`) + } + + const commandParts = pkg.watchCommand.split(" ") + const [cmd, ...args] = commandParts + + if (!cmd) { + throw new Error(`Invalid watch command for ${pkg.name}`) + } + + console.log(`👀 Watching for changes to ${pkg.sourcePath} with ${cmd} ${args.join(" ")}`) + + const child = spawn(cmd, args, { + cwd: path.resolve(__dirname, "..", pkg.sourcePath), + stdio: "pipe", + shell: true, + }) + + let debounceTimer: NodeJS.Timeout | null = null + + const DEBOUNCE_DELAY = 500 + + if (child.stdout) { + child.stdout.on("data", (data: Buffer) => { + const output = data.toString() + + const isStarting = pkg.watchOutput?.start.some((start) => output.includes(start)) + + const isDone = pkg.watchOutput?.stop.some((stop) => output.includes(stop)) + + if (isStarting) { + console.log(`🔨 Building ${pkg.name}...`) + + if (debounceTimer) { + clearTimeout(debounceTimer) + debounceTimer = null + } + } + + if (isDone) { + console.log(`✅ Built ${pkg.name}`) + + if (debounceTimer) { + clearTimeout(debounceTimer) + } + + debounceTimer = setTimeout(() => { + linkPackage(pkg) + + console.log(`♻️ Copied ${pkg.name} to ${pkg.targetPaths.length} paths\n`) + + debounceTimer = null + }, DEBOUNCE_DELAY) + } + }) + } + + if (child.stderr) { + child.stderr.on("data", (data: Buffer) => { + console.log(`❌ "${data.toString()}"`) + }) + } + + return { child } +} + +function main(): void { + if (unlink) { + packages.forEach(unlinkPackage) + + console.log("\n📦 Restoring npm packages...") + + try { + execSync("pnpm install", { cwd: __dirname, stdio: "ignore" }) + console.log("✅ npm packages restored") + } catch (error) { + console.error(`❌ Failed to restore packages: ${error instanceof Error ? error.message : String(error)}`) + + console.log(" Run 'pnpm install' manually if needed") + } + } else { + packages.forEach((pkg) => { + linkPackage(pkg) + + if (pkg.replacePath) { + const replacePattern = path.resolve(__dirname, "..", pkg.replacePath) + + try { + const matchedPaths = glob.sync(replacePattern) + + if (matchedPaths.length > 0) { + matchedPaths.forEach((matchedPath: string) => { + if (pathExists(matchedPath)) { + fs.rmSync(matchedPath, { recursive: true, force: true }) + console.log(`🗑️ Removed ${pkg.name} from ${matchedPath}`) + } + }) + } else { + if (pathExists(replacePattern)) { + fs.rmSync(replacePattern, { recursive: true, force: true }) + console.log(`🗑️ Removed ${pkg.name} from ${replacePattern}`) + } + } + } catch (error) { + console.error( + `❌ Error processing replace path: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + }) + + if (watchMode) { + const packagesWithWatch = packages.filter( + (pkg): pkg is PackageConfig & { watchCommand: string } => pkg.watchCommand !== undefined, + ) + + const watchers = packagesWithWatch.map(startWatch) + + if (watchers.length > 0) { + process.on("SIGINT", () => { + console.log("\n👋 Stopping watchers...") + + watchers.forEach((w) => { + if (w.child) { + w.child.kill() + } + }) + + process.exit(0) + }) + } + } + } +} + +main() diff --git a/src/__tests__/command-integration.spec.ts b/src/__tests__/command-integration.spec.ts index e884325b68..66621dbb3a 100644 --- a/src/__tests__/command-integration.spec.ts +++ b/src/__tests__/command-integration.spec.ts @@ -15,7 +15,7 @@ describe("Command Integration Tests", () => { commands.forEach((command) => { expect(command.name).toBeDefined() expect(typeof command.name).toBe("string") - expect(command.source).toMatch(/^(project|global)$/) + expect(command.source).toMatch(/^(project|global|built-in)$/) expect(command.content).toBeDefined() expect(typeof command.content).toBe("string") }) @@ -43,7 +43,7 @@ describe("Command Integration Tests", () => { expect(loadedCommand).toBeDefined() expect(loadedCommand?.name).toBe(firstCommand.name) - expect(loadedCommand?.source).toMatch(/^(project|global)$/) + expect(loadedCommand?.source).toMatch(/^(project|global|built-in)$/) expect(loadedCommand?.content).toBeDefined() expect(typeof loadedCommand?.content).toBe("string") } diff --git a/src/__tests__/command-mentions.spec.ts b/src/__tests__/command-mentions.spec.ts index d4de0bbba7..b120f3720c 100644 --- a/src/__tests__/command-mentions.spec.ts +++ b/src/__tests__/command-mentions.spec.ts @@ -62,16 +62,31 @@ describe("Command Mentions", () => { }) it("should handle multiple commands in message", async () => { + const setupContent = "# Setup Environment\n\nRun the following commands:\n```bash\nnpm install\n```" + const deployContent = "# Deploy Environment\n\nRun the following commands:\n```bash\nnpm run deploy\n```" + mockGetCommand .mockResolvedValueOnce({ name: "setup", - content: "# Setup instructions", + content: setupContent, source: "project", filePath: "/project/.roo/commands/setup.md", }) .mockResolvedValueOnce({ name: "deploy", - content: "# Deploy instructions", + content: deployContent, + source: "project", + filePath: "/project/.roo/commands/deploy.md", + }) + .mockResolvedValueOnce({ + name: "setup", + content: setupContent, + source: "project", + filePath: "/project/.roo/commands/setup.md", + }) + .mockResolvedValueOnce({ + name: "deploy", + content: deployContent, source: "project", filePath: "/project/.roo/commands/deploy.md", }) @@ -82,33 +97,55 @@ describe("Command Mentions", () => { expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup") expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy") - expect(mockGetCommand).toHaveBeenCalledTimes(2) // Both commands called + expect(mockGetCommand).toHaveBeenCalledTimes(2) // Each unique command called once (optimized) expect(result).toContain(' ') - expect(result).toContain("# Setup instructions") + expect(result).toContain("# Setup Environment") expect(result).toContain(' ') - expect(result).toContain("# Deploy instructions") + expect(result).toContain("# Deploy Environment") }) - it("should handle non-existent command gracefully", async () => { + it("should leave non-existent commands unchanged", async () => { + mockGetCommand.mockReset() mockGetCommand.mockResolvedValue(undefined) const input = "/nonexistent command" const result = await callParseMentions(input) expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent") - expect(result).toContain(' ') - expect(result).toContain("Command 'nonexistent' not found") - expect(result).toContain(" ") + // The command should remain unchanged in the text + expect(result).toBe("/nonexistent command") + // Should not contain any command tags + expect(result).not.toContain('') + expect(result).not.toContain("Command 'nonexistent' not found") }) - it("should handle command loading errors", async () => { + it("should handle command loading errors during existence check", async () => { + mockGetCommand.mockReset() mockGetCommand.mockRejectedValue(new Error("Failed to load command")) const input = "/error-command test" const result = await callParseMentions(input) + // When getCommand throws an error during existence check, + // the command is treated as non-existent and left unchanged + expect(result).toBe("/error-command test") + expect(result).not.toContain(' ') + }) + + it("should handle command loading errors during processing", async () => { + // With optimization, command is loaded once and cached + mockGetCommand.mockResolvedValue({ + name: "error-command", + content: "# Error command", + source: "project", + filePath: "/project/.roo/commands/error-command.md", + }) + + const input = "/error-command test" + const result = await callParseMentions(input) + expect(result).toContain(' ') - expect(result).toContain("Error loading command") + expect(result).toContain("# Error command") expect(result).toContain(" ") }) @@ -246,13 +283,29 @@ npm install }) describe("command mention text transformation", () => { - it("should transform command mentions at start of message", async () => { + it("should transform existing command mentions at start of message", async () => { + mockGetCommand.mockResolvedValue({ + name: "setup", + content: "# Setup instructions", + source: "project", + filePath: "/project/.roo/commands/setup.md", + }) + const input = "/setup the project" const result = await callParseMentions(input) expect(result).toContain("Command 'setup' (see below for command content)") }) + it("should leave non-existent command mentions unchanged", async () => { + mockGetCommand.mockResolvedValue(undefined) + + const input = "/nonexistent the project" + const result = await callParseMentions(input) + + expect(result).toBe("/nonexistent the project") + }) + it("should process multiple commands in message", async () => { mockGetCommand .mockResolvedValueOnce({ diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index bd925b0e90..0534f24782 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -97,6 +97,9 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt await visibleProvider.removeClineFromStack() await visibleProvider.postStateToWebview() await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) + // Send focusInput action immediately after chatButtonClicked + // This ensures the focus happens after the view has switched + await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" }) }, mcpButtonClicked: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) @@ -237,7 +240,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omiteditor.viewColumn || 0)) // Check if there are any visible text editors, otherwise open a new group diff --git a/src/api/index.ts b/src/api/index.ts index a28cde55ea..7f76143e6f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,11 +8,11 @@ import { GlamaHandler, AnthropicHandler, AwsBedrockHandler, + CerebrasHandler, OpenRouterHandler, VertexHandler, AnthropicVertexHandler, OpenAiHandler, - OllamaHandler, LmStudioHandler, GeminiHandler, OpenAiNativeHandler, @@ -31,7 +31,16 @@ import { LiteLLMHandler, ClaudeCodeHandler, WatsonxAIHandler, + QwenCodeHandler, + SambaNovaHandler, + IOIntelligenceHandler, + DoubaoHandler, + ZAiHandler, + FireworksHandler, + RooHandler, + FeatherlessHandler, } from "./providers" +import { NativeOllamaHandler } from "./providers/native-ollama" export interface SingleCompletionHandler { completePrompt(prompt: string): Promise @@ -40,6 +49,13 @@ export interface SingleCompletionHandler { export interface ApiHandlerCreateMessageMetadata { mode?: string taskId: string + previousResponseId?: string + /** + * When true, the provider must NOT fall back to internal continuity state + * (e.g., lastResponseId) if previousResponseId is absent. + * Used to enforce "skip once" after a condense operation. + */ + suppressPreviousResponseId?: boolean } export interface ApiHandler { @@ -83,7 +99,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { case "openai": return new OpenAiHandler(options) case "ollama": - return new OllamaHandler(options) + return new NativeOllamaHandler(options) case "lmstudio": return new LmStudioHandler(options) case "gemini": @@ -92,6 +108,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "doubao": + return new DoubaoHandler(options) + case "qwen-code": + return new QwenCodeHandler(options) case "moonshot": return new MoonshotHandler(options) case "vscode-lm": @@ -118,6 +138,22 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new LiteLLMHandler(options) case "watsonx": return new WatsonxAIHandler(options) + case "cerebras": + return new CerebrasHandler(options) + case "sambanova": + return new SambaNovaHandler(options) + case "zai": + return new ZAiHandler(options) + case "fireworks": + return new FireworksHandler(options) + case "io-intelligence": + return new IOIntelligenceHandler(options) + case "roo": + // Never throw exceptions from provider constructors + // The provider-proxy server will handle authentication and return appropriate error codes + return new RooHandler(options) + case "featherless": + return new FeatherlessHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index ad0ae2bdb5..8df495ca9f 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -25,6 +25,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { import { AwsBedrockHandler } from "../bedrock" import { ConverseStreamCommand, BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime" +import { BEDROCK_CLAUDE_SONNET_4_MODEL_ID } from "@roo-code/types" import type { Anthropic } from "@anthropic-ai/sdk" @@ -564,4 +565,188 @@ describe("AwsBedrockHandler", () => { expect(typeof model.info.supportsPromptCache).toBe("boolean") }) }) + + describe("1M context beta feature", () => { + it("should enable 1M context window when awsBedrock1MContext is true for Claude Sonnet 4", () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: true, + }) + + const model = handler.getModel() + + // Should have 1M context window when enabled + expect(model.info.contextWindow).toBe(1_000_000) + }) + + it("should use default context window when awsBedrock1MContext is false for Claude Sonnet 4", () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: false, + }) + + const model = handler.getModel() + + // Should use default context window (200k) + expect(model.info.contextWindow).toBe(200_000) + }) + + it("should not affect context window for non-Claude Sonnet 4 models", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: true, + }) + + const model = handler.getModel() + + // Should use default context window for non-Sonnet 4 models + expect(model.info.contextWindow).toBe(200_000) + }) + + it("should include anthropic_beta parameter when 1M context is enabled", async () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: true, + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Test message", + }, + ] + + const generator = handler.createMessage("", messages) + await generator.next() // Start the generator + + // Verify the command was created with the right payload + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any + + // Should include anthropic_beta in additionalModelRequestFields + expect(commandArg.additionalModelRequestFields).toBeDefined() + expect(commandArg.additionalModelRequestFields.anthropic_beta).toEqual(["context-1m-2025-08-07"]) + // Should not include anthropic_version since thinking is not enabled + expect(commandArg.additionalModelRequestFields.anthropic_version).toBeUndefined() + }) + + it("should not include anthropic_beta parameter when 1M context is disabled", async () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: false, + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Test message", + }, + ] + + const generator = handler.createMessage("", messages) + await generator.next() // Start the generator + + // Verify the command was created with the right payload + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any + + // Should not include anthropic_beta in additionalModelRequestFields + expect(commandArg.additionalModelRequestFields).toBeUndefined() + }) + + it("should not include anthropic_beta parameter for non-Claude Sonnet 4 models", async () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsBedrock1MContext: true, + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Test message", + }, + ] + + const generator = handler.createMessage("", messages) + await generator.next() // Start the generator + + // Verify the command was created with the right payload + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any + + // Should not include anthropic_beta for non-Sonnet 4 models + expect(commandArg.additionalModelRequestFields).toBeUndefined() + }) + + it("should enable 1M context window with cross-region inference for Claude Sonnet 4", () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseCrossRegionInference: true, + awsBedrock1MContext: true, + }) + + const model = handler.getModel() + + // Should have 1M context window even with cross-region prefix + expect(model.info.contextWindow).toBe(1_000_000) + // Model ID should have cross-region prefix + expect(model.id).toBe(`us.${BEDROCK_CLAUDE_SONNET_4_MODEL_ID}`) + }) + + it("should include anthropic_beta parameter with cross-region inference for Claude Sonnet 4", async () => { + const handler = new AwsBedrockHandler({ + apiModelId: BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseCrossRegionInference: true, + awsBedrock1MContext: true, + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Test message", + }, + ] + + const generator = handler.createMessage("", messages) + await generator.next() // Start the generator + + // Verify the command was created with the right payload + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[ + mockConverseStreamCommand.mock.calls.length - 1 + ][0] as any + + // Should include anthropic_beta in additionalModelRequestFields + expect(commandArg.additionalModelRequestFields).toBeDefined() + expect(commandArg.additionalModelRequestFields.anthropic_beta).toEqual(["context-1m-2025-08-07"]) + // Should not include anthropic_version since thinking is not enabled + expect(commandArg.additionalModelRequestFields.anthropic_version).toBeUndefined() + // Model ID should have cross-region prefix + expect(commandArg.modelId).toBe(`us.${BEDROCK_CLAUDE_SONNET_4_MODEL_ID}`) + }) + }) }) diff --git a/src/api/providers/__tests__/cerebras.spec.ts b/src/api/providers/__tests__/cerebras.spec.ts new file mode 100644 index 0000000000..2b7668435f --- /dev/null +++ b/src/api/providers/__tests__/cerebras.spec.ts @@ -0,0 +1,178 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" + +// Mock i18n +vi.mock("../../i18n", () => ({ + t: vi.fn((key: string, params?: Record ) => { + // Return a simplified mock translation for testing + if (key.startsWith("common:errors.cerebras.")) { + return `Mocked: ${key.replace("common:errors.cerebras.", "")}` + } + return key + }), +})) + +// Mock DEFAULT_HEADERS +vi.mock("../constants", () => ({ + DEFAULT_HEADERS: { + "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", + "X-Title": "Roo Code", + "User-Agent": "RooCode/1.0.0", + }, +})) + +import { CerebrasHandler } from "../cerebras" +import { cerebrasModels, type CerebrasModelId } from "@roo-code/types" + +// Mock fetch globally +global.fetch = vi.fn() + +describe("CerebrasHandler", () => { + let handler: CerebrasHandler + const mockOptions = { + cerebrasApiKey: "test-api-key", + apiModelId: "llama-3.3-70b" as CerebrasModelId, + } + + beforeEach(() => { + vi.clearAllMocks() + handler = new CerebrasHandler(mockOptions) + }) + + describe("constructor", () => { + it("should throw error when API key is missing", () => { + expect(() => new CerebrasHandler({ cerebrasApiKey: "" })).toThrow("Cerebras API key is required") + }) + + it("should initialize with valid API key", () => { + expect(() => new CerebrasHandler(mockOptions)).not.toThrow() + }) + }) + + describe("getModel", () => { + it("should return correct model info", () => { + const { id, info } = handler.getModel() + expect(id).toBe("llama-3.3-70b") + expect(info).toEqual(cerebrasModels["llama-3.3-70b"]) + }) + + it("should fallback to default model when apiModelId is not provided", () => { + const handlerWithoutModel = new CerebrasHandler({ cerebrasApiKey: "test" }) + const { id } = handlerWithoutModel.getModel() + expect(id).toBe("qwen-3-coder-480b") // cerebrasDefaultModelId (routed) + }) + }) + + describe("message conversion", () => { + it("should strip thinking tokens from assistant messages", () => { + // This would test the stripThinkingTokens function + // Implementation details would test the regex functionality + }) + + it("should flatten complex message content to strings", () => { + // This would test the flattenMessageContent function + // Test various content types: strings, arrays, image objects + }) + + it("should convert OpenAI messages to Cerebras format", () => { + // This would test the convertToCerebrasMessages function + // Ensure all messages have string content and proper role/content structure + }) + }) + + describe("createMessage", () => { + it("should make correct API request", async () => { + // Mock successful API response + const mockResponse = { + ok: true, + body: { + getReader: () => ({ + read: vi.fn().mockResolvedValueOnce({ done: true, value: new Uint8Array() }), + releaseLock: vi.fn(), + }), + }, + } + vi.mocked(fetch).mockResolvedValueOnce(mockResponse as any) + + const generator = handler.createMessage("System prompt", []) + await generator.next() // Actually start the generator to trigger the fetch call + + // Test that fetch was called with correct parameters + expect(fetch).toHaveBeenCalledWith( + "https://api.cerebras.ai/v1/chat/completions", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + Authorization: "Bearer test-api-key", + "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", + "X-Title": "Roo Code", + "User-Agent": "RooCode/1.0.0", + }), + }), + ) + }) + + it("should handle API errors properly", async () => { + const mockErrorResponse = { + ok: false, + status: 400, + text: () => Promise.resolve('{"error": {"message": "Bad Request"}}'), + } + vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any) + + const generator = handler.createMessage("System prompt", []) + // Since the mock isn't working, let's just check that an error is thrown + await expect(generator.next()).rejects.toThrow() + }) + + it("should parse streaming responses correctly", async () => { + // Test streaming response parsing + // Mock ReadableStream with various data chunks + // Verify thinking token extraction and usage tracking + }) + + it("should handle temperature clamping", async () => { + const handlerWithTemp = new CerebrasHandler({ + ...mockOptions, + modelTemperature: 2.0, // Above Cerebras max of 1.5 + }) + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + body: { getReader: () => ({ read: () => Promise.resolve({ done: true }), releaseLock: vi.fn() }) }, + } as any) + + await handlerWithTemp.createMessage("test", []).next() + + const requestBody = JSON.parse(vi.mocked(fetch).mock.calls[0][1]?.body as string) + expect(requestBody.temperature).toBe(1.5) // Should be clamped + }) + }) + + describe("completePrompt", () => { + it("should handle non-streaming completion", async () => { + const mockResponse = { + ok: true, + json: () => + Promise.resolve({ + choices: [{ message: { content: "Test response" } }], + }), + } + vi.mocked(fetch).mockResolvedValueOnce(mockResponse as any) + + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("Test response") + }) + }) + + describe("token usage and cost calculation", () => { + it("should track token usage properly", () => { + // Test that lastUsage is updated correctly + // Test getApiCost returns calculated cost based on actual usage + }) + + it("should provide usage estimates when API doesn't return usage", () => { + // Test fallback token estimation logic + }) + }) +}) diff --git a/src/api/providers/__tests__/chutes.spec.ts b/src/api/providers/__tests__/chutes.spec.ts index 35cb183dae..7a6b1aaa70 100644 --- a/src/api/providers/__tests__/chutes.spec.ts +++ b/src/api/providers/__tests__/chutes.spec.ts @@ -163,6 +163,28 @@ describe("ChutesHandler", () => { expect(model.info).toEqual(expect.objectContaining(chutesModels[testModelId])) }) + it("should return DeepSeek V3.1 model with correct configuration", () => { + const testModelId: ChutesModelId = "deepseek-ai/DeepSeek-V3.1" + const handlerWithModel = new ChutesHandler({ + apiModelId: testModelId, + chutesApiKey: "test-chutes-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 163840, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek V3.1 model.", + temperature: 0.5, // Non-R1 DeepSeek models use default temperature + }), + ) + }) + it("should return Qwen3-235B-A22B-Instruct-2507 model with correct configuration", () => { const testModelId: ChutesModelId = "Qwen/Qwen3-235B-A22B-Instruct-2507" const handlerWithModel = new ChutesHandler({ @@ -208,6 +230,73 @@ describe("ChutesHandler", () => { ) }) + it("should return zai-org/GLM-4.5-FP8 model with correct configuration", () => { + const testModelId: ChutesModelId = "zai-org/GLM-4.5-FP8" + const handlerWithModel = new ChutesHandler({ + apiModelId: testModelId, + chutesApiKey: "test-chutes-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "GLM-4.5-FP8 model with 128k token context window, optimized for agent-based applications with MoE architecture.", + temperature: 0.5, // Default temperature for non-DeepSeek models + }), + ) + }) + + it("should return Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8 model with correct configuration", () => { + const testModelId: ChutesModelId = "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8" + const handlerWithModel = new ChutesHandler({ + apiModelId: testModelId, + chutesApiKey: "test-chutes-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 262144, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.", + temperature: 0.5, // Default temperature for non-DeepSeek models + }), + ) + }) + + it("should return moonshotai/Kimi-K2-Instruct-75k model with correct configuration", () => { + const testModelId: ChutesModelId = "moonshotai/Kimi-K2-Instruct-75k" + const handlerWithModel = new ChutesHandler({ + apiModelId: testModelId, + chutesApiKey: "test-chutes-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 75000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1481, + outputPrice: 0.5926, + description: "Moonshot AI Kimi K2 Instruct model with 75k context window.", + temperature: 0.5, // Default temperature for non-DeepSeek models + }), + ) + }) + it("completePrompt method should return text from Chutes API", async () => { const expectedResponse = "This is a test response from Chutes" mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) @@ -327,11 +416,11 @@ describe("ChutesHandler", () => { expect.objectContaining({ model: modelId, max_tokens: modelInfo.maxTokens, - temperature: 0.5, messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), stream: true, stream_options: { include_usage: true }, }), + undefined, ) }) diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 175a5bc44b..50cabfa922 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -154,12 +154,26 @@ describe("DeepSeekHandler", () => { const model = handler.getModel() expect(model.id).toBe(mockOptions.apiModelId) expect(model.info).toBeDefined() - expect(model.info.maxTokens).toBe(8192) - expect(model.info.contextWindow).toBe(64_000) + expect(model.info.maxTokens).toBe(8192) // deepseek-chat has 8K max + expect(model.info.contextWindow).toBe(128_000) expect(model.info.supportsImages).toBe(false) expect(model.info.supportsPromptCache).toBe(true) // Should be true now }) + it("should return correct model info for deepseek-reasoner", () => { + const handlerWithReasoner = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-reasoner", + }) + const model = handlerWithReasoner.getModel() + expect(model.id).toBe("deepseek-reasoner") + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(65536) // deepseek-reasoner has 64K max + expect(model.info.contextWindow).toBe(128_000) + expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsPromptCache).toBe(true) + }) + it("should return provided model ID with default model info if model does not exist", () => { const handlerWithInvalidModel = new DeepSeekHandler({ ...mockOptions, diff --git a/src/api/providers/__tests__/featherless.spec.ts b/src/api/providers/__tests__/featherless.spec.ts new file mode 100644 index 0000000000..b0b4c01b86 --- /dev/null +++ b/src/api/providers/__tests__/featherless.spec.ts @@ -0,0 +1,286 @@ +// npx vitest run api/providers/__tests__/featherless.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + type FeatherlessModelId, + featherlessDefaultModelId, + featherlessModels, + DEEP_SEEK_DEFAULT_TEMPERATURE, +} from "@roo-code/types" + +import { FeatherlessHandler } from "../featherless" + +// Create mock functions +const mockCreate = vi.fn() + +// Mock OpenAI module +vi.mock("openai", () => ({ + default: vi.fn(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), +})) + +describe("FeatherlessHandler", () => { + let handler: FeatherlessHandler + + beforeEach(() => { + vi.clearAllMocks() + // Set up default mock implementation + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + })) + handler = new FeatherlessHandler({ featherlessApiKey: "test-key" }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should use the correct Featherless base URL", () => { + new FeatherlessHandler({ featherlessApiKey: "test-featherless-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.featherless.ai/v1" })) + }) + + it("should use the provided API key", () => { + const featherlessApiKey = "test-featherless-api-key" + new FeatherlessHandler({ featherlessApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: featherlessApiKey })) + }) + + it("should handle DeepSeek R1 reasoning format", async () => { + // Override the mock for this specific test + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: " Thinking..." }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: " Hello" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } + }, + })) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + vi.spyOn(handler, "getModel").mockReturnValue({ + id: "deepseek-ai/DeepSeek-R1-0528", + info: { maxTokens: 1024, temperature: 0.7 }, + } as any) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "reasoning", text: "Thinking..." }, + { type: "text", text: "Hello" }, + { type: "usage", inputTokens: 10, outputTokens: 5 }, + ]) + }) + + it("should fall back to base provider for non-DeepSeek models", async () => { + // Use default mock implementation which returns text content + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + vi.spyOn(handler, "getModel").mockReturnValue({ + id: "some-other-model", + info: { maxTokens: 1024, temperature: 0.7 }, + } as any) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "text", text: "Test response" }, + { type: "usage", inputTokens: 10, outputTokens: 5 }, + ]) + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(featherlessDefaultModelId) + expect(model.info).toEqual(expect.objectContaining(featherlessModels[featherlessDefaultModelId])) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(expect.objectContaining(featherlessModels[testModelId])) + }) + + it("completePrompt method should return text from Featherless API", async () => { + const expectedResponse = "This is a test response from Featherless" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Featherless API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Featherless completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Featherless stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Featherless client for DeepSeek R1", async () => { + const modelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + + // Clear previous mocks and set up new implementation + mockCreate.mockClear() + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + // Empty stream for this test + }, + })) + + const handlerWithModel = new FeatherlessHandler({ + apiModelId: modelId, + featherlessApiKey: "test-featherless-api-key", + }) + + const systemPrompt = "Test system prompt for Featherless" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Featherless" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + messages: [ + { + role: "user", + content: `${systemPrompt}\n${messages[0].content}`, + }, + ], + }), + ) + }) + + it("should apply DeepSeek default temperature for R1 models", () => { + const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.info.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE) + }) + + it("should use default temperature for non-DeepSeek models", () => { + const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.info.temperature).toBe(0.5) + }) +}) diff --git a/src/api/providers/__tests__/fireworks.spec.ts b/src/api/providers/__tests__/fireworks.spec.ts new file mode 100644 index 0000000000..ed1e119a99 --- /dev/null +++ b/src/api/providers/__tests__/fireworks.spec.ts @@ -0,0 +1,460 @@ +// npx vitest run api/providers/__tests__/fireworks.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { type FireworksModelId, fireworksDefaultModelId, fireworksModels } from "@roo-code/types" + +import { FireworksHandler } from "../fireworks" + +// Create mock functions +const mockCreate = vi.fn() + +// Mock OpenAI module +vi.mock("openai", () => ({ + default: vi.fn(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), +})) + +describe("FireworksHandler", () => { + let handler: FireworksHandler + + beforeEach(() => { + vi.clearAllMocks() + // Set up default mock implementation + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + })) + handler = new FireworksHandler({ fireworksApiKey: "test-key" }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should use the correct Fireworks base URL", () => { + new FireworksHandler({ fireworksApiKey: "test-fireworks-api-key" }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://api.fireworks.ai/inference/v1" }), + ) + }) + + it("should use the provided API key", () => { + const fireworksApiKey = "test-fireworks-api-key" + new FireworksHandler({ fireworksApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: fireworksApiKey })) + }) + + it("should throw error when API key is not provided", () => { + expect(() => new FireworksHandler({})).toThrow("API key is required") + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(fireworksDefaultModelId) + expect(model.info).toEqual(expect.objectContaining(fireworksModels[fireworksDefaultModelId])) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(expect.objectContaining(fireworksModels[testModelId])) + }) + + it("should return Kimi K2 Instruct model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.6, + outputPrice: 2.5, + description: expect.stringContaining("Kimi K2 is a state-of-the-art mixture-of-experts"), + }), + ) + }) + + it("should return Qwen3 235B model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 256000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.22, + outputPrice: 0.88, + description: + "Latest Qwen3 thinking model, competitive against the best closed source models in Jul 2025.", + }), + ) + }) + + it("should return DeepSeek R1 model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/deepseek-r1-0528" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 20480, + contextWindow: 160000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 3, + outputPrice: 8, + description: expect.stringContaining("05/28 updated checkpoint of Deepseek R1"), + }), + ) + }) + + it("should return DeepSeek V3 model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/deepseek-v3" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.9, + outputPrice: 0.9, + description: expect.stringContaining("strong Mixture-of-Experts (MoE) language model"), + }), + ) + }) + + it("should return DeepSeek V3.1 model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/deepseek-v3p1" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 163840, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.56, + outputPrice: 1.68, + description: expect.stringContaining("DeepSeek v3.1 is an improved version"), + }), + ) + }) + + it("should return GLM-4.5 model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/glm-4p5" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.55, + outputPrice: 2.19, + description: expect.stringContaining("Z.ai GLM-4.5 with 355B total parameters"), + }), + ) + }) + + it("should return GLM-4.5-Air model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/glm-4p5-air" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.55, + outputPrice: 2.19, + description: expect.stringContaining("Z.ai GLM-4.5-Air with 106B total parameters"), + }), + ) + }) + + it("should return gpt-oss-20b model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/gpt-oss-20b" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.07, + outputPrice: 0.3, + description: expect.stringContaining("OpenAI gpt-oss-20b: Compact model for local/edge deployments"), + }), + ) + }) + + it("should return gpt-oss-120b model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/gpt-oss-120b" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + description: expect.stringContaining("OpenAI gpt-oss-120b: Production-grade, general-purpose model"), + }), + ) + }) + + it("completePrompt method should return text from Fireworks API", async () => { + const expectedResponse = "This is a test response from Fireworks" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Fireworks API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Fireworks completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Fireworks stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Fireworks client", async () => { + const modelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct" + const modelInfo = fireworksModels[modelId] + const handlerWithModel = new FireworksHandler({ + apiModelId: modelId, + fireworksApiKey: "test-fireworks-api-key", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for Fireworks" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Fireworks" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + undefined, + ) + }) + + it("should use default temperature of 0.5", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + // The temperature is set in the constructor as defaultTemperature: 0.5 + // This test verifies the handler is configured with the correct default temperature + expect(handlerWithModel).toBeDefined() + }) + + it("should handle empty response in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe("") + }) + + it("should handle missing choices in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ choices: [] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe("") + }) + + it("createMessage should handle stream with multiple chunks", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Hello" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: " world" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 10, + total_tokens: 15, + }, + } + }, + })) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "text", text: "Hello" }, + { type: "text", text: " world" }, + { type: "usage", inputTokens: 5, outputTokens: 10 }, + ]) + }) +}) diff --git a/src/api/providers/__tests__/groq.spec.ts b/src/api/providers/__tests__/groq.spec.ts index 72a834b21d..52846617f4 100644 --- a/src/api/providers/__tests__/groq.spec.ts +++ b/src/api/providers/__tests__/groq.spec.ts @@ -108,13 +108,63 @@ describe("GroqHandler", () => { const firstChunk = await stream.next() expect(firstChunk.done).toBe(false) - expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + expect(firstChunk.value).toMatchObject({ + type: "usage", + inputTokens: 10, + outputTokens: 20, + cacheWriteTokens: 0, + cacheReadTokens: 0, + }) + // Check that totalCost is a number (we don't need to test the exact value as that's tested in cost.spec.ts) + expect(typeof firstChunk.value.totalCost).toBe("number") + }) + + it("createMessage should handle cached tokens in usage data", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + choices: [{ delta: {} }], + usage: { + prompt_tokens: 100, + completion_tokens: 50, + prompt_tokens_details: { + cached_tokens: 30, + }, + }, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toMatchObject({ + type: "usage", + inputTokens: 70, // 100 total - 30 cached + outputTokens: 50, + cacheWriteTokens: 0, + cacheReadTokens: 30, + }) + expect(typeof firstChunk.value.totalCost).toBe("number") }) it("createMessage should pass correct parameters to Groq client", async () => { const modelId: GroqModelId = "llama-3.1-8b-instant" const modelInfo = groqModels[modelId] - const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" }) + const handlerWithModel = new GroqHandler({ + apiModelId: modelId, + groqApiKey: "test-groq-api-key", + modelTemperature: 0.5, // Explicitly set temperature for this test + }) mockCreate.mockImplementationOnce(() => { return { @@ -141,6 +191,80 @@ describe("GroqHandler", () => { stream: true, stream_options: { include_usage: true }, }), + undefined, + ) + }) + + it("should omit temperature when modelTemperature is undefined", async () => { + const modelId: GroqModelId = "llama-3.1-8b-instant" + const handlerWithoutTemp = new GroqHandler({ + apiModelId: modelId, + groqApiKey: "test-groq-api-key", + // modelTemperature is not set + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] + + const messageGenerator = handlerWithoutTemp.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + }), + undefined, + ) + + // Verify temperature is NOT included + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("temperature") + }) + + it("should include temperature when modelTemperature is explicitly set", async () => { + const modelId: GroqModelId = "llama-3.1-8b-instant" + const handlerWithTemp = new GroqHandler({ + apiModelId: modelId, + groqApiKey: "test-groq-api-key", + modelTemperature: 0.7, + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] + + const messageGenerator = handlerWithTemp.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + temperature: 0.7, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + }), + undefined, ) }) }) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts new file mode 100644 index 0000000000..56baf711cd --- /dev/null +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -0,0 +1,303 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { IOIntelligenceHandler } from "../io-intelligence" +import type { ApiHandlerOptions } from "../../../shared/api" +import { Anthropic } from "@anthropic-ai/sdk" + +const mockCreate = vi.fn() + +// Mock OpenAI +vi.mock("openai", () => ({ + default: class MockOpenAI { + baseURL: string + apiKey: string + chat = { + completions: { + create: vi.fn(), + }, + } + constructor(options: any) { + this.baseURL = options.baseURL + this.apiKey = options.apiKey + this.chat.completions.create = mockCreate + } + }, +})) + +// Mock the fetcher functions +vi.mock("../fetchers/io-intelligence", () => ({ + getIOIntelligenceModels: vi.fn(), + getCachedIOIntelligenceModels: vi.fn(() => ({ + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + maxTokens: 8192, + contextWindow: 430000, + description: "Llama 4 Maverick 17B model", + supportsImages: true, + supportsPromptCache: false, + }, + "deepseek-ai/DeepSeek-R1-0528": { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + description: "DeepSeek R1 reasoning model", + }, + "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { + maxTokens: 4096, + contextWindow: 106000, + supportsImages: false, + supportsPromptCache: false, + description: "Qwen3 Coder 480B specialized for coding", + }, + "openai/gpt-oss-120b": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + description: "OpenAI GPT-OSS 120B model", + }, + })), +})) + +// Mock constants +vi.mock("../constants", () => ({ + DEFAULT_HEADERS: { "User-Agent": "roo-cline" }, +})) + +// Mock transform functions +vi.mock("../../transform/openai-format", () => ({ + convertToOpenAiMessages: vi.fn((messages) => messages), +})) + +describe("IOIntelligenceHandler", () => { + let handler: IOIntelligenceHandler + let mockOptions: ApiHandlerOptions + + beforeEach(() => { + vi.clearAllMocks() + mockOptions = { + ioIntelligenceApiKey: "test-api-key", + apiModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", + modelTemperature: 0.7, + includeMaxTokens: false, + modelMaxTokens: undefined, + } as ApiHandlerOptions + + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + })) + handler = new IOIntelligenceHandler(mockOptions) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should create OpenAI client with correct configuration", () => { + const ioIntelligenceApiKey = "test-io-intelligence-api-key" + const handler = new IOIntelligenceHandler({ ioIntelligenceApiKey }) + // Verify that the handler was created successfully + expect(handler).toBeInstanceOf(IOIntelligenceHandler) + expect(handler["client"]).toBeDefined() + // Verify the client has the expected properties + expect(handler["client"].baseURL).toBe("https://api.intelligence.io.solutions/api/v1") + expect(handler["client"].apiKey).toBe(ioIntelligenceApiKey) + }) + + it("should initialize with correct configuration", () => { + expect(handler).toBeInstanceOf(IOIntelligenceHandler) + expect(handler["client"]).toBeDefined() + expect(handler["options"]).toEqual({ + ...mockOptions, + apiKey: mockOptions.ioIntelligenceApiKey, + }) + }) + + it("should throw error when API key is missing", () => { + const optionsWithoutKey = { ...mockOptions } + delete optionsWithoutKey.ioIntelligenceApiKey + + expect(() => new IOIntelligenceHandler(optionsWithoutKey)).toThrow("IO Intelligence API key is required") + }) + + it("should handle streaming response correctly", async () => { + const mockStream = [ + { + choices: [{ delta: { content: "Hello" } }], + usage: null, + }, + { + choices: [{ delta: { content: " world" } }], + usage: null, + }, + { + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }, + ] + + mockCreate.mockResolvedValue({ + [Symbol.asyncIterator]: async function* () { + for (const chunk of mockStream) { + yield chunk + } + }, + }) + + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + + const stream = handler.createMessage("System prompt", messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(3) + expect(results[0]).toEqual({ type: "text", text: "Hello" }) + expect(results[1]).toEqual({ type: "text", text: " world" }) + expect(results[2]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 5, + }) + }) + + it("completePrompt method should return text from IO Intelligence API", async () => { + const expectedResponse = "This is a test response from IO Intelligence" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "IO Intelligence API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `IO Intelligence completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from IO Intelligence stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("should return model info from cache when available", () => { + const model = handler.getModel() + expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8") + expect(model.info).toEqual({ + maxTokens: 8192, + contextWindow: 430000, + description: "Llama 4 Maverick 17B model", + supportsImages: true, + supportsPromptCache: false, + }) + }) + + it("should return fallback model info when not in cache", () => { + const handlerWithUnknownModel = new IOIntelligenceHandler({ + ...mockOptions, + apiModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", + }) + const model = handlerWithUnknownModel.getModel() + expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8") + expect(model.info).toEqual({ + maxTokens: 8192, + contextWindow: 430000, + description: "Llama 4 Maverick 17B model", + supportsImages: true, + supportsPromptCache: false, + }) + }) + + it("should use default model when no model is specified", () => { + const handlerWithoutModel = new IOIntelligenceHandler({ + ...mockOptions, + apiModelId: undefined, + }) + const model = handlerWithoutModel.getModel() + expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8") + }) + + it("should handle empty response from completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [{ message: { content: null } }], + }) + + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) + + it("should handle missing choices in completePrompt response", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [], + }) + + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) +}) diff --git a/src/api/providers/__tests__/lm-studio-timeout.spec.ts b/src/api/providers/__tests__/lm-studio-timeout.spec.ts new file mode 100644 index 0000000000..659fcaaf67 --- /dev/null +++ b/src/api/providers/__tests__/lm-studio-timeout.spec.ts @@ -0,0 +1,91 @@ +// npx vitest run api/providers/__tests__/lm-studio-timeout.spec.ts + +import { LmStudioHandler } from "../lm-studio" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock the timeout config utility +vitest.mock("../utils/timeout-config", () => ({ + getApiRequestTimeout: vitest.fn(), +})) + +import { getApiRequestTimeout } from "../utils/timeout-config" + +// Mock OpenAI +const mockOpenAIConstructor = vitest.fn() +vitest.mock("openai", () => { + return { + __esModule: true, + default: vitest.fn().mockImplementation((config) => { + mockOpenAIConstructor(config) + return { + chat: { + completions: { + create: vitest.fn(), + }, + }, + } + }), + } +}) + +describe("LmStudioHandler timeout configuration", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should use default timeout of 600 seconds when no configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + lmStudioModelId: "llama2", + lmStudioBaseUrl: "http://localhost:1234", + } + + new LmStudioHandler(options) + + expect(getApiRequestTimeout).toHaveBeenCalled() + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "http://localhost:1234/v1", + apiKey: "noop", + timeout: 600000, // 600 seconds in milliseconds + }), + ) + }) + + it("should use custom timeout when configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(1200000) // 20 minutes + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + lmStudioModelId: "llama2", + lmStudioBaseUrl: "http://localhost:1234", + } + + new LmStudioHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 1200000, // 1200 seconds in milliseconds + }), + ) + }) + + it("should handle zero timeout (no timeout)", () => { + ;(getApiRequestTimeout as any).mockReturnValue(0) + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + lmStudioModelId: "llama2", + } + + new LmStudioHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 0, // No timeout + }), + ) + }) +}) diff --git a/src/api/providers/__tests__/mistral.spec.ts b/src/api/providers/__tests__/mistral.spec.ts index 73861ecdc0..ff3c5d3d8b 100644 --- a/src/api/providers/__tests__/mistral.spec.ts +++ b/src/api/providers/__tests__/mistral.spec.ts @@ -1,5 +1,6 @@ // Mock Mistral client - must come before other imports const mockCreate = vi.fn() +const mockComplete = vi.fn() vi.mock("@mistralai/mistralai", () => { return { Mistral: vi.fn().mockImplementation(() => ({ @@ -21,6 +22,17 @@ vi.mock("@mistralai/mistralai", () => { } return stream }), + complete: mockComplete.mockImplementation(async (_options) => { + return { + choices: [ + { + message: { + content: "Test response", + }, + }, + ], + } + }), }, })), } @@ -29,7 +41,7 @@ vi.mock("@mistralai/mistralai", () => { import type { Anthropic } from "@anthropic-ai/sdk" import { MistralHandler } from "../mistral" import type { ApiHandlerOptions } from "../../../shared/api" -import type { ApiStreamTextChunk } from "../../transform/stream" +import type { ApiStreamTextChunk, ApiStreamReasoningChunk } from "../../transform/stream" describe("MistralHandler", () => { let handler: MistralHandler @@ -44,6 +56,7 @@ describe("MistralHandler", () => { } handler = new MistralHandler(mockOptions) mockCreate.mockClear() + mockComplete.mockClear() }) describe("constructor", () => { @@ -122,5 +135,134 @@ describe("MistralHandler", () => { mockCreate.mockRejectedValueOnce(new Error("API Error")) await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error") }) + + it("should handle thinking content as reasoning chunks", async () => { + // Mock stream with thinking content matching new SDK structure + mockCreate.mockImplementationOnce(async (_options) => { + const stream = { + [Symbol.asyncIterator]: async function* () { + yield { + data: { + choices: [ + { + delta: { + content: [ + { + type: "thinking", + thinking: [{ type: "text", text: "Let me think about this..." }], + }, + { type: "text", text: "Here's the answer" }, + ], + }, + index: 0, + }, + ], + }, + } + }, + } + return stream + }) + + const iterator = handler.createMessage(systemPrompt, messages) + const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] + + for await (const chunk of iterator) { + if ("text" in chunk) { + results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk) + } + } + + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ type: "reasoning", text: "Let me think about this..." }) + expect(results[1]).toEqual({ type: "text", text: "Here's the answer" }) + }) + + it("should handle mixed content arrays correctly", async () => { + // Mock stream with mixed content matching new SDK structure + mockCreate.mockImplementationOnce(async (_options) => { + const stream = { + [Symbol.asyncIterator]: async function* () { + yield { + data: { + choices: [ + { + delta: { + content: [ + { type: "text", text: "First text" }, + { + type: "thinking", + thinking: [{ type: "text", text: "Some reasoning" }], + }, + { type: "text", text: "Second text" }, + ], + }, + index: 0, + }, + ], + }, + } + }, + } + return stream + }) + + const iterator = handler.createMessage(systemPrompt, messages) + const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] + + for await (const chunk of iterator) { + if ("text" in chunk) { + results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk) + } + } + + expect(results).toHaveLength(3) + expect(results[0]).toEqual({ type: "text", text: "First text" }) + expect(results[1]).toEqual({ type: "reasoning", text: "Some reasoning" }) + expect(results[2]).toEqual({ type: "text", text: "Second text" }) + }) + }) + + describe("completePrompt", () => { + it("should complete prompt successfully", async () => { + const prompt = "Test prompt" + const result = await handler.completePrompt(prompt) + + expect(mockComplete).toHaveBeenCalledWith({ + model: mockOptions.apiModelId, + messages: [{ role: "user", content: prompt }], + temperature: 0, + }) + + expect(result).toBe("Test response") + }) + + it("should filter out thinking content in completePrompt", async () => { + mockComplete.mockImplementationOnce(async (_options) => { + return { + choices: [ + { + message: { + content: [ + { type: "thinking", text: "Let me think..." }, + { type: "text", text: "Answer part 1" }, + { type: "text", text: "Answer part 2" }, + ], + }, + }, + ], + } + }) + + const prompt = "Test prompt" + const result = await handler.completePrompt(prompt) + + expect(result).toBe("Answer part 1Answer part 2") + }) + + it("should handle errors in completePrompt", async () => { + mockComplete.mockRejectedValueOnce(new Error("API Error")) + await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Mistral completion error: API Error") + }) }) }) diff --git a/src/api/providers/__tests__/native-ollama.spec.ts b/src/api/providers/__tests__/native-ollama.spec.ts new file mode 100644 index 0000000000..f8792937db --- /dev/null +++ b/src/api/providers/__tests__/native-ollama.spec.ts @@ -0,0 +1,162 @@ +// npx vitest run api/providers/__tests__/native-ollama.spec.ts + +import { NativeOllamaHandler } from "../native-ollama" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock the ollama package +const mockChat = vitest.fn() +vitest.mock("ollama", () => { + return { + Ollama: vitest.fn().mockImplementation(() => ({ + chat: mockChat, + })), + Message: vitest.fn(), + } +}) + +// Mock the getOllamaModels function +vitest.mock("../fetchers/ollama", () => ({ + getOllamaModels: vitest.fn().mockResolvedValue({ + llama2: { + contextWindow: 4096, + maxTokens: 4096, + supportsImages: false, + supportsPromptCache: false, + }, + }), +})) + +describe("NativeOllamaHandler", () => { + let handler: NativeOllamaHandler + + beforeEach(() => { + vitest.clearAllMocks() + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + ollamaModelId: "llama2", + ollamaBaseUrl: "http://localhost:11434", + } + + handler = new NativeOllamaHandler(options) + }) + + describe("createMessage", () => { + it("should stream messages from Ollama", async () => { + // Mock the chat response as an async generator + mockChat.mockImplementation(async function* () { + yield { + message: { content: "Hello" }, + eval_count: undefined, + prompt_eval_count: undefined, + } + yield { + message: { content: " world" }, + eval_count: 2, + prompt_eval_count: 10, + } + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hi there" }] + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(3) + expect(results[0]).toEqual({ type: "text", text: "Hello" }) + expect(results[1]).toEqual({ type: "text", text: " world" }) + expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 }) + }) + + it("should handle DeepSeek R1 models with reasoning detection", async () => { + const options: ApiHandlerOptions = { + apiModelId: "deepseek-r1", + ollamaModelId: "deepseek-r1", + ollamaBaseUrl: "http://localhost:11434", + } + + handler = new NativeOllamaHandler(options) + + // Mock response with thinking tags + mockChat.mockImplementation(async function* () { + yield { message: { content: "Let me think" } } + yield { message: { content: " about this " } } + yield { message: { content: "The answer is 42" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Question?" }]) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + // Should detect reasoning vs regular text + expect(results.some((r) => r.type === "reasoning")).toBe(true) + expect(results.some((r) => r.type === "text")).toBe(true) + }) + }) + + describe("completePrompt", () => { + it("should complete a prompt without streaming", async () => { + mockChat.mockResolvedValue({ + message: { content: "This is the response" }, + }) + + const result = await handler.completePrompt("Tell me a joke") + + expect(mockChat).toHaveBeenCalledWith({ + model: "llama2", + messages: [{ role: "user", content: "Tell me a joke" }], + stream: false, + options: { + temperature: 0, + }, + }) + expect(result).toBe("This is the response") + }) + }) + + describe("error handling", () => { + it("should handle connection refused errors", async () => { + const error = new Error("ECONNREFUSED") as any + error.code = "ECONNREFUSED" + mockChat.mockRejectedValue(error) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Ollama service is not running") + }) + + it("should handle model not found errors", async () => { + const error = new Error("Not found") as any + error.status = 404 + mockChat.mockRejectedValue(error) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Model llama2 not found in Ollama") + }) + }) + + describe("getModel", () => { + it("should return the configured model", () => { + const model = handler.getModel() + expect(model.id).toBe("llama2") + expect(model.info).toBeDefined() + }) + }) +}) diff --git a/src/api/providers/__tests__/ollama-timeout.spec.ts b/src/api/providers/__tests__/ollama-timeout.spec.ts new file mode 100644 index 0000000000..db78f206c0 --- /dev/null +++ b/src/api/providers/__tests__/ollama-timeout.spec.ts @@ -0,0 +1,108 @@ +// npx vitest run api/providers/__tests__/ollama-timeout.spec.ts + +import { OllamaHandler } from "../ollama" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock the timeout config utility +vitest.mock("../utils/timeout-config", () => ({ + getApiRequestTimeout: vitest.fn(), +})) + +import { getApiRequestTimeout } from "../utils/timeout-config" + +// Mock OpenAI +const mockOpenAIConstructor = vitest.fn() +vitest.mock("openai", () => { + return { + __esModule: true, + default: vitest.fn().mockImplementation((config) => { + mockOpenAIConstructor(config) + return { + chat: { + completions: { + create: vitest.fn(), + }, + }, + } + }), + } +}) + +describe("OllamaHandler timeout configuration", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should use default timeout of 600 seconds when no configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + ollamaModelId: "llama2", + ollamaBaseUrl: "http://localhost:11434", + } + + new OllamaHandler(options) + + expect(getApiRequestTimeout).toHaveBeenCalled() + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "http://localhost:11434/v1", + apiKey: "ollama", + timeout: 600000, // 600 seconds in milliseconds + }), + ) + }) + + it("should use custom timeout when configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(3600000) // 1 hour + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + ollamaModelId: "llama2", + } + + new OllamaHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 3600000, // 3600 seconds in milliseconds + }), + ) + }) + + it("should handle zero timeout (no timeout)", () => { + ;(getApiRequestTimeout as any).mockReturnValue(0) + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + ollamaModelId: "llama2", + ollamaBaseUrl: "http://localhost:11434", + } + + new OllamaHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 0, // No timeout + }), + ) + }) + + it("should use default base URL when not provided", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiModelId: "llama2", + ollamaModelId: "llama2", + } + + new OllamaHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "http://localhost:11434/v1", + }), + ) + }) +}) diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 64080b4cac..0acdb6202e 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -160,8 +160,12 @@ describe("OpenAiNativeHandler", () => { expect(results.length).toBe(1) expect(results[0].type).toBe("usage") // Use type assertion to avoid TypeScript errors - expect((results[0] as any).inputTokens).toBe(0) - expect((results[0] as any).outputTokens).toBe(0) + const usageResult = results[0] as any + expect(usageResult.inputTokens).toBe(0) + expect(usageResult.outputTokens).toBe(0) + // When no cache tokens are present, they should be undefined + expect(usageResult.cacheWriteTokens).toBeUndefined() + expect(usageResult.cacheReadTokens).toBeUndefined() // Verify developer role is used for system prompt with o1 model expect(mockCreate).toHaveBeenCalledWith({ @@ -254,6 +258,68 @@ describe("OpenAiNativeHandler", () => { }) }) + it("should not include verbosity parameter for models that don't support it", async () => { + // Test with gpt-4.1 which does NOT support verbosity + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-4.1", + verbosity: "high", // Set verbosity but it should be ignored + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that verbosity is NOT included in the request + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("verbosity") + expect(callArgs.model).toBe("gpt-4.1") + expect(callArgs.temperature).toBe(0) + expect(callArgs.stream).toBe(true) + }) + + it("should not include verbosity for gpt-4o models", async () => { + // Test with gpt-4o which does NOT support verbosity + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-4o", + verbosity: "medium", // Set verbosity but it should be ignored + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that verbosity is NOT included in the request + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("verbosity") + expect(callArgs.model).toBe("gpt-4o") + }) + + it("should not include verbosity for gpt-4.1-mini models", async () => { + // Test with gpt-4.1-mini which does NOT support verbosity + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-4.1-mini", + verbosity: "low", // Set verbosity but it should be ignored + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that verbosity is NOT included in the request + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("verbosity") + expect(callArgs.model).toBe("gpt-4.1-mini") + }) + it("should handle empty delta content", async () => { const mockStream = [ { choices: [{ delta: {} }], usage: null }, @@ -286,6 +352,111 @@ describe("OpenAiNativeHandler", () => { expect((results[1] as any).outputTokens).toBe(5) expect((results[1] as any).totalCost).toBeCloseTo(0.00006, 6) }) + + it("should handle cache tokens in streaming response", async () => { + const mockStream = [ + { choices: [{ delta: { content: "Hello" } }], usage: null }, + { choices: [{ delta: { content: " cached" } }], usage: null }, + { + choices: [{ delta: { content: " response" } }], + usage: { + prompt_tokens: 100, + completion_tokens: 10, + prompt_tokens_details: { + cached_tokens: 80, + audio_tokens: 0, + }, + completion_tokens_details: { + reasoning_tokens: 0, + audio_tokens: 0, + accepted_prediction_tokens: 0, + rejected_prediction_tokens: 0, + }, + }, + }, + ] + + mockCreate.mockResolvedValueOnce( + (async function* () { + for (const chunk of mockStream) { + yield chunk + } + })(), + ) + + const generator = handler.createMessage(systemPrompt, messages) + const results = [] + for await (const result of generator) { + results.push(result) + } + + // Verify text responses + expect(results.length).toBe(4) + expect(results[0]).toMatchObject({ type: "text", text: "Hello" }) + expect(results[1]).toMatchObject({ type: "text", text: " cached" }) + expect(results[2]).toMatchObject({ type: "text", text: " response" }) + + // Check usage data includes cache tokens + expect(results[3].type).toBe("usage") + const usageChunk = results[3] as any + expect(usageChunk.inputTokens).toBe(100) // Total input tokens (includes cached) + expect(usageChunk.outputTokens).toBe(10) + expect(usageChunk.cacheReadTokens).toBe(80) // Cached tokens from prompt_tokens_details + expect(usageChunk.cacheWriteTokens).toBeUndefined() // No cache write tokens in standard response + + // Verify cost calculation takes cache into account + // GPT-4.1 pricing: input $2/1M, output $8/1M, cache read $0.5/1M + // OpenAI's prompt_tokens includes cached tokens, so we need to calculate: + // - Non-cached input tokens: 100 - 80 = 20 + // - Cost for non-cached input: (20 / 1_000_000) * 2.0 + // - Cost for cached input: (80 / 1_000_000) * 0.5 + // - Cost for output: (10 / 1_000_000) * 8.0 + const nonCachedInputTokens = 100 - 80 + const expectedNonCachedInputCost = (nonCachedInputTokens / 1_000_000) * 2.0 + const expectedCacheReadCost = (80 / 1_000_000) * 0.5 + const expectedOutputCost = (10 / 1_000_000) * 8.0 + const expectedTotalCost = expectedNonCachedInputCost + expectedCacheReadCost + expectedOutputCost + expect(usageChunk.totalCost).toBeCloseTo(expectedTotalCost, 10) + }) + + it("should handle cache write tokens if present", async () => { + const mockStream = [ + { choices: [{ delta: { content: "Test" } }], usage: null }, + { + choices: [{ delta: {} }], + usage: { + prompt_tokens: 150, + completion_tokens: 5, + prompt_tokens_details: { + cached_tokens: 50, + }, + cache_creation_input_tokens: 30, // Cache write tokens + }, + }, + ] + + mockCreate.mockResolvedValueOnce( + (async function* () { + for (const chunk of mockStream) { + yield chunk + } + })(), + ) + + const generator = handler.createMessage(systemPrompt, messages) + const results = [] + for await (const result of generator) { + results.push(result) + } + + // Check usage data includes both cache read and write tokens + const usageChunk = results.find((r) => r.type === "usage") as any + expect(usageChunk).toBeDefined() + expect(usageChunk.inputTokens).toBe(150) + expect(usageChunk.outputTokens).toBe(5) + expect(usageChunk.cacheReadTokens).toBe(50) + expect(usageChunk.cacheWriteTokens).toBe(30) + }) }) describe("completePrompt", () => { @@ -455,8 +626,1193 @@ describe("OpenAiNativeHandler", () => { openAiNativeApiKey: "test-api-key", }) const modelInfo = handlerWithoutModel.getModel() - expect(modelInfo.id).toBe("gpt-4.1") // Default model + expect(modelInfo.id).toBe("gpt-5-2025-08-07") // Default model expect(modelInfo.info).toBeDefined() }) }) + + describe("GPT-5 models", () => { + it("should handle GPT-5 model with Responses API", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Simulate actual GPT-5 Responses API SSE stream format + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.created","response":{"id":"test","status":"in_progress"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Hello"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":" world"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"usage":{"prompt_tokens":10,"completion_tokens":2}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify Responses API is called with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + Authorization: "Bearer test-api-key", + Accept: "text/event-stream", + }), + body: expect.any(String), + }), + ) + const body1 = (mockFetch.mock.calls[0][1] as any).body as string + expect(body1).toContain('"model":"gpt-5-2025-08-07"') + expect(body1).toContain('"input":"Developer: You are a helpful assistant.\\n\\nUser: Hello!"') + expect(body1).toContain('"effort":"medium"') + expect(body1).toContain('"summary":"auto"') + expect(body1).toContain('"verbosity":"medium"') + expect(body1).toContain('"temperature":1') + expect(body1).toContain('"max_output_tokens"') + + // Verify the streamed content + const textChunks = chunks.filter((c) => c.type === "text") + expect(textChunks).toHaveLength(2) + expect(textChunks[0].text).toBe("Hello") + expect(textChunks[1].text).toBe(" world") + + // Clean up + delete (global as any).fetch + }) + + it("should handle GPT-5-mini model with Responses API", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-mini-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify correct model and default parameters + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.stringContaining('"model":"gpt-5-mini-2025-08-07"'), + }), + ) + + // Clean up + delete (global as any).fetch + }) + + it("should handle GPT-5-nano model with Responses API", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Nano response"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-nano-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify correct model + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.stringContaining('"model":"gpt-5-nano-2025-08-07"'), + }), + ) + + // Clean up + delete (global as any).fetch + }) + + it("should support verbosity control for GPT-5", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Low verbosity"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + verbosity: "low", // Set verbosity through options + }) + + // Create a message to verify verbosity is passed + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that verbosity is passed in the request + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.stringContaining('"verbosity":"low"'), + }), + ) + + // Clean up + delete (global as any).fetch + }) + + it("should support minimal reasoning effort for GPT-5", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Minimal effort"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + reasoningEffort: "minimal" as any, // GPT-5 supports minimal + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // With minimal reasoning effort, the model should pass it through + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.stringContaining('"effort":"minimal"'), + }), + ) + + // Clean up + delete (global as any).fetch + }) + + it("should support low reasoning effort for GPT-5", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Low effort response"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + reasoningEffort: "low", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should use Responses API with low reasoning effort + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.any(String), + }), + ) + const body2 = (mockFetch.mock.calls[0][1] as any).body as string + expect(body2).toContain('"model":"gpt-5-2025-08-07"') + expect(body2).toContain('"effort":"low"') + expect(body2).toContain('"summary":"auto"') + expect(body2).toContain('"verbosity":"medium"') + expect(body2).toContain('"temperature":1') + expect(body2).toContain('"max_output_tokens"') + + // Clean up + delete (global as any).fetch + }) + + it("should support both verbosity and reasoning effort together for GPT-5", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"High verbosity minimal effort"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + verbosity: "high", + reasoningEffort: "minimal" as any, + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should use Responses API with both parameters + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + body: expect.any(String), + }), + ) + const body3 = (mockFetch.mock.calls[0][1] as any).body as string + expect(body3).toContain('"model":"gpt-5-2025-08-07"') + expect(body3).toContain('"effort":"minimal"') + expect(body3).toContain('"summary":"auto"') + expect(body3).toContain('"verbosity":"high"') + expect(body3).toContain('"temperature":1') + expect(body3).toContain('"max_output_tokens"') + + // Clean up + delete (global as any).fetch + }) + + it("should handle actual GPT-5 Responses API format", async () => { + // Mock fetch with actual response format from GPT-5 + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Test actual GPT-5 response format + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.created","response":{"id":"test","status":"in_progress"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.in_progress","response":{"status":"in_progress"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"First text"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":" Second text"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"reasoning","text":"Some reasoning"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"usage":{"prompt_tokens":100,"completion_tokens":20}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should handle the actual format correctly + const textChunks = chunks.filter((c) => c.type === "text") + const reasoningChunks = chunks.filter((c) => c.type === "reasoning") + + expect(textChunks).toHaveLength(2) + expect(textChunks[0].text).toBe("First text") + expect(textChunks[1].text).toBe(" Second text") + + expect(reasoningChunks).toHaveLength(1) + expect(reasoningChunks[0].text).toBe("Some reasoning") + + // Should also have usage information with cost + const usageChunks = chunks.filter((c) => c.type === "usage") + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toMatchObject({ + type: "usage", + inputTokens: 100, + outputTokens: 20, + totalCost: expect.any(Number), + }) + + // Verify cost calculation (GPT-5 pricing: input $1.25/M, output $10/M) + const expectedInputCost = (100 / 1_000_000) * 1.25 + const expectedOutputCost = (20 / 1_000_000) * 10.0 + const expectedTotalCost = expectedInputCost + expectedOutputCost + expect(usageChunks[0].totalCost).toBeCloseTo(expectedTotalCost, 10) + + // Clean up + delete (global as any).fetch + }) + + it("should handle Responses API with no content gracefully", async () => { + // Mock fetch with empty response + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('data: {"someField":"value"}\n\n')) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + + // Should not throw, just warn + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should have no content chunks when stream is empty + const contentChunks = chunks.filter((c) => c.type === "text" || c.type === "reasoning") + + expect(contentChunks).toHaveLength(0) + + // Clean up + delete (global as any).fetch + }) + + it("should support previous_response_id for conversation continuity", async () => { + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Include response ID in the response + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.created","response":{"id":"resp_123","status":"in_progress"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response with ID"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"id":"resp_123","usage":{"prompt_tokens":10,"completion_tokens":3}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + // First request - should not have previous_response_id + const stream1 = handler.createMessage(systemPrompt, messages) + const chunks1: any[] = [] + for await (const chunk of stream1) { + chunks1.push(chunk) + } + + // Verify first request doesn't include previous_response_id + let firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(firstCallBody.previous_response_id).toBeUndefined() + + // Second request with metadata - should include previous_response_id + const stream2 = handler.createMessage(systemPrompt, messages, { + taskId: "test-task", + previousResponseId: "resp_456", + }) + const chunks2: any[] = [] + for await (const chunk of stream2) { + chunks2.push(chunk) + } + + // Verify second request includes the provided previous_response_id + let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body) + expect(secondCallBody.previous_response_id).toBe("resp_456") + + // Clean up + delete (global as any).fetch + }) + + it("should handle unhandled stream events gracefully", async () => { + // Mock fetch for the fallback SSE path (which is what gets used when SDK fails) + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Hello"}}\n\n', + ), + ) + // This event is not handled, so it should be ignored + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.audio.delta","delta":"..."}\n\n'), + ) + controller.enqueue(new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n')) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + // Also mock the SDK to throw an error so it falls back to fetch + const mockClient = { + responses: { + create: vitest.fn().mockRejectedValue(new Error("SDK not available")), + }, + } + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + // Replace the client with our mock + ;(handler as any).client = mockClient + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + const errors: any[] = [] + + try { + for await (const chunk of stream) { + chunks.push(chunk) + } + } catch (error) { + errors.push(error) + } + + // Log for debugging + if (chunks.length === 0 && errors.length === 0) { + console.log("No chunks and no errors received") + } + if (errors.length > 0) { + console.log("Errors:", errors) + } + + expect(errors.length).toBe(0) + const textChunks = chunks.filter((c) => c.type === "text") + expect(textChunks.length).toBeGreaterThan(0) + expect(textChunks[0].text).toBe("Hello") + + delete (global as any).fetch + }) + + it("should use stored response ID when metadata doesn't provide one", async () => { + // Mock fetch for Responses API + const mockFetch = vitest + .fn() + .mockResolvedValueOnce({ + ok: true, + body: new ReadableStream({ + start(controller) { + // First response with ID + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"id":"resp_789","output":[{"type":"text","content":[{"type":"text","text":"First"}]}],"usage":{"prompt_tokens":10,"completion_tokens":1}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + .mockResolvedValueOnce({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Second response + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Second"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + // First request - establishes response ID + const stream1 = handler.createMessage(systemPrompt, messages) + for await (const chunk of stream1) { + // consume stream + } + + // Second request without metadata - should use stored response ID + const stream2 = handler.createMessage(systemPrompt, messages, { taskId: "test-task" }) + for await (const chunk of stream2) { + // consume stream + } + + // Verify second request uses the stored response ID from first request + let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body) + expect(secondCallBody.previous_response_id).toBe("resp_789") + + // Clean up + delete (global as any).fetch + }) + + it("should only send latest message when using previous_response_id", async () => { + // Mock fetch for Responses API + const mockFetch = vitest + .fn() + .mockResolvedValueOnce({ + ok: true, + body: new ReadableStream({ + start(controller) { + // First response with ID + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"id":"resp_001","output":[{"type":"text","content":[{"type":"text","text":"First"}]}],"usage":{"prompt_tokens":50,"completion_tokens":1}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + .mockResolvedValueOnce({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Second response + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Second"}}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"id":"resp_002","usage":{"prompt_tokens":10,"completion_tokens":1}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + // First request with full conversation + const firstMessages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + { role: "user", content: "How are you?" }, + ] + + const stream1 = handler.createMessage(systemPrompt, firstMessages) + for await (const chunk of stream1) { + // consume stream + } + + // Verify first request sends full conversation + let firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(firstCallBody.input).toContain("Hello") + expect(firstCallBody.input).toContain("Hi there!") + expect(firstCallBody.input).toContain("How are you?") + expect(firstCallBody.previous_response_id).toBeUndefined() + + // Second request with previous_response_id - should only send latest message + const secondMessages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + { role: "user", content: "How are you?" }, + { role: "assistant", content: "I'm doing well!" }, + { role: "user", content: "What's the weather?" }, // Latest message + ] + + const stream2 = handler.createMessage(systemPrompt, secondMessages, { + taskId: "test-task", + previousResponseId: "resp_001", + }) + for await (const chunk of stream2) { + // consume stream + } + + // Verify second request only sends the latest user message + let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body) + expect(secondCallBody.input).toBe("User: What's the weather?") + expect(secondCallBody.input).not.toContain("Hello") + expect(secondCallBody.input).not.toContain("Hi there!") + expect(secondCallBody.input).not.toContain("How are you?") + expect(secondCallBody.previous_response_id).toBe("resp_001") + + // Clean up + delete (global as any).fetch + }) + + it("should correctly prepare GPT-5 input with conversation continuity", () => { + const gpt5Handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + // @ts-expect-error - private method + const { formattedInput, previousResponseId } = gpt5Handler.prepareGpt5Input(systemPrompt, messages, { + taskId: "task1", + previousResponseId: "resp_123", + }) + + expect(previousResponseId).toBe("resp_123") + expect(formattedInput).toBe("User: Hello!") + }) + + it("should provide helpful error messages for different error codes", async () => { + const testCases = [ + { status: 400, expectedMessage: "Invalid request to GPT-5 API" }, + { status: 401, expectedMessage: "Authentication failed" }, + { status: 403, expectedMessage: "Access denied" }, + { status: 404, expectedMessage: "GPT-5 API endpoint not found" }, + { status: 429, expectedMessage: "Rate limit exceeded" }, + { status: 500, expectedMessage: "OpenAI service error" }, + ] + + for (const { status, expectedMessage } of testCases) { + // Mock fetch with error response + const mockFetch = vitest.fn().mockResolvedValue({ + ok: false, + status, + statusText: "Error", + text: async () => JSON.stringify({ error: { message: "Test error" } }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + + await expect(async () => { + for await (const chunk of stream) { + // Should throw before yielding anything + } + }).rejects.toThrow(expectedMessage) + } + + // Clean up + delete (global as any).fetch + }) + }) +}) + +// Added tests for GPT-5 streaming event coverage per PR_review_gpt5_final.md + +describe("GPT-5 streaming event coverage (additional)", () => { + it("should handle reasoning delta events for GPT-5", async () => { + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.reasoning.delta","delta":"Thinking about the problem..."}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.text.delta","delta":"The answer is..."}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + // @ts-ignore + global.fetch = mockFetch + + const handler = new OpenAiNativeHandler({ + apiModelId: "gpt-5-2025-08-07", + openAiNativeApiKey: "test-api-key", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }] + const stream = handler.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const reasoningChunks = chunks.filter((c) => c.type === "reasoning") + const textChunks = chunks.filter((c) => c.type === "text") + + expect(reasoningChunks).toHaveLength(1) + expect(reasoningChunks[0].text).toBe("Thinking about the problem...") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("The answer is...") + + // @ts-ignore + delete global.fetch + }) + + it("should handle refusal delta events for GPT-5 and prefix output", async () => { + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.refusal.delta","delta":"I cannot comply with this request."}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + // @ts-ignore + global.fetch = mockFetch + + const handler = new OpenAiNativeHandler({ + apiModelId: "gpt-5-2025-08-07", + openAiNativeApiKey: "test-api-key", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Do something disallowed" }] + const stream = handler.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const textChunks = chunks.filter((c) => c.type === "text") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("[Refusal] I cannot comply with this request.") + + // @ts-ignore + delete global.fetch + }) + + it("should ignore malformed JSON lines in SSE stream", async () => { + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"Before"}}\n\n', + ), + ) + // Malformed JSON line + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Bad"\n\n'), + ) + // Valid line after malformed + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_item.added","item":{"type":"text","text":"After"}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + // @ts-ignore + global.fetch = mockFetch + + const handler = new OpenAiNativeHandler({ + apiModelId: "gpt-5-2025-08-07", + openAiNativeApiKey: "test-api-key", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }] + const stream = handler.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // It should not throw and still capture the valid texts around the malformed line + const textChunks = chunks.filter((c) => c.type === "text") + expect(textChunks.map((c: any) => c.text)).toEqual(["Before", "After"]) + + // @ts-ignore + delete global.fetch + }) + + describe("Codex Mini Model", () => { + let handler: OpenAiNativeHandler + const mockOptions: ApiHandlerOptions = { + openAiNativeApiKey: "test-api-key", + apiModelId: "codex-mini-latest", + } + + it("should handle codex-mini-latest streaming response", async () => { + // Mock fetch for Codex Mini responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + // Codex Mini uses the same responses API format + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":"Hello"}\n\n'), + ) + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":" from"}\n\n'), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_text.delta","delta":" Codex"}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_text.delta","delta":" Mini!"}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.done","response":{"usage":{"prompt_tokens":50,"completion_tokens":10}}}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "codex-mini-latest", + }) + + const systemPrompt = "You are a helpful coding assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Write a hello world function" }, + ] + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify text chunks + const textChunks = chunks.filter((c) => c.type === "text") + expect(textChunks).toHaveLength(4) + expect(textChunks.map((c) => c.text).join("")).toBe("Hello from Codex Mini!") + + // Verify usage data from API + const usageChunks = chunks.filter((c) => c.type === "usage") + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toMatchObject({ + type: "usage", + inputTokens: 50, + outputTokens: 10, + totalCost: expect.any(Number), // Codex Mini has pricing: $1.5/M input, $6/M output + }) + + // Verify cost is calculated correctly based on API usage data + const expectedCost = (50 / 1_000_000) * 1.5 + (10 / 1_000_000) * 6 + expect(usageChunks[0].totalCost).toBeCloseTo(expectedCost, 10) + + // Verify the request was made with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + "https://api.openai.com/v1/responses", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + Authorization: "Bearer test-api-key", + Accept: "text/event-stream", + }), + body: expect.any(String), + }), + ) + + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(requestBody).toMatchObject({ + model: "codex-mini-latest", + input: "Developer: You are a helpful coding assistant.\n\nUser: Write a hello world function", + stream: true, + }) + + // Clean up + delete (global as any).fetch + }) + + it("should handle codex-mini-latest non-streaming completion", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "codex-mini-latest", + }) + + // Codex Mini now uses the same Responses API as GPT-5, which doesn't support non-streaming + await expect(handler.completePrompt("Write a hello world function in Python")).rejects.toThrow( + "completePrompt is not supported for codex-mini-latest. Use createMessage (Responses API) instead.", + ) + }) + + it("should handle codex-mini-latest API errors", async () => { + // Mock fetch with error response + const mockFetch = vitest.fn().mockResolvedValue({ + ok: false, + status: 429, + statusText: "Too Many Requests", + text: async () => "Rate limit exceeded", + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "codex-mini-latest", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + + const stream = handler.createMessage(systemPrompt, messages) + + // Should throw an error (using the same error format as GPT-5) + await expect(async () => { + for await (const chunk of stream) { + // consume stream + } + }).rejects.toThrow("Rate limit exceeded") + + // Clean up + delete (global as any).fetch + }) + + it("should handle codex-mini-latest with multiple user messages", async () => { + // Mock fetch for streaming response + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_text.delta","delta":"Combined response"}\n\n', + ), + ) + controller.enqueue(new TextEncoder().encode('data: {"type":"response.completed"}\n\n')) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "codex-mini-latest", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First question" }, + { role: "assistant", content: "First answer" }, + { role: "user", content: "Second question" }, + ] + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify the request body includes full conversation like GPT-5 + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(requestBody.input).toContain("Developer: You are a helpful assistant") + expect(requestBody.input).toContain("User: First question") + expect(requestBody.input).toContain("Assistant: First answer") + expect(requestBody.input).toContain("User: Second question") + + // Clean up + delete (global as any).fetch + }) + + it("should handle codex-mini-latest stream error events", async () => { + // Mock fetch with error event in stream + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.output_text.delta","delta":"Partial"}\n\n', + ), + ) + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.error","error":{"message":"Model overloaded"}}\n\n', + ), + ) + // The error handler will throw, but we still need to close the stream + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "codex-mini-latest", + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + + const stream = handler.createMessage(systemPrompt, messages) + + // Should throw an error when encountering error event + await expect(async () => { + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + }).rejects.toThrow("Responses API error: Model overloaded") + + // Clean up + delete (global as any).fetch + }) + }) }) diff --git a/src/api/providers/__tests__/openai-timeout.spec.ts b/src/api/providers/__tests__/openai-timeout.spec.ts new file mode 100644 index 0000000000..2a09fd94ff --- /dev/null +++ b/src/api/providers/__tests__/openai-timeout.spec.ts @@ -0,0 +1,144 @@ +// npx vitest run api/providers/__tests__/openai-timeout.spec.ts + +import { OpenAiHandler } from "../openai" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock the timeout config utility +vitest.mock("../utils/timeout-config", () => ({ + getApiRequestTimeout: vitest.fn(), +})) + +import { getApiRequestTimeout } from "../utils/timeout-config" + +// Mock OpenAI and AzureOpenAI +const mockOpenAIConstructor = vitest.fn() +const mockAzureOpenAIConstructor = vitest.fn() + +vitest.mock("openai", () => { + return { + __esModule: true, + default: vitest.fn().mockImplementation((config) => { + mockOpenAIConstructor(config) + return { + chat: { + completions: { + create: vitest.fn(), + }, + }, + } + }), + AzureOpenAI: vitest.fn().mockImplementation((config) => { + mockAzureOpenAIConstructor(config) + return { + chat: { + completions: { + create: vitest.fn(), + }, + }, + } + }), + } +}) + +describe("OpenAiHandler timeout configuration", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should use default timeout for standard OpenAI", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiModelId: "gpt-4", + openAiModelId: "gpt-4", + openAiApiKey: "test-key", + } + + new OpenAiHandler(options) + + expect(getApiRequestTimeout).toHaveBeenCalled() + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.openai.com/v1", + apiKey: "test-key", + timeout: 600000, // 600 seconds in milliseconds + }), + ) + }) + + it("should use custom timeout for OpenAI-compatible providers", () => { + ;(getApiRequestTimeout as any).mockReturnValue(1800000) // 30 minutes + + const options: ApiHandlerOptions = { + apiModelId: "custom-model", + openAiModelId: "custom-model", + openAiBaseUrl: "http://localhost:8080/v1", + openAiApiKey: "test-key", + } + + new OpenAiHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "http://localhost:8080/v1", + timeout: 1800000, // 1800 seconds in milliseconds + }), + ) + }) + + it("should use timeout for Azure OpenAI", () => { + ;(getApiRequestTimeout as any).mockReturnValue(900000) // 15 minutes + + const options: ApiHandlerOptions = { + apiModelId: "gpt-4", + openAiModelId: "gpt-4", + openAiBaseUrl: "https://myinstance.openai.azure.com", + openAiApiKey: "test-key", + openAiUseAzure: true, + } + + new OpenAiHandler(options) + + expect(mockAzureOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 900000, // 900 seconds in milliseconds + }), + ) + }) + + it("should use timeout for Azure AI Inference", () => { + ;(getApiRequestTimeout as any).mockReturnValue(1200000) // 20 minutes + + const options: ApiHandlerOptions = { + apiModelId: "deepseek", + openAiModelId: "deepseek", + openAiBaseUrl: "https://myinstance.services.ai.azure.com", + openAiApiKey: "test-key", + } + + new OpenAiHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 1200000, // 1200 seconds in milliseconds + }), + ) + }) + + it("should handle zero timeout (no timeout)", () => { + ;(getApiRequestTimeout as any).mockReturnValue(0) + + const options: ApiHandlerOptions = { + apiModelId: "gpt-4", + openAiModelId: "gpt-4", + } + + new OpenAiHandler(options) + + expect(mockOpenAIConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 0, // No timeout + }), + ) + }) +}) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index b4b5f29204..14ed35430a 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -1,11 +1,12 @@ // npx vitest run api/providers/__tests__/openai.spec.ts -import { OpenAiHandler } from "../openai" +import { OpenAiHandler, getOpenAiModels } from "../openai" import { ApiHandlerOptions } from "../../../shared/api" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { openAiModelInfoSaneDefaults } from "@roo-code/types" import { Package } from "../../../shared/package" +import axios from "axios" const mockCreate = vitest.fn() @@ -68,6 +69,13 @@ vitest.mock("openai", () => { } }) +// Mock axios for getOpenAiModels tests +vitest.mock("axios", () => ({ + default: { + get: vitest.fn(), + }, +})) + describe("OpenAiHandler", () => { let handler: OpenAiHandler let mockOptions: ApiHandlerOptions @@ -107,6 +115,7 @@ describe("OpenAiHandler", () => { "X-Title": "Roo Code", "User-Agent": `RooCode/${Package.version}`, }, + timeout: expect.any(Number), }) }) }) @@ -306,6 +315,71 @@ describe("OpenAiHandler", () => { const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.max_completion_tokens).toBe(4096) }) + + it("should omit temperature when modelTemperature is undefined", async () => { + const optionsWithoutTemperature: ApiHandlerOptions = { + ...mockOptions, + // modelTemperature is not set, should not include temperature + } + const handlerWithoutTemperature = new OpenAiHandler(optionsWithoutTemperature) + const stream = handlerWithoutTemperature.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called without temperature + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("temperature") + }) + + it("should include temperature when modelTemperature is explicitly set to 0", async () => { + const optionsWithZeroTemperature: ApiHandlerOptions = { + ...mockOptions, + modelTemperature: 0, + } + const handlerWithZeroTemperature = new OpenAiHandler(optionsWithZeroTemperature) + const stream = handlerWithZeroTemperature.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with temperature: 0 + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.temperature).toBe(0) + }) + + it("should include temperature when modelTemperature is set to a non-zero value", async () => { + const optionsWithCustomTemperature: ApiHandlerOptions = { + ...mockOptions, + modelTemperature: 0.7, + } + const handlerWithCustomTemperature = new OpenAiHandler(optionsWithCustomTemperature) + const stream = handlerWithCustomTemperature.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with temperature: 0.7 + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.temperature).toBe(0.7) + }) + + it("should include DEEP_SEEK_DEFAULT_TEMPERATURE for deepseek-reasoner models when temperature is not set", async () => { + const deepseekOptions: ApiHandlerOptions = { + ...mockOptions, + openAiModelId: "deepseek-reasoner", + // modelTemperature is not set + } + const deepseekHandler = new OpenAiHandler(deepseekOptions) + const stream = deepseekHandler.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with DEEP_SEEK_DEFAULT_TEMPERATURE (0.6) + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.temperature).toBe(0.6) + }) }) describe("error handling", () => { @@ -441,7 +515,7 @@ describe("OpenAiHandler", () => { ], stream: true, stream_options: { include_usage: true }, - temperature: 0, + // temperature should be omitted when not set }, { path: "/models/chat/completions" }, ) @@ -776,3 +850,143 @@ describe("OpenAiHandler", () => { }) }) }) + +describe("getOpenAiModels", () => { + beforeEach(() => { + vi.mocked(axios.get).mockClear() + }) + + it("should return empty array when baseUrl is not provided", async () => { + const result = await getOpenAiModels(undefined, "test-key") + expect(result).toEqual([]) + expect(axios.get).not.toHaveBeenCalled() + }) + + it("should return empty array when baseUrl is empty string", async () => { + const result = await getOpenAiModels("", "test-key") + expect(result).toEqual([]) + expect(axios.get).not.toHaveBeenCalled() + }) + + it("should trim whitespace from baseUrl", async () => { + const mockResponse = { + data: { + data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + const result = await getOpenAiModels(" https://api.openai.com/v1 ", "test-key") + + expect(axios.get).toHaveBeenCalledWith("https://api.openai.com/v1/models", expect.any(Object)) + expect(result).toEqual(["gpt-4", "gpt-3.5-turbo"]) + }) + + it("should handle baseUrl with trailing spaces", async () => { + const mockResponse = { + data: { + data: [{ id: "model-1" }, { id: "model-2" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + const result = await getOpenAiModels("https://api.example.com/v1 ", "test-key") + + expect(axios.get).toHaveBeenCalledWith("https://api.example.com/v1/models", expect.any(Object)) + expect(result).toEqual(["model-1", "model-2"]) + }) + + it("should handle baseUrl with leading spaces", async () => { + const mockResponse = { + data: { + data: [{ id: "model-1" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + const result = await getOpenAiModels(" https://api.example.com/v1", "test-key") + + expect(axios.get).toHaveBeenCalledWith("https://api.example.com/v1/models", expect.any(Object)) + expect(result).toEqual(["model-1"]) + }) + + it("should return empty array for invalid URL after trimming", async () => { + const result = await getOpenAiModels(" not-a-valid-url ", "test-key") + expect(result).toEqual([]) + expect(axios.get).not.toHaveBeenCalled() + }) + + it("should include authorization header when apiKey is provided", async () => { + const mockResponse = { + data: { + data: [{ id: "model-1" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + await getOpenAiModels("https://api.example.com/v1", "test-api-key") + + expect(axios.get).toHaveBeenCalledWith( + "https://api.example.com/v1/models", + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer test-api-key", + }), + }), + ) + }) + + it("should include custom headers when provided", async () => { + const mockResponse = { + data: { + data: [{ id: "model-1" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + const customHeaders = { + "X-Custom-Header": "custom-value", + } + + await getOpenAiModels("https://api.example.com/v1", "test-key", customHeaders) + + expect(axios.get).toHaveBeenCalledWith( + "https://api.example.com/v1/models", + expect.objectContaining({ + headers: expect.objectContaining({ + "X-Custom-Header": "custom-value", + Authorization: "Bearer test-key", + }), + }), + ) + }) + + it("should handle API errors gracefully", async () => { + vi.mocked(axios.get).mockRejectedValueOnce(new Error("Network error")) + + const result = await getOpenAiModels("https://api.example.com/v1", "test-key") + + expect(result).toEqual([]) + }) + + it("should handle malformed response data", async () => { + vi.mocked(axios.get).mockResolvedValueOnce({ data: null }) + + const result = await getOpenAiModels("https://api.example.com/v1", "test-key") + + expect(result).toEqual([]) + }) + + it("should deduplicate model IDs", async () => { + const mockResponse = { + data: { + data: [{ id: "gpt-4" }, { id: "gpt-4" }, { id: "gpt-3.5-turbo" }, { id: "gpt-4" }], + }, + } + vi.mocked(axios.get).mockResolvedValueOnce(mockResponse) + + const result = await getOpenAiModels("https://api.example.com/v1", "test-key") + + expect(result).toEqual(["gpt-4", "gpt-3.5-turbo"]) + }) +}) diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index ea850c47be..ae36fc1399 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -98,9 +98,11 @@ describe("OpenRouterHandler", () => { }) const result = await handler.fetchModel() - expect(result.maxTokens).toBe(128000) // Use actual implementation value - expect(result.reasoningBudget).toBeUndefined() // Use actual implementation value - expect(result.temperature).toBe(0) // Use actual implementation value + // With the new clamping logic, 128000 tokens (64% of 200000 context window) + // gets clamped to 20% of context window: 200000 * 0.2 = 40000 + expect(result.maxTokens).toBe(40000) + expect(result.reasoningBudget).toBeUndefined() + expect(result.temperature).toBe(0) }) it("does not honor custom maxTokens for non-thinking models", async () => { diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index 55fb976fd1..4d5037ed9e 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -65,6 +65,21 @@ describe("RequestyHandler", () => { }) }) + it("can use a base URL instead of the default", () => { + const handler = new RequestyHandler({ ...mockOptions, requestyBaseUrl: "https://custom.requesty.ai/v1" }) + expect(handler).toBeInstanceOf(RequestyHandler) + + expect(OpenAI).toHaveBeenCalledWith({ + baseURL: "https://custom.requesty.ai/v1", + apiKey: mockOptions.requestyApiKey, + defaultHeaders: { + "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", + "X-Title": "Roo Code", + "User-Agent": `RooCode/${Package.version}`, + }, + }) + }) + describe("fetchModel", () => { it("returns correct model info when options are provided", async () => { const handler = new RequestyHandler(mockOptions) diff --git a/src/api/providers/__tests__/roo.spec.ts b/src/api/providers/__tests__/roo.spec.ts new file mode 100644 index 0000000000..57b397b8f8 --- /dev/null +++ b/src/api/providers/__tests__/roo.spec.ts @@ -0,0 +1,449 @@ +// npx vitest run api/providers/__tests__/roo.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import { rooDefaultModelId, rooModels } from "@roo-code/types" + +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock OpenAI client +const mockCreate = vitest.fn() + +vitest.mock("openai", () => { + return { + __esModule: true, + default: vitest.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate.mockImplementation(async (options) => { + if (!options.stream) { + return { + id: "test-completion", + choices: [ + { + message: { role: "assistant", content: "Test response" }, + finish_reason: "stop", + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + } + + return { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + } + }), + }, + }, + })), + } +}) + +// Mock CloudService - Define functions outside to avoid initialization issues +const mockGetSessionToken = vitest.fn() +const mockHasInstance = vitest.fn() + +// Create mock functions that we can control +const mockGetSessionTokenFn = vitest.fn() +const mockHasInstanceFn = vitest.fn() + +vitest.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: () => mockHasInstanceFn(), + get instance() { + return { + authService: { + getSessionToken: () => mockGetSessionTokenFn(), + }, + } + }, + }, +})) + +// Mock i18n +vitest.mock("../../../i18n", () => ({ + t: vitest.fn((key: string) => { + if (key === "common:errors.roo.authenticationRequired") { + return "Authentication required for Roo Code Cloud" + } + return key + }), +})) + +// Import after mocks are set up +import { RooHandler } from "../roo" +import { CloudService } from "@roo-code/cloud" +import { t } from "../../../i18n" + +describe("RooHandler", () => { + let handler: RooHandler + let mockOptions: ApiHandlerOptions + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + beforeEach(() => { + mockOptions = { + apiModelId: "roo/sonic", + } + // Set up CloudService mocks for successful authentication + mockHasInstanceFn.mockReturnValue(true) + mockGetSessionTokenFn.mockReturnValue("test-session-token") + mockCreate.mockClear() + vitest.clearAllMocks() + }) + + describe("constructor", () => { + it("should initialize with valid session token", () => { + handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + expect(handler.getModel().id).toBe(mockOptions.apiModelId) + }) + + it("should not throw error if CloudService is not available", () => { + mockHasInstanceFn.mockReturnValue(false) + expect(() => { + new RooHandler(mockOptions) + }).not.toThrow() + // Constructor should succeed even without CloudService + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + }) + + it("should not throw error if session token is not available", () => { + mockHasInstanceFn.mockReturnValue(true) + mockGetSessionTokenFn.mockReturnValue(null) + expect(() => { + new RooHandler(mockOptions) + }).not.toThrow() + // Constructor should succeed even without session token + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + }) + + it("should initialize with default model if no model specified", () => { + handler = new RooHandler({}) + expect(handler).toBeInstanceOf(RooHandler) + expect(handler.getModel().id).toBe(rooDefaultModelId) + }) + + it("should pass correct configuration to base class", () => { + handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + // The handler should be initialized with correct base URL and API key + // We can't directly test the parent class constructor, but we can verify the handler works + expect(handler).toBeDefined() + }) + }) + + describe("createMessage", () => { + beforeEach(() => { + handler = new RooHandler(mockOptions) + }) + + it("should handle streaming responses", async () => { + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response") + }) + + it("should include usage information", async () => { + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0].inputTokens).toBe(10) + expect(usageChunks[0].outputTokens).toBe(5) + }) + + it("should handle API errors", async () => { + mockCreate.mockRejectedValueOnce(new Error("API Error")) + const stream = handler.createMessage(systemPrompt, messages) + await expect(async () => { + for await (const _chunk of stream) { + // Should not reach here + } + }).rejects.toThrow("API Error") + }) + + it("should handle empty response content", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: null }, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 0, + total_tokens: 10, + }, + } + }, + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(0) + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks).toHaveLength(1) + }) + + it("should handle multiple messages in conversation", async () => { + const multipleMessages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "First response" }, + { role: "user", content: "Second message" }, + ] + + const stream = handler.createMessage(systemPrompt, multipleMessages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ role: "system", content: systemPrompt }), + expect.objectContaining({ role: "user", content: "First message" }), + expect.objectContaining({ role: "assistant", content: "First response" }), + expect.objectContaining({ role: "user", content: "Second message" }), + ]), + }), + undefined, + ) + }) + }) + + describe("completePrompt", () => { + beforeEach(() => { + handler = new RooHandler(mockOptions) + }) + + it("should complete prompt successfully", async () => { + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("Test response") + expect(mockCreate).toHaveBeenCalledWith({ + model: mockOptions.apiModelId, + messages: [{ role: "user", content: "Test prompt" }], + }) + }) + + it("should handle API errors", async () => { + mockCreate.mockRejectedValueOnce(new Error("API Error")) + await expect(handler.completePrompt("Test prompt")).rejects.toThrow( + "Roo Code Cloud completion error: API Error", + ) + }) + + it("should handle empty response", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [{ message: { content: "" } }], + }) + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) + + it("should handle missing response content", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [{ message: {} }], + }) + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) + }) + + describe("getModel", () => { + beforeEach(() => { + handler = new RooHandler(mockOptions) + }) + + it("should return model info for specified model", () => { + const modelInfo = handler.getModel() + expect(modelInfo.id).toBe(mockOptions.apiModelId) + expect(modelInfo.info).toBeDefined() + // roo/sonic is a valid model in rooModels + expect(modelInfo.info).toBe(rooModels["roo/sonic"]) + }) + + it("should return default model when no model specified", () => { + const handlerWithoutModel = new RooHandler({}) + const modelInfo = handlerWithoutModel.getModel() + expect(modelInfo.id).toBe(rooDefaultModelId) + expect(modelInfo.info).toBeDefined() + expect(modelInfo.info).toBe(rooModels[rooDefaultModelId]) + }) + + it("should handle unknown model ID with fallback info", () => { + const handlerWithUnknownModel = new RooHandler({ + apiModelId: "unknown-model-id", + }) + const modelInfo = handlerWithUnknownModel.getModel() + expect(modelInfo.id).toBe("unknown-model-id") + expect(modelInfo.info).toBeDefined() + // Should return fallback info for unknown models + expect(modelInfo.info.maxTokens).toBe(16_384) + expect(modelInfo.info.contextWindow).toBe(262_144) + expect(modelInfo.info.supportsImages).toBe(false) + expect(modelInfo.info.supportsPromptCache).toBe(true) + expect(modelInfo.info.inputPrice).toBe(0) + expect(modelInfo.info.outputPrice).toBe(0) + }) + + it("should return correct model info for all Roo models", () => { + // Test each model in rooModels + const modelIds = Object.keys(rooModels) as Array+ + for (const modelId of modelIds) { + const handlerWithModel = new RooHandler({ apiModelId: modelId }) + const modelInfo = handlerWithModel.getModel() + expect(modelInfo.id).toBe(modelId) + expect(modelInfo.info).toBe(rooModels[modelId]) + } + }) + }) + + describe("temperature and model configuration", () => { + it("should omit temperature when not explicitly set", async () => { + handler = new RooHandler(mockOptions) + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // Consume stream + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.not.objectContaining({ + temperature: expect.anything(), + }), + undefined, + ) + }) + + it("should respect custom temperature setting", async () => { + handler = new RooHandler({ + ...mockOptions, + modelTemperature: 0.9, + }) + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // Consume stream + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + temperature: 0.9, + }), + undefined, + ) + }) + + it("should use correct API endpoint", () => { + // The base URL should be set to Roo's API endpoint + // We can't directly test the OpenAI client configuration, but we can verify the handler initializes + handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + // The handler should work with the Roo API endpoint + }) + }) + + describe("authentication flow", () => { + it("should use session token as API key", () => { + const testToken = "test-session-token-123" + mockGetSessionTokenFn.mockReturnValue(testToken) + + handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + expect(mockGetSessionTokenFn).toHaveBeenCalled() + }) + + it("should handle undefined auth service gracefully", () => { + mockHasInstanceFn.mockReturnValue(true) + // Mock CloudService with undefined authService + const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get + + try { + Object.defineProperty(CloudService, "instance", { + get: () => ({ authService: undefined }), + configurable: true, + }) + + expect(() => { + new RooHandler(mockOptions) + }).not.toThrow() + // Constructor should succeed even with undefined auth service + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + } finally { + // Always restore original getter, even if test fails + if (originalGetter) { + Object.defineProperty(CloudService, "instance", { + get: originalGetter, + configurable: true, + }) + } + } + }) + + it("should handle empty session token gracefully", () => { + mockGetSessionTokenFn.mockReturnValue("") + + expect(() => { + new RooHandler(mockOptions) + }).not.toThrow() + // Constructor should succeed even with empty session token + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) + }) + }) +}) diff --git a/src/api/providers/__tests__/sambanova.spec.ts b/src/api/providers/__tests__/sambanova.spec.ts new file mode 100644 index 0000000000..81de3058c8 --- /dev/null +++ b/src/api/providers/__tests__/sambanova.spec.ts @@ -0,0 +1,154 @@ +// npx vitest run src/api/providers/__tests__/sambanova.spec.ts + +// Mock vscode first to avoid import errors +vitest.mock("vscode", () => ({})) + +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { type SambaNovaModelId, sambaNovaDefaultModelId, sambaNovaModels } from "@roo-code/types" + +import { SambaNovaHandler } from "../sambanova" + +vitest.mock("openai", () => { + const createMock = vitest.fn() + return { + default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })), + } +}) + +describe("SambaNovaHandler", () => { + let handler: SambaNovaHandler + let mockCreate: any + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate = (OpenAI as unknown as any)().chat.completions.create + handler = new SambaNovaHandler({ sambaNovaApiKey: "test-sambanova-api-key" }) + }) + + it("should use the correct SambaNova base URL", () => { + new SambaNovaHandler({ sambaNovaApiKey: "test-sambanova-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.sambanova.ai/v1" })) + }) + + it("should use the provided API key", () => { + const sambaNovaApiKey = "test-sambanova-api-key" + new SambaNovaHandler({ sambaNovaApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: sambaNovaApiKey })) + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(sambaNovaDefaultModelId) + expect(model.info).toEqual(sambaNovaModels[sambaNovaDefaultModelId]) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: SambaNovaModelId = "Meta-Llama-3.3-70B-Instruct" + const handlerWithModel = new SambaNovaHandler({ + apiModelId: testModelId, + sambaNovaApiKey: "test-sambanova-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(sambaNovaModels[testModelId]) + }) + + it("completePrompt method should return text from SambaNova API", async () => { + const expectedResponse = "This is a test response from SambaNova" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "SambaNova API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `SambaNova completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from SambaNova stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to SambaNova client", async () => { + const modelId: SambaNovaModelId = "Meta-Llama-3.3-70B-Instruct" + const modelInfo = sambaNovaModels[modelId] + const handlerWithModel = new SambaNovaHandler({ + apiModelId: modelId, + sambaNovaApiKey: "test-sambanova-api-key", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for SambaNova" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for SambaNova" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + undefined, + ) + }) +}) diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts new file mode 100644 index 0000000000..6882cfe448 --- /dev/null +++ b/src/api/providers/__tests__/zai.spec.ts @@ -0,0 +1,231 @@ +// npx vitest run src/api/providers/__tests__/zai.spec.ts + +// Mock vscode first to avoid import errors +vitest.mock("vscode", () => ({})) + +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { + type InternationalZAiModelId, + type MainlandZAiModelId, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, + internationalZAiModels, + mainlandZAiModels, + ZAI_DEFAULT_TEMPERATURE, +} from "@roo-code/types" + +import { ZAiHandler } from "../zai" + +vitest.mock("openai", () => { + const createMock = vitest.fn() + return { + default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })), + } +}) + +describe("ZAiHandler", () => { + let handler: ZAiHandler + let mockCreate: any + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate = (OpenAI as unknown as any)().chat.completions.create + }) + + describe("International Z AI", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + }) + + it("should use the correct international Z AI base URL", () => { + new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + }) + + it("should use the provided API key for international", () => { + const zaiApiKey = "test-zai-api-key" + new ZAiHandler({ zaiApiKey, zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey })) + }) + + it("should return international default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(internationalZAiDefaultModelId) + expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId]) + }) + + it("should return specified international model when valid model is provided", () => { + const testModelId: InternationalZAiModelId = "glm-4.5-air" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(internationalZAiModels[testModelId]) + }) + }) + + describe("China Z AI", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" }) + }) + + it("should use the correct China Z AI base URL", () => { + new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://open.bigmodel.cn/api/paas/v4" }), + ) + }) + + it("should use the provided API key for China", () => { + const zaiApiKey = "test-zai-api-key" + new ZAiHandler({ zaiApiKey, zaiApiLine: "china" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey })) + }) + + it("should return China default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(mainlandZAiDefaultModelId) + expect(model.info).toEqual(mainlandZAiModels[mainlandZAiDefaultModelId]) + }) + + it("should return specified China model when valid model is provided", () => { + const testModelId: MainlandZAiModelId = "glm-4.5-air" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "china", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(mainlandZAiModels[testModelId]) + }) + }) + + describe("Default behavior", () => { + it("should default to international when no zaiApiLine is specified", () => { + const handlerDefault = new ZAiHandler({ zaiApiKey: "test-zai-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + + const model = handlerDefault.getModel() + expect(model.id).toBe(internationalZAiDefaultModelId) + expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId]) + }) + + it("should use 'not-provided' as default API key when none is specified", () => { + new ZAiHandler({ zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "not-provided" })) + }) + }) + + describe("API Methods", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + }) + + it("completePrompt method should return text from Z AI API", async () => { + const expectedResponse = "This is a test response from Z AI" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Z AI API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Z AI completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Z AI stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Z AI client", async () => { + const modelId: InternationalZAiModelId = "glm-4.5" + const modelInfo = internationalZAiModels[modelId] + const handlerWithModel = new ZAiHandler({ + apiModelId: modelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for Z AI" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Z AI" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + undefined, + ) + }) + }) +}) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 52dec1ae55..cb48492b60 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -45,8 +45,14 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { id: modelId, betas = [], maxTokens, temperature, reasoning: thinking } = this.getModel() + // Add 1M context beta flag if enabled for Claude Sonnet 4 + if (modelId === "claude-sonnet-4-20250514" && this.options.anthropicBeta1MContext) { + betas.push("context-1m-2025-08-07") + } + switch (modelId) { case "claude-sonnet-4-20250514": + case "claude-opus-4-1-20250805": case "claude-opus-4-20250514": case "claude-3-7-sonnet-20250219": case "claude-3-5-sonnet-20241022": @@ -105,6 +111,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa // Then check for models that support prompt caching switch (modelId) { case "claude-sonnet-4-20250514": + case "claude-opus-4-1-20250805": case "claude-opus-4-20250514": case "claude-3-7-sonnet-20250219": case "claude-3-5-sonnet-20241022": @@ -234,7 +241,23 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa getModel() { const modelId = this.options.apiModelId let id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId - const info: ModelInfo = anthropicModels[id] + let info: ModelInfo = anthropicModels[id] + + // If 1M context beta is enabled for Claude Sonnet 4, update the model info + if (id === "claude-sonnet-4-20250514" && this.options.anthropicBeta1MContext) { + // Use the tier pricing for 1M context + const tier = info.tiers?.[0] + if (tier) { + info = { + ...info, + contextWindow: tier.contextWindow, + inputPrice: tier.inputPrice, + outputPrice: tier.outputPrice, + cacheWritesPrice: tier.cacheWritesPrice, + cacheReadsPrice: tier.cacheReadsPrice, + } + } + } const params = getModelParams({ format: "anthropic", diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index f196b5f309..d079e22a1c 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -62,28 +62,39 @@ export abstract class BaseOpenAiCompatibleProvider }) } - override async *createMessage( + protected createStream( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, - ): ApiStream { + requestOptions?: OpenAI.RequestOptions, + ) { const { id: model, info: { maxTokens: max_tokens }, } = this.getModel() - const temperature = this.options.modelTemperature ?? this.defaultTemperature - const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model, max_tokens, - temperature, messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, } - const stream = await this.client.chat.completions.create(params) + // Only include temperature if explicitly set + if (this.options.modelTemperature !== undefined) { + params.temperature = this.options.modelTemperature + } + + return this.client.chat.completions.create(params, requestOptions) + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const stream = await this.createStream(systemPrompt, messages, metadata) for await (const chunk of stream) { const delta = chunk.choices[0]?.delta diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 76e502e6c7..c6a0b35df4 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -21,6 +21,7 @@ import { BEDROCK_MAX_TOKENS, BEDROCK_DEFAULT_CONTEXT, AWS_INFERENCE_PROFILE_MAPPING, + BEDROCK_CLAUDE_SONNET_4_MODEL_ID, } from "@roo-code/types" import { ApiStream } from "../transform/stream" @@ -46,12 +47,14 @@ interface BedrockInferenceConfig { topP?: number } -// Define interface for Bedrock thinking configuration -interface BedrockThinkingConfig { - thinking: { +// Define interface for Bedrock additional model request fields +// This includes thinking configuration, 1M context beta, and other model-specific parameters +interface BedrockAdditionalModelFields { + thinking?: { type: "enabled" budget_tokens: number } + anthropic_beta?: string[] [key: string]: any // Add index signature to be compatible with DocumentType } @@ -62,7 +65,7 @@ interface BedrockPayload { system?: SystemContentBlock[] inferenceConfig: BedrockInferenceConfig anthropic_version?: string - additionalModelRequestFields?: BedrockThinkingConfig + additionalModelRequestFields?: BedrockAdditionalModelFields } // Define specific types for content block events to avoid 'as any' usage @@ -226,6 +229,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH // Use API key/token-based authentication if enabled and API key is set clientConfig.token = { token: this.options.awsApiKey } clientConfig.authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. + clientConfig.requestHandler = { + // This should be the default anyway, but without setting something + // this provider fails to work with LiteLLM passthrough. + requestTimeout: 0, + } } else if (this.options.awsUseProfile && this.options.awsProfile) { // Use profile-based credentials if enabled and profile is set clientConfig.credentials = fromIni({ @@ -334,7 +342,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH conversationId, ) - let additionalModelRequestFields: BedrockThinkingConfig | undefined + let additionalModelRequestFields: BedrockAdditionalModelFields | undefined let thinkingEnabled = false // Determine if thinking should be enabled @@ -370,13 +378,26 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH inferenceConfig.topP = 0.1 } + // Check if 1M context is enabled for Claude Sonnet 4 + // Use parseBaseModelId to handle cross-region inference prefixes + const baseModelId = this.parseBaseModelId(modelConfig.id) + const is1MContextEnabled = baseModelId === BEDROCK_CLAUDE_SONNET_4_MODEL_ID && this.options.awsBedrock1MContext + + // Add anthropic_beta for 1M context to additionalModelRequestFields + if (is1MContextEnabled) { + if (!additionalModelRequestFields) { + additionalModelRequestFields = {} as BedrockAdditionalModelFields + } + additionalModelRequestFields.anthropic_beta = ["context-1m-2025-08-07"] + } + const payload: BedrockPayload = { modelId: modelConfig.id, messages: formatted.messages, system: formatted.system, inferenceConfig, ...(additionalModelRequestFields && { additionalModelRequestFields }), - // Add anthropic_version when using thinking features + // Add anthropic_version at top level when using thinking features ...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }), } @@ -955,6 +976,17 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } + // Check if 1M context is enabled for Claude Sonnet 4 + // Use parseBaseModelId to handle cross-region inference prefixes + const baseModelId = this.parseBaseModelId(modelConfig.id) + if (baseModelId === BEDROCK_CLAUDE_SONNET_4_MODEL_ID && this.options.awsBedrock1MContext) { + // Update context window to 1M tokens when 1M context beta is enabled + modelConfig.info = { + ...modelConfig.info, + contextWindow: 1_000_000, + } + } + // Get model params including reasoning configuration const params = getModelParams({ format: "anthropic", diff --git a/src/api/providers/cerebras.ts b/src/api/providers/cerebras.ts new file mode 100644 index 0000000000..a0421844e8 --- /dev/null +++ b/src/api/providers/cerebras.ts @@ -0,0 +1,336 @@ +import { Anthropic } from "@anthropic-ai/sdk" + +import { type CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" +import { calculateApiCostOpenAI } from "../../shared/cost" +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { XmlMatcher } from "../../utils/xml-matcher" + +import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" +import { BaseProvider } from "./base-provider" +import { DEFAULT_HEADERS } from "./constants" +import { t } from "../../i18n" + +const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1" +const CEREBRAS_DEFAULT_TEMPERATURE = 0 + +/** + * Removes thinking tokens from text to prevent model confusion when processing conversation history. + * This is crucial because models can get confused by their own thinking tokens in input. + */ +function stripThinkingTokens(text: string): string { + // Remove ... blocks entirely, including nested ones + return text.replace(/[\s\S]*?<\/think>/g, "").trim() +} + +/** + * Flattens OpenAI message content to simple strings that Cerebras can handle. + * Cerebras doesn't support complex content arrays like OpenAI does. + */ +function flattenMessageContent(content: any): string { + if (typeof content === "string") { + return content + } + + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === "string") { + return part + } + if (part.type === "text") { + return part.text || "" + } + if (part.type === "image_url") { + return "[Image]" // Placeholder for images since Cerebras doesn't support images + } + return "" + }) + .filter(Boolean) + .join("\n") + } + + // Fallback for any other content types + return String(content || "") +} + +/** + * Converts OpenAI messages to Cerebras-compatible format with simple string content. + * Also strips thinking tokens from assistant messages to prevent model confusion. + */ +function convertToCerebrasMessages(openaiMessages: any[]): Array<{ role: string; content: string }> { + return openaiMessages + .map((msg) => { + let content = flattenMessageContent(msg.content) + + // Strip thinking tokens from assistant messages to prevent confusion + if (msg.role === "assistant") { + content = stripThinkingTokens(content) + } + + return { + role: msg.role, + content, + } + }) + .filter((msg) => msg.content.trim() !== "") // Remove empty messages +} + +export class CerebrasHandler extends BaseProvider implements SingleCompletionHandler { + private apiKey: string + private providerModels: typeof cerebrasModels + private defaultProviderModelId: CerebrasModelId + private options: ApiHandlerOptions + private lastUsage: { inputTokens: number; outputTokens: number } = { inputTokens: 0, outputTokens: 0 } + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + this.apiKey = options.cerebrasApiKey || "" + this.providerModels = cerebrasModels + this.defaultProviderModelId = cerebrasDefaultModelId + + if (!this.apiKey) { + throw new Error("Cerebras API key is required") + } + } + + getModel(): { id: CerebrasModelId; info: (typeof cerebrasModels)[CerebrasModelId] } { + const originalModelId = (this.options.apiModelId as CerebrasModelId) || this.defaultProviderModelId + + // Route both qwen coder models to the same actual model ID for API calls + // This allows them to have different rate limits/descriptions in the UI + // while using the same underlying model + let apiModelId = originalModelId + if (originalModelId === "qwen-3-coder-480b-free") { + apiModelId = "qwen-3-coder-480b" + } + + return { + id: apiModelId, + info: this.providerModels[originalModelId], // Use original model info for rate limits/descriptions + } + } + + async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { + id: model, + info: { maxTokens: max_tokens }, + } = this.getModel() + const temperature = this.options.modelTemperature ?? CEREBRAS_DEFAULT_TEMPERATURE + + // Convert Anthropic messages to OpenAI format, then flatten for Cerebras + // This will automatically strip thinking tokens from assistant messages + const openaiMessages = convertToOpenAiMessages(messages) + const cerebrasMessages = convertToCerebrasMessages(openaiMessages) + + // Prepare request body following Cerebras API specification exactly + const requestBody = { + model, + messages: [{ role: "system", content: systemPrompt }, ...cerebrasMessages], + stream: true, + // Use max_completion_tokens (Cerebras-specific parameter) + ...(max_tokens && max_tokens > 0 && max_tokens <= 32768 ? { max_completion_tokens: max_tokens } : {}), + // Clamp temperature to Cerebras range (0 to 1.5) + ...(temperature !== undefined && temperature !== CEREBRAS_DEFAULT_TEMPERATURE + ? { + temperature: Math.max(0, Math.min(1.5, temperature)), + } + : {}), + } + + try { + const response = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, { + method: "POST", + headers: { + ...DEFAULT_HEADERS, + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(requestBody), + }) + + if (!response.ok) { + const errorText = await response.text() + + let errorMessage = "Unknown error" + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.message || errorJson.message || JSON.stringify(errorJson, null, 2) + } catch { + errorMessage = errorText || `HTTP ${response.status}` + } + + // Provide more actionable error messages + if (response.status === 401) { + throw new Error(t("common:errors.cerebras.authenticationFailed")) + } else if (response.status === 403) { + throw new Error(t("common:errors.cerebras.accessForbidden")) + } else if (response.status === 429) { + throw new Error(t("common:errors.cerebras.rateLimitExceeded")) + } else if (response.status >= 500) { + throw new Error(t("common:errors.cerebras.serverError", { status: response.status })) + } else { + throw new Error( + t("common:errors.cerebras.genericError", { status: response.status, message: errorMessage }), + ) + } + } + + if (!response.body) { + throw new Error(t("common:errors.cerebras.noResponseBody")) + } + + // Initialize XmlMatcher to parse ... tags + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + let inputTokens = 0 + let outputTokens = 0 + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" // Keep the last incomplete line in the buffer + + for (const line of lines) { + if (line.trim() === "") continue + + try { + if (line.startsWith("data: ")) { + const jsonStr = line.slice(6).trim() + if (jsonStr === "[DONE]") { + continue + } + + const parsed = JSON.parse(jsonStr) + + // Handle text content - parse for thinking tokens + if (parsed.choices?.[0]?.delta?.content) { + const content = parsed.choices[0].delta.content + + // Use XmlMatcher to parse... tags + for (const chunk of matcher.update(content)) { + yield chunk + } + } + + // Handle usage information if available + if (parsed.usage) { + inputTokens = parsed.usage.prompt_tokens || 0 + outputTokens = parsed.usage.completion_tokens || 0 + } + } + } catch (error) { + // Silently ignore malformed streaming data lines + } + } + } + } finally { + reader.releaseLock() + } + + // Process any remaining content in the matcher + for (const chunk of matcher.final()) { + yield chunk + } + + // Provide token usage estimate if not available from API + if (inputTokens === 0 || outputTokens === 0) { + const inputText = systemPrompt + cerebrasMessages.map((m) => m.content).join("") + inputTokens = inputTokens || Math.ceil(inputText.length / 4) // Rough estimate: 4 chars per token + outputTokens = outputTokens || Math.ceil((max_tokens || 1000) / 10) // Rough estimate + } + + // Store usage for cost calculation + this.lastUsage = { inputTokens, outputTokens } + + yield { + type: "usage", + inputTokens, + outputTokens, + } + } catch (error) { + if (error instanceof Error) { + throw new Error(t("common:errors.cerebras.completionError", { error: error.message })) + } + throw error + } + } + + async completePrompt(prompt: string): Promise{ + const { id: model } = this.getModel() + + // Prepare request body for non-streaming completion + const requestBody = { + model, + messages: [{ role: "user", content: prompt }], + stream: false, + } + + try { + const response = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, { + method: "POST", + headers: { + ...DEFAULT_HEADERS, + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(requestBody), + }) + + if (!response.ok) { + const errorText = await response.text() + + // Provide consistent error handling with createMessage + if (response.status === 401) { + throw new Error(t("common:errors.cerebras.authenticationFailed")) + } else if (response.status === 403) { + throw new Error(t("common:errors.cerebras.accessForbidden")) + } else if (response.status === 429) { + throw new Error(t("common:errors.cerebras.rateLimitExceeded")) + } else if (response.status >= 500) { + throw new Error(t("common:errors.cerebras.serverError", { status: response.status })) + } else { + throw new Error( + t("common:errors.cerebras.genericError", { status: response.status, message: errorText }), + ) + } + } + + const result = await response.json() + return result.choices?.[0]?.message?.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(t("common:errors.cerebras.completionError", { error: error.message })) + } + throw error + } + } + + getApiCost(metadata: ApiHandlerCreateMessageMetadata): number { + const { info } = this.getModel() + // Use actual token usage from the last request + const { inputTokens, outputTokens } = this.lastUsage + return calculateApiCostOpenAI(info, inputTokens, outputTokens) + } +} diff --git a/src/api/providers/doubao.ts b/src/api/providers/doubao.ts new file mode 100644 index 0000000000..a1337ed558 --- /dev/null +++ b/src/api/providers/doubao.ts @@ -0,0 +1,81 @@ +import { OpenAiHandler } from "./openai" +import type { ApiHandlerOptions } from "../../shared/api" +import { DOUBAO_API_BASE_URL, doubaoDefaultModelId, doubaoModels } from "@roo-code/types" +import { getModelParams } from "../transform/model-params" +import { ApiStreamUsageChunk } from "../transform/stream" + +// Core types for Doubao API +interface ChatCompletionMessageParam { + role: "system" | "user" | "assistant" | "developer" + content: + | string + | Array<{ + type: "text" | "image_url" + text?: string + image_url?: { url: string } + }> +} + +interface ChatCompletionParams { + model: string + messages: ChatCompletionMessageParam[] + temperature?: number + stream?: boolean + stream_options?: { include_usage: boolean } + max_completion_tokens?: number +} + +interface ChatCompletion { + choices: Array<{ + message: { + content: string + } + }> + usage?: { + prompt_tokens: number + completion_tokens: number + } +} + +interface ChatCompletionChunk { + choices: Array<{ + delta: { + content?: string + } + }> + usage?: { + prompt_tokens: number + completion_tokens: number + } +} + +export class DoubaoHandler extends OpenAiHandler { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + openAiApiKey: options.doubaoApiKey ?? "not-provided", + openAiModelId: options.apiModelId ?? doubaoDefaultModelId, + openAiBaseUrl: options.doubaoBaseUrl ?? DOUBAO_API_BASE_URL, + openAiStreamingEnabled: true, + includeMaxTokens: true, + }) + } + + override getModel() { + const id = this.options.apiModelId ?? doubaoDefaultModelId + const info = doubaoModels[id as keyof typeof doubaoModels] || doubaoModels[doubaoDefaultModelId] + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + return { id, info, ...params } + } + + // Override to handle Doubao's usage metrics, including caching. + protected override processUsageMetrics(usage: any): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage?.prompt_tokens || 0, + outputTokens: usage?.completion_tokens || 0, + cacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens, + cacheReadTokens: usage?.prompt_tokens_details?.cached_tokens, + } + } +} diff --git a/src/api/providers/featherless.ts b/src/api/providers/featherless.ts new file mode 100644 index 0000000000..2a985e2a87 --- /dev/null +++ b/src/api/providers/featherless.ts @@ -0,0 +1,108 @@ +import { + DEEP_SEEK_DEFAULT_TEMPERATURE, + type FeatherlessModelId, + featherlessDefaultModelId, + featherlessModels, +} from "@roo-code/types" +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import type { ApiHandlerOptions } from "../../shared/api" +import { XmlMatcher } from "../../utils/xml-matcher" +import { convertToR1Format } from "../transform/r1-format" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +export class FeatherlessHandler extends BaseOpenAiCompatibleProvider { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + providerName: "Featherless", + baseURL: "https://api.featherless.ai/v1", + apiKey: options.featherlessApiKey, + defaultProviderModelId: featherlessDefaultModelId, + providerModels: featherlessModels, + defaultTemperature: 0.5, + }) + } + + private getCompletionParams( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + ): OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming { + const { + id: model, + info: { maxTokens: max_tokens }, + } = this.getModel() + + const temperature = this.options.modelTemperature ?? this.getModel().info.temperature + + return { + model, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + } + } + + override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() + + if (model.id.includes("DeepSeek-R1")) { + const stream = await this.client.chat.completions.create({ + ...this.getCompletionParams(systemPrompt, messages), + messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]), + }) + + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + + if (delta?.content) { + for (const processedChunk of matcher.update(delta.content)) { + yield processedChunk + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + + // Process any remaining content + for (const processedChunk of matcher.final()) { + yield processedChunk + } + } else { + yield* super.createMessage(systemPrompt, messages) + } + } + + override getModel() { + const model = super.getModel() + const isDeepSeekR1 = model.id.includes("DeepSeek-R1") + return { + ...model, + info: { + ...model.info, + temperature: isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : this.defaultTemperature, + }, + } + } +} diff --git a/src/api/providers/fetchers/__tests__/lmstudio.test.ts b/src/api/providers/fetchers/__tests__/lmstudio.test.ts index 98fe5db32e..8e7e36c73f 100644 --- a/src/api/providers/fetchers/__tests__/lmstudio.test.ts +++ b/src/api/providers/fetchers/__tests__/lmstudio.test.ts @@ -118,7 +118,7 @@ describe("LMStudio Fetcher", () => { expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl }) expect(mockListDownloadedModels).toHaveBeenCalledTimes(1) expect(mockListDownloadedModels).toHaveBeenCalledWith("llm") - expect(mockListLoaded).not.toHaveBeenCalled() + expect(mockListLoaded).toHaveBeenCalled() // we now call it to get context data const expectedParsedModel = parseLMStudioModel(mockLLMInfo) expect(result).toEqual({ [mockLLMInfo.path]: expectedParsedModel }) @@ -143,6 +143,228 @@ describe("LMStudio Fetcher", () => { expect(result).toEqual({ [mockRawModel.modelKey]: expectedParsedModel }) }) + it("should deduplicate models when both downloaded and loaded", async () => { + const mockDownloadedModel: LLMInfo = { + type: "llm" as const, + modelKey: "mistralai/devstral-small-2505", + format: "safetensors", + displayName: "Devstral Small 2505", + path: "mistralai/devstral-small-2505", + sizeBytes: 13277565112, + architecture: "mistral", + vision: false, + trainedForToolUse: false, + maxContextLength: 131072, + } + + const mockLoadedModel: LLMInstanceInfo = { + type: "llm", + modelKey: "devstral-small-2505", // Different key but should match case-insensitively + format: "safetensors", + displayName: "Devstral Small 2505", + path: "mistralai/devstral-small-2505", + sizeBytes: 13277565112, + architecture: "mistral", + identifier: "mistralai/devstral-small-2505", + instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ", + vision: false, + trainedForToolUse: false, + maxContextLength: 131072, + contextLength: 7161, // Runtime context info + } + + mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) + mockListDownloadedModels.mockResolvedValueOnce([mockDownloadedModel]) + mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }]) + + const result = await getLMStudioModels(baseUrl) + + // Should only have one model, with the loaded model replacing the downloaded one + expect(Object.keys(result)).toHaveLength(1) + + // The loaded model's key should be used, with loaded model's data + const expectedParsedModel = parseLMStudioModel(mockLoadedModel) + expect(result[mockLoadedModel.modelKey]).toEqual(expectedParsedModel) + + // The downloaded model should have been removed + expect(result[mockDownloadedModel.path]).toBeUndefined() + }) + + it("should handle deduplication with path-based matching", async () => { + const mockDownloadedModel: LLMInfo = { + type: "llm" as const, + modelKey: "Meta/Llama-3.1/8B-Instruct", + format: "gguf", + displayName: "Llama 3.1 8B Instruct", + path: "Meta/Llama-3.1/8B-Instruct", + sizeBytes: 8000000000, + architecture: "llama", + vision: false, + trainedForToolUse: false, + maxContextLength: 8192, + } + + const mockLoadedModel: LLMInstanceInfo = { + type: "llm", + modelKey: "Llama-3.1", // Should match the path segment + format: "gguf", + displayName: "Llama 3.1", + path: "Meta/Llama-3.1/8B-Instruct", + sizeBytes: 8000000000, + architecture: "llama", + identifier: "Meta/Llama-3.1/8B-Instruct", + instanceReference: "ABC123", + vision: false, + trainedForToolUse: false, + maxContextLength: 8192, + contextLength: 4096, + } + + mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) + mockListDownloadedModels.mockResolvedValueOnce([mockDownloadedModel]) + mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }]) + + const result = await getLMStudioModels(baseUrl) + + expect(Object.keys(result)).toHaveLength(1) + expect(result[mockLoadedModel.modelKey]).toBeDefined() + expect(result[mockDownloadedModel.path]).toBeUndefined() + }) + + it("should not deduplicate models with similar but distinct names", async () => { + const mockDownloadedModels: LLMInfo[] = [ + { + type: "llm" as const, + modelKey: "mistral-7b", + format: "gguf", + displayName: "Mistral 7B", + path: "mistralai/mistral-7b-instruct", + sizeBytes: 7000000000, + architecture: "mistral", + vision: false, + trainedForToolUse: false, + maxContextLength: 4096, + }, + { + type: "llm" as const, + modelKey: "codellama", + format: "gguf", + displayName: "Code Llama", + path: "meta/codellama/7b", + sizeBytes: 7000000000, + architecture: "llama", + vision: false, + trainedForToolUse: false, + maxContextLength: 4096, + }, + ] + + const mockLoadedModel: LLMInstanceInfo = { + type: "llm", + modelKey: "llama", // Should not match "codellama" or "mistral-7b" + format: "gguf", + displayName: "Llama", + path: "meta/llama/7b", + sizeBytes: 7000000000, + architecture: "llama", + identifier: "meta/llama/7b", + instanceReference: "XYZ789", + vision: false, + trainedForToolUse: false, + maxContextLength: 4096, + contextLength: 2048, + } + + mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) + mockListDownloadedModels.mockResolvedValueOnce(mockDownloadedModels) + mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }]) + + const result = await getLMStudioModels(baseUrl) + + // Should have 3 models: mistral-7b (not deduped), codellama (not deduped), and llama (loaded) + expect(Object.keys(result)).toHaveLength(3) + expect(result["mistralai/mistral-7b-instruct"]).toBeDefined() // Should NOT be removed + expect(result["meta/codellama/7b"]).toBeDefined() // Should NOT be removed (codellama != llama) + expect(result[mockLoadedModel.modelKey]).toBeDefined() + }) + + it("should handle multiple loaded models with various duplicate scenarios", async () => { + const mockDownloadedModels: LLMInfo[] = [ + { + type: "llm" as const, + modelKey: "mistral-7b", + format: "gguf", + displayName: "Mistral 7B", + path: "mistralai/mistral-7b/instruct", + sizeBytes: 7000000000, + architecture: "mistral", + vision: false, + trainedForToolUse: false, + maxContextLength: 8192, + }, + { + type: "llm" as const, + modelKey: "llama-3.1", + format: "gguf", + displayName: "Llama 3.1", + path: "meta/llama-3.1/8b", + sizeBytes: 8000000000, + architecture: "llama", + vision: false, + trainedForToolUse: false, + maxContextLength: 8192, + }, + ] + + const mockLoadedModels: LLMInstanceInfo[] = [ + { + type: "llm", + modelKey: "mistral-7b", // Exact match with path segment + format: "gguf", + displayName: "Mistral 7B", + path: "mistralai/mistral-7b/instruct", + sizeBytes: 7000000000, + architecture: "mistral", + identifier: "mistralai/mistral-7b/instruct", + instanceReference: "REF1", + vision: false, + trainedForToolUse: false, + maxContextLength: 8192, + contextLength: 4096, + }, + { + type: "llm", + modelKey: "gpt-4", // No match, new model + format: "gguf", + displayName: "GPT-4", + path: "openai/gpt-4", + sizeBytes: 10000000000, + architecture: "gpt", + identifier: "openai/gpt-4", + instanceReference: "REF2", + vision: true, + trainedForToolUse: true, + maxContextLength: 32768, + contextLength: 16384, + }, + ] + + mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) + mockListDownloadedModels.mockResolvedValueOnce(mockDownloadedModels) + mockListLoaded.mockResolvedValueOnce( + mockLoadedModels.map((model) => ({ getModelInfo: vi.fn().mockResolvedValueOnce(model) })), + ) + + const result = await getLMStudioModels(baseUrl) + + // Should have 3 models: llama-3.1 (downloaded), mistral-7b (loaded, replaced), gpt-4 (loaded, new) + expect(Object.keys(result)).toHaveLength(3) + expect(result["meta/llama-3.1/8b"]).toBeDefined() // Downloaded, not replaced + expect(result["mistralai/mistral-7b/instruct"]).toBeUndefined() // Downloaded, replaced + expect(result["mistral-7b"]).toBeDefined() // Loaded, replaced downloaded + expect(result["gpt-4"]).toBeDefined() // Loaded, new + }) + it("should use default baseUrl if an empty string is provided", async () => { const defaultBaseUrl = "http://localhost:1234" const defaultLmsUrl = "ws://localhost:1234" diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 69369a2ce8..2a72ef1cc5 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -24,6 +24,7 @@ vi.mock("../openrouter") vi.mock("../requesty") vi.mock("../glama") vi.mock("../unbound") +vi.mock("../io-intelligence") // Then imports import type { Mock } from "vitest" @@ -33,15 +34,18 @@ import { getOpenRouterModels } from "../openrouter" import { getRequestyModels } from "../requesty" import { getGlamaModels } from "../glama" import { getUnboundModels } from "../unbound" +import { getIOIntelligenceModels } from "../io-intelligence" const mockGetLiteLLMModels = getLiteLLMModels as Mock const mockGetOpenRouterModels = getOpenRouterModels as Mock const mockGetRequestyModels = getRequestyModels as Mock const mockGetGlamaModels = getGlamaModels as Mock const mockGetUnboundModels = getUnboundModels as Mock +const mockGetIOIntelligenceModels = getIOIntelligenceModels as Mock const DUMMY_REQUESTY_KEY = "requesty-key-for-testing" const DUMMY_UNBOUND_KEY = "unbound-key-for-testing" +const DUMMY_IOINTELLIGENCE_KEY = "io-intelligence-key-for-testing" describe("getModels with new GetModelsOptions", () => { beforeEach(() => { @@ -99,7 +103,7 @@ describe("getModels with new GetModelsOptions", () => { const result = await getModels({ provider: "requesty", apiKey: DUMMY_REQUESTY_KEY }) - expect(mockGetRequestyModels).toHaveBeenCalledWith(DUMMY_REQUESTY_KEY) + expect(mockGetRequestyModels).toHaveBeenCalledWith(undefined, DUMMY_REQUESTY_KEY) expect(result).toEqual(mockModels) }) @@ -137,6 +141,23 @@ describe("getModels with new GetModelsOptions", () => { expect(result).toEqual(mockModels) }) + it("calls IOIntelligenceModels for IO-Intelligence provider", async () => { + const mockModels = { + "io-intelligence/model": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "IO Intelligence Model", + }, + } + mockGetIOIntelligenceModels.mockResolvedValue(mockModels) + + const result = await getModels({ provider: "io-intelligence", apiKey: DUMMY_IOINTELLIGENCE_KEY }) + + expect(mockGetIOIntelligenceModels).toHaveBeenCalled() + expect(result).toEqual(mockModels) + }) + it("handles errors and re-throws them", async () => { const expectedError = new Error("LiteLLM connection failed") mockGetLiteLLMModels.mockRejectedValue(expectedError) diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index f0ebead30f..e0ab7f5c9a 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -11,7 +11,7 @@ import { OPEN_ROUTER_REQUIRED_REASONING_BUDGET_MODELS, } from "@roo-code/types" -import { getOpenRouterModelEndpoints, getOpenRouterModels } from "../openrouter" +import { getOpenRouterModelEndpoints, getOpenRouterModels, parseOpenRouterModel } from "../openrouter" nockBack.fixtures = path.join(__dirname, "fixtures") nockBack.setMode("lockdown") @@ -24,6 +24,7 @@ describe("OpenRouter API", () => { const models = await getOpenRouterModels() const openRouterSupportedCaching = Object.entries(models) + .filter(([id, _]) => id.startsWith("anthropic/claude") || id.startsWith("google/gemini")) // only these support cache_control breakpoints (https://openrouter.ai/docs/features/prompt-caching) .filter(([_, model]) => model.supportsPromptCache) .map(([id, _]) => id) @@ -32,6 +33,7 @@ describe("OpenRouter API", () => { "google/gemini-2.5-pro-preview", // Excluded due to lag issue (#4487) "google/gemini-2.5-flash", // OpenRouter doesn't report this as supporting prompt caching "google/gemini-2.5-flash-lite-preview-06-17", // OpenRouter doesn't report this as supporting prompt caching + "anthropic/claude-opus-4.1", // Not yet available in OpenRouter API ]) const ourCachingModels = Array.from(OPEN_ROUTER_PROMPT_CACHING_MODELS).filter( @@ -48,12 +50,20 @@ describe("OpenRouter API", () => { expect(ourCachingModels.sort()).toEqual(expectedCachingModels) + const excludedComputerUseModels = new Set([ + "anthropic/claude-opus-4.1", // Not yet available in OpenRouter API + ]) + + const expectedComputerUseModels = Array.from(OPEN_ROUTER_COMPUTER_USE_MODELS) + .filter((id) => !excludedComputerUseModels.has(id)) + .sort() + expect( Object.entries(models) .filter(([_, model]) => model.supportsComputerUse) .map(([id, _]) => id) .sort(), - ).toEqual(Array.from(OPEN_ROUTER_COMPUTER_USE_MODELS).sort()) + ).toEqual(expectedComputerUseModels) expect( Object.entries(models) @@ -67,6 +77,7 @@ describe("OpenRouter API", () => { "anthropic/claude-3.7-sonnet:beta", "anthropic/claude-3.7-sonnet:thinking", "anthropic/claude-opus-4", + // "anthropic/claude-opus-4.1", // Not yet available in OpenRouter API "anthropic/claude-sonnet-4", "arliai/qwq-32b-arliai-rpr-v1:free", "cognitivecomputations/dolphin3.0-r1-mistral-24b:free", @@ -122,6 +133,7 @@ describe("OpenRouter API", () => { "google/gemini-2.5-flash", "google/gemini-2.5-flash-lite-preview-06-17", "google/gemini-2.5-pro", + "anthropic/claude-opus-4.1", // Not yet available in OpenRouter API ]) const expectedReasoningBudgetModels = Array.from(OPEN_ROUTER_REASONING_BUDGET_MODELS) @@ -218,7 +230,7 @@ describe("OpenRouter API", () => { const endpoints = await getOpenRouterModelEndpoints("google/gemini-2.5-pro-preview") expect(endpoints).toEqual({ - Google: { + "google-vertex": { maxTokens: 65535, contextWindow: 1048576, supportsImages: true, @@ -232,7 +244,7 @@ describe("OpenRouter API", () => { supportsReasoningEffort: undefined, supportedParameters: undefined, }, - "Google AI Studio": { + "google-ai-studio": { maxTokens: 65536, contextWindow: 1048576, supportsImages: true, @@ -251,4 +263,75 @@ describe("OpenRouter API", () => { nockDone() }) }) + + describe("parseOpenRouterModel", () => { + it("sets horizon-alpha model to 32k max tokens", () => { + const mockModel = { + name: "Horizon Alpha", + description: "Test model", + context_length: 128000, + max_completion_tokens: 128000, + pricing: { + prompt: "0.000003", + completion: "0.000015", + }, + } + + const result = parseOpenRouterModel({ + id: "openrouter/horizon-alpha", + model: mockModel, + modality: "text", + maxTokens: 128000, + }) + + expect(result.maxTokens).toBe(32768) + expect(result.contextWindow).toBe(128000) + }) + + it("sets horizon-beta model to 32k max tokens", () => { + const mockModel = { + name: "Horizon Beta", + description: "Test model", + context_length: 128000, + max_completion_tokens: 128000, + pricing: { + prompt: "0.000003", + completion: "0.000015", + }, + } + + const result = parseOpenRouterModel({ + id: "openrouter/horizon-beta", + model: mockModel, + modality: "text", + maxTokens: 128000, + }) + + expect(result.maxTokens).toBe(32768) + expect(result.contextWindow).toBe(128000) + }) + + it("does not override max tokens for other models", () => { + const mockModel = { + name: "Other Model", + description: "Test model", + context_length: 128000, + max_completion_tokens: 64000, + pricing: { + prompt: "0.000003", + completion: "0.000015", + }, + } + + const result = parseOpenRouterModel({ + id: "openrouter/other-model", + model: mockModel, + modality: "text", + maxTokens: 64000, + }) + + expect(result.maxTokens).toBe(64000) + expect(result.contextWindow).toBe(128000) + }) + }) }) diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts new file mode 100644 index 0000000000..326cefb0cc --- /dev/null +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -0,0 +1,189 @@ +import axios from "axios" +import { z } from "zod" +import type { ModelInfo } from "@roo-code/types" +import { IO_INTELLIGENCE_CACHE_DURATION } from "@roo-code/types" +import type { ModelRecord } from "../../../shared/api" + +/** + * IO Intelligence Model Schema + */ +const ioIntelligenceModelSchema = z.object({ + id: z.string(), + object: z.literal("model"), + created: z.number(), + owned_by: z.string(), + root: z.string().nullable().optional(), + parent: z.string().nullable().optional(), + max_model_len: z.number().nullable().optional(), + permission: z.array( + z.object({ + id: z.string(), + object: z.literal("model_permission"), + created: z.number(), + allow_create_engine: z.boolean(), + allow_sampling: z.boolean(), + allow_logprobs: z.boolean(), + allow_search_indices: z.boolean(), + allow_view: z.boolean(), + allow_fine_tuning: z.boolean(), + organization: z.string(), + group: z.string().nullable(), + is_blocking: z.boolean(), + }), + ), +}) + +export type IOIntelligenceModel = z.infer + +/** + * IO Intelligence API Response Schema + */ +const ioIntelligenceApiResponseSchema = z.object({ + object: z.literal("list"), + data: z.array(ioIntelligenceModelSchema), +}) + +type IOIntelligenceApiResponse = z.infer + +/** + * Cache entry for storing fetched models + */ +interface CacheEntry { + data: ModelRecord + timestamp: number +} + +let cache: CacheEntry | null = null + +/** + * Model context length mapping based on the documentation + * 1 + */ +const MODEL_CONTEXT_LENGTHS: Record= { + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": 430000, + "deepseek-ai/DeepSeek-R1-0528": 128000, + "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": 106000, + "openai/gpt-oss-120b": 131072, +} + +/** + * Vision models that support images + */ +const VISION_MODELS = new Set([ + "Qwen/Qwen2.5-VL-32B-Instruct", + "meta-llama/Llama-3.2-90B-Vision-Instruct", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", +]) + +/** + * Parse an IO Intelligence model into ModelInfo format + */ +function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo { + const contextLength = MODEL_CONTEXT_LENGTHS[model.id] || 8192 + // Cap maxTokens at 32k for very large context windows, or 20% of context length, whichever is smaller + const maxTokens = Math.min(contextLength, Math.ceil(contextLength * 0.2), 32768) + const supportsImages = VISION_MODELS.has(model.id) + + return { + maxTokens, + contextWindow: contextLength, + supportsImages, + supportsPromptCache: false, + supportsComputerUse: false, + description: `${model.id} via IO Intelligence`, + } +} + +/** + * Fetches available models from IO Intelligence + * 1 + */ +export async function getIOIntelligenceModels(apiKey?: string): Promise{ + const now = Date.now() + + // Check cache + if (cache && now - cache.timestamp < IO_INTELLIGENCE_CACHE_DURATION) { + return cache.data + } + + const models: ModelRecord = {} + + try { + const headers: Record = { + "Content-Type": "application/json", + } + + // Add authorization header if API key is provided + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}` + } else { + console.error("IO Intelligence API key is required") + throw new Error("IO Intelligence API key is required") + } + + const response = await axios.get