Initial commit

This commit is contained in:
2025-11-04 00:23:51 +00:00
commit a397a14cab
15 changed files with 9102 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# --- Python --------------------------------------------------------------
venv/
.env/
.venv/
env/
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.dll
*.dylib
# Build / distribution
build/
dist/
*.egg-info/
.eggs/
*.egg
*.whl
*.zip
*.tar.gz
*.tgz
*.rar
*.tar.bz2
*.gz
# Virtual environment activation scripts
activate*
activate.csh
activate.fish
activate.ps1
activate
pyvenv.cfg
# Coverage / testing
htmlcov/
coverage.xml
.tox/
.env
.env.*
# --- Node / JavaScript -----------------------------------------------
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Transpiled output (if any)
dist/
build/
lib/
src/
# --- OS / IDE ---------------------------------------------------------
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp
*.swo
*.bak
*.tmp
*.temp
*.orig
*.sublime-workspace
*.sublime-project
# --- Binaries / compiled packages --------------------------------------
lib/python3.13/site-packages/
lib64/
bin/
# --- Runtime data / large blobs ---------------------------------------
chroma_db/
chroma.sqlite3
# --- Misc ----------------------------------------------------------------
# (Leave this at the end to catch anything not matched above)
# (but **do NOT** put a blanket "*" here!)
+5232
View File
File diff suppressed because one or more lines are too long
+124
View File
@@ -0,0 +1,124 @@
# compact_toon.py
"""
Ultra-compact Toon format translator for maximum token efficiency
"""
import re
from typing import Dict, List, Any, Optional
class CompactToon:
"""Minimal Toon format translator for maximum token savings."""
@staticmethod
def search_results(result_string: str, query: str = "") -> str:
"""Convert search results to minimal Toon format."""
if not result_string or "No results found" in result_string:
return result_string
results = result_string.split("---")
toon_blocks = []
for result in results:
if not result.strip():
continue
toon_block = CompactToon._minimal_result(result.strip())
if toon_block:
toon_blocks.append(toon_block)
return "\n\n".join(toon_blocks)
@staticmethod
def _minimal_result(result_text: str) -> Optional[str]:
"""Create minimal Toon block for a single result."""
try:
# Fast regex extraction
file_match = re.search(r"File:\s*(.+?)\n", result_text)
line_match = re.search(r"Line:\s*(\d+)", result_text)
type_match = re.search(r"Type:\s*(.+?)\n", result_text)
score_match = re.search(r"score:\s*([\d.]+)", result_text)
if not file_match:
return None
file_path = file_match.group(1).strip()
line_num = line_match.group(1) if line_match else "1"
# Extract content efficiently
lines = result_text.split('\n')
content_lines = []
in_content = False
for line in lines:
if in_content:
content_lines.append(line)
elif line.strip() and not any(line.startswith(x) for x in
['###', 'File:', 'Line:', 'Type:', 'Language:']):
in_content = True
content_lines.append(line)
content = '\n'.join(content_lines).strip()
# Build minimal Toon format
toon_lines = []
toon_lines.append(f"file://{file_path}:{line_num}")
if type_match:
toon_lines.append(f" type: {type_match.group(1).strip()}")
if score_match:
toon_lines.append(f" score: {score_match.group(1)}")
toon_lines.append(" content: |")
# Add content with minimal processing
for line in content.split('\n'):
toon_lines.append(f" {line}")
return "\n".join(toon_lines)
except Exception:
return None
@staticmethod
def references(result_string: str, symbol: str = "") -> str:
"""Convert reference results to minimal format."""
if not result_string or "No references found" in result_string:
return result_string
results = result_string.split("---")
toon_blocks = []
for result in results:
if not result.strip():
continue
toon_block = CompactToon._minimal_reference(result.strip())
if toon_block:
toon_blocks.append(toon_block)
return "\n\n".join(toon_blocks)
@staticmethod
def _minimal_reference(result_text: str) -> Optional[str]:
"""Create minimal reference block."""
try:
file_match = re.search(r"File:\s*(.+?)\n", result_text)
line_match = re.search(r"Line:\s*(\d+)", result_text)
if not file_match or not line_match:
return None
# Extract context efficiently
context_match = re.search(r"Context:\s*(.+)", result_text, re.DOTALL)
context = context_match.group(1).strip() if context_match else ""
toon_lines = []
toon_lines.append(f"file://{file_match.group(1).strip()}:{line_match.group(1)}")
toon_lines.append(" type: reference")
toon_lines.append(" content: |")
for line in context.split('\n'):
toon_lines.append(f" {line}")
return "\n".join(toon_lines)
except Exception:
return None
+219
View File
@@ -0,0 +1,219 @@
# enhanced_toon.py
"""
Official TOON (Token-Oriented Object Notation) formatter following the spec from github.com/toon-format/toon
Provides 30-60% token savings compared to JSON for LLM communication.
"""
import re
from typing import List, Dict, Any, Optional
class EnhancedToon:
"""
TOON formatter implementing the official specification from toon-format/toon
Optimized for uniform arrays of objects with significant token reduction.
"""
@staticmethod
def search_results(search_output: str, query: str) -> str:
"""
Convert search results to proper TOON format following official spec.
Args:
search_output: Raw search results from search_codebase
query: Original search query
Returns:
Proper TOON formatted results
"""
if not search_output or "No results" in search_output:
return f"# Search: {query}\nNo results found.\n"
# If it's already an error message, return as is
if "Error reading file" in search_output:
return search_output
try:
# Parse individual results
results = []
current_result = {}
lines = search_output.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('### Result'):
# Save previous result if exists
if current_result and current_result.get('file'):
results.append(current_result)
# Start new result
current_result = {
'file': '',
'line': '',
'type': '',
'content': []
}
# Extract score from header (remove relevance text, keep only score)
score_match = re.search(r'score:\s*([\d.]+)', line)
if score_match:
current_result['score'] = float(score_match.group(1))
else:
current_result['score'] = 0.0
i += 1
elif line.startswith('File:') and current_result:
# Only take the first File: line, ignore duplicates
if not current_result['file']:
current_result['file'] = line.replace('File:', '').strip()
i += 1
elif line.startswith('Line:') and current_result:
# Only take the first Line: line, ignore duplicates
if not current_result['line']:
current_result['line'] = line.replace('Line:', '').strip()
i += 1
elif line.startswith('Type:') and current_result:
# Only take the first Type: line, ignore duplicates
if not current_result['type']:
current_result['type'] = line.replace('Type:', '').strip()
i += 1
elif line == '---':
# End of result
i += 1
elif line and not line.startswith('###') and current_result:
# Skip all metadata lines (File:, Type:, Name:, Language:, Doc:, SQL:, Code:)
if not any(line.startswith(prefix) for prefix in
['File:', 'Line:', 'Type:', 'Name:', 'Language:', 'Doc:', 'SQL:', 'Code:']):
current_result['content'].append(line)
i += 1
else:
i += 1
# Add the last result if valid
if current_result and current_result.get('file'):
results.append(current_result)
# Convert to proper TOON format
return EnhancedToon._format_search_toon(results, query)
except Exception as e:
# If parsing fails, return original output but clean it up
return EnhancedToon._clean_raw_output(search_output, query)
@staticmethod
def _clean_raw_output(raw_output: str, query: str) -> str:
"""
Fallback: Clean up raw output by removing duplicate metadata lines.
"""
lines = raw_output.split('\n')
cleaned_lines = []
skip_next_metadata = False
for line in lines:
stripped = line.strip()
# Skip duplicate metadata lines that appear after the first occurrence
if any(stripped.startswith(prefix) for prefix in ['File:', 'Type:', 'Name:', 'Language:', 'Doc:']):
if skip_next_metadata:
continue
skip_next_metadata = True
elif stripped.startswith('### Result') or stripped == '---':
skip_next_metadata = False
cleaned_lines.append(line)
else:
cleaned_lines.append(line)
# Also remove relevance text, keep only score
final_lines = []
for line in cleaned_lines:
# Remove "relevance: HIGH/MEDIUM/LOW" text, keep only score
line = re.sub(r'relevance:\s*\w+,\s*', '', line)
final_lines.append(line)
return "\n".join(final_lines)
@staticmethod
def _format_search_toon(results: List[Dict], query: str) -> str:
"""
Format search results following official TOON specification.
"""
if not results:
return f"# Search: {query}\nNo results found.\n"
lines = [f"# Search: {query}", f"results[{len(results)}]{{file,line,score,content}}:"]
for i, result in enumerate(results, 1):
# Prepare content - join and clean
content = ' '.join(result.get('content', [])).strip()
content = re.sub(r'\s+', ' ', content) # Normalize whitespace
# Remove any remaining metadata patterns
content = re.sub(r'^File:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^Type:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^Name:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^Language:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^Doc:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^SQL:.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^Code:.*$', '', content, flags=re.MULTILINE)
content = content.strip()
# Build TOON row - simple format without relevance text
row = [
result.get('file', ''),
result.get('line', ''),
f"{result.get('score', 0.0):.2f}",
content[:300] # Reasonable limit
]
# Escape fields if needed and join with comma
escaped_row = [EnhancedToon._escape_toon_field(str(field)) for field in row]
lines.append(" " + ",".join(escaped_row))
return "\n".join(lines)
@staticmethod
def _escape_toon_field(field: str) -> str:
"""
Escape TOON field according to specification.
Only quote fields that contain commas, quotes, or newlines.
"""
if any(char in field for char in [',', '"', '\n', '\r']):
# Escape quotes and wrap in quotes
escaped = field.replace('"', '\\"')
return f'"{escaped}"'
return field
@staticmethod
def reference_results(reference_output: str, symbol: str) -> str:
"""
Convert reference results to clean format.
"""
if "Error reading file" in reference_output:
return reference_output
# Clean up reference output similarly
return EnhancedToon._clean_raw_output(reference_output, f"References: {symbol}")
@staticmethod
def file_content_results(file_output: str, path: str) -> str:
"""
Convert file content results, preserving the full content without truncation.
"""
# Remove any truncation messages and return full content
if "truncated to 100 lines" in file_output:
# This indicates the file was truncated, we want to avoid that
# For now, just return the original output but remove the truncation notice
lines = file_output.split('\n')
cleaned_lines = []
for line in lines:
if "truncated to" not in line and "Context:" not in line:
cleaned_lines.append(line)
return "\n".join(cleaned_lines)
return file_output
+164
View File
@@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */
Symlink
+1
View File
@@ -0,0 +1 @@
lib
+2333
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "ragmcp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@typescript-eslint/typescript-estree": "^8.46.2"
}
}
+86
View File
@@ -0,0 +1,86 @@
import re
import ast
import sqlglot
from typing import Dict, List, Any
from pathlib import Path
def extract_sql_schema(sql_code: str) -> Dict[str, List[str]]:
"""
Extracts table -> columns mapping from SQL code using sqlglot.
Returns a canonical schema.
"""
schema: Dict[str, List[str]] = {}
try:
statements = sqlglot.parse(sql_code, read="postgres")
except Exception:
return schema
for stmt in statements:
if stmt.key and stmt.key.upper() == "CREATE":
for table in stmt.find_all(sqlglot.exp.Create):
try:
tname = table.this.this
cols = []
for coldef in table.find_all(sqlglot.exp.ColumnDef):
cname = getattr(coldef.this, "name", None)
if cname:
cols.append(cname)
if tname and cols:
schema[tname] = cols
except Exception:
continue
return schema
def extract_python_structure(code: str) -> Dict[str, List[str]]:
"""Return module structure: functions and classes."""
try:
tree = ast.parse(code)
except SyntaxError:
return {}
funcs = [n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]
classes = [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
return {"functions": funcs, "classes": classes}
def extract_go_structure(code: str) -> Dict[str, List[str]]:
"""Simple regex-based Go structure detection (lightweight)."""
funcs = re.findall(r"func\s+([A-Z]\w+)", code)
structs = re.findall(r"type\s+(\w+)\s+struct", code)
return {"functions": funcs, "structs": structs}
def extract_rust_structure(code: str) -> Dict[str, List[str]]:
"""Heuristic Rust index."""
structs = re.findall(r"struct\s+(\w+)", code)
traits = re.findall(r"trait\s+(\w+)", code)
funcs = re.findall(r"fn\s+(\w+)", code)
return {"functions": funcs, "structs": structs, "traits": traits}
def extract_svelte_structure(code: str) -> Dict[str, List[str]]:
"""Minimal Svelte export/prop finder."""
exports = re.findall(r"export\s+let\s+(\w+)", code)
funcs = re.findall(r"function\s+(\w+)", code)
return {"props": exports, "functions": funcs}
def build_quick_index(language: str, code: str, filepath: Path) -> Dict[str, Any]:
"""Dispatch to the appropriate structure extractor."""
data = {}
if language == "sql":
data = extract_sql_schema(code)
elif language == "python":
data = extract_python_structure(code)
elif language == "go":
data = extract_go_structure(code)
elif language == "rust":
data = extract_rust_structure(code)
elif language == "svelte":
data = extract_svelte_structure(code)
return {
"text": f"Quick index for {filepath.name}:\n{data}",
"metadata": {"language": language, "file": str(filepath), "type": "index"}
}
+28
View File
@@ -0,0 +1,28 @@
# MCP server
fastmcp==0.1.0
# Ollama embeddings
langchain-ollama==0.2.0
langchain-community==0.3.31
# Vector DB
chromadb==0.4.13
# BM25 search
rank-bm25==0.2.2
# AST parsing
javalang==0.13.0
# File ignore parsing
pathspec==0.10.3
# HTTP requests (for optional reranking calls)
requests>=2.32.5,<3.0.0
# Optional: subprocess utilities
psutil==5.9.5
# --- SQL parsing stack ---
sqlglot==25.3.0 # Portable SQL AST + transpiler (fast for SELECTs, DML)
sqlparse==0.5.1 # Lightweight fallback tokenizer (simple/heuristic parsing)
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""
HTTP server for MCP codebase RAG with REST API
"""
import uvicorn
import logging
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
import signal
import sys
import json
from enhanced_toon import EnhancedToon
from mcp_codebase import (
startup,
search_codebase as _search_codebase,
find_code_references as _find_code_references,
read_file_lines,
rebuild_index as _rebuild_index
)
logger = logging.getLogger("rag-mcp")
def signal_handler(sig, frame):
"""Handle graceful shutdown on Ctrl+C."""
logger.info("\n=== HTTP Server Shutdown ===")
logger.info("Goodbye!\n")
sys.exit(0)
# Request/Response models
class SearchRequest(BaseModel):
query: str
top_k: int = 5
rerank: bool = True
class ReferenceRequest(BaseModel):
symbol: str
top_k: int = 20
class ReadFileRequest(BaseModel):
path: str
start: int = 1
end: Optional[int] = None
class SymbolRequest(BaseModel):
symbol: str
def create_app():
"""Create FastAPI app with MCP tool endpoints"""
app = FastAPI(
title="MCP Codebase RAG Server",
description="Codebase search and analysis via MCP tools over HTTP",
version="1.0.0"
)
# Add CORS middleware for remote access
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure this appropriately for production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Health check endpoint
@app.get("/")
async def root():
return {
"status": "running",
"service": "MCP Codebase RAG Server",
"version": "1.0.0",
"endpoints": {
"docs": "/docs",
"health": "/health",
"tools": "/tools",
"search": "POST /search",
"references": "POST /references",
"read_file": "POST /read_file",
"rebuild": "POST /rebuild"
}
}
@app.get("/health")
async def health():
"""Get server health and statistics"""
try:
# Import here to avoid circular import issues
from mcp_codebase import health_check
result = health_check()
# Parse JSON string to dict for better API response
try:
result_dict = json.loads(result)
return result_dict
except:
return {"result": result}
except Exception as e:
logger.exception("Health check failed")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/tools")
async def list_tools():
"""List available MCP tools"""
return {
"tools": [
{
"name": "search_codebase",
"description": "Search the entire codebase using hybrid RAG",
"endpoint": "POST /search",
"parameters": {
"query": "string (required)",
"top_k": "int (default: 5)",
"rerank": "bool (default: true)"
}
},
{
"name": "find_code_references",
"description": "Find all references to a specific symbol",
"endpoint": "POST /references",
"parameters": {
"symbol": "string (required)",
"top_k": "int (default: 20)"
}
},
{
"name": "read_file_lines",
"description": "Read specific lines from a file with context",
"endpoint": "POST /read_file",
"parameters": {
"path": "string (required)",
"start": "int (default: 1)",
"end": "int (optional)"
}
},
{
"name": "rebuild_index",
"description": "Force rebuild of search indexes",
"endpoint": "POST /rebuild",
"parameters": {}
}
]
}
@app.post("/search")
async def search(request: SearchRequest):
"""Search codebase - returns enhanced Toon format"""
try:
result = _search_codebase(
query=request.query,
top_k=request.top_k,
rerank=request.rerank
)
enhanced_result = EnhancedToon.search_results(result, request.query)
return {
"result": enhanced_result,
"query": request.query,
"top_k": request.top_k
}
except Exception as e:
logger.exception("Search failed")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/references")
async def references(request: ReferenceRequest):
"""Find all references to a symbol - returns enhanced Toon format"""
try:
result = _find_code_references(
symbol=request.symbol,
top_k=request.top_k
)
enhanced_result = EnhancedToon.reference_results(result, request.symbol)
return {
"result": enhanced_result,
"symbol": request.symbol,
"top_k": request.top_k
}
except Exception as e:
logger.exception("Reference search failed")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/read_file")
async def read_file(request: ReadFileRequest):
"""Read file lines with context - returns enhanced Toon format"""
try:
result = read_file_lines(
path=request.path,
start=request.start,
end=request.end
)
enhanced_result = EnhancedToon.file_content_results(result, request.path)
return {
"result": enhanced_result,
"path": request.path,
"start_line": request.start,
"end_line": request.end
}
except Exception as e:
logger.exception("Read file failed")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/rebuild")
async def rebuild():
"""Rebuild search indexes"""
try:
result = _rebuild_index()
# For rebuild, we might not need enhanced format as it's usually a simple status message
return {"result": result}
except Exception as e:
logger.exception("Rebuild failed")
raise HTTPException(status_code=500, detail=str(e))
return app
if __name__ == "__main__":
# Register signal handlers
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
# Initialize the indexes (this will load or build them)
startup()
logger.info("="*60)
logger.info("🚀 Starting HTTP server on: http://0.0.0.0:8000")
logger.info("📚 REST API endpoints:")
logger.info(" GET / - Service info")
logger.info(" GET /health - Server health & stats")
logger.info(" GET /tools - List available tools")
logger.info(" POST /search - Search codebase")
logger.info(" POST /references - Find symbol references")
logger.info(" POST /read_file - Read file with context")
logger.info(" POST /rebuild - Rebuild indexes")
logger.info("📖 API docs available at: http://0.0.0.0:8000/docs")
logger.info("⏹️ Press Ctrl+C to stop")
logger.info("="*60)
# Create and run app
app = create_app()
uvicorn.run(
app,
host="0.0.0.0",
port=8000,
log_level="info",
access_log=True
)
except KeyboardInterrupt:
signal_handler(signal.SIGINT, None)
except Exception as e:
logger.exception("Fatal error during HTTP server startup")
sys.exit(1)
+188
View File
@@ -0,0 +1,188 @@
'\" -*- coding: us-ascii -*-
.if \n(.g .ds T< \\FC
.if \n(.g .ds T> \\F[\n[.fam]]
.de URL
\\$2 \(la\\$1\(ra\\$3
..
.if \n(.g .mso www.tmac
.TH isympy 1 2007-10-8 "" ""
.SH NAME
isympy \- interactive shell for SymPy
.SH SYNOPSIS
'nh
.fi
.ad l
\fBisympy\fR \kx
.if (\nx>(\n(.l/2)) .nr x (\n(.l/5)
'in \n(.iu+\nxu
[\fB-c\fR | \fB--console\fR] [\fB-p\fR ENCODING | \fB--pretty\fR ENCODING] [\fB-t\fR TYPE | \fB--types\fR TYPE] [\fB-o\fR ORDER | \fB--order\fR ORDER] [\fB-q\fR | \fB--quiet\fR] [\fB-d\fR | \fB--doctest\fR] [\fB-C\fR | \fB--no-cache\fR] [\fB-a\fR | \fB--auto\fR] [\fB-D\fR | \fB--debug\fR] [
-- | PYTHONOPTIONS]
'in \n(.iu-\nxu
.ad b
'hy
'nh
.fi
.ad l
\fBisympy\fR \kx
.if (\nx>(\n(.l/2)) .nr x (\n(.l/5)
'in \n(.iu+\nxu
[
{\fB-h\fR | \fB--help\fR}
|
{\fB-v\fR | \fB--version\fR}
]
'in \n(.iu-\nxu
.ad b
'hy
.SH DESCRIPTION
isympy is a Python shell for SymPy. It is just a normal python shell
(ipython shell if you have the ipython package installed) that executes
the following commands so that you don't have to:
.PP
.nf
\*(T<
>>> from __future__ import division
>>> from sympy import *
>>> x, y, z = symbols("x,y,z")
>>> k, m, n = symbols("k,m,n", integer=True)
\*(T>
.fi
.PP
So starting isympy is equivalent to starting python (or ipython) and
executing the above commands by hand. It is intended for easy and quick
experimentation with SymPy. For more complicated programs, it is recommended
to write a script and import things explicitly (using the "from sympy
import sin, log, Symbol, ..." idiom).
.SH OPTIONS
.TP
\*(T<\fB\-c \fR\*(T>\fISHELL\fR, \*(T<\fB\-\-console=\fR\*(T>\fISHELL\fR
Use the specified shell (python or ipython) as
console backend instead of the default one (ipython
if present or python otherwise).
Example: isympy -c python
\fISHELL\fR could be either
\&'ipython' or 'python'
.TP
\*(T<\fB\-p \fR\*(T>\fIENCODING\fR, \*(T<\fB\-\-pretty=\fR\*(T>\fIENCODING\fR
Setup pretty printing in SymPy. By default, the most pretty, unicode
printing is enabled (if the terminal supports it). You can use less
pretty ASCII printing instead or no pretty printing at all.
Example: isympy -p no
\fIENCODING\fR must be one of 'unicode',
\&'ascii' or 'no'.
.TP
\*(T<\fB\-t \fR\*(T>\fITYPE\fR, \*(T<\fB\-\-types=\fR\*(T>\fITYPE\fR
Setup the ground types for the polys. By default, gmpy ground types
are used if gmpy2 or gmpy is installed, otherwise it falls back to python
ground types, which are a little bit slower. You can manually
choose python ground types even if gmpy is installed (e.g., for testing purposes).
Note that sympy ground types are not supported, and should be used
only for experimental purposes.
Note that the gmpy1 ground type is primarily intended for testing; it the
use of gmpy even if gmpy2 is available.
This is the same as setting the environment variable
SYMPY_GROUND_TYPES to the given ground type (e.g.,
SYMPY_GROUND_TYPES='gmpy')
The ground types can be determined interactively from the variable
sympy.polys.domains.GROUND_TYPES inside the isympy shell itself.
Example: isympy -t python
\fITYPE\fR must be one of 'gmpy',
\&'gmpy1' or 'python'.
.TP
\*(T<\fB\-o \fR\*(T>\fIORDER\fR, \*(T<\fB\-\-order=\fR\*(T>\fIORDER\fR
Setup the ordering of terms for printing. The default is lex, which
orders terms lexicographically (e.g., x**2 + x + 1). You can choose
other orderings, such as rev-lex, which will use reverse
lexicographic ordering (e.g., 1 + x + x**2).
Note that for very large expressions, ORDER='none' may speed up
printing considerably, with the tradeoff that the order of the terms
in the printed expression will have no canonical order
Example: isympy -o rev-lax
\fIORDER\fR must be one of 'lex', 'rev-lex', 'grlex',
\&'rev-grlex', 'grevlex', 'rev-grevlex', 'old', or 'none'.
.TP
\*(T<\fB\-q\fR\*(T>, \*(T<\fB\-\-quiet\fR\*(T>
Print only Python's and SymPy's versions to stdout at startup, and nothing else.
.TP
\*(T<\fB\-d\fR\*(T>, \*(T<\fB\-\-doctest\fR\*(T>
Use the same format that should be used for doctests. This is
equivalent to '\fIisympy -c python -p no\fR'.
.TP
\*(T<\fB\-C\fR\*(T>, \*(T<\fB\-\-no\-cache\fR\*(T>
Disable the caching mechanism. Disabling the cache may slow certain
operations down considerably. This is useful for testing the cache,
or for benchmarking, as the cache can result in deceptive benchmark timings.
This is the same as setting the environment variable SYMPY_USE_CACHE
to 'no'.
.TP
\*(T<\fB\-a\fR\*(T>, \*(T<\fB\-\-auto\fR\*(T>
Automatically create missing symbols. Normally, typing a name of a
Symbol that has not been instantiated first would raise NameError,
but with this option enabled, any undefined name will be
automatically created as a Symbol. This only works in IPython 0.11.
Note that this is intended only for interactive, calculator style
usage. In a script that uses SymPy, Symbols should be instantiated
at the top, so that it's clear what they are.
This will not override any names that are already defined, which
includes the single character letters represented by the mnemonic
QCOSINE (see the "Gotchas and Pitfalls" document in the
documentation). You can delete existing names by executing "del
name" in the shell itself. You can see if a name is defined by typing
"'name' in globals()".
The Symbols that are created using this have default assumptions.
If you want to place assumptions on symbols, you should create them
using symbols() or var().
Finally, this only works in the top level namespace. So, for
example, if you define a function in isympy with an undefined
Symbol, it will not work.
.TP
\*(T<\fB\-D\fR\*(T>, \*(T<\fB\-\-debug\fR\*(T>
Enable debugging output. This is the same as setting the
environment variable SYMPY_DEBUG to 'True'. The debug status is set
in the variable SYMPY_DEBUG within isympy.
.TP
-- \fIPYTHONOPTIONS\fR
These options will be passed on to \fIipython (1)\fR shell.
Only supported when ipython is being used (standard python shell not supported).
Two dashes (--) are required to separate \fIPYTHONOPTIONS\fR
from the other isympy options.
For example, to run iSymPy without startup banner and colors:
isympy -q -c ipython -- --colors=NoColor
.TP
\*(T<\fB\-h\fR\*(T>, \*(T<\fB\-\-help\fR\*(T>
Print help output and exit.
.TP
\*(T<\fB\-v\fR\*(T>, \*(T<\fB\-\-version\fR\*(T>
Print isympy version information and exit.
.SH FILES
.TP
\*(T<\fI${HOME}/.sympy\-history\fR\*(T>
Saves the history of commands when using the python
shell as backend.
.SH BUGS
The upstreams BTS can be found at \(lahttps://github.com/sympy/sympy/issues\(ra
Please report all bugs that you find in there, this will help improve
the overall quality of SymPy.
.SH "SEE ALSO"
\fBipython\fR(1), \fBpython\fR(1)
BIN
View File
Binary file not shown.
+194
View File
@@ -0,0 +1,194 @@
// # Build from project root:
// go build -o tools/parse_go_ast tools/parse_go_ast.go
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"os"
"strings"
)
type GoDecl struct {
Name string `json:"name"`
Type string `json:"type"` // "func", "method", "type", "struct", "interface", "var", "package"
Receiver string `json:"receiver,omitempty"` // e.g., "*User"
FullName string `json:"full_name,omitempty"`
Fields []string `json:"fields,omitempty"` // for structs
Methods []string `json:"methods,omitempty"` // for interfaces
DocComment string `json:"doc_comment,omitempty"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
}
func astTypeToString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + astTypeToString(t.X)
case *ast.SelectorExpr:
return astTypeToString(t.X) + "." + t.Sel.Name
case *ast.ArrayType:
return "[]" + astTypeToString(t.Elt)
case *ast.MapType:
return "map[" + astTypeToString(t.Key) + "]" + astTypeToString(t.Value)
default:
return "unknown"
}
}
func extractDocComment(comments []*ast.CommentGroup, pos token.Pos, fset *token.FileSet) string {
if len(comments) == 0 {
return ""
}
line := fset.Position(pos).Line
for i := len(comments) - 1; i >= 0; i-- {
cg := comments[i]
cgLine := fset.Position(cg.End()).Line
if cgLine < line && line-cgLine <= 5 {
return strings.TrimSpace(cg.Text())
}
}
return ""
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: parse_go_ast <file.go>")
os.Exit(1)
}
filename := os.Args[1]
src, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Printf("ERR: %v\n", err)
os.Exit(2)
}
fset := token.NewFileSet()
fileNode, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
if err != nil {
fmt.Printf("ERR: %v\n", err)
os.Exit(3)
}
decls := []GoDecl{}
// Package comment (first comment group before package decl)
pkgComment := ""
for _, cg := range fileNode.Comments {
if cg.Pos() < fileNode.Package {
pkgComment = strings.TrimSpace(cg.Text())
} else {
break
}
}
if pkgComment != "" {
decls = append(decls, GoDecl{
Name: "package",
Type: "package",
DocComment: pkgComment,
StartLine: 1,
EndLine: 1,
})
}
// Process declarations
for _, d := range fileNode.Decls {
switch d := d.(type) {
case *ast.FuncDecl:
name := d.Name.Name
recvType := ""
if d.Recv != nil && len(d.Recv.List) > 0 {
recvType = astTypeToString(d.Recv.List[0].Type)
}
typ := "func"
fullName := name
if recvType != "" {
typ = "method"
fullName = fmt.Sprintf("(%s).%s", recvType, name)
}
doc := extractDocComment(fileNode.Comments, d.Pos(), fset)
decls = append(decls, GoDecl{
Name: name,
Type: typ,
Receiver: recvType,
FullName: fullName,
DocComment: doc,
StartLine: fset.Position(d.Pos()).Line,
EndLine: fset.Position(d.End()).Line,
})
case *ast.GenDecl:
for _, spec := range d.Specs {
switch s := spec.(type) {
case *ast.TypeSpec:
typName := s.Name.Name
declType := "type"
var fields []string
var methods []string
switch t := s.Type.(type) {
case *ast.StructType:
declType = "struct"
for _, f := range t.Fields.List {
for _, n := range f.Names {
field := n.Name
if f.Tag != nil {
field += " " + f.Tag.Value
}
fields = append(fields, field)
}
if f.Names == nil {
// Embedded field
fields = append(fields, astTypeToString(f.Type))
}
}
case *ast.InterfaceType:
declType = "interface"
for _, m := range t.Methods.List {
if len(m.Names) > 0 {
methods = append(methods, m.Names[0].Name)
}
}
}
doc := extractDocComment(fileNode.Comments, s.Pos(), fset)
decls = append(decls, GoDecl{
Name: typName,
Type: declType,
Fields: fields,
Methods: methods,
DocComment: doc,
StartLine: fset.Position(d.Pos()).Line,
EndLine: fset.Position(d.End()).Line,
})
case *ast.ValueSpec:
for _, name := range s.Names {
doc := extractDocComment(fileNode.Comments, name.Pos(), fset)
// Use the individual identifier's position, not the declaration's position
startLine := fset.Position(name.Pos()).Line
endLine := fset.Position(name.End()).Line
decls = append(decls, GoDecl{
Name: name.Name,
Type: "var",
DocComment: doc,
StartLine: startLine,
EndLine: endLine,
})
}
}
}
}
}
out, _ := json.Marshal(decls)
fmt.Println(string(out))
}
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env node
// Usage: node parse_ts.js file.ts
const fs = require('fs');
const path = require('path');
const { parse } = require('@typescript-eslint/typescript-estree');
function getLoc(node) {
if (node.loc) {
return { start: node.loc.start.line, end: node.loc.end.line };
}
return { start: 1, end: 1 };
}
function extractLeadingComment(node, sourceCode) {
if (!node.range || !sourceCode) return "";
const startIdx = node.range[0];
let commentEnd = startIdx;
// Look backward for comments
let i = startIdx - 1;
let commentLines = [];
let inBlock = false;
while (i >= 0) {
const char = sourceCode[i];
if (char === '\n') break;
i--;
}
const lineStart = i + 1;
const lineAbove = sourceCode.slice(lineStart, startIdx).trim();
// Check for // comment on same line before node
if (lineAbove.startsWith('//')) {
return lineAbove.substring(2).trim();
}
// Look further up for multi-line or JSDoc
const lines = sourceCode.substring(0, lineStart).split('\n');
for (let j = lines.length - 1; j >= Math.max(0, lines.length - 5); j--) {
const line = lines[j].trim();
if (line.startsWith('//')) {
commentLines.unshift(line.substring(2).trim());
} else if (line.endsWith('*/')) {
inBlock = true;
commentLines.unshift(line.slice(0, -2).trim());
} else if (inBlock) {
if (line.startsWith('/*') || line.startsWith('/**')) {
commentLines.unshift(line.slice(2).trim());
break;
} else {
commentLines.unshift(line);
}
} else if (line === '') {
if (commentLines.length > 0) continue;
else break;
} else {
break;
}
}
return commentLines.join('\n').trim();
}
function extractDeclarations(ast, sourceCode) {
const decls = [];
function visit(node) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) {
node.forEach(n => visit(n));
return;
}
let docComment = extractLeadingComment(node, sourceCode);
if (node.type === 'FunctionDeclaration' && node.id?.name) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'function',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
else if (
node.type === 'VariableDeclarator' &&
node.id?.type === 'Identifier' &&
node.init?.type === 'ArrowFunctionExpression'
) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'function',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
else if (node.type === 'ClassDeclaration' && node.id?.name) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'class',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
else if (node.type === 'TSInterfaceDeclaration' && node.id?.name) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'interface',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
else if (node.type === 'TSTypeAliasDeclaration' && node.id?.name) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'type',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
else if (
node.type === 'VariableDeclarator' &&
node.id?.type === 'Identifier'
) {
const parent = node.parent;
if (
parent?.type === 'VariableDeclaration' &&
['const', 'let'].includes(parent.kind)
) {
const loc = getLoc(node);
decls.push({
name: node.id.name,
type: 'variable',
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end
});
}
}
Object.values(node).forEach(visit);
}
visit(ast);
return decls;
}
if (process.argv.length < 3) {
console.error('Usage: node parse_ts.js <file.ts>');
process.exit(1);
}
const filePath = path.resolve(process.argv[2]);
try {
const code = fs.readFileSync(filePath, 'utf8');
const ast = parse(code, {
sourceType: 'module',
loc: true,
range: true,
comment: false // we extract manually for simplicity
});
const decls = extractDeclarations(ast, code);
console.log(JSON.stringify(decls, null, 2));
} catch (e) {
console.error(`ERR: ${e.message}`);
process.exit(2);
}