Compare commits

..

No commits in common. "add-groq-provider" and "main" have entirely different histories.

11 changed files with 142 additions and 306 deletions

View file

@ -31,7 +31,7 @@ class Config(metaclass=ConfigMeta):
# ═══════════════════════════════════════════════════════════════════════
# PROVIDER CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════
# Provider: "local" (Ollama), "openai" (OpenAI API), or "groq" (Groq API)
# Provider: "local" (Ollama) or "openai" (OpenAI API)
PROVIDER: str = os.getenv("CLAUDE_OS_PROVIDER", "local")
# ═══════════════════════════════════════════════════════════════════════
@ -49,13 +49,6 @@ class Config(metaclass=ConfigMeta):
OPENAI_EMBED_MODEL: str = os.getenv("OPENAI_EMBED_MODEL", "text-embedding-3-small")
OPENAI_EMBED_DIMENSIONS: int = 1536 # OpenAI text-embedding-3-small dimensions
# ═══════════════════════════════════════════════════════════════════════
# GROQ CONFIGURATION (for groq provider)
# ═══════════════════════════════════════════════════════════════════════
# Groq has no embeddings API - embeddings always stay on Ollama/OpenAI when using this provider.
GROQ_API_KEY: str = os.getenv("GROQ_API_KEY", "")
GROQ_MODEL: str = os.getenv("GROQ_MODEL", "openai/gpt-oss-120b")
# ═══════════════════════════════════════════════════════════════════════
# MODEL CONFIGURATION (auto-selected based on provider)
# ═══════════════════════════════════════════════════════════════════════
@ -224,9 +217,9 @@ class Config(metaclass=ConfigMeta):
@classmethod
def get_provider(cls) -> str:
"""Get the active provider (local, openai, or groq)."""
"""Get the active provider (local or openai)."""
provider = os.getenv("CLAUDE_OS_PROVIDER", "local").lower()
if provider not in ("local", "openai", "groq", "custom"):
if provider not in ("local", "openai", "custom"):
return "local"
return provider
@ -240,11 +233,6 @@ class Config(metaclass=ConfigMeta):
"""Check if using OpenAI provider."""
return cls.get_provider() == "openai"
@classmethod
def is_groq_provider(cls) -> bool:
"""Check if using Groq provider."""
return cls.get_provider() == "groq"
@classmethod
def get_active_llm_model(cls) -> str:
"""Get the active LLM model based on provider."""
@ -255,8 +243,6 @@ class Config(metaclass=ConfigMeta):
if cls.is_openai_provider():
return cls.OPENAI_LLM_MODEL
if cls.is_groq_provider():
return cls.GROQ_MODEL
return cls.OLLAMA_MODEL
@classmethod
@ -285,12 +271,6 @@ class Config(metaclass=ConfigMeta):
key = os.getenv("OPENAI_API_KEY", "")
return key if key else None
@classmethod
def get_groq_api_key(cls) -> Optional[str]:
"""Get the Groq API key if configured."""
key = os.getenv("GROQ_API_KEY", "")
return key if key else None
@classmethod
def validate_provider_config(cls) -> List[str]:
"""Validate provider-specific configuration. Returns list of errors."""
@ -301,10 +281,6 @@ class Config(metaclass=ConfigMeta):
if not cls.get_openai_api_key():
errors.append("OPENAI_API_KEY is required when using OpenAI provider")
elif provider == "groq":
if not cls.get_groq_api_key():
errors.append("GROQ_API_KEY is required when using Groq provider")
elif provider == "local":
# Could add Ollama connectivity check here
pass

View file

@ -326,8 +326,6 @@ def ingest_documents(
# Directories to always skip during ingestion
SKIP_DIRECTORIES = {
'node_modules',
'venv',
'.venv',
'.git',
'.svn',
'.hg',

View file

@ -102,12 +102,9 @@ class InsightExtractor:
# Build prompt
prompt = EXTRACTION_PROMPT.format(transcript=session_summary)
# Call the configured LLM provider
# Call Ollama
try:
if Config.is_groq_provider():
insights = await self._call_groq(prompt)
else:
insights = await self._call_ollama(prompt)
insights = await self._call_ollama(prompt)
# Filter by requested types
filtered_insights = [
@ -155,7 +152,25 @@ class InsightExtractor:
result = response.json()
response_text = result.get("response", "")
return self._insights_from_response_text(response_text)
# Parse JSON response
insights_data = self._parse_llm_response(response_text)
# Convert to Insight objects
insights = []
for item in insights_data.get("insights", []):
try:
insight = Insight(
type=item.get("type", "pattern"),
title=item.get("title", "Untitled"),
content=item.get("content", ""),
confidence=float(item.get("confidence", 0.5))
)
insights.append(insight)
except (ValueError, KeyError) as e:
logger.warning(f"Failed to parse insight item: {e}")
continue
return insights
except httpx.HTTPError as e:
logger.error(f"Ollama API error: {e}")
@ -164,65 +179,6 @@ class InsightExtractor:
logger.error(f"Unexpected error calling Ollama: {e}")
raise
async def _call_groq(self, prompt: str) -> List[Insight]:
"""
Call Groq's OpenAI-compatible chat completions API to extract insights.
Args:
prompt: Extraction prompt with session transcript
Returns:
List of Insight objects
"""
url = "https://api.groq.com/openai/v1/chat/completions"
payload = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3, # Lower temperature for more focused extraction
"max_tokens": 2048, # Allow longer responses for multiple insights
"top_p": 0.9,
"response_format": {"type": "json_object"}, # Request JSON response
}
headers = {"Authorization": f"Bearer {Config.GROQ_API_KEY}"}
try:
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(url, json=payload, headers=headers)
response.raise_for_status()
result = response.json()
response_text = result["choices"][0]["message"]["content"]
return self._insights_from_response_text(response_text)
except httpx.HTTPError as e:
logger.error(f"Groq API error: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error calling Groq: {e}")
raise
def _insights_from_response_text(self, response_text: str) -> List[Insight]:
"""Parse a raw LLM response string into Insight objects."""
insights_data = self._parse_llm_response(response_text)
insights = []
for item in insights_data.get("insights", []):
try:
insight = Insight(
type=item.get("type", "pattern"),
title=item.get("title", "Untitled"),
content=item.get("content", ""),
confidence=float(item.get("confidence", 0.5))
)
insights.append(insight)
except (ValueError, KeyError) as e:
logger.warning(f"Failed to parse insight item: {e}")
continue
return insights
def _parse_llm_response(self, response_text: str) -> Dict[str, Any]:
"""
Parse LLM response JSON.

View file

@ -343,25 +343,10 @@ Source documents:
Write the consolidated document:"""
try:
if Config.is_groq_provider():
response = requests.post(
"https://api.groq.com/openai/v1/chat/completions",
json={
"model": Config.get_active_llm_model(),
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 2048
},
headers={"Authorization": f"Bearer {Config.GROQ_API_KEY}"},
timeout=120
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
response = requests.post(
f"{Config.OLLAMA_HOST}/api/generate",
json={
"model": Config.get_active_llm_model(),
"model": Config.OLLAMA_MODEL,
"prompt": prompt,
"stream": False,
"options": {

View file

@ -15,7 +15,7 @@ from llama_index.core.tools import QueryEngineTool
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core.postprocessor import SentenceTransformerRerank, SimilarityPostprocessor
from llama_index.core.schema import TextNode, NodeWithScore
from llama_index.core.schema import TextNode
# BM25 retriever - needs to be installed separately
try:
@ -85,33 +85,25 @@ class RAGEngine:
"""
self.collection_name = collection_name
if Config.is_groq_provider():
from llama_index.llms.groq import Groq
self.llm = Groq(
model=Config.get_active_llm_model(),
api_key=Config.GROQ_API_KEY,
temperature=0.1, # Very low for factual RAG responses
)
else:
# Initialize Ollama LLM optimized for Apple M4 Pro (14 cores, 48GB RAM)
# Using native Ollama with Metal GPU acceleration
self.llm = Ollama(
model="llama3.1:latest", # 8B model runs great on M4 Pro
base_url="http://localhost:11434", # Native Ollama (not Docker)
request_timeout=120.0, # Increased to 120s for LLM generation
context_window=8192, # Larger context with 48GB RAM
num_ctx=8192, # Match context_window
temperature=0.1, # Very low for factual RAG responses
num_predict=512, # Can handle more tokens with M4 Pro
top_k=40, # Better quality sampling
top_p=0.9, # Nucleus sampling for better quality
num_thread=12, # Use most cores (leave 2 for system)
num_gpu=99, # Use all GPU layers on Apple Silicon
repeat_penalty=1.1, # Avoid repetition
num_batch=512, # Larger batch size for M4 Pro
use_mmap=True, # Memory-map model for faster loading
use_mlock=True, # Lock model in RAM (plenty available)
)
# Initialize Ollama LLM optimized for Apple M4 Pro (14 cores, 48GB RAM)
# Using native Ollama with Metal GPU acceleration
self.llm = Ollama(
model="llama3.1:latest", # 8B model runs great on M4 Pro
base_url="http://localhost:11434", # Native Ollama (not Docker)
request_timeout=120.0, # Increased to 120s for LLM generation
context_window=8192, # Larger context with 48GB RAM
num_ctx=8192, # Match context_window
temperature=0.1, # Very low for factual RAG responses
num_predict=512, # Can handle more tokens with M4 Pro
top_k=40, # Better quality sampling
top_p=0.9, # Nucleus sampling for better quality
num_thread=12, # Use most cores (leave 2 for system)
num_gpu=99, # Use all GPU layers on Apple Silicon
repeat_penalty=1.1, # Avoid repetition
num_batch=512, # Larger batch size for M4 Pro
use_mmap=True, # Memory-map model for faster loading
use_mlock=True, # Lock model in RAM (plenty available)
)
Settings.llm = self.llm
# Initialize Ollama embeddings
@ -252,7 +244,7 @@ class RAGEngine:
# Synthesize answer
synthesizer = get_response_synthesizer()
response = synthesizer.synthesize(question, nodes=self._to_scored_nodes(fused_nodes))
response = synthesizer.synthesize(question, nodes=fused_nodes)
return self._format_response(response, fused_nodes)
@ -336,7 +328,7 @@ class RAGEngine:
try:
# Synthesize answer using LLM
synthesizer = get_response_synthesizer()
response = synthesizer.synthesize(question, nodes=self._to_scored_nodes(filtered_nodes))
response = synthesizer.synthesize(question, nodes=filtered_nodes)
logger.info(f"Generated answer: {str(response)[:200]}")
@ -369,13 +361,6 @@ class RAGEngine:
answer_parts.append(f"\n\nFor complete information, see the sources below.")
return "".join(answer_parts)
def _to_scored_nodes(self, nodes: List[TextNode]) -> List[NodeWithScore]:
"""Wrap raw TextNodes as NodeWithScore, which response synthesizers require."""
return [
NodeWithScore(node=node, score=(node.metadata or {}).get("similarity_score", 0.0))
for node in nodes
]
def _format_response(self, response, source_nodes) -> Dict[str, any]:
"""Format query response for return."""
sources = []

View file

@ -1,9 +1,4 @@
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8051';
function authHeaders(): Record<string, string> {
const token = localStorage.getItem('auth_token');
return token ? { Authorization: `Bearer ${token}` } : {};
}
const API_BASE = 'http://localhost:8051';
export interface ChatResponse {
response: string;
@ -35,7 +30,7 @@ export async function chat(
): Promise<ChatResponse> {
const res = await fetch(`${API_BASE}/api/kb/${kbName}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
use_hybrid: options?.useHybrid ?? false,
@ -52,7 +47,7 @@ export async function chat(
}
export async function getKBStats(kbName: string): Promise<KBStats> {
const res = await fetch(`${API_BASE}/api/kb/${kbName}/stats`, { headers: authHeaders() });
const res = await fetch(`${API_BASE}/api/kb/${kbName}/stats`);
if (!res.ok) {
throw new Error(`Failed to get KB stats: ${res.statusText}`);
}
@ -60,7 +55,7 @@ export async function getKBStats(kbName: string): Promise<KBStats> {
}
export async function listDocuments(kbName: string): Promise<Document[]> {
const res = await fetch(`${API_BASE}/api/kb/${kbName}/documents`, { headers: authHeaders() });
const res = await fetch(`${API_BASE}/api/kb/${kbName}/documents`);
if (!res.ok) {
throw new Error(`Failed to list documents: ${res.statusText}`);
}
@ -74,7 +69,6 @@ export async function uploadDocument(kbName: string, file: File): Promise<void>
const res = await fetch(`${API_BASE}/api/kb/${kbName}/upload`, {
method: 'POST',
headers: authHeaders(),
body: formData,
});
@ -86,7 +80,6 @@ export async function uploadDocument(kbName: string, file: File): Promise<void>
export async function deleteDocument(kbName: string, filename: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/kb/${kbName}/documents/${encodeURIComponent(filename)}`, {
method: 'DELETE',
headers: authHeaders(),
});
if (!res.ok) {

View file

@ -1,20 +1,6 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import axios from 'axios';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8051';
// Every component that talks to the API via axios imports the same default
// instance, so registering the interceptor once here attaches the stored
// token everywhere - without it, auth only ever covers the two fetch() calls
// in this file, and every axios-based request (projects, kanban, hooks,
// most of the app) goes out unauthenticated.
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
const API_BASE = 'http://localhost:8051';
interface AuthContextType {
isAuthenticated: boolean;

View file

@ -21,12 +21,7 @@ ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
# Security
# auto_error=False: let get_current_user itself decide whether a missing
# token is OK (auth not configured -> guest) or a 401 (auth configured).
# The HTTPBearer default (auto_error=True) rejects any request with no
# Authorization header before get_current_user's body ever runs, which
# breaks the "auth disabled by default" behavior entirely.
security = HTTPBearer(auto_error=False)
security = HTTPBearer()
def verify_password(plain_password: str, hashed_password: str) -> bool:
@ -106,7 +101,7 @@ def decode_access_token(token: str) -> Optional[dict]:
return None
def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Security(security)) -> dict:
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:
"""
Dependency to get the current authenticated user.
Use in FastAPI routes: current_user = Depends(get_current_user)
@ -117,23 +112,7 @@ def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Secur
if not creds:
return {"email": "guest", "auth_disabled": True}
if credentials is None:
raise HTTPException(
status_code=401,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
token = credentials.credentials
# Service token: lets trusted local integrations (e.g. the code-forge
# stdio MCP server) call the API without a user login. Only relevant
# once auth is actually enabled - with no CLAUDE_OS_EMAIL set, everyone
# already gets the guest bypass above regardless of this.
service_token = os.getenv("CLAUDE_OS_SERVICE_TOKEN")
if service_token and secrets.compare_digest(token, service_token):
return {"email": "service", "service_account": True}
payload = decode_access_token(token)
if payload is None:

View file

@ -32,27 +32,12 @@ from pathlib import Path
from typing import Any
import httpx
from dotenv import load_dotenv
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# This process is launched directly (e.g. over SSH as a stdio MCP server),
# not necessarily with claude-os's own .env sourced into its environment or
# its working directory set to the project root - so load it explicitly by
# path rather than relying on load_dotenv()'s cwd-relative default search.
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
# Configuration
API_BASE = os.environ.get("CLAUDE_OS_API", "http://localhost:8051")
SERVICE_TOKEN = os.environ.get("CLAUDE_OS_SERVICE_TOKEN")
def _auth_headers() -> dict:
"""Auth header for calling claude-os's own API. No-op unless a service
token is configured, which only matters once CLAUDE_OS_EMAIL/PASSWORD
are set - the API allows all requests through as a guest otherwise."""
return {"Authorization": f"Bearer {SERVICE_TOKEN}"} if SERVICE_TOKEN else {}
# Health cache — tracks last health check time per KB and any warnings
HEALTH_CACHE: dict[str, dict] = {}
@ -93,7 +78,7 @@ async def _maybe_check_health(kb_name: str) -> list[str]:
# Run health check
try:
async with httpx.AsyncClient(headers=_auth_headers()) as client:
async with httpx.AsyncClient() as client:
resp = await client.get(api_url(f"/api/kb/{kb_name}/lifecycle/health"), timeout=15.0)
resp.raise_for_status()
health = resp.json()
@ -124,7 +109,7 @@ def api_url(path: str) -> str:
async def api_get(path: str) -> dict:
"""Make GET request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
async with httpx.AsyncClient() as client:
response = await client.get(api_url(path), timeout=30.0)
response.raise_for_status()
return response.json()
@ -132,7 +117,7 @@ async def api_get(path: str) -> dict:
async def api_post(path: str, data: dict = None) -> dict:
"""Make POST request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
async with httpx.AsyncClient() as client:
response = await client.post(api_url(path), json=data or {}, timeout=60.0)
response.raise_for_status()
return response.json()
@ -140,7 +125,7 @@ async def api_post(path: str, data: dict = None) -> dict:
async def api_delete(path: str) -> dict:
"""Make DELETE request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
async with httpx.AsyncClient() as client:
response = await client.delete(api_url(path), timeout=30.0)
response.raise_for_status()
return response.json()
@ -148,7 +133,7 @@ async def api_delete(path: str) -> dict:
async def api_put(path: str, data: dict = None) -> dict:
"""Make PUT request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
async with httpx.AsyncClient() as client:
response = await client.put(api_url(path), json=data or {}, timeout=30.0)
response.raise_for_status()
return response.json()

View file

@ -14,12 +14,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI, Request, UploadFile, File, HTTPException, Depends, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from mcp_server.auth import (
authenticate_user,
create_access_token,
is_auth_enabled,
get_current_user
)
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import uvicorn
@ -570,14 +564,14 @@ class SearchAllRequest(BaseModel):
# REST API Endpoints for UI
@app.get("/api/kb")
async def api_list_knowledge_bases(current_user: dict = Depends(get_current_user)):
async def api_list_knowledge_bases():
"""REST API: List all knowledge bases."""
kbs = await list_knowledge_bases()
return {"knowledge_bases": kbs}
@app.post("/api/kb")
async def api_create_knowledge_base(request: CreateKBRequest, current_user: dict = Depends(get_current_user)):
async def api_create_knowledge_base(request: CreateKBRequest):
"""REST API: Create a new knowledge base."""
result = await create_knowledge_base(
name=request.name,
@ -591,7 +585,7 @@ async def api_create_knowledge_base(request: CreateKBRequest, current_user: dict
@app.post("/api/kb/search-all")
@limiter.limit("10/minute")
async def api_search_all(request: Request, body: SearchAllRequest, current_user: dict = Depends(get_current_user)):
async def api_search_all(request: Request, body: SearchAllRequest):
"""REST API: Search across multiple knowledge bases."""
try:
from llama_index.embeddings.ollama import OllamaEmbedding
@ -640,7 +634,7 @@ async def api_search_all(request: Request, body: SearchAllRequest, current_user:
@app.delete("/api/kb/{kb_name}")
async def api_delete_knowledge_base(kb_name: str, current_user: dict = Depends(get_current_user)):
async def api_delete_knowledge_base(kb_name: str):
"""REST API: Delete a knowledge base."""
try:
db_manager = get_sqlite_manager()
@ -660,7 +654,7 @@ async def api_delete_knowledge_base(kb_name: str, current_user: dict = Depends(g
@app.get("/api/kb/{kb_name}/stats")
async def api_get_kb_stats(kb_name: str, current_user: dict = Depends(get_current_user)):
async def api_get_kb_stats(kb_name: str):
"""REST API: Get knowledge base statistics."""
stats = await get_kb_stats(kb_name)
if "error" in stats:
@ -669,7 +663,7 @@ async def api_get_kb_stats(kb_name: str, current_user: dict = Depends(get_curren
@app.get("/api/kb/{kb_name}/documents")
async def api_list_documents(kb_name: str, current_user: dict = Depends(get_current_user)):
async def api_list_documents(kb_name: str):
"""REST API: List documents in a knowledge base."""
docs = await list_documents(kb_name)
return {"documents": docs}
@ -677,7 +671,7 @@ async def api_list_documents(kb_name: str, current_user: dict = Depends(get_curr
@app.post("/api/kb/{kb_name}/chat")
@limiter.limit("20/minute") # Rate limit: 20 queries per minute per IP
async def api_chat(request: Request, kb_name: str, chat_request: ChatRequest, current_user: dict = Depends(get_current_user)):
async def api_chat(request: Request, kb_name: str, chat_request: ChatRequest):
"""REST API: Chat with a knowledge base."""
result = await search_knowledge_base(
kb_name=kb_name,
@ -692,7 +686,7 @@ async def api_chat(request: Request, kb_name: str, chat_request: ChatRequest, cu
@app.post("/api/kb/{kb_name}/upload")
async def api_upload_document(kb_name: str, file: UploadFile = File(...), current_user: dict = Depends(get_current_user)):
async def api_upload_document(kb_name: str, file: UploadFile = File(...)):
"""REST API: Upload a single document to a knowledge base."""
import tempfile
import os
@ -739,7 +733,7 @@ class ContentUploadRequest(BaseModel):
@app.post("/api/kb/{kb_name}/documents/content")
async def api_upload_content(kb_name: str, request: ContentUploadRequest, current_user: dict = Depends(get_current_user)):
async def api_upload_content(kb_name: str, request: ContentUploadRequest):
"""REST API: Upload raw text content directly to a knowledge base (no file needed)."""
import tempfile
import os as _os
@ -783,7 +777,7 @@ async def api_upload_content(kb_name: str, request: ContentUploadRequest, curren
@app.post("/api/kb/{kb_name}/import")
async def api_import_directory(kb_name: str, directory_path: str, current_user: dict = Depends(get_current_user)):
async def api_import_directory(kb_name: str, directory_path: str):
"""REST API: Import all files from a directory into a knowledge base."""
from app.core.ingestion import ingest_directory
@ -816,7 +810,7 @@ async def api_import_directory(kb_name: str, directory_path: str, current_user:
@app.delete("/api/kb/{kb_name}/documents/{filename}")
async def api_delete_document(kb_name: str, filename: str, current_user: dict = Depends(get_current_user)):
async def api_delete_document(kb_name: str, filename: str):
"""REST API: Delete a document from a knowledge base by filename."""
db_manager = get_sqlite_manager()
@ -892,7 +886,7 @@ class SemanticIndexRequest(BaseModel):
@app.post("/api/kb/{kb_name}/index-structural")
async def api_index_structural(kb_name: str, request: StructuralIndexRequest, current_user: dict = Depends(get_current_user)):
async def api_index_structural(kb_name: str, request: StructuralIndexRequest):
"""
REST API: Fast structural indexing using tree-sitter.
Phase 1 of hybrid indexing - completes in ~30 seconds for 10k files.
@ -1000,7 +994,7 @@ def _run_semantic_indexing_background(job_id: str, kb_name: str, project_path: s
Updates INDEXING_JOBS with progress.
"""
from app.core.tree_sitter_indexer import TreeSitterIndexer
from app.core.ingestion import ingest_directory, ingest_file, should_skip_path
from app.core.ingestion import ingest_directory, ingest_file
def update_job(status: str, progress: int = 0, message: str = "", error: str = None):
with INDEXING_JOBS_LOCK:
@ -1037,9 +1031,7 @@ def _run_semantic_indexing_background(job_id: str, kb_name: str, project_path: s
docs_patterns = ["*.md", "*.txt", "*.rst"]
doc_files = []
for pattern in docs_patterns:
doc_files.extend(
f for f in Path(project_path).rglob(pattern) if not should_skip_path(f)
)
doc_files.extend(Path(project_path).rglob(pattern))
all_files = list(set(important_files + [str(f.relative_to(project_path)) for f in doc_files]))
total_files = len(all_files)
@ -1093,7 +1085,7 @@ def _run_semantic_indexing_sync(kb_name: str, project_path: str, selective: bool
Returns the final result dict.
"""
from app.core.tree_sitter_indexer import TreeSitterIndexer
from app.core.ingestion import ingest_directory, ingest_file, should_skip_path
from app.core.ingestion import ingest_directory, ingest_file
start_time = time.time()
@ -1110,9 +1102,7 @@ def _run_semantic_indexing_sync(kb_name: str, project_path: str, selective: bool
docs_patterns = ["*.md", "*.txt", "*.rst"]
doc_files = []
for pattern in docs_patterns:
doc_files.extend(
f for f in Path(project_path).rglob(pattern) if not should_skip_path(f)
)
doc_files.extend(Path(project_path).rglob(pattern))
all_files = list(set(important_files + [str(f.relative_to(project_path)) for f in doc_files]))
@ -1155,7 +1145,7 @@ def _run_semantic_indexing_sync(kb_name: str, project_path: str, selective: bool
@app.post("/api/kb/{kb_name}/index-semantic")
async def api_index_semantic(kb_name: str, request: SemanticIndexRequest, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
async def api_index_semantic(kb_name: str, request: SemanticIndexRequest, background_tasks: BackgroundTasks):
"""
REST API: Semantic indexing with selective embedding.
Phase 2 of hybrid indexing.
@ -1234,7 +1224,7 @@ async def api_index_semantic(kb_name: str, request: SemanticIndexRequest, backgr
@app.get("/api/jobs/{job_id}")
async def api_get_job_status(job_id: str, current_user: dict = Depends(get_current_user)):
async def api_get_job_status(job_id: str):
"""Get the status of a background indexing job."""
with INDEXING_JOBS_LOCK:
if job_id not in INDEXING_JOBS:
@ -1243,7 +1233,7 @@ async def api_get_job_status(job_id: str, current_user: dict = Depends(get_curre
@app.get("/api/jobs")
async def api_list_jobs(current_user: dict = Depends(get_current_user)):
async def api_list_jobs():
"""List all indexing jobs (recent first)."""
with INDEXING_JOBS_LOCK:
jobs = list(INDEXING_JOBS.values())
@ -1258,7 +1248,7 @@ async def api_get_repo_map(
token_budget: int = 1024,
project_path: Optional[str] = None,
personalization: Optional[str] = None # JSON string
, current_user: dict = Depends(get_current_user)):
):
"""
REST API: Get compact repo map for Claude's prompt context.
Returns the most important symbols fitting within token budget.
@ -1371,7 +1361,7 @@ class ProjectDocumentIngestRequest(BaseModel):
@app.get("/api/projects")
async def api_list_projects(current_user: dict = Depends(get_current_user)):
async def api_list_projects():
"""REST API: List all projects."""
try:
db_manager = get_sqlite_manager()
@ -1383,7 +1373,7 @@ async def api_list_projects(current_user: dict = Depends(get_current_user)):
@app.post("/api/projects")
async def api_create_project(request: ProjectRequest, current_user: dict = Depends(get_current_user)):
async def api_create_project(request: ProjectRequest):
"""REST API: Create a new project with 4 required MCPs."""
try:
db_manager = get_sqlite_manager()
@ -1439,7 +1429,7 @@ async def api_create_project(request: ProjectRequest, current_user: dict = Depen
@app.get("/api/projects/{project_id}")
async def api_get_project(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_project(project_id: int):
"""REST API: Get project details with MCP assignments."""
try:
db_manager = get_sqlite_manager()
@ -1466,7 +1456,7 @@ async def api_get_project(project_id: int, current_user: dict = Depends(get_curr
@app.get("/api/projects/{project_id}/mcps")
async def api_get_project_mcps(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_project_mcps(project_id: int):
"""REST API: Get detailed MCP info for a project (KB names and IDs)."""
try:
db_manager = get_sqlite_manager()
@ -1505,7 +1495,7 @@ async def api_get_project_mcps(project_id: int, current_user: dict = Depends(get
@app.post("/api/projects/{project_id}/folders")
async def api_set_kb_folder(project_id: int, request: ProjectKBFolderRequest, current_user: dict = Depends(get_current_user)):
async def api_set_kb_folder(project_id: int, request: ProjectKBFolderRequest):
"""REST API: Set the folder path for a project's MCP KB."""
try:
db_manager = get_sqlite_manager()
@ -1590,7 +1580,7 @@ async def api_set_kb_folder(project_id: int, request: ProjectKBFolderRequest, cu
@app.get("/api/projects/{project_id}/folders")
async def api_get_kb_folders(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_kb_folders(project_id: int):
"""REST API: Get folder configurations for a project's MCPs."""
try:
db_manager = get_sqlite_manager()
@ -1616,7 +1606,7 @@ async def api_get_kb_folders(project_id: int, current_user: dict = Depends(get_c
@app.post("/api/projects/{project_id}/ingest-document")
async def api_ingest_document(project_id: int, request: ProjectDocumentIngestRequest, current_user: dict = Depends(get_current_user)):
async def api_ingest_document(project_id: int, request: ProjectDocumentIngestRequest):
"""REST API: Ingest a document directly into a project's MCP KB."""
try:
db_manager = get_sqlite_manager()
@ -1679,7 +1669,7 @@ async def api_ingest_document(project_id: int, request: ProjectDocumentIngestReq
@app.delete("/api/projects/{project_id}")
async def api_delete_project(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_delete_project(project_id: int):
"""REST API: Delete a project and its associated KBs."""
try:
db_manager = get_sqlite_manager()
@ -1744,7 +1734,7 @@ class SyncRequest(BaseModel):
@app.post("/api/projects/{project_id}/hooks/{mcp_type}/enable")
async def api_enable_hook(project_id: int, mcp_type: str, request: HookRequest, current_user: dict = Depends(get_current_user)):
async def api_enable_hook(project_id: int, mcp_type: str, request: HookRequest):
"""Enable automatic KB synchronization for a folder."""
try:
hook = get_project_hook(project_id)
@ -1769,7 +1759,7 @@ async def api_enable_hook(project_id: int, mcp_type: str, request: HookRequest,
@app.post("/api/projects/{project_id}/hooks/{mcp_type}/disable")
async def api_disable_hook(project_id: int, mcp_type: str, current_user: dict = Depends(get_current_user)):
async def api_disable_hook(project_id: int, mcp_type: str):
"""Disable automatic KB synchronization."""
try:
hook = get_project_hook(project_id)
@ -1787,7 +1777,7 @@ async def api_disable_hook(project_id: int, mcp_type: str, current_user: dict =
@app.post("/api/projects/{project_id}/hooks/sync")
async def api_sync_hooks(project_id: int, request: Optional[SyncRequest] = None, current_user: dict = Depends(get_current_user)):
async def api_sync_hooks(project_id: int, request: Optional[SyncRequest] = None):
"""Manually sync KB folders for a project."""
try:
hook = get_project_hook(project_id)
@ -1813,7 +1803,7 @@ async def api_sync_hooks(project_id: int, request: Optional[SyncRequest] = None,
@app.get("/api/projects/{project_id}/hooks")
async def api_get_hooks_status(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_hooks_status(project_id: int):
"""Get hook status for a project."""
try:
hook = get_project_hook(project_id)
@ -1834,7 +1824,7 @@ async def api_get_hooks_status(project_id: int, current_user: dict = Depends(get
# ============================================================================
@app.post("/api/watcher/start/{project_id}")
async def api_start_watcher(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_start_watcher(project_id: int):
"""Start file watcher for a project."""
try:
watcher = get_global_watcher()
@ -1852,7 +1842,7 @@ async def api_start_watcher(project_id: int, current_user: dict = Depends(get_cu
@app.post("/api/watcher/stop/{project_id}")
async def api_stop_watcher(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_stop_watcher(project_id: int):
"""Stop file watcher for a project."""
try:
watcher = get_global_watcher()
@ -1870,7 +1860,7 @@ async def api_stop_watcher(project_id: int, current_user: dict = Depends(get_cur
@app.post("/api/watcher/restart/{project_id}")
async def api_restart_watcher(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_restart_watcher(project_id: int):
"""Restart file watcher for a project."""
try:
watcher = get_global_watcher()
@ -1888,7 +1878,7 @@ async def api_restart_watcher(project_id: int, current_user: dict = Depends(get_
@app.get("/api/watcher/status")
async def api_watcher_status(current_user: dict = Depends(get_current_user)):
async def api_watcher_status():
"""Get file watcher status."""
try:
watcher = get_global_watcher()
@ -1903,7 +1893,7 @@ async def api_watcher_status(current_user: dict = Depends(get_current_user)):
# ============================================================================
@app.post("/api/spec-watcher/start/{project_id}")
async def api_start_spec_watcher(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_start_spec_watcher(project_id: int):
"""Start spec watcher for a project."""
try:
from app.core.spec_watcher import get_global_spec_watcher
@ -1928,7 +1918,7 @@ async def api_start_spec_watcher(project_id: int, current_user: dict = Depends(g
@app.post("/api/spec-watcher/stop/{project_id}")
async def api_stop_spec_watcher(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_stop_spec_watcher(project_id: int):
"""Stop spec watcher for a project."""
try:
from app.core.spec_watcher import get_global_spec_watcher
@ -1947,7 +1937,7 @@ async def api_stop_spec_watcher(project_id: int, current_user: dict = Depends(ge
@app.post("/api/spec-watcher/start-all")
async def api_start_all_spec_watchers(current_user: dict = Depends(get_current_user)):
async def api_start_all_spec_watchers():
"""Start spec watchers for all projects."""
try:
from app.core.spec_watcher import get_global_spec_watcher
@ -1965,7 +1955,7 @@ async def api_start_all_spec_watchers(current_user: dict = Depends(get_current_u
@app.get("/api/spec-watcher/status")
async def api_spec_watcher_status(current_user: dict = Depends(get_current_user)):
async def api_spec_watcher_status():
"""Get spec watcher status."""
try:
from app.core.spec_watcher import get_global_spec_watcher
@ -1982,7 +1972,7 @@ async def api_spec_watcher_status(current_user: dict = Depends(get_current_user)
# ============================================================================
@app.get("/api/projects/{project_id}/specs")
async def api_get_project_specs(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_project_specs(project_id: int):
"""Get all specs for a project."""
try:
manager = SpecManager()
@ -1994,7 +1984,7 @@ async def api_get_project_specs(project_id: int, current_user: dict = Depends(ge
@app.get("/api/specs/{spec_id}/tasks")
async def api_get_spec_tasks(spec_id: int, current_user: dict = Depends(get_current_user)):
async def api_get_spec_tasks(spec_id: int):
"""Get all tasks for a spec."""
try:
manager = SpecManager()
@ -2011,7 +2001,7 @@ class TaskStatusUpdate(BaseModel):
@app.patch("/api/tasks/{task_id}/status")
async def api_update_task_status(task_id: int, update: TaskStatusUpdate, current_user: dict = Depends(get_current_user)):
async def api_update_task_status(task_id: int, update: TaskStatusUpdate):
"""Update a task's status."""
try:
manager = SpecManager()
@ -2033,7 +2023,7 @@ async def api_update_task_status(task_id: int, update: TaskStatusUpdate, current
@app.post("/api/projects/{project_id}/specs/sync")
async def api_sync_project_specs(project_id: int, current_user: dict = Depends(get_current_user)):
async def api_sync_project_specs(project_id: int):
"""Sync specs from project's agent-os folder."""
try:
# Get project path
@ -2059,7 +2049,7 @@ async def api_sync_project_specs(project_id: int, current_user: dict = Depends(g
@app.get("/api/projects/{project_id}/kanban")
async def api_get_kanban_view(project_id: int, include_archived: bool = False, current_user: dict = Depends(get_current_user)):
async def api_get_kanban_view(project_id: int, include_archived: bool = False):
"""Get Kanban board view for a project."""
try:
manager = SpecManager()
@ -2110,7 +2100,7 @@ async def api_get_kanban_view(project_id: int, include_archived: bool = False, c
@app.post("/api/specs/{spec_id}/archive")
async def api_archive_spec(spec_id: int, current_user: dict = Depends(get_current_user)):
async def api_archive_spec(spec_id: int):
"""Archive a spec."""
try:
manager = SpecManager()
@ -2122,7 +2112,7 @@ async def api_archive_spec(spec_id: int, current_user: dict = Depends(get_curren
@app.post("/api/specs/{spec_id}/unarchive")
async def api_unarchive_spec(spec_id: int, current_user: dict = Depends(get_current_user)):
async def api_unarchive_spec(spec_id: int):
"""Unarchive a spec."""
try:
manager = SpecManager()
@ -2157,7 +2147,7 @@ class SessionExtractRequest(BaseModel):
@app.get("/api/sessions/list")
async def api_list_sessions(project_path: str, limit: int = 50, current_user: dict = Depends(get_current_user)):
async def api_list_sessions(project_path: str, limit: int = 50):
"""
REST API: List Claude Code session files for a project.
@ -2187,7 +2177,7 @@ async def api_list_sessions(project_path: str, limit: int = 50, current_user: di
@app.post("/api/sessions/extract")
async def api_extract_session_insights(request: SessionExtractRequest, current_user: dict = Depends(get_current_user)):
async def api_extract_session_insights(request: SessionExtractRequest):
"""
REST API: Extract insights from a Claude Code session file.
@ -2335,7 +2325,7 @@ class SkillInstallRequest(BaseModel):
@app.get("/api/skills")
async def api_list_skills(project_path: Optional[str] = None, include_content: bool = False, current_user: dict = Depends(get_current_user)):
async def api_list_skills(project_path: Optional[str] = None, include_content: bool = False):
"""
REST API: List all skills (global and project).
@ -2362,7 +2352,7 @@ async def api_list_skills(project_path: Optional[str] = None, include_content: b
@app.get("/api/skills/templates")
async def api_list_skill_templates(category: Optional[str] = None, current_user: dict = Depends(get_current_user)):
async def api_list_skill_templates(category: Optional[str] = None):
"""
REST API: List available skill templates.
@ -2388,7 +2378,7 @@ async def api_list_skill_templates(category: Optional[str] = None, current_user:
@app.get("/api/skills/{scope}/{name}")
async def api_get_skill(scope: str, name: str, project_path: Optional[str] = None, current_user: dict = Depends(get_current_user)):
async def api_get_skill(scope: str, name: str, project_path: Optional[str] = None):
"""
REST API: Get skill details including content.
@ -2422,7 +2412,7 @@ async def api_get_skill(scope: str, name: str, project_path: Optional[str] = Non
@app.post("/api/skills/install")
async def api_install_skill_template(request: SkillInstallRequest, project_path: str, current_user: dict = Depends(get_current_user)):
async def api_install_skill_template(request: SkillInstallRequest, project_path: str):
"""
REST API: Install a skill template to a project.
@ -2456,7 +2446,7 @@ async def api_install_skill_template(request: SkillInstallRequest, project_path:
@app.post("/api/skills")
async def api_create_skill(request: SkillCreateRequest, project_path: str, current_user: dict = Depends(get_current_user)):
async def api_create_skill(request: SkillCreateRequest, project_path: str):
"""
REST API: Create a custom skill for a project.
@ -2493,7 +2483,7 @@ async def api_create_skill(request: SkillCreateRequest, project_path: str, curre
@app.put("/api/skills/{name}")
async def api_update_skill(name: str, request: SkillUpdateRequest, project_path: str, current_user: dict = Depends(get_current_user)):
async def api_update_skill(name: str, request: SkillUpdateRequest, project_path: str):
"""
REST API: Update an existing project skill.
@ -2530,7 +2520,7 @@ async def api_update_skill(name: str, request: SkillUpdateRequest, project_path:
@app.delete("/api/skills/{name}")
async def api_delete_skill(name: str, project_path: str, current_user: dict = Depends(get_current_user)):
async def api_delete_skill(name: str, project_path: str):
"""
REST API: Delete a project skill.
@ -2572,7 +2562,7 @@ class CommunitySkillInstallRequest(BaseModel):
@app.get("/api/skills/community/sources")
async def api_list_community_sources(current_user: dict = Depends(get_current_user)):
async def api_list_community_sources():
"""
REST API: List available community skill sources.
@ -2592,7 +2582,7 @@ async def api_list_community_sources(current_user: dict = Depends(get_current_us
@app.get("/api/skills/community")
async def api_list_community_skills(source: Optional[str] = None, current_user: dict = Depends(get_current_user)):
async def api_list_community_skills(source: Optional[str] = None):
"""
REST API: List skills from community repositories.
@ -2630,7 +2620,7 @@ async def api_list_community_skills(source: Optional[str] = None, current_user:
async def api_install_community_skill(
request: CommunitySkillInstallRequest,
project_path: str
, current_user: dict = Depends(get_current_user)):
):
"""
REST API: Install a community skill to a project.
@ -2675,7 +2665,7 @@ async def api_install_community_skill(
# ============================================================================
@app.get("/api/ollama/models")
async def api_list_ollama_models(current_user: dict = Depends(get_current_user)):
async def api_list_ollama_models():
"""REST API: List available Ollama models."""
try:
response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=2)
@ -2777,7 +2767,7 @@ def check_port_listening(port: int) -> bool:
@app.get("/api/services/status")
async def api_get_services_status(current_user: dict = Depends(get_current_user)):
async def api_get_services_status():
"""
Get status of all Claude OS services.
@ -2915,7 +2905,7 @@ async def api_get_services_status(current_user: dict = Depends(get_current_user)
@app.post("/api/services/{service_type}/control")
async def api_control_service(service_type: str, request: ServiceControlRequest, current_user: dict = Depends(get_current_user)):
async def api_control_service(service_type: str, request: ServiceControlRequest):
"""
Control a service (start/stop/restart).
@ -2976,7 +2966,7 @@ async def api_control_service(service_type: str, request: ServiceControlRequest,
@app.get("/api/browse-directory")
async def api_browse_directory(path: str = None, current_user: dict = Depends(get_current_user)):
async def api_browse_directory(path: str = None):
"""REST API: Browse directories and return subdirectories."""
try:
from pathlib import Path
@ -3062,19 +3052,19 @@ async def api_browse_directory(path: str = None, current_user: dict = Depends(ge
# MCP Protocol Endpoints
@app.post("/")
async def mcp_root(request: Request, current_user: dict = Depends(get_current_user)):
async def mcp_root(request: Request):
"""Main MCP endpoint - handles JSON-RPC requests."""
return await handle_mcp_request(request)
@app.post("/mcp")
async def mcp_endpoint(request: Request, current_user: dict = Depends(get_current_user)):
async def mcp_endpoint(request: Request):
"""Global MCP endpoint - exposes all KBs."""
return await handle_mcp_request(request, kb_filter=None)
@app.post("/mcp/kb/{kb_slug}")
async def mcp_kb_endpoint(kb_slug: str, request: Request, current_user: dict = Depends(get_current_user)):
async def mcp_kb_endpoint(kb_slug: str, request: Request):
"""KB-specific MCP endpoint - only exposes tools for this KB."""
# Get KB name from slug
db_manager = get_sqlite_manager()
@ -3273,6 +3263,12 @@ async def handle_mcp_request(request: Request, kb_filter: Optional[str] = None)
# Authentication Endpoints
# ============================================================================
from mcp_server.auth import (
authenticate_user,
create_access_token,
is_auth_enabled,
get_current_user
)
from datetime import timedelta
@ -3493,7 +3489,7 @@ def _run_consolidation_background(job_id: str, kb_name: str, doc_ids: list, new_
@app.post("/api/kb/{kb_name}/lifecycle/dedup-scan")
async def api_lifecycle_dedup_scan(kb_name: str, request: DedupScanRequest, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_dedup_scan(kb_name: str, request: DedupScanRequest, background_tasks: BackgroundTasks):
"""Scan KB for duplicate/near-duplicate documents."""
import uuid
@ -3524,7 +3520,7 @@ async def api_lifecycle_dedup_scan(kb_name: str, request: DedupScanRequest, back
@app.post("/api/kb/{kb_name}/lifecycle/dedup-merge")
async def api_lifecycle_dedup_merge(kb_name: str, request: DedupMergeRequest, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_dedup_merge(kb_name: str, request: DedupMergeRequest):
"""Merge duplicate documents by keeping one and removing the rest."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3536,7 +3532,7 @@ async def api_lifecycle_dedup_merge(kb_name: str, request: DedupMergeRequest, cu
@app.post("/api/kb/{kb_name}/lifecycle/consolidate")
async def api_lifecycle_consolidate(kb_name: str, request: ConsolidateRequest, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_consolidate(kb_name: str, request: ConsolidateRequest, background_tasks: BackgroundTasks):
"""Consolidate related documents into a single merged document using LLM."""
import uuid
@ -3563,7 +3559,7 @@ async def api_lifecycle_consolidate(kb_name: str, request: ConsolidateRequest, b
@app.get("/api/kb/{kb_name}/lifecycle/health")
async def api_lifecycle_health(kb_name: str, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_health(kb_name: str):
"""Get comprehensive health report for a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3574,7 +3570,7 @@ async def api_lifecycle_health(kb_name: str, current_user: dict = Depends(get_cu
@app.get("/api/kb/{kb_name}/lifecycle/growth")
async def api_lifecycle_growth(kb_name: str, granularity: str = "month", current_user: dict = Depends(get_current_user)):
async def api_lifecycle_growth(kb_name: str, granularity: str = "month"):
"""Get document growth timeline for a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3585,7 +3581,7 @@ async def api_lifecycle_growth(kb_name: str, granularity: str = "month", current
@app.post("/api/kb/{kb_name}/lifecycle/archive")
async def api_lifecycle_archive(kb_name: str, request: ArchiveRequest, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_archive(kb_name: str, request: ArchiveRequest):
"""Archive documents in a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3596,7 +3592,7 @@ async def api_lifecycle_archive(kb_name: str, request: ArchiveRequest, current_u
@app.post("/api/kb/{kb_name}/lifecycle/restore")
async def api_lifecycle_restore(kb_name: str, request: RestoreRequest, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_restore(kb_name: str, request: RestoreRequest):
"""Restore archived documents in a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3607,7 +3603,7 @@ async def api_lifecycle_restore(kb_name: str, request: RestoreRequest, current_u
@app.get("/api/kb/{kb_name}/lifecycle/archived")
async def api_lifecycle_archived(kb_name: str, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_archived(kb_name: str):
"""List archived documents in a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3618,7 +3614,7 @@ async def api_lifecycle_archived(kb_name: str, current_user: dict = Depends(get_
@app.get("/api/kb/{kb_name}/lifecycle/stale")
async def api_lifecycle_stale(kb_name: str, stale_days: int = 90, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_stale(kb_name: str, stale_days: int = 90):
"""Find stale documents older than the specified threshold."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):
@ -3629,7 +3625,7 @@ async def api_lifecycle_stale(kb_name: str, stale_days: int = 90, current_user:
@app.get("/api/kb/{kb_name}/lifecycle/logs")
async def api_lifecycle_logs(kb_name: str, operation_type: Optional[str] = None, limit: int = 50, current_user: dict = Depends(get_current_user)):
async def api_lifecycle_logs(kb_name: str, operation_type: Optional[str] = None, limit: int = 50):
"""Get lifecycle operation logs for a knowledge base."""
db_manager = get_sqlite_manager()
if not db_manager.collection_exists(kb_name):

View file

@ -2,7 +2,6 @@
llama-index>=0.10.0
llama-index-llms-ollama>=0.1.0
llama-index-embeddings-ollama>=0.1.0
llama-index-llms-groq>=0.5.0
# Vector Database (SQLite-based)
sqlite-vec>=0.1.0
@ -26,9 +25,7 @@ sentence-transformers>=2.2.0
rank-bm25>=0.2.2
# MCP Server
# Pinned <2.0.0: claude_code_mcp.py uses the low-level Server API
# (@server.list_tools() etc.) which mcp 2.0.0 removed/renamed.
mcp>=1.0.0,<2.0.0
mcp>=1.0.0
fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.0.0