This commit is contained in:
marko-kraemer 2025-08-29 02:37:12 -07:00
parent 84c83f29d8
commit 764ab6bb1b
34 changed files with 12 additions and 1233 deletions

View File

@ -130,108 +130,6 @@ REDIS_PASSWORD=
---
## Feature Flags
The backend includes a Redis-backed feature flag system that allows you to control feature availability without code deployments.
### Setup
The feature flag system uses the existing Redis service and is automatically available when Redis is running.
### CLI Management
Use the CLI tool to manage feature flags:
```bash
cd backend/flags
python setup.py <command> [arguments]
```
#### Available Commands
**Enable a feature flag:**
```bash
python setup.py enable test_flag "Test decsription"
```
**Disable a feature flag:**
```bash
python setup.py disable test_flag
```
**List all feature flags:**
```bash
python setup.py list
```
### API Endpoints
Feature flags are accessible via REST API:
**Get all feature flags:**
```bash
GET /feature-flags
```
**Get specific feature flag:**
```bash
GET /feature-flags/{flag_name}
```
Example response:
```json
{
"test_flag": {
"enabled": true,
"description": "Test flag",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
### Backend Integration
Use feature flags in your Python code:
```python
from flags.flags import is_enabled
# Check if a feature is enabled
if await is_enabled('test_flag'):
# Feature-specific logic
pass
# With fallback value
enabled = await is_enabled('new_feature', default=False)
```
### Current Feature Flags
The system currently supports these feature flags:
- **`custom_agents`**: Controls custom agent creation and management
- **`agent_marketplace`**: Controls agent marketplace functionality
### Error Handling
The feature flag system includes robust error handling:
- If Redis is unavailable, flags default to `False`
- API endpoints return empty objects on Redis errors
- CLI operations show clear error messages
### Caching
- Backend operations are direct Redis calls (no caching)
- Frontend includes 5-minute caching for performance
- Use `clearCache()` in frontend to force refresh
---
## Production Setup

View File

@ -5,7 +5,6 @@ from utils.auth_utils import get_current_user_id_from_jwt
from utils.logger import logger
from utils.config import config, EnvMode
from utils.pagination import PaginationParams
from flags.flags import is_enabled
from models import model_manager
from ..models import (
@ -24,11 +23,6 @@ async def update_agent(
agent_data: AgentUpdateRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agent currently disabled. This feature is not available at the moment."
)
logger.debug(f"Updating agent {agent_id} for user: {user_id}")
# Debug logging for icon fields
@ -443,11 +437,6 @@ async def update_agent(
@router.delete("/agents/{agent_id}")
async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agent currently disabled. This feature is not available at the moment."
)
logger.debug(f"Deleting agent: {agent_id}")
client = await utils.db.client
@ -525,11 +514,6 @@ async def get_agents(
tools: Optional[str] = Query(None, description="Comma-separated list of tools to filter by"),
content_type: Optional[str] = Query(None, description="Content type filter: 'agents', 'templates', or None for agents only")
):
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
try:
from .agent_service import AgentService, AgentFilters
@ -587,11 +571,6 @@ async def get_agents(
@router.get("/agents/{agent_id}", response_model=AgentResponse)
async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
logger.debug(f"Fetching agent {agent_id} for user: {user_id}")
@ -721,11 +700,6 @@ async def create_agent(
user_id: str = Depends(get_current_user_id_from_jwt)
):
logger.debug(f"Creating new agent for user: {user_id}")
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
client = await utils.db.client
from ..utils import check_agent_count_limit

View File

@ -5,7 +5,6 @@ from uuid import uuid4
from utils.auth_utils import get_current_user_id_from_jwt
from utils.logger import logger
from flags.flags import is_enabled
from templates.template_service import MCPRequirementValue, ConfigType, ProfileId, QualifiedName
from ..models import JsonAnalysisRequest, JsonAnalysisResponse, JsonImportRequestModel, JsonImportResponse
@ -409,11 +408,6 @@ async def analyze_json_for_import(
"""Analyze imported JSON to determine required credentials and configurations"""
logger.debug(f"Analyzing JSON for import - user: {user_id}")
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
try:
import_service = JsonImportService(utils.db)
@ -433,11 +427,6 @@ async def import_agent_from_json(
):
logger.debug(f"Importing agent from JSON - user: {user_id}")
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
client = await utils.db.client
from ..utils import check_agent_count_limit

View File

@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException, Depends, Request, Body
from utils.auth_utils import get_current_user_id_from_jwt
from utils.logger import logger
from flags.flags import is_enabled
from sandbox.sandbox import get_or_start_sandbox
from services.supabase import DBConnection
from agentpress.thread_manager import ThreadManager
@ -319,8 +318,6 @@ async def get_agent_tools(
agent_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("custom_agents"):
raise HTTPException(status_code=403, detail="Custom agents currently disabled")
logger.debug(f"Fetching enabled tools for agent: {agent_id} by user: {user_id}")
client = await utils.db.client

View File

@ -316,14 +316,14 @@ class PromptManager:
# Construct a well-formatted knowledge base section
kb_section = f"""
=== AGENT KNOWLEDGE BASE ===
NOTICE: The following is your specialized knowledge base. This information should be considered authoritative for your responses and should take precedence over general knowledge when relevant.
=== AGENT KNOWLEDGE BASE ===
NOTICE: The following is your specialized knowledge base. This information should be considered authoritative for your responses and should take precedence over general knowledge when relevant.
{kb_result.data}
{kb_result.data}
=== END AGENT KNOWLEDGE BASE ===
=== END AGENT KNOWLEDGE BASE ===
IMPORTANT: Always reference and utilize the knowledge base information above when it's relevant to user queries. This knowledge is specific to your role and capabilities."""
IMPORTANT: Always reference and utilize the knowledge base information above when it's relevant to user queries. This knowledge is specific to your role and capabilities."""
system_content += kb_section
else:

View File

@ -195,11 +195,6 @@ Approach each research task methodically, starting with broad searches and then
client = await self.db.client
from flags.flags import is_enabled
if not await is_enabled("custom_agents"):
return self.fail_response(
"Custom agents are currently disabled. This feature is not available at the moment."
)
from agent.utils import check_agent_count_limit
limit_check = await check_agent_count_limit(client, account_id)

View File

@ -23,7 +23,6 @@ from agent import api as agent_api
from sandbox import api as sandbox_api
from services import billing as billing_api
from flags import api as feature_flags_api
from services import transcription as transcription_api
import sys
from services import email_api
@ -159,7 +158,6 @@ api_router = APIRouter()
api_router.include_router(agent_api.router)
api_router.include_router(sandbox_api.router)
api_router.include_router(billing_api.router)
api_router.include_router(feature_flags_api.router)
api_router.include_router(api_keys_api.router)
from mcp_module import api as mcp_api

View File

@ -1,33 +0,0 @@
from fastapi import APIRouter
from utils.logger import logger
from .flags import list_flags, is_enabled, get_flag_details
router = APIRouter()
@router.get("/feature-flags")
async def get_feature_flags():
try:
flags = await list_flags()
return {"flags": flags}
except Exception as e:
logger.error(f"Error fetching feature flags: {str(e)}")
return {"flags": {}}
@router.get("/feature-flags/{flag_name}")
async def get_feature_flag(flag_name: str):
try:
enabled = await is_enabled(flag_name)
details = await get_flag_details(flag_name)
return {
"flag_name": flag_name,
"enabled": enabled,
"details": details
}
except Exception as e:
logger.error(f"Error fetching feature flag {flag_name}: {str(e)}")
return {
"flag_name": flag_name,
"enabled": False,
"details": None
}

View File

@ -1,181 +0,0 @@
import json
import logging
import os
from datetime import datetime
from typing import Dict, List, Optional
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from services import redis
logger = logging.getLogger(__name__)
class FeatureFlagManager:
def __init__(self):
"""Initialize with existing Redis service"""
self.flag_prefix = "feature_flag:"
self.flag_list_key = "feature_flags:list"
async def set_flag(self, key: str, enabled: bool, description: str = "") -> bool:
"""Set a feature flag to enabled or disabled"""
try:
flag_key = f"{self.flag_prefix}{key}"
flag_data = {
'enabled': str(enabled).lower(),
'description': description,
'updated_at': datetime.utcnow().isoformat()
}
# Use the existing Redis service
redis_client = await redis.get_client()
await redis_client.hset(flag_key, mapping=flag_data)
await redis_client.sadd(self.flag_list_key, key)
logger.debug(f"Set feature flag {key} to {enabled}")
return True
except Exception as e:
logger.error(f"Failed to set feature flag {key}: {e}")
return False
async def is_enabled(self, key: str) -> bool:
"""Check if a feature flag is enabled"""
try:
flag_key = f"{self.flag_prefix}{key}"
redis_client = await redis.get_client()
enabled = await redis_client.hget(flag_key, 'enabled')
return enabled == 'true' if enabled else False
except Exception as e:
logger.error(f"Failed to check feature flag {key}: {e}")
# Return False by default if Redis is unavailable
return False
async def get_flag(self, key: str) -> Optional[Dict[str, str]]:
"""Get feature flag details"""
try:
flag_key = f"{self.flag_prefix}{key}"
redis_client = await redis.get_client()
flag_data = await redis_client.hgetall(flag_key)
return flag_data if flag_data else None
except Exception as e:
logger.error(f"Failed to get feature flag {key}: {e}")
return None
async def delete_flag(self, key: str) -> bool:
"""Delete a feature flag"""
try:
flag_key = f"{self.flag_prefix}{key}"
redis_client = await redis.get_client()
deleted = await redis_client.delete(flag_key)
if deleted:
await redis_client.srem(self.flag_list_key, key)
logger.debug(f"Deleted feature flag: {key}")
return True
return False
except Exception as e:
logger.error(f"Failed to delete feature flag {key}: {e}")
return False
async def list_flags(self) -> Dict[str, bool]:
"""List all feature flags with their status"""
try:
redis_client = await redis.get_client()
flag_keys = await redis_client.smembers(self.flag_list_key)
flags = {}
for key in flag_keys:
flags[key] = await self.is_enabled(key)
return flags
except Exception as e:
logger.error(f"Failed to list feature flags: {e}")
return {}
async def get_all_flags_details(self) -> Dict[str, Dict[str, str]]:
"""Get all feature flags with detailed information"""
try:
redis_client = await redis.get_client()
flag_keys = await redis_client.smembers(self.flag_list_key)
flags = {}
for key in flag_keys:
flag_data = await self.get_flag(key)
if flag_data:
flags[key] = flag_data
return flags
except Exception as e:
logger.error(f"Failed to get all flags details: {e}")
return {}
_flag_manager: Optional[FeatureFlagManager] = None
def get_flag_manager() -> FeatureFlagManager:
"""Get the global feature flag manager instance"""
global _flag_manager
if _flag_manager is None:
_flag_manager = FeatureFlagManager()
return _flag_manager
# Async convenience functions
async def set_flag(key: str, enabled: bool, description: str = "") -> bool:
return await get_flag_manager().set_flag(key, enabled, description)
async def is_enabled(key: str) -> bool:
return await get_flag_manager().is_enabled(key)
async def enable_flag(key: str, description: str = "") -> bool:
return await set_flag(key, True, description)
async def disable_flag(key: str, description: str = "") -> bool:
return await set_flag(key, False, description)
async def delete_flag(key: str) -> bool:
return await get_flag_manager().delete_flag(key)
async def list_flags() -> Dict[str, bool]:
return await get_flag_manager().list_flags()
async def get_flag_details(key: str) -> Optional[Dict[str, str]]:
return await get_flag_manager().get_flag(key)
# Feature Flags
# Custom agents feature flag
custom_agents = True
# MCP module feature flag
mcp_module = True
# Templates API feature flag
templates_api = True
# Triggers API feature flag
triggers_api = True
# Workflows API feature flag
workflows_api = True
# Knowledge base feature flag
knowledge_base = True
# Pipedream integration feature flag
pipedream = True
# Credentials API feature flag
credentials_api = True
# Suna default agent feature flag
suna_default_agent = True

View File

@ -1,160 +0,0 @@
#!/usr/bin/env python3
import sys
import argparse
import asyncio
from flags import enable_flag, disable_flag, is_enabled, list_flags, delete_flag, get_flag_details
async def enable_command(flag_name: str, description: str = ""):
if await enable_flag(flag_name, description):
print(f"✓ Enabled flag: {flag_name}")
if description:
print(f" Description: {description}")
else:
print(f"✗ Failed to enable flag: {flag_name}")
async def disable_command(flag_name: str, description: str = ""):
if await disable_flag(flag_name, description):
print(f"✓ Disabled flag: {flag_name}")
if description:
print(f" Description: {description}")
else:
print(f"✗ Failed to disable flag: {flag_name}")
async def list_command():
flags = await list_flags()
if not flags:
print("No feature flags found.")
return
print("Feature Flags:")
print("-" * 50)
for flag_name, enabled in flags.items():
details = await get_flag_details(flag_name)
description = details.get('description', 'No description') if details else 'No description'
updated_at = details.get('updated_at', 'Unknown') if details else 'Unknown'
status_icon = "" if enabled else ""
status_text = "ENABLED" if enabled else "DISABLED"
print(f"{status_icon} {flag_name}: {status_text}")
print(f" Description: {description}")
print(f" Updated: {updated_at}")
print()
async def status_command(flag_name: str):
details = await get_flag_details(flag_name)
if not details:
print(f"✗ Flag '{flag_name}' not found.")
return
enabled = await is_enabled(flag_name)
status_icon = "" if enabled else ""
status_text = "ENABLED" if enabled else "DISABLED"
print(f"Flag: {flag_name}")
print(f"Status: {status_icon} {status_text}")
print(f"Description: {details.get('description', 'No description')}")
print(f"Updated: {details.get('updated_at', 'Unknown')}")
async def delete_command(flag_name: str):
if not await get_flag_details(flag_name):
print(f"✗ Flag '{flag_name}' not found.")
return
confirm = input(f"Are you sure you want to delete flag '{flag_name}'? (y/N): ")
if confirm.lower() in ['y', 'yes']:
if await delete_flag(flag_name):
print(f"✓ Deleted flag: {flag_name}")
else:
print(f"✗ Failed to delete flag: {flag_name}")
else:
print("Cancelled.")
async def toggle_command(flag_name: str, description: str = ""):
current_status = await is_enabled(flag_name)
if current_status:
await disable_command(flag_name, description)
else:
await enable_command(flag_name, description)
async def main():
parser = argparse.ArgumentParser(
description="Feature Flag Management Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python setup.py enable new_ui "Enable new user interface"
python setup.py disable beta_features "Disable beta features"
python setup.py list
python setup.py status new_ui
python setup.py toggle maintenance_mode "Toggle maintenance mode"
python setup.py delete old_feature
"""
)
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Enable command
enable_parser = subparsers.add_parser('enable', help='Enable a feature flag')
enable_parser.add_argument('flag_name', help='Name of the feature flag')
enable_parser.add_argument('description', nargs='?', default='', help='Optional description')
# Disable command
disable_parser = subparsers.add_parser('disable', help='Disable a feature flag')
disable_parser.add_argument('flag_name', help='Name of the feature flag')
disable_parser.add_argument('description', nargs='?', default='', help='Optional description')
# List command
subparsers.add_parser('list', help='List all feature flags')
# Status command
status_parser = subparsers.add_parser('status', help='Show status of a feature flag')
status_parser.add_argument('flag_name', help='Name of the feature flag')
# Delete command
delete_parser = subparsers.add_parser('delete', help='Delete a feature flag')
delete_parser.add_argument('flag_name', help='Name of the feature flag')
# Toggle command
toggle_parser = subparsers.add_parser('toggle', help='Toggle a feature flag')
toggle_parser.add_argument('flag_name', help='Name of the feature flag')
toggle_parser.add_argument('description', nargs='?', default='', help='Optional description')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
if args.command == 'enable':
await enable_command(args.flag_name, args.description)
elif args.command == 'disable':
await disable_command(args.flag_name, args.description)
elif args.command == 'list':
await list_command()
elif args.command == 'status':
await status_command(args.flag_name)
elif args.command == 'delete':
await delete_command(args.flag_name)
elif args.command == 'toggle':
await toggle_command(args.flag_name, args.description)
except KeyboardInterrupt:
print("\nOperation cancelled.")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -6,7 +6,6 @@ from utils.auth_utils import get_current_user_id_from_jwt, verify_agent_access
from services.supabase import DBConnection
from knowledge_base.file_processor import FileProcessor
from utils.logger import logger
from flags.flags import is_enabled
router = APIRouter(prefix="/knowledge-base", tags=["knowledge-base"])
@ -72,11 +71,6 @@ async def get_agent_knowledge_base(
include_inactive: bool = False,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Get all knowledge base entries for an agent"""
try:
@ -130,11 +124,6 @@ async def create_agent_knowledge_base_entry(
entry_data: CreateKnowledgeBaseEntryRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Create a new knowledge base entry for an agent"""
try:
@ -185,11 +174,6 @@ async def upload_file_to_agent_kb(
file: UploadFile = File(...),
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Upload and process a file for agent knowledge base"""
try:
@ -244,11 +228,6 @@ async def update_knowledge_base_entry(
entry_data: UpdateKnowledgeBaseEntryRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Update an agent knowledge base entry"""
try:
@ -317,11 +296,6 @@ async def delete_knowledge_base_entry(
entry_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Delete an agent knowledge base entry"""
try:
@ -357,11 +331,6 @@ async def get_knowledge_base_entry(
entry_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Get a specific agent knowledge base entry"""
try:
client = await db.client
@ -409,11 +378,6 @@ async def get_agent_processing_jobs(
limit: int = 10,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Get processing jobs for an agent"""
try:
@ -506,11 +470,6 @@ async def get_agent_knowledge_base_context(
max_tokens: int = 4000,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("knowledge_base"):
raise HTTPException(
status_code=403,
detail="This feature is not available at the moment."
)
"""Get knowledge base context for agent prompts"""
try:

View File

@ -11,7 +11,6 @@ import hmac
from services.supabase import DBConnection
from utils.auth_utils import get_current_user_id_from_jwt
from utils.logger import logger
from flags.flags import is_enabled
from utils.config import config
from services.billing import check_billing_status, can_use_model
@ -215,8 +214,6 @@ async def sync_triggers_to_version_config(agent_id: str):
@router.get("/providers")
async def get_providers():
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
provider_service = get_provider_service(db)
@ -233,8 +230,6 @@ async def get_agent_triggers(
agent_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
await verify_agent_access(agent_id, user_id)
@ -273,8 +268,6 @@ async def get_agent_triggers(
async def get_all_user_triggers(
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
client = await db.client
@ -368,8 +361,6 @@ async def get_agent_upcoming_runs(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Get upcoming scheduled runs for agent triggers"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
await verify_agent_access(agent_id, user_id)
@ -442,8 +433,6 @@ async def create_agent_trigger(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Create a new trigger for an agent"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
await verify_agent_access(agent_id, user_id)
@ -491,8 +480,6 @@ async def get_trigger(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Get a trigger by ID"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
trigger_service = get_trigger_service(db)
@ -532,8 +519,6 @@ async def update_trigger(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Update a trigger"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
trigger_service = get_trigger_service(db)
@ -585,8 +570,6 @@ async def delete_trigger(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Delete a trigger"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
trigger_service = get_trigger_service(db)
@ -619,8 +602,6 @@ async def trigger_webhook(
request: Request
):
"""Handle incoming webhook for a trigger"""
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
# Simple header-based auth using a shared secret

View File

@ -1,5 +1,3 @@
import { agentPlaygroundFlagFrontend } from '@/flags';
import { isFlagEnabled } from '@/lib/feature-flags';
import { Metadata } from 'next';
import { redirect } from 'next/navigation';

View File

@ -6,7 +6,6 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useAgents, useUpdateAgent, useDeleteAgent, useOptimisticAgentUpdate, useAgentDeletionState } from '@/hooks/react-query/agents/use-agents';
import { useMarketplaceTemplates, useInstallTemplate, useMyTemplates, useUnpublishTemplate, usePublishTemplate, useCreateTemplate, useDeleteTemplate } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
import { useFeatureFlag } from '@/lib/feature-flags';
import { useAuth } from '@/components/AuthProvider';
import { StreamlinedInstallDialog } from '@/components/agents/installation/streamlined-install-dialog';
@ -44,16 +43,8 @@ interface PublishDialogData {
export default function AgentsPage() {
const { user } = useAuth();
const { enabled: customAgentsEnabled, loading: agentsFlagLoading } = useFeatureFlag("custom_agents");
const { enabled: agentMarketplaceEnabled, loading: marketplaceFlagLoading } = useFeatureFlag("agent_marketplace");
const router = useRouter();
const flagLoading = agentsFlagLoading || marketplaceFlagLoading;
useEffect(() => {
if (!flagLoading && !customAgentsEnabled) {
router.replace("/dashboard");
}
}, [flagLoading, customAgentsEnabled, router]);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
@ -549,29 +540,7 @@ export default function AgentsPage() {
};
};
if (flagLoading) {
return (
<div className="min-h-screen">
<div className="container max-w-7xl mx-auto px-4 py-8">
<AgentsPageHeader />
</div>
<div className="sticky top-0 z-50 bg-background/95 backdrop-blur-md border-b border-border/40 shadow-sm">
<div className="container max-w-7xl mx-auto px-4 py-4">
<TabsNavigation activeTab={activeTab} onTabChange={handleTabChange} onCreateAgent={handleCreateNewAgent} />
</div>
</div>
<div className="container max-w-7xl mx-auto px-4 py-8">
<LoadingSkeleton />
</div>
</div>
);
}
if (!customAgentsEnabled) {
return null;
}
return (
<div className="min-h-screen">

View File

@ -3,7 +3,6 @@ import { DashboardContent } from "../../../components/dashboard/dashboard-conten
import { BackgroundAALChecker } from "@/components/auth/background-aal-checker";
import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { isFlagEnabled } from "@/lib/feature-flags";
export default async function DashboardPage() {
return (

View File

@ -4,8 +4,6 @@ import React, { useState, useEffect } from 'react';
import { Key, Plus, Trash2, Copy, Shield, ExternalLink, Sparkles } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
import { useFeatureFlag } from '@/lib/feature-flags';
import { Button } from '@/components/ui/button';
import {
@ -59,9 +57,6 @@ interface NewAPIKeyData {
}
export default function APIKeysPage() {
const { enabled: customAgentsEnabled, loading: flagLoading } =
useFeatureFlag('custom_agents');
const router = useRouter();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [newKeyData, setNewKeyData] = useState<NewAPIKeyData>({
title: '',
@ -73,11 +68,6 @@ export default function APIKeysPage() {
const [showCreatedKey, setShowCreatedKey] = useState(false);
const queryClient = useQueryClient();
useEffect(() => {
if (!flagLoading && !customAgentsEnabled) {
router.replace('/dashboard');
}
}, [flagLoading, customAgentsEnabled, router]);
// Fetch API keys
const {
@ -211,26 +201,6 @@ export default function APIKeysPage() {
return new Date(expiresAt) < new Date();
};
if (flagLoading) {
return (
<div className="container mx-auto max-w-6xl px-6 py-6">
<div className="space-y-6">
<div className="animate-pulse space-y-4">
<div className="h-12 bg-muted rounded-lg"></div>
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="h-32 bg-muted rounded-lg"></div>
))}
</div>
</div>
</div>
</div>
);
}
if (!customAgentsEnabled) {
return null;
}
return (
<div className="container mx-auto max-w-6xl px-6 py-6">

View File

@ -5,40 +5,9 @@ import {
Zap
} from 'lucide-react';
import { ComposioConnectionsSection } from '../../../../components/agents/composio/composio-connections-section';
import { useRouter } from 'next/navigation';
import { useFeatureFlag } from '@/lib/feature-flags';
import { PageHeader } from '@/components/ui/page-header';
export default function AppProfilesPage() {
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
const router = useRouter();
useEffect(() => {
if (!flagLoading && !customAgentsEnabled) {
router.replace("/dashboard");
}
}, [flagLoading, customAgentsEnabled, router]);
if (flagLoading) {
return (
<div className="container mx-auto max-w-7xl px-4 py-8">
<div className="space-y-6">
<div className="animate-pulse space-y-4">
<div className="h-32 bg-muted rounded-3xl"></div>
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="h-32 bg-muted rounded-lg"></div>
))}
</div>
</div>
</div>
</div>
);
}
if (!customAgentsEnabled) {
return null;
}
return (
<div className="container mx-auto max-w-7xl px-4 py-8">

View File

@ -1,7 +0,0 @@
import { maintenanceNoticeFlag } from '@/lib/edge-flags';
import { NextResponse } from 'next/server';
export async function GET() {
const maintenanceNotice = await maintenanceNoticeFlag();
return NextResponse.json(maintenanceNotice);
}

View File

@ -19,7 +19,6 @@ import {
import { useAuth } from '@/components/AuthProvider';
import { useAuthMethodTracking } from '@/lib/stores/auth-tracking';
import { toast } from 'sonner';
import { useFeatureFlag } from '@/lib/feature-flags';
import {
Dialog,
@ -41,7 +40,6 @@ function LoginContent() {
const mode = searchParams.get('mode');
const returnUrl = searchParams.get('returnUrl');
const message = searchParams.get('message');
const { enabled: customAgentsEnabled } = useFeatureFlag("custom_agents");
const isSignUp = mode === 'signup';
const isMobile = useMediaQuery('(max-width: 768px)');
@ -287,7 +285,7 @@ function LoginContent() {
</div>
<div className="w-full max-w-sm">
<div className="mb-4 flex items-center flex-col gap-3 sm:gap-4 justify-center">
{customAgentsEnabled && <ReleaseBadge className='mb-2 sm:mb-4' text="Custom Agents, Playbooks, and more!" link="/changelog" />}
<ReleaseBadge className='mb-2 sm:mb-4' text="Custom Agents, Playbooks, and more!" link="/changelog" />
<h1 className="text-xl sm:text-2xl font-semibold text-foreground text-center leading-tight">
{isSignUp ? 'Create your account' : 'Log into your account'}
</h1>

View File

@ -28,7 +28,6 @@ import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
import { KortixLogo } from '../sidebar/kortix-logo';
import { AgentRunLimitDialog } from '@/components/thread/agent-run-limit-dialog';
import { useFeatureFlag } from '@/lib/feature-flags';
import { CustomAgentsSection } from './custom-agents-section';
import { toast } from 'sonner';
import { ReleaseBadge } from '../auth/release-badge';
@ -102,7 +101,6 @@ export function DashboardContent() {
} = useDashboardTour();
// Feature flag for custom agents section
const { enabled: customAgentsEnabled } = useFeatureFlag('custom_agents');
// Fetch agents to get the selected agent's name
const { data: agentsResponse } = useAgents({
@ -341,7 +339,7 @@ export function DashboardContent() {
<div className="flex flex-col h-screen w-full overflow-hidden">
<div className="flex-1 overflow-y-auto">
<div className="min-h-full flex flex-col">
{customAgentsEnabled && (
{(
<div className="flex justify-center px-4 pt-4 md:pt-8">
<ReleaseBadge text="Custom Agents, Playbooks, and more!" link="/agents?tab=my-agents" />
</div>
@ -376,7 +374,7 @@ export function DashboardContent() {
</div>
</div>
</div>
{enabledEnvironment && customAgentsEnabled && (
{enabledEnvironment && (
<div className="w-full px-4 pb-8" data-tour="custom-agents">
<div className="max-w-7xl mx-auto">
<CustomAgentsSection

View File

@ -15,7 +15,6 @@ import { DeleteOperationProvider } from '@/contexts/DeleteOperationContext';
import { StatusOverlay } from '@/components/ui/status-overlay';
import { MaintenanceNotice } from './maintenance-notice';
import { MaintenanceBanner } from './maintenance-banner';
import { useMaintenanceNoticeQuery } from '@/hooks/react-query/edge-flags';
import { useProjects, useThreads } from '@/hooks/react-query/sidebar/use-sidebar';
import { useIsMobile } from '@/hooks/use-mobile';
@ -29,7 +28,6 @@ interface DashboardLayoutContentProps {
export default function DashboardLayoutContent({
children,
}: DashboardLayoutContentProps) {
const maintenanceNoticeQuery = useMaintenanceNoticeQuery();
// const [showPricingAlert, setShowPricingAlert] = useState(false)
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false);
const { data: accounts } = useAccounts();
@ -80,31 +78,7 @@ export default function DashboardLayoutContent({
}
}, [user, isLoading, router]);
if (maintenanceNoticeQuery.data?.enabled) {
const now = new Date();
const startTime = new Date(maintenanceNoticeQuery.data.startTime);
const endTime = new Date(maintenanceNoticeQuery.data.endTime);
if (now > startTime) {
return (
<div className="w-screen h-screen flex items-center justify-center">
<div className="max-w-xl">
<MaintenanceNotice endTime={endTime.toISOString()} />
</div>
</div>
);
}
}
let mantenanceBanner: React.ReactNode | null = null;
if (maintenanceNoticeQuery.data?.enabled) {
mantenanceBanner = (
<MaintenanceBanner
startTime={maintenanceNoticeQuery.data.startTime}
endTime={maintenanceNoticeQuery.data.endTime}
/>
);
}
// Show loading state while checking auth or health
if (isLoading || isCheckingHealth) {

View File

@ -52,7 +52,6 @@ import {
import { createClient } from '@/lib/supabase/client';
import { useTheme } from 'next-themes';
import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags';
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
export function NavUserWithTeams({
@ -69,7 +68,6 @@ export function NavUserWithTeams({
const { data: accounts } = useAccounts();
const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false);
const { theme, setTheme } = useTheme();
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
// Prepare personal account and team accounts
const personalAccount = React.useMemo(
@ -295,7 +293,7 @@ export function NavUserWithTeams({
Billing
</Link>
</DropdownMenuItem>
{!flagLoading && customAgentsEnabled && (
{(
<DropdownMenuItem asChild>
<Link href="/settings/credentials">
<Plug className="h-4 w-4" />
@ -303,7 +301,7 @@ export function NavUserWithTeams({
</Link>
</DropdownMenuItem>
)}
{!flagLoading && customAgentsEnabled && (
{(
<DropdownMenuItem asChild>
<Link href="/settings/api-keys">
<Key className="h-4 w-4" />

View File

@ -41,7 +41,6 @@ import { Button } from '@/components/ui/button';
import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from '@/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
import { useFeatureFlags } from '@/lib/feature-flags';
import posthog from 'posthog-js';
// Floating mobile menu button component
function FloatingMobileMenuButton() {
@ -88,9 +87,6 @@ export function SidebarLeft({
const pathname = usePathname();
const searchParams = useSearchParams();
const { flags, loading: flagsLoading } = useFeatureFlags(['custom_agents', 'agent_marketplace']);
const customAgentsEnabled = flags.custom_agents;
const marketplaceEnabled = flags.agent_marketplace;
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
// Close mobile menu on page navigation
@ -201,7 +197,7 @@ export function SidebarLeft({
</span>
</SidebarMenuButton>
</Link>
{!flagsLoading && customAgentsEnabled && (
{(
<SidebarMenu>
<Collapsible
defaultOpen={true}

View File

@ -9,7 +9,6 @@ import { VoiceRecorder } from './voice-recorder';
import { UnifiedConfigMenu } from './unified-config-menu';
import { canAccessModel, SubscriptionStatus } from './_use-model-selection';
import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags';
import { TooltipContent } from '@/components/ui/tooltip';
import { Tooltip } from '@/components/ui/tooltip';
import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
@ -94,7 +93,6 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
) => {
const [billingModalOpen, setBillingModalOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents');
useEffect(() => {
setMounted(true);
@ -158,7 +156,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
};
const renderDropdown = () => {
const showAdvancedFeatures = isLoggedIn && (enableAdvancedConfig || (customAgentsEnabled && !flagsLoading));
const showAdvancedFeatures = isLoggedIn && enableAdvancedConfig;
// Don't render dropdown components until after hydration to prevent ID mismatches
if (!mounted) {
return <div className="flex items-center gap-2 h-8" />; // Placeholder with same height

View File

@ -20,7 +20,6 @@ import { ShareModal } from "@/components/sidebar/share-modal"
import { useQueryClient } from "@tanstack/react-query";
import { projectKeys } from "@/hooks/react-query/sidebar/keys";
import { threadKeys } from "@/hooks/react-query/threads/keys";
import { useFeatureFlags } from "@/lib/feature-flags";
interface ThreadSiteHeaderProps {
threadId?: string;
@ -53,8 +52,6 @@ export function SiteHeader({
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
const [copied, setCopied] = useState(false);
const queryClient = useQueryClient();
const { flags, loading: flagsLoading } = useFeatureFlags(['knowledge_base']);
const knowledgeBaseEnabled = flags.knowledge_base;
const isMobile = useIsMobile() || isMobileView
const updateProjectMutation = useUpdateProject()

View File

@ -1,6 +0,0 @@
import { isLocalMode } from './lib/config';
export const agentPlaygroundFlagFrontend = isLocalMode();
export const marketplaceFlagFrontend = isLocalMode();
export const agentPlaygroundEnabled = isLocalMode();
export const marketplaceEnabled = isLocalMode();

View File

@ -1,6 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { createClient } from '@/lib/supabase/client';
import { isFlagEnabled } from '@/lib/feature-flags';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
@ -20,10 +19,6 @@ interface AgentToolsResponse {
}
const fetchAgentTools = async (agentId: string): Promise<AgentToolsResponse> => {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();

View File

@ -1,5 +1,4 @@
import { createClient } from "@/lib/supabase/client";
import { isFlagEnabled } from "@/lib/feature-flags";
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
@ -169,10 +168,6 @@ export type AgentUpdateRequest = {
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -217,10 +212,6 @@ export const getAgents = async (params: AgentsParams = {}): Promise<AgentsRespon
export const getAgent = async (agentId: string): Promise<Agent> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -251,10 +242,6 @@ export const getAgent = async (agentId: string): Promise<Agent> => {
export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -297,10 +284,6 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent>
export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest): Promise<Agent> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -332,10 +315,6 @@ export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest
export const deleteAgent = async (agentId: string): Promise<void> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -363,10 +342,6 @@ export const deleteAgent = async (agentId: string): Promise<void> => {
export const getThreadAgent = async (threadId: string): Promise<ThreadAgentResponse> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -399,10 +374,6 @@ export const getThreadAgent = async (threadId: string): Promise<ThreadAgentRespo
export const getAgentVersions = async (agentId: string): Promise<AgentVersion[]> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -434,10 +405,6 @@ export const createAgentVersion = async (
data: AgentVersionCreateRequest
): Promise<AgentVersion> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -472,10 +439,6 @@ export const activateAgentVersion = async (
versionId: string
): Promise<void> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -508,10 +471,6 @@ export const getAgentVersion = async (
versionId: string
): Promise<AgentVersion> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();

View File

@ -1,5 +1,4 @@
import { createClient } from "@/lib/supabase/client";
import { isFlagEnabled } from "@/lib/feature-flags";
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
@ -144,10 +143,6 @@ export const generateLLMWorkflowPrompt = (workflow: AgentWorkflow): string => {
export const getAgentWorkflows = async (agentId: string): Promise<AgentWorkflow[]> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -178,10 +173,6 @@ export const getAgentWorkflows = async (agentId: string): Promise<AgentWorkflow[
export const createAgentWorkflow = async (agentId: string, workflow: CreateWorkflowRequest): Promise<AgentWorkflow> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -217,10 +208,6 @@ export const updateAgentWorkflow = async (
workflow: UpdateWorkflowRequest
): Promise<AgentWorkflow> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -252,10 +239,6 @@ export const updateAgentWorkflow = async (
export const deleteAgentWorkflow = async (agentId: string, workflowId: string): Promise<void> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -293,10 +276,6 @@ export const executeWorkflow = async (
message?: string;
}> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -332,10 +311,6 @@ export const getWorkflowExecutions = async (
limit: number = 20
): Promise<WorkflowExecution[]> => {
try {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();

View File

@ -1,24 +0,0 @@
'use client';
import { createQueryHook, createQueryKeys } from '@/hooks/use-query';
import { IMaintenanceNotice } from '@/lib/edge-flags';
const maintenanceNoticeKeysBase = ['maintenance-notice'] as const;
export const maintenanceNoticeKeys = createQueryKeys({
all: maintenanceNoticeKeysBase,
});
export const useMaintenanceNoticeQuery = createQueryHook(
maintenanceNoticeKeys.all,
async (): Promise<IMaintenanceNotice> => {
const response = await fetch('/api/edge-flags');
return response.json();
},
{
staleTime: 30 * 1000,
refetchInterval: 60 * 1000,
refetchOnWindowFocus: true,
retry: 3,
},
);

View File

@ -1,55 +0,0 @@
import { flag } from 'flags/next';
import { getAll } from '@vercel/edge-config';
export type IMaintenanceNotice =
| {
enabled: true;
startTime: string; // Date
endTime: string; // Date
}
| {
enabled: false;
startTime?: undefined;
endTime?: undefined;
};
export const maintenanceNoticeFlag = flag({
key: 'maintenance-notice',
async decide() {
try {
if (!process.env.EDGE_CONFIG) {
return { enabled: false } as const;
}
const flags = await getAll([
'maintenance-notice_start-time',
'maintenance-notice_end-time',
'maintenance-notice_enabled',
]);
if (!flags['maintenance-notice_enabled']) {
return { enabled: false } as const;
}
const startTime = new Date(flags['maintenance-notice_start-time']);
const endTime = new Date(flags['maintenance-notice_end-time']);
if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
throw new Error(
`Invalid maintenance notice start or end time: ${flags['maintenance-notice_start-time']} or ${flags['maintenance-notice_end-time']}`,
);
}
return {
enabled: true,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
} as const;
} catch (cause) {
console.error(
new Error('Failed to get maintenance notice flag', { cause }),
);
return { enabled: false } as const;
}
},
});

View File

@ -1,333 +0,0 @@
import React from 'react';
import { useQuery, useQueries } from '@tanstack/react-query';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
export interface FeatureFlag {
flag_name: string;
enabled: boolean;
details?: {
description?: string;
updated_at?: string;
} | null;
}
export interface FeatureFlagsResponse {
flags: Record<string, boolean>;
}
const flagCache = new Map<string, { value: boolean; timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000;
let globalFlagsCache: { flags: Record<string, boolean>; timestamp: number } | null = null;
export class FeatureFlagManager {
private static instance: FeatureFlagManager;
private constructor() {}
static getInstance(): FeatureFlagManager {
if (!FeatureFlagManager.instance) {
FeatureFlagManager.instance = new FeatureFlagManager();
}
return FeatureFlagManager.instance;
}
async isEnabled(flagName: string): Promise<boolean> {
try {
const cached = flagCache.get(flagName);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.value;
}
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.warn(`Failed to fetch feature flag ${flagName}: ${response.status}`);
return false;
}
const data: FeatureFlag = await response.json();
flagCache.set(flagName, {
value: data.enabled,
timestamp: Date.now(),
});
return data.enabled;
} catch (error) {
console.error(`Error checking feature flag ${flagName}:`, error);
return false;
}
}
async getFlagDetails(flagName: string): Promise<FeatureFlag | null> {
try {
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.warn(`Failed to fetch feature flag details for ${flagName}: ${response.status}`);
return null;
}
const data: FeatureFlag = await response.json();
return data;
} catch (error) {
console.error(`Error fetching feature flag details for ${flagName}:`, error);
return null;
}
}
async getAllFlags(): Promise<Record<string, boolean>> {
try {
if (globalFlagsCache && Date.now() - globalFlagsCache.timestamp < CACHE_DURATION) {
return globalFlagsCache.flags;
}
const response = await fetch(`${API_URL}/feature-flags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.warn(`Failed to fetch all feature flags: ${response.status}`);
return {};
}
const data: FeatureFlagsResponse = await response.json();
globalFlagsCache = {
flags: data.flags,
timestamp: Date.now(),
};
Object.entries(data.flags).forEach(([flagName, enabled]) => {
flagCache.set(flagName, {
value: enabled,
timestamp: Date.now(),
});
});
return data.flags;
} catch (error) {
console.error('Error fetching all feature flags:', error);
return {};
}
}
clearCache(): void {
flagCache.clear();
globalFlagsCache = null;
}
async preloadFlags(flagNames: string[]): Promise<void> {
try {
const promises = flagNames.map(flagName => this.isEnabled(flagName));
await Promise.all(promises);
} catch (error) {
console.error('Error preloading feature flags:', error);
}
}
}
const featureFlagManager = FeatureFlagManager.getInstance();
export const isEnabled = (flagName: string): Promise<boolean> => {
return featureFlagManager.isEnabled(flagName);
};
export const isFlagEnabled = isEnabled;
export const getFlagDetails = (flagName: string): Promise<FeatureFlag | null> => {
return featureFlagManager.getFlagDetails(flagName);
};
export const getAllFlags = (): Promise<Record<string, boolean>> => {
return featureFlagManager.getAllFlags();
};
export const clearFlagCache = (): void => {
featureFlagManager.clearCache();
};
export const preloadFlags = (flagNames: string[]): Promise<void> => {
return featureFlagManager.preloadFlags(flagNames);
};
// React Query key factories
export const featureFlagKeys = {
all: ['feature-flags'] as const,
flag: (flagName: string) => [...featureFlagKeys.all, 'flag', flagName] as const,
flagDetails: (flagName: string) => [...featureFlagKeys.all, 'details', flagName] as const,
allFlags: () => [...featureFlagKeys.all, 'allFlags'] as const,
};
// Query functions
const fetchFeatureFlag = async (flagName: string): Promise<boolean> => {
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch feature flag ${flagName}: ${response.status}`);
}
const data: FeatureFlag = await response.json();
return data.enabled;
};
const fetchFeatureFlagDetails = async (flagName: string): Promise<FeatureFlag> => {
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch feature flag details for ${flagName}: ${response.status}`);
}
const data: FeatureFlag = await response.json();
return data;
};
const fetchAllFeatureFlags = async (): Promise<Record<string, boolean>> => {
const response = await fetch(`${API_URL}/feature-flags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch all feature flags: ${response.status}`);
}
const data: FeatureFlagsResponse = await response.json();
return data.flags;
};
// React Query Hooks
export const useFeatureFlag = (flagName: string, options?: {
enabled?: boolean;
staleTime?: number;
gcTime?: number;
refetchOnWindowFocus?: boolean;
}) => {
const query = useQuery({
queryKey: featureFlagKeys.flag(flagName),
queryFn: () => fetchFeatureFlag(flagName),
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? false,
enabled: options?.enabled ?? true,
retry: (failureCount, error) => {
// Don't retry on 4xx errors, but retry on network errors
if (error instanceof Error && error.message.includes('4')) {
return false;
}
return failureCount < 3;
},
meta: {
errorMessage: `Failed to fetch feature flag: ${flagName}`,
},
});
// Return backward-compatible interface
return {
enabled: query.data ?? false,
loading: query.isLoading,
// Also expose React Query properties for advanced usage
...query,
};
};
export const useFeatureFlagDetails = (flagName: string, options?: {
enabled?: boolean;
staleTime?: number;
gcTime?: number;
}) => {
return useQuery({
queryKey: featureFlagKeys.flagDetails(flagName),
queryFn: () => fetchFeatureFlagDetails(flagName),
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
enabled: options?.enabled ?? true,
retry: (failureCount, error) => {
if (error instanceof Error && error.message.includes('4')) {
return false;
}
return failureCount < 3;
},
});
};
export const useAllFeatureFlags = (options?: {
enabled?: boolean;
staleTime?: number;
gcTime?: number;
}) => {
return useQuery({
queryKey: featureFlagKeys.allFlags(),
queryFn: fetchAllFeatureFlags,
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
enabled: options?.enabled ?? true,
retry: (failureCount, error) => {
if (error instanceof Error && error.message.includes('4')) {
return false;
}
return failureCount < 3;
},
});
};
export const useFeatureFlags = (flagNames: string[], options?: {
enabled?: boolean;
staleTime?: number;
gcTime?: number;
}) => {
const queries = useQueries({
queries: flagNames.map((flagName) => ({
queryKey: featureFlagKeys.flag(flagName),
queryFn: () => fetchFeatureFlag(flagName),
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
enabled: options?.enabled ?? true,
retry: (failureCount: number, error: Error) => {
if (error.message.includes('4')) {
return false;
}
return failureCount < 3;
},
})),
});
// Transform the results into a more convenient format
const flags = React.useMemo(() => {
const result: Record<string, boolean> = {};
flagNames.forEach((flagName, index) => {
const query = queries[index];
result[flagName] = query.data ?? false;
});
return result;
}, [queries, flagNames]);
const loading = queries.some(query => query.isLoading);
const error = queries.find(query => query.error)?.error?.message ?? null;
return { flags, loading, error };
};

View File

@ -1,16 +1,10 @@
import { createClient } from '@/lib/supabase/client';
import { IApiClient } from '../repositories/interfaces';
import { isFlagEnabled } from '@/lib/feature-flags';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
export class SupabaseApiClient implements IApiClient {
private async getAuthHeaders(): Promise<Record<string, string>> {
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
if (!agentPlaygroundEnabled) {
throw new Error('Custom agents is not enabled');
}
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();