glyphh-code 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glyphh_code/CLAUDE.md +95 -0
- glyphh_code/__init__.py +3 -0
- glyphh_code/ast_extract.py +403 -0
- glyphh_code/banner.py +76 -0
- glyphh_code/cli.py +65 -0
- glyphh_code/compile.py +381 -0
- glyphh_code/config.yaml +13 -0
- glyphh_code/drift.py +74 -0
- glyphh_code/encoder.py +1119 -0
- glyphh_code/hooks/enforce-glyphh-search.sh +32 -0
- glyphh_code/hooks/post-commit-compile.sh +104 -0
- glyphh_code/hooks/post-git-compile.sh +148 -0
- glyphh_code/manifest.yaml +18 -0
- glyphh_code/plugin.py +357 -0
- glyphh_code/setup.py +378 -0
- glyphh_code-0.2.0.dist-info/METADATA +288 -0
- glyphh_code-0.2.0.dist-info/RECORD +21 -0
- glyphh_code-0.2.0.dist-info/WHEEL +5 -0
- glyphh_code-0.2.0.dist-info/entry_points.txt +5 -0
- glyphh_code-0.2.0.dist-info/licenses/LICENSE +661 -0
- glyphh_code-0.2.0.dist-info/top_level.txt +1 -0
glyphh_code/CLAUDE.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Glyphh Code Intelligence
|
|
2
|
+
|
|
3
|
+
This project uses Glyphh for codebase search.
|
|
4
|
+
The Glyphh index is compiled from every file in this repo.
|
|
5
|
+
Follow these rules in every session without exception.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Tools available
|
|
9
|
+
|
|
10
|
+
glyphh_search find files by natural language query
|
|
11
|
+
glyphh_related find files related to a given file
|
|
12
|
+
glyphh_stats index statistics
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Navigation rules
|
|
16
|
+
|
|
17
|
+
ALWAYS call glyphh_search before reading any file.
|
|
18
|
+
ALWAYS call glyphh_related before editing a file.
|
|
19
|
+
NEVER use Grep to find files. Use glyphh_search instead.
|
|
20
|
+
NEVER use Glob to find files. Use glyphh_search instead.
|
|
21
|
+
NEVER use the Agent tool to explore the codebase. Use glyphh_search instead.
|
|
22
|
+
NEVER scan directories to find relevant code.
|
|
23
|
+
NEVER read multiple files speculatively.
|
|
24
|
+
Only fall back to Grep or Glob if glyphh_search returns no results above 0.50.
|
|
25
|
+
|
|
26
|
+
Search results include top_tokens and imports for each file.
|
|
27
|
+
Use top_tokens to understand what the file is about.
|
|
28
|
+
Use imports to understand what it depends on.
|
|
29
|
+
Only read the file if top_tokens and imports do not answer the question.
|
|
30
|
+
Prefer files with confidence above 0.70.
|
|
31
|
+
If the result state is ASK, tell the user the candidates and ask which to use.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Debugging rules
|
|
35
|
+
|
|
36
|
+
When investigating a bug or error:
|
|
37
|
+
1. Call glyphh_search with the error type or concept from the stack trace
|
|
38
|
+
2. Check top_tokens and imports from results before reading any file
|
|
39
|
+
3. Read only files with confidence above 0.70
|
|
40
|
+
4. Call glyphh_related on the target file before making any change
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Editing rules
|
|
44
|
+
|
|
45
|
+
Before editing any file:
|
|
46
|
+
1. Call glyphh_related to understand blast radius
|
|
47
|
+
2. Review top_tokens and imports of related files
|
|
48
|
+
|
|
49
|
+
After editing:
|
|
50
|
+
A Claude Code PostToolUse hook runs compile.py --incremental in the
|
|
51
|
+
background after every git commit to update the index automatically.
|
|
52
|
+
No manual recompile needed.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Query guide
|
|
56
|
+
|
|
57
|
+
Good queries for glyphh_search use specific domain vocabulary:
|
|
58
|
+
auth token validation
|
|
59
|
+
stripe webhook handler
|
|
60
|
+
user profile fetch
|
|
61
|
+
database connection pool
|
|
62
|
+
error boundary component
|
|
63
|
+
payment retry logic
|
|
64
|
+
session expiry check
|
|
65
|
+
|
|
66
|
+
Poor queries are too generic and will return low-confidence results:
|
|
67
|
+
utils
|
|
68
|
+
helper
|
|
69
|
+
index
|
|
70
|
+
common
|
|
71
|
+
base
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Search result shape
|
|
75
|
+
|
|
76
|
+
glyphh_search returns:
|
|
77
|
+
|
|
78
|
+
state DONE or ASK
|
|
79
|
+
matches list of results when state is DONE
|
|
80
|
+
file relative file path
|
|
81
|
+
confidence 0.0 to 1.0, prefer above 0.70
|
|
82
|
+
top_tokens dominant concepts in the file
|
|
83
|
+
imports what the file depends on
|
|
84
|
+
extension file type
|
|
85
|
+
candidates list of options when state is ASK
|
|
86
|
+
|
|
87
|
+
glyphh_related returns:
|
|
88
|
+
|
|
89
|
+
state DONE or ASK
|
|
90
|
+
file the queried file
|
|
91
|
+
related list of semantically similar files
|
|
92
|
+
file relative file path
|
|
93
|
+
similarity 0.0 to 1.0
|
|
94
|
+
top_tokens dominant concepts
|
|
95
|
+
imports dependencies
|
glyphh_code/__init__.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Language-agnostic AST extraction for Glyphh Code model.
|
|
3
|
+
|
|
4
|
+
Uses tree-sitter to extract structural signals from source files:
|
|
5
|
+
- defines: top-level class/function/method names (split into words)
|
|
6
|
+
- imports: module/package dependencies
|
|
7
|
+
- docstring: module-level description (first docstring or comment block)
|
|
8
|
+
- file_role: source, test, config, docs, example, script
|
|
9
|
+
|
|
10
|
+
Supports any language with a tree-sitter grammar installed.
|
|
11
|
+
Falls back to regex extraction for unsupported languages.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from ast_extract import extract_file_symbols
|
|
15
|
+
|
|
16
|
+
result = extract_file_symbols("src/server/auth.py", content)
|
|
17
|
+
# {"defines": "AuthMiddleware check_scope ...",
|
|
18
|
+
# "imports": "fastmcp.server.middleware ...",
|
|
19
|
+
# "docstring": "Authorization middleware for ...",
|
|
20
|
+
# "file_role": "source"}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Tree-sitter grammar loading
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
_PARSERS: dict[str, object] = {}
|
|
31
|
+
_TS_AVAILABLE = False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from tree_sitter import Language, Parser
|
|
35
|
+
_TS_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# Extension → (grammar module name, tree-sitter language name)
|
|
40
|
+
_GRAMMAR_MAP: dict[str, tuple[str, str]] = {
|
|
41
|
+
".py": ("tree_sitter_python", "python"),
|
|
42
|
+
".js": ("tree_sitter_javascript", "javascript"),
|
|
43
|
+
".jsx": ("tree_sitter_javascript", "javascript"),
|
|
44
|
+
".ts": ("tree_sitter_typescript", "typescript"),
|
|
45
|
+
".tsx": ("tree_sitter_typescript", "tsx"),
|
|
46
|
+
".go": ("tree_sitter_go", "go"),
|
|
47
|
+
".rs": ("tree_sitter_rust", "rust"),
|
|
48
|
+
".java": ("tree_sitter_java", "java"),
|
|
49
|
+
".c": ("tree_sitter_c", "c"),
|
|
50
|
+
".h": ("tree_sitter_c", "c"),
|
|
51
|
+
".cpp": ("tree_sitter_cpp", "cpp"),
|
|
52
|
+
".hpp": ("tree_sitter_cpp", "cpp"),
|
|
53
|
+
".rb": ("tree_sitter_ruby", "ruby"),
|
|
54
|
+
".cs": ("tree_sitter_c_sharp", "c_sharp"),
|
|
55
|
+
".swift": ("tree_sitter_swift", "swift"),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Node types for definitions across languages
|
|
59
|
+
_DEFINE_TYPES = frozenset({
|
|
60
|
+
# Python
|
|
61
|
+
"function_definition", "class_definition",
|
|
62
|
+
# JS/TS
|
|
63
|
+
"function_declaration", "class_declaration",
|
|
64
|
+
"method_definition", "arrow_function",
|
|
65
|
+
"export_statement",
|
|
66
|
+
# Go
|
|
67
|
+
"function_declaration", "method_declaration",
|
|
68
|
+
"type_declaration",
|
|
69
|
+
# Rust
|
|
70
|
+
"function_item", "struct_item", "enum_item",
|
|
71
|
+
"impl_item", "trait_item", "type_item",
|
|
72
|
+
# Java
|
|
73
|
+
"method_declaration", "class_declaration",
|
|
74
|
+
"interface_declaration", "enum_declaration",
|
|
75
|
+
# C/C++
|
|
76
|
+
"function_definition", "struct_specifier",
|
|
77
|
+
"class_specifier", "enum_specifier",
|
|
78
|
+
# Ruby
|
|
79
|
+
"method", "class", "module",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
# Node types for imports across languages
|
|
83
|
+
_IMPORT_TYPES = frozenset({
|
|
84
|
+
# Python
|
|
85
|
+
"import_statement", "import_from_statement",
|
|
86
|
+
# JS/TS
|
|
87
|
+
"import_statement", "import_declaration",
|
|
88
|
+
# Go
|
|
89
|
+
"import_declaration", "import_spec",
|
|
90
|
+
# Rust
|
|
91
|
+
"use_declaration",
|
|
92
|
+
# Java
|
|
93
|
+
"import_declaration",
|
|
94
|
+
# C/C++
|
|
95
|
+
"preproc_include",
|
|
96
|
+
# Ruby
|
|
97
|
+
"call", # require/require_relative — filtered by content
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_parser(ext: str):
|
|
102
|
+
"""Get or create a tree-sitter parser for the given file extension."""
|
|
103
|
+
if not _TS_AVAILABLE:
|
|
104
|
+
return None
|
|
105
|
+
if ext in _PARSERS:
|
|
106
|
+
return _PARSERS[ext]
|
|
107
|
+
|
|
108
|
+
grammar_info = _GRAMMAR_MAP.get(ext)
|
|
109
|
+
if not grammar_info:
|
|
110
|
+
_PARSERS[ext] = None
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
module_name, lang_name = grammar_info
|
|
114
|
+
try:
|
|
115
|
+
import importlib
|
|
116
|
+
mod = importlib.import_module(module_name)
|
|
117
|
+
# tree-sitter 0.22+ API: language() function returns Language
|
|
118
|
+
if hasattr(mod, "language"):
|
|
119
|
+
lang = Language(mod.language())
|
|
120
|
+
else:
|
|
121
|
+
# tree-sitter 0.21 API: use Language.build_library or direct path
|
|
122
|
+
_PARSERS[ext] = None
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
parser = Parser(lang)
|
|
126
|
+
_PARSERS[ext] = parser
|
|
127
|
+
return parser
|
|
128
|
+
except (ImportError, Exception):
|
|
129
|
+
_PARSERS[ext] = None
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Tree-sitter extraction
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _split_name(name: str) -> str:
|
|
138
|
+
"""Split CamelCase and snake_case into space-separated words.
|
|
139
|
+
|
|
140
|
+
AuthorizationMiddleware → authorization middleware
|
|
141
|
+
check_scope → check scope
|
|
142
|
+
SSETransport → sse transport
|
|
143
|
+
"""
|
|
144
|
+
# Insert space before uppercase runs: SSETransport → SSE Transport
|
|
145
|
+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", name)
|
|
146
|
+
# Insert space before single uppercase: checkScope → check Scope
|
|
147
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", s)
|
|
148
|
+
# Replace underscores with spaces
|
|
149
|
+
s = s.replace("_", " ")
|
|
150
|
+
return s.lower().strip()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _extract_name_from_node(node) -> str:
|
|
154
|
+
"""Extract the name identifier from a definition node."""
|
|
155
|
+
for child in node.children:
|
|
156
|
+
if child.type in ("identifier", "name", "property_identifier",
|
|
157
|
+
"type_identifier"):
|
|
158
|
+
return child.text.decode("utf-8")
|
|
159
|
+
# For export statements, look deeper
|
|
160
|
+
if child.type in ("function_declaration", "class_declaration",
|
|
161
|
+
"lexical_declaration", "variable_declaration"):
|
|
162
|
+
return _extract_name_from_node(child)
|
|
163
|
+
return ""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _extract_ts(content: str, ext: str) -> dict:
|
|
167
|
+
"""Extract symbols using tree-sitter."""
|
|
168
|
+
parser = _get_parser(ext)
|
|
169
|
+
if parser is None:
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
173
|
+
root = tree.root_node
|
|
174
|
+
|
|
175
|
+
defines = []
|
|
176
|
+
imports = []
|
|
177
|
+
docstring = ""
|
|
178
|
+
|
|
179
|
+
for node in root.children:
|
|
180
|
+
# Top-level definitions
|
|
181
|
+
if node.type in _DEFINE_TYPES:
|
|
182
|
+
name = _extract_name_from_node(node)
|
|
183
|
+
if name and not name.startswith("_"):
|
|
184
|
+
defines.append(name)
|
|
185
|
+
|
|
186
|
+
# Imports
|
|
187
|
+
elif node.type in _IMPORT_TYPES:
|
|
188
|
+
text = node.text.decode("utf-8").strip()
|
|
189
|
+
imports.append(text)
|
|
190
|
+
|
|
191
|
+
# Module docstring — first expression_statement containing a string
|
|
192
|
+
elif not docstring and node.type == "expression_statement":
|
|
193
|
+
for child in node.children:
|
|
194
|
+
if child.type in ("string", "concatenated_string"):
|
|
195
|
+
raw = child.text.decode("utf-8")
|
|
196
|
+
# Strip quotes
|
|
197
|
+
for q in ('"""', "'''", '"', "'"):
|
|
198
|
+
if raw.startswith(q) and raw.endswith(q):
|
|
199
|
+
raw = raw[len(q):-len(q)]
|
|
200
|
+
break
|
|
201
|
+
docstring = raw.strip()
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# Module docstring — first comment block
|
|
205
|
+
elif not docstring and node.type == "comment":
|
|
206
|
+
docstring = node.text.decode("utf-8").lstrip("/#* ").strip()
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"defines_raw": defines,
|
|
210
|
+
"imports_raw": imports,
|
|
211
|
+
"docstring": docstring,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Regex fallback extraction
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
# Patterns for common definition syntaxes
|
|
220
|
+
_DEF_PATTERNS = [
|
|
221
|
+
# Python: def name, class Name
|
|
222
|
+
re.compile(r"^(?:def|class)\s+(\w+)", re.MULTILINE),
|
|
223
|
+
# JS/TS: function name, class Name, export function name
|
|
224
|
+
re.compile(r"^(?:export\s+)?(?:function|class)\s+(\w+)", re.MULTILINE),
|
|
225
|
+
# Go: func Name, func (r *Receiver) Name, type Name struct
|
|
226
|
+
re.compile(r"^func\s+(?:\([^)]*\)\s+)?(\w+)", re.MULTILINE),
|
|
227
|
+
re.compile(r"^type\s+(\w+)\s+(?:struct|interface)", re.MULTILINE),
|
|
228
|
+
# Rust: fn name, struct Name, enum Name, impl Name
|
|
229
|
+
re.compile(r"^(?:pub\s+)?(?:fn|struct|enum|trait|impl)\s+(\w+)", re.MULTILINE),
|
|
230
|
+
# Java/C#: public class Name, void methodName
|
|
231
|
+
re.compile(r"^(?:public|private|protected)?\s*(?:static\s+)?(?:class|interface|enum)\s+(\w+)", re.MULTILINE),
|
|
232
|
+
# C/C++: return_type function_name(
|
|
233
|
+
re.compile(r"^(?:\w+\s+)+(\w+)\s*\(", re.MULTILINE),
|
|
234
|
+
# Ruby: def name, class Name, module Name
|
|
235
|
+
re.compile(r"^(?:def|class|module)\s+(\w+)", re.MULTILINE),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
_IMPORT_PATTERNS = [
|
|
239
|
+
# Python: import x, from x import y
|
|
240
|
+
re.compile(r"^(?:from\s+([\w.]+)\s+)?import\s+([\w., ]+)", re.MULTILINE),
|
|
241
|
+
# JS/TS: import ... from "module"
|
|
242
|
+
re.compile(r"""^import\s+.*?from\s+['"]([^'"]+)['"]""", re.MULTILINE),
|
|
243
|
+
# Go: import "package"
|
|
244
|
+
re.compile(r"""^\s*"([^"]+)"$""", re.MULTILINE),
|
|
245
|
+
# Rust: use crate::path
|
|
246
|
+
re.compile(r"^use\s+([\w:]+)", re.MULTILINE),
|
|
247
|
+
# C/C++: #include <file> or "file"
|
|
248
|
+
re.compile(r'^#include\s+[<"]([^>"]+)[>"]', re.MULTILINE),
|
|
249
|
+
# Ruby: require "file"
|
|
250
|
+
re.compile(r"""^require(?:_relative)?\s+['"]([^'"]+)['"]""", re.MULTILINE),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _extract_regex(content: str) -> dict:
|
|
255
|
+
"""Fallback: extract symbols using regex patterns."""
|
|
256
|
+
defines = []
|
|
257
|
+
for pat in _DEF_PATTERNS:
|
|
258
|
+
for m in pat.finditer(content):
|
|
259
|
+
name = m.group(1)
|
|
260
|
+
if name and not name.startswith("_") and name not in defines:
|
|
261
|
+
defines.append(name)
|
|
262
|
+
|
|
263
|
+
imports = []
|
|
264
|
+
for pat in _IMPORT_PATTERNS:
|
|
265
|
+
for m in pat.finditer(content):
|
|
266
|
+
# Take the last non-None group
|
|
267
|
+
for g in reversed(m.groups()):
|
|
268
|
+
if g:
|
|
269
|
+
imports.append(g.strip())
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
# Docstring: first triple-quoted string or comment block
|
|
273
|
+
docstring = ""
|
|
274
|
+
m = re.search(r'^(?:"""(.*?)"""|\'\'\'(.*?)\'\'\')', content, re.DOTALL)
|
|
275
|
+
if m:
|
|
276
|
+
docstring = (m.group(1) or m.group(2) or "").strip()
|
|
277
|
+
elif not docstring:
|
|
278
|
+
# First comment block
|
|
279
|
+
lines = content.split("\n")
|
|
280
|
+
comment_lines = []
|
|
281
|
+
for line in lines:
|
|
282
|
+
stripped = line.strip()
|
|
283
|
+
if stripped.startswith(("#", "//", "*", "/*")):
|
|
284
|
+
comment_lines.append(stripped.lstrip("#/* "))
|
|
285
|
+
elif comment_lines:
|
|
286
|
+
break
|
|
287
|
+
elif stripped:
|
|
288
|
+
break
|
|
289
|
+
if comment_lines:
|
|
290
|
+
docstring = " ".join(comment_lines)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"defines_raw": defines,
|
|
294
|
+
"imports_raw": imports,
|
|
295
|
+
"docstring": docstring,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Role detection
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def _detect_role(file_path: str) -> str:
|
|
304
|
+
"""Detect file role from path heuristics."""
|
|
305
|
+
parts = Path(file_path).parts
|
|
306
|
+
name = Path(file_path).stem
|
|
307
|
+
ext = Path(file_path).suffix
|
|
308
|
+
|
|
309
|
+
# Test files
|
|
310
|
+
if any(p in ("tests", "test", "__tests__", "spec") for p in parts):
|
|
311
|
+
return "test"
|
|
312
|
+
if name.startswith("test_") or name.endswith("_test") or name.endswith(".test"):
|
|
313
|
+
return "test"
|
|
314
|
+
if name.startswith("spec_") or name.endswith("_spec") or name.endswith(".spec"):
|
|
315
|
+
return "test"
|
|
316
|
+
|
|
317
|
+
# Examples
|
|
318
|
+
if any(p in ("examples", "example", "demo", "demos", "samples") for p in parts):
|
|
319
|
+
return "example"
|
|
320
|
+
|
|
321
|
+
# Config
|
|
322
|
+
if ext in (".yaml", ".yml", ".toml", ".json", ".ini", ".cfg", ".conf"):
|
|
323
|
+
return "config"
|
|
324
|
+
if name in ("setup", "pyproject", "package", "tsconfig", "webpack",
|
|
325
|
+
"Makefile", "Dockerfile", "docker-compose", "Cargo"):
|
|
326
|
+
return "config"
|
|
327
|
+
|
|
328
|
+
# Docs
|
|
329
|
+
if ext in (".md", ".rst", ".txt"):
|
|
330
|
+
return "docs"
|
|
331
|
+
if any(p in ("docs", "doc", "documentation") for p in parts):
|
|
332
|
+
return "docs"
|
|
333
|
+
|
|
334
|
+
# Scripts
|
|
335
|
+
if ext in (".sh", ".bash", ".zsh"):
|
|
336
|
+
return "script"
|
|
337
|
+
|
|
338
|
+
return "source"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Public API
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
def extract_file_symbols(file_path: str, content: str) -> dict:
|
|
346
|
+
"""Extract structural symbols from a source file.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
file_path: Relative path to the file (for role detection + extension)
|
|
350
|
+
content: File contents as string
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
dict with keys:
|
|
354
|
+
defines — space-separated words from top-level symbol names
|
|
355
|
+
imports — space-separated import module/package names
|
|
356
|
+
docstring — module-level description (first docstring/comment)
|
|
357
|
+
file_role — source, test, config, docs, example, script
|
|
358
|
+
"""
|
|
359
|
+
ext = Path(file_path).suffix
|
|
360
|
+
|
|
361
|
+
# Try tree-sitter first, fall back to regex
|
|
362
|
+
result = _extract_ts(content, ext)
|
|
363
|
+
if not result:
|
|
364
|
+
result = _extract_regex(content)
|
|
365
|
+
|
|
366
|
+
# Split define names into searchable words
|
|
367
|
+
define_words = []
|
|
368
|
+
for name in result.get("defines_raw", []):
|
|
369
|
+
define_words.append(name) # Keep original name
|
|
370
|
+
split = _split_name(name)
|
|
371
|
+
if split != name.lower():
|
|
372
|
+
define_words.append(split)
|
|
373
|
+
|
|
374
|
+
# Clean up imports into module names
|
|
375
|
+
import_names = []
|
|
376
|
+
for imp in result.get("imports_raw", []):
|
|
377
|
+
# Extract module name from full import statement
|
|
378
|
+
# "from fastmcp.server import auth" → "fastmcp server auth"
|
|
379
|
+
cleaned = re.sub(r"^(?:from|import|use|require|include)\s+", "", imp)
|
|
380
|
+
cleaned = re.sub(r"\s+import\s+.*", "", cleaned)
|
|
381
|
+
cleaned = cleaned.replace(".", " ").replace("::", " ").replace("/", " ")
|
|
382
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_ ]", "", cleaned)
|
|
383
|
+
if cleaned.strip():
|
|
384
|
+
import_names.append(cleaned.strip())
|
|
385
|
+
|
|
386
|
+
docstring = result.get("docstring", "")
|
|
387
|
+
# Truncate long docstrings — first sentence is usually enough
|
|
388
|
+
if len(docstring) > 200:
|
|
389
|
+
# Cut at first period or newline
|
|
390
|
+
for sep in (".\n", ". ", "\n\n", "\n"):
|
|
391
|
+
idx = docstring.find(sep)
|
|
392
|
+
if 20 < idx < 200:
|
|
393
|
+
docstring = docstring[:idx + 1]
|
|
394
|
+
break
|
|
395
|
+
else:
|
|
396
|
+
docstring = docstring[:200]
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
"defines": " ".join(define_words),
|
|
400
|
+
"imports": " ".join(import_names),
|
|
401
|
+
"docstring": docstring.strip(),
|
|
402
|
+
"file_role": _detect_role(file_path),
|
|
403
|
+
}
|
glyphh_code/banner.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Banner for the glyphh-code CLI.
|
|
3
|
+
Reuses the Glyphh brand theme from the runtime.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from glyphh.cli import theme
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Fallback if runtime not installed yet
|
|
14
|
+
class _FallbackTheme:
|
|
15
|
+
PRIMARY = "magenta"
|
|
16
|
+
ACCENT = "bright_magenta"
|
|
17
|
+
MUTED = "bright_black"
|
|
18
|
+
SUCCESS = "green"
|
|
19
|
+
WARNING = "yellow"
|
|
20
|
+
ERROR = "red"
|
|
21
|
+
INFO = "cyan"
|
|
22
|
+
TEXT = "white"
|
|
23
|
+
TEXT_DIM = "bright_black"
|
|
24
|
+
theme = _FallbackTheme()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Characters per second for streaming effect
|
|
28
|
+
_CPS = 800
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _stream(text: str, fg: str | None = None, bold: bool = False):
|
|
32
|
+
"""Print text character-by-character with optional color."""
|
|
33
|
+
delay = 1.0 / _CPS
|
|
34
|
+
styled = click.style(text, fg=fg, bold=bold) if (fg or bold) else text
|
|
35
|
+
i = 0
|
|
36
|
+
while i < len(styled):
|
|
37
|
+
if styled[i] == '\x1b':
|
|
38
|
+
j = i + 1
|
|
39
|
+
while j < len(styled) and styled[j] != 'm':
|
|
40
|
+
j += 1
|
|
41
|
+
sys.stdout.write(styled[i:j + 1])
|
|
42
|
+
i = j + 1
|
|
43
|
+
else:
|
|
44
|
+
sys.stdout.write(styled[i])
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
time.sleep(delay)
|
|
47
|
+
i += 1
|
|
48
|
+
sys.stdout.write('\n')
|
|
49
|
+
sys.stdout.flush()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_banner():
|
|
53
|
+
"""Print the glyphh-code welcome banner."""
|
|
54
|
+
click.echo()
|
|
55
|
+
_stream(" _ _ _ _", fg=theme.PRIMARY)
|
|
56
|
+
_stream(" __ _| |_ _ _ __ | |__ | |__ __ _(_)", fg=theme.PRIMARY)
|
|
57
|
+
_stream(" / _` | | | | | '_ \\| '_ \\| '_ \\ / _` | |", fg=theme.PRIMARY)
|
|
58
|
+
_stream(" | (_| | | |_| | |_) | | | | | | | | (_| | |", fg=theme.ACCENT)
|
|
59
|
+
_stream(" \\__, |_|\\__, | .__/|_| |_|_| |_| \\__,_|_|", fg="cyan")
|
|
60
|
+
_stream(" |___/ |___/|_| code", fg="bright_cyan")
|
|
61
|
+
click.echo()
|
|
62
|
+
_stream(" codebase intelligence for claude code", fg="bright_cyan")
|
|
63
|
+
click.echo()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_status(repo: str, port: int, mcp_url: str, file_count: int):
|
|
67
|
+
"""Print init status after setup completes."""
|
|
68
|
+
dot = click.style("●", fg=theme.SUCCESS)
|
|
69
|
+
click.echo(f" {dot} {click.style('ready', fg=theme.SUCCESS)}")
|
|
70
|
+
click.echo()
|
|
71
|
+
click.secho(f" Repo: {repo}", fg=theme.TEXT_DIM)
|
|
72
|
+
click.secho(f" Files: {file_count} indexed", fg=theme.TEXT_DIM)
|
|
73
|
+
click.secho(f" MCP: {mcp_url}", fg=theme.ACCENT)
|
|
74
|
+
click.secho(f" Storage: SQLite (local)", fg=theme.TEXT_DIM)
|
|
75
|
+
click.secho(f" Auth: none (local mode)", fg=theme.TEXT_DIM)
|
|
76
|
+
click.echo()
|
glyphh_code/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
glyphh-code CLI entry point.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
glyphh-code init [path] Set up Glyphh Code for a repository
|
|
6
|
+
glyphh-code compile [path] Recompile the index
|
|
7
|
+
glyphh-code serve [path] Start the MCP server
|
|
8
|
+
glyphh-code status Show current status
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.version_option(__version__, prog_name="glyphh-code")
|
|
18
|
+
def cli():
|
|
19
|
+
"""Glyphh Code — codebase intelligence for Claude Code."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli.command()
|
|
24
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
25
|
+
@click.option("--port", "-p", default=8002, type=int, help="Server port (default: 8002)")
|
|
26
|
+
def init(path, port):
|
|
27
|
+
"""Set up Glyphh Code for a repository.
|
|
28
|
+
|
|
29
|
+
Compiles the codebase, starts the MCP server, and configures Claude Code.
|
|
30
|
+
Everything is local — no account, no Docker, no auth required.
|
|
31
|
+
"""
|
|
32
|
+
from .setup import run_init
|
|
33
|
+
run_init(path, port)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
38
|
+
def compile(path):
|
|
39
|
+
"""Recompile the index for a repository."""
|
|
40
|
+
from .setup import run_compile
|
|
41
|
+
run_compile(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cli.command()
|
|
45
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
46
|
+
@click.option("--port", "-p", default=8002, type=int, help="Server port (default: 8002)")
|
|
47
|
+
def serve(path, port):
|
|
48
|
+
"""Start the MCP server."""
|
|
49
|
+
from .setup import run_serve
|
|
50
|
+
run_serve(path, port)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cli.command()
|
|
54
|
+
def status():
|
|
55
|
+
"""Show Glyphh Code status."""
|
|
56
|
+
from .setup import run_status
|
|
57
|
+
run_status()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
cli()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|