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 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
@@ -0,0 +1,3 @@
1
+ """Glyphh Code — codebase intelligence for Claude Code."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()