cmdop-coder 0.1.1__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.
@@ -0,0 +1,28 @@
1
+ """cmdop-coder — code analysis skill using tree-sitter AST parsing."""
2
+
3
+ from cmdop_coder._analysis import analyze_file, extract_functions, find_symbol, get_outline
4
+ from cmdop_coder._models import (
5
+ AnalyzeResult,
6
+ FunctionInfo,
7
+ FunctionsResult,
8
+ OutlineItem,
9
+ OutlineResult,
10
+ SymbolMatch,
11
+ SymbolsResult,
12
+ )
13
+ from cmdop_coder._skill import skill
14
+
15
+ __all__ = [
16
+ "AnalyzeResult",
17
+ "FunctionInfo",
18
+ "FunctionsResult",
19
+ "OutlineItem",
20
+ "OutlineResult",
21
+ "SymbolMatch",
22
+ "SymbolsResult",
23
+ "analyze_file",
24
+ "extract_functions",
25
+ "find_symbol",
26
+ "get_outline",
27
+ "skill",
28
+ ]
@@ -0,0 +1,191 @@
1
+ """Code analysis functions using tree-sitter AST."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ._models import (
8
+ AnalyzeResult,
9
+ FunctionInfo,
10
+ FunctionsResult,
11
+ OutlineItem,
12
+ OutlineResult,
13
+ SymbolMatch,
14
+ SymbolsResult,
15
+ )
16
+ from ._parser import detect_language, parse_file
17
+
18
+ # Node types that represent function/method definitions per language
19
+ _FUNCTION_NODES: dict[str, list[str]] = {
20
+ "python": ["function_definition", "async_function_definition"],
21
+ "javascript": ["function_declaration", "arrow_function", "method_definition", "function_expression"],
22
+ "typescript": ["function_declaration", "arrow_function", "method_definition", "function_expression"],
23
+ "tsx": ["function_declaration", "arrow_function", "method_definition", "function_expression"],
24
+ "go": ["function_declaration", "method_declaration"],
25
+ "rust": ["function_item"],
26
+ "java": ["method_declaration", "constructor_declaration"],
27
+ "c": ["function_definition"],
28
+ "cpp": ["function_definition"],
29
+ "ruby": ["method", "singleton_method"],
30
+ "php": ["function_definition", "method_declaration"],
31
+ "swift": ["function_declaration"],
32
+ "kotlin": ["function_declaration"],
33
+ "c_sharp": ["method_declaration", "constructor_declaration"],
34
+ "lua": ["function_definition", "local_function"],
35
+ "scala": ["function_definition"],
36
+ "elixir": ["def", "defp"],
37
+ }
38
+
39
+ # Node types for outline (structural elements) per language
40
+ _OUTLINE_NODES: dict[str, list[str]] = {
41
+ "python": ["import_statement", "import_from_statement", "class_definition",
42
+ "function_definition", "async_function_definition"],
43
+ "javascript": ["import_declaration", "class_declaration", "function_declaration",
44
+ "lexical_declaration", "variable_declaration"],
45
+ "typescript": ["import_declaration", "class_declaration", "function_declaration",
46
+ "interface_declaration", "type_alias_declaration", "enum_declaration"],
47
+ "tsx": ["import_declaration", "class_declaration", "function_declaration",
48
+ "interface_declaration", "type_alias_declaration"],
49
+ "go": ["import_declaration", "type_declaration", "function_declaration",
50
+ "method_declaration", "var_declaration", "const_declaration"],
51
+ "rust": ["use_declaration", "struct_item", "enum_item", "function_item",
52
+ "impl_item", "trait_item", "mod_item"],
53
+ "java": ["import_declaration", "class_declaration", "interface_declaration",
54
+ "method_declaration"],
55
+ "c": ["preproc_include", "struct_specifier", "function_definition", "type_definition"],
56
+ "cpp": ["preproc_include", "class_specifier", "struct_specifier",
57
+ "function_definition", "namespace_definition"],
58
+ }
59
+
60
+ _IDENTIFIER_TYPES = frozenset({
61
+ "identifier", "name", "property_identifier",
62
+ "type_identifier", "field_identifier",
63
+ })
64
+
65
+
66
+ def _node_name(node: object, source: bytes) -> str:
67
+ """Extract the name identifier text from an AST node."""
68
+ for child in node.children: # type: ignore[union-attr]
69
+ if child.type in _IDENTIFIER_TYPES:
70
+ return source[child.start_byte:child.end_byte].decode(errors="replace")
71
+ raw = source[node.start_byte:node.start_byte + 60] # type: ignore[union-attr]
72
+ return raw.decode(errors="replace").split("\n")[0]
73
+
74
+
75
+ def _collect_nodes(node: object, types: frozenset[str], depth: int = 0, max_depth: int = 20) -> list[object]:
76
+ """Walk AST and collect nodes matching the given types."""
77
+ if depth > max_depth:
78
+ return []
79
+ results: list[object] = []
80
+ if node.type in types: # type: ignore[union-attr]
81
+ results.append(node)
82
+ for child in node.children: # type: ignore[union-attr]
83
+ results.extend(_collect_nodes(child, types, depth + 1, max_depth))
84
+ return results
85
+
86
+
87
+ def _first_line(node: object, source: bytes) -> str:
88
+ """Return the first line of a node's source text, truncated to 120 chars."""
89
+ raw = source[node.start_byte:node.end_byte] # type: ignore[union-attr]
90
+ return raw.decode(errors="replace").split("\n")[0][:120]
91
+
92
+
93
+ def extract_functions(path: str | Path) -> FunctionsResult:
94
+ """Extract function/method signatures with line numbers from a source file."""
95
+ tree, source, lang = parse_file(path)
96
+ types = frozenset(_FUNCTION_NODES.get(lang, ["function_definition", "function_declaration"]))
97
+ nodes = _collect_nodes(tree.root_node, types) # type: ignore[union-attr]
98
+
99
+ functions = [
100
+ FunctionInfo(
101
+ line=node.start_point[0] + 1, # type: ignore[union-attr]
102
+ name=_node_name(node, source),
103
+ signature=_first_line(node, source),
104
+ )
105
+ for node in nodes
106
+ ]
107
+ return FunctionsResult(file=str(path), language=lang, count=len(functions), functions=functions)
108
+
109
+
110
+ def find_symbol(symbol: str, path: str | Path = ".") -> SymbolsResult:
111
+ """Find all occurrences of a symbol across source files."""
112
+ p = Path(path)
113
+ candidates = [p] if p.is_file() else list(p.rglob("*"))
114
+ matches: list[SymbolMatch] = []
115
+
116
+ for f in candidates:
117
+ if not f.is_file() or detect_language(f) is None:
118
+ continue
119
+ try:
120
+ lines = f.read_bytes().decode(errors="replace").splitlines()
121
+ except OSError:
122
+ continue
123
+ for i, line in enumerate(lines, 1):
124
+ if symbol in line:
125
+ matches.append(SymbolMatch(file=str(f), line=i, text=line.strip()))
126
+
127
+ return SymbolsResult(symbol=symbol, count=len(matches), matches=matches)
128
+
129
+
130
+ def get_outline(path: str | Path) -> OutlineResult:
131
+ """Get structural outline of a source file (imports, classes, functions, types)."""
132
+ tree, source, lang = parse_file(path)
133
+ types = frozenset(_OUTLINE_NODES.get(lang, []))
134
+
135
+ if not types:
136
+ items = _fallback_outline(tree.root_node, source) # type: ignore[union-attr]
137
+ return OutlineResult(file=str(path), language=lang, count=len(items), outline=items)
138
+
139
+ nodes = _collect_nodes(tree.root_node, types, max_depth=3) # type: ignore[union-attr]
140
+ items = [
141
+ OutlineItem(
142
+ line=node.start_point[0] + 1, # type: ignore[union-attr]
143
+ type=node.type, # type: ignore[union-attr]
144
+ name=_node_name(node, source),
145
+ )
146
+ for node in nodes
147
+ ]
148
+ return OutlineResult(file=str(path), language=lang, count=len(items), outline=items)
149
+
150
+
151
+ def _fallback_outline(root: object, source: bytes) -> list[OutlineItem]:
152
+ """Return top-level nodes as outline when language has no specific config."""
153
+ return [
154
+ OutlineItem(
155
+ line=child.start_point[0] + 1, # type: ignore[union-attr]
156
+ type=child.type, # type: ignore[union-attr]
157
+ name=_first_line(child, source)[:60],
158
+ )
159
+ for child in root.children # type: ignore[union-attr]
160
+ ]
161
+
162
+
163
+ def analyze_file(path: str | Path) -> AnalyzeResult:
164
+ """Analyze a source file: language detection, line counts, function count."""
165
+ p = Path(path)
166
+ source = p.read_bytes()
167
+ lang = detect_language(p)
168
+ lines = source.decode(errors="replace").splitlines()
169
+
170
+ blank = sum(1 for ln in lines if not ln.strip())
171
+ comment = sum(1 for ln in lines if ln.strip().startswith(("#", "//", "/*", "*")))
172
+ code = len(lines) - blank - comment
173
+
174
+ function_count: int | None = None
175
+ if lang and lang in _FUNCTION_NODES:
176
+ try:
177
+ function_count = extract_functions(p).count
178
+ except Exception:
179
+ pass
180
+
181
+ return AnalyzeResult(
182
+ file=str(p),
183
+ language=lang or "unknown",
184
+ extension=p.suffix,
185
+ size_bytes=len(source),
186
+ total_lines=len(lines),
187
+ code_lines=code,
188
+ blank_lines=blank,
189
+ comment_lines=comment,
190
+ function_count=function_count,
191
+ )
cmdop_coder/_models.py ADDED
@@ -0,0 +1,67 @@
1
+ """Data models for cmdop-coder results."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class FunctionInfo(BaseModel):
7
+ """A single function or method extracted from source code."""
8
+
9
+ line: int
10
+ name: str
11
+ signature: str
12
+
13
+
14
+ class FunctionsResult(BaseModel):
15
+ """Result of extracting functions from a file."""
16
+
17
+ file: str
18
+ language: str
19
+ count: int
20
+ functions: list[FunctionInfo]
21
+
22
+
23
+ class SymbolMatch(BaseModel):
24
+ """A single occurrence of a symbol in source code."""
25
+
26
+ file: str
27
+ line: int
28
+ text: str
29
+
30
+
31
+ class SymbolsResult(BaseModel):
32
+ """Result of searching for a symbol across files."""
33
+
34
+ symbol: str
35
+ count: int
36
+ matches: list[SymbolMatch]
37
+
38
+
39
+ class OutlineItem(BaseModel):
40
+ """A structural element in a source file outline."""
41
+
42
+ line: int
43
+ type: str
44
+ name: str
45
+
46
+
47
+ class OutlineResult(BaseModel):
48
+ """Result of generating a structural outline for a file."""
49
+
50
+ file: str
51
+ language: str
52
+ count: int
53
+ outline: list[OutlineItem]
54
+
55
+
56
+ class AnalyzeResult(BaseModel):
57
+ """Statistics for a source file."""
58
+
59
+ file: str
60
+ language: str
61
+ extension: str
62
+ size_bytes: int
63
+ total_lines: int
64
+ code_lines: int
65
+ blank_lines: int
66
+ comment_lines: int
67
+ function_count: int | None = None
cmdop_coder/_parser.py ADDED
@@ -0,0 +1,76 @@
1
+ """Tree-sitter parser utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ # Language detection by file extension
8
+ _EXT_TO_LANG: dict[str, str] = {
9
+ ".py": "python",
10
+ ".js": "javascript",
11
+ ".jsx": "javascript",
12
+ ".ts": "typescript",
13
+ ".tsx": "tsx",
14
+ ".go": "go",
15
+ ".rs": "rust",
16
+ ".java": "java",
17
+ ".c": "c",
18
+ ".h": "c",
19
+ ".cpp": "cpp",
20
+ ".cc": "cpp",
21
+ ".cxx": "cpp",
22
+ ".hpp": "cpp",
23
+ ".rb": "ruby",
24
+ ".php": "php",
25
+ ".swift": "swift",
26
+ ".kt": "kotlin",
27
+ ".cs": "c_sharp",
28
+ ".css": "css",
29
+ ".html": "html",
30
+ ".json": "json",
31
+ ".yaml": "yaml",
32
+ ".yml": "yaml",
33
+ ".toml": "toml",
34
+ ".sh": "bash",
35
+ ".bash": "bash",
36
+ ".sql": "sql",
37
+ ".lua": "lua",
38
+ ".r": "r",
39
+ ".scala": "scala",
40
+ ".ex": "elixir",
41
+ ".exs": "elixir",
42
+ ".elm": "elm",
43
+ ".hs": "haskell",
44
+ ".ml": "ocaml",
45
+ ".tf": "hcl",
46
+ }
47
+
48
+
49
+ def detect_language(path: str | Path) -> str | None:
50
+ """Detect tree-sitter language name from file path."""
51
+ p = Path(path)
52
+ name = p.name.lower()
53
+ if name == "dockerfile" or name.startswith("dockerfile."):
54
+ return "dockerfile"
55
+ return _EXT_TO_LANG.get(p.suffix.lower())
56
+
57
+
58
+ def get_parser(language: str) -> object:
59
+ """Return a tree-sitter parser for the given language name."""
60
+ try:
61
+ from tree_sitter_language_pack import get_parser as _get_parser # type: ignore[import]
62
+ return _get_parser(language)
63
+ except Exception as e:
64
+ raise RuntimeError(f"Cannot load tree-sitter parser for {language!r}: {e}") from e
65
+
66
+
67
+ def parse_file(path: str | Path) -> tuple[object, bytes, str]:
68
+ """Parse a source file, returning (tree, source_bytes, language)."""
69
+ p = Path(path)
70
+ lang = detect_language(p)
71
+ if lang is None:
72
+ raise ValueError(f"Unsupported file type: {p.suffix!r}")
73
+ source = p.read_bytes()
74
+ parser = get_parser(lang)
75
+ tree = parser.parse(source) # type: ignore[union-attr]
76
+ return tree, source, lang
cmdop_coder/_skill.py ADDED
@@ -0,0 +1,45 @@
1
+ """cmdop-coder CMDOP skill — code analysis with tree-sitter AST."""
2
+
3
+ from cmdop_skill import Arg, Skill
4
+
5
+ from cmdop_coder._analysis import analyze_file, extract_functions, find_symbol, get_outline
6
+
7
+ skill = Skill()
8
+
9
+
10
+ @skill.command
11
+ def functions(
12
+ path: str = Arg(help="Path to source file", required=True),
13
+ ) -> dict:
14
+ """Extract function/method signatures with line numbers."""
15
+ return extract_functions(path).model_dump()
16
+
17
+
18
+ @skill.command
19
+ def symbols(
20
+ symbol: str = Arg(help="Symbol name to find", required=True),
21
+ path: str = Arg("--path", default=".", help="File or directory to search (default: current dir)"),
22
+ ) -> dict:
23
+ """Find all occurrences of a symbol across source files."""
24
+ return find_symbol(symbol, path).model_dump()
25
+
26
+
27
+ @skill.command
28
+ def outline(
29
+ path: str = Arg(help="Path to source file", required=True),
30
+ ) -> dict:
31
+ """Get structural outline: imports, classes, functions, types."""
32
+ return get_outline(path).model_dump()
33
+
34
+
35
+ @skill.command
36
+ def analyze(
37
+ path: str = Arg(help="Path to source file", required=True),
38
+ ) -> dict:
39
+ """Analyze a file: language, line counts, function count."""
40
+ return analyze_file(path).model_dump()
41
+
42
+
43
+ def main() -> None:
44
+ """CLI entry point."""
45
+ skill.run()
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdop-coder
3
+ Version: 0.1.1
4
+ Summary: CMDOP skill — code analysis with tree-sitter AST parsing
5
+ Project-URL: Homepage, https://cmdop.com/skills/cmdop-coder/
6
+ Author-email: CMDOP Team <team@cmdop.com>
7
+ License-Expression: MIT
8
+ Keywords: ast,cmdop,code,skill,tree-sitter
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: cmdop
18
+ Requires-Dist: cmdop-skill
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: tree-sitter-language-pack>=0.1
21
+ Requires-Dist: tree-sitter>=0.21
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
24
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # cmdop-coder
30
+
31
+ > **[CMDOP Skill](https://cmdop.com/skills/cmdop-coder/)** — install and use via [CMDOP agent](https://cmdop.com):
32
+ > ```
33
+ > cmdop-skill install cmdop-coder
34
+ > ```
35
+
36
+ Code analysis using tree-sitter AST parsing. Extract functions, find symbols, get structural outlines. Supports 40+ languages.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install cmdop-coder
42
+ ```
43
+
44
+ Or as a CMDOP skill:
45
+
46
+ ```bash
47
+ cmdop-skill install path/to/cmdop-coder
48
+ ```
49
+
50
+ ## CLI
51
+
52
+ ### Extract functions
53
+
54
+ ```bash
55
+ cmdop-coder functions --path src/main.py
56
+ ```
57
+
58
+ ```json
59
+ {
60
+ "file": "src/main.py",
61
+ "language": "python",
62
+ "count": 3,
63
+ "functions": [
64
+ {"line": 5, "name": "hello", "signature": "def hello(name: str) -> str:"},
65
+ {"line": 9, "name": "fetch", "signature": "async def fetch(url: str) -> bytes:"}
66
+ ]
67
+ }
68
+ ```
69
+
70
+ ### Find symbol
71
+
72
+ ```bash
73
+ cmdop-coder symbols --symbol MyClass --path ./src
74
+ ```
75
+
76
+ ### Structural outline
77
+
78
+ ```bash
79
+ cmdop-coder outline --path internal/agent/core/agent.go
80
+ ```
81
+
82
+ ### File statistics
83
+
84
+ ```bash
85
+ cmdop-coder analyze --path service.py
86
+ ```
87
+
88
+ ## Python API
89
+
90
+ ```python
91
+ from cmdop_coder import extract_functions, find_symbol, get_outline, analyze_file
92
+
93
+ # Extract all functions from a file
94
+ result = extract_functions("src/main.py")
95
+ for fn in result.functions:
96
+ print(fn.line, fn.name, fn.signature)
97
+
98
+ # Find symbol across a directory
99
+ matches = find_symbol("MyClass", "./src")
100
+ for m in matches.matches:
101
+ print(m.file, m.line, m.text)
102
+
103
+ # Structural outline
104
+ outline = get_outline("main.go")
105
+ for item in outline.outline:
106
+ print(item.line, item.type, item.name)
107
+
108
+ # File statistics
109
+ stats = analyze_file("service.py")
110
+ print(stats.language, stats.total_lines, stats.function_count)
111
+ ```
112
+
113
+ ## Supported Languages
114
+
115
+ Go, Python, JavaScript, TypeScript, TSX, Rust, Java, C, C++, Ruby, PHP,
116
+ Swift, Kotlin, C#, CSS, HTML, JSON, YAML, TOML, Bash, SQL, Lua, Scala,
117
+ Elixir, Elm, Haskell, OCaml, HCL, Dockerfile and more.
@@ -0,0 +1,9 @@
1
+ cmdop_coder/__init__.py,sha256=HuOe8Snmf-KFnBlZ4SYAUQDHJF27yHWm6q2AXQau6gQ,624
2
+ cmdop_coder/_analysis.py,sha256=dB3TFUTFhe-p6ZseGdFW7o1c_Eu-S82Br4PK6FCmPiY,7943
3
+ cmdop_coder/_models.py,sha256=sA0fBTEfm9-64KB6Qn17jX1rjGI5MIwvWNehI6pisO8,1259
4
+ cmdop_coder/_parser.py,sha256=lm0JOsZMaAAldA3S38An2d6RYI-ZaC_eC2EwjwXAXqo,2003
5
+ cmdop_coder/_skill.py,sha256=oL2KCj3z6e5Wrd3u5cHKHDHburSTInd5nuWzWpwNk7c,1254
6
+ cmdop_coder-0.1.1.dist-info/METADATA,sha256=u3zXfQLP4N1AjFrcbLslpVsenNS8nsJWrPx25mYccBo,2954
7
+ cmdop_coder-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ cmdop_coder-0.1.1.dist-info/entry_points.txt,sha256=ikr1Pfh1IWXbvxPXhkICjAZ2DLWx4VFqkf6EJPX-ZAI,56
9
+ cmdop_coder-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cmdop-coder = cmdop_coder._skill:main