Initial commit
This commit is contained in:
+82
@@ -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
File diff suppressed because one or more lines are too long
+124
@@ -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
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
+2333
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
Executable
BIN
Binary file not shown.
@@ -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))
|
||||
}
|
||||
Executable
+177
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user