tamfis-code 0.2.3__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.
- tamfis_code/__init__.py +58 -0
- tamfis_code/__main__.py +4 -0
- tamfis_code/agents.py +371 -0
- tamfis_code/api_client.py +461 -0
- tamfis_code/cli.py +2351 -0
- tamfis_code/completion.py +137 -0
- tamfis_code/config.py +199 -0
- tamfis_code/doctor.py +173 -0
- tamfis_code/enforcer.py +318 -0
- tamfis_code/indexer.py +567 -0
- tamfis_code/instructions.py +121 -0
- tamfis_code/interactive.py +985 -0
- tamfis_code/local_chat.py +117 -0
- tamfis_code/local_tools.py +93 -0
- tamfis_code/mcp.py +538 -0
- tamfis_code/metrics.py +105 -0
- tamfis_code/providers.py +423 -0
- tamfis_code/references.py +336 -0
- tamfis_code/render.py +575 -0
- tamfis_code/runner.py +635 -0
- tamfis_code/runner_local.py +364 -0
- tamfis_code/safety.py +164 -0
- tamfis_code/screenshot.py +382 -0
- tamfis_code/sessions.py +253 -0
- tamfis_code/state.py +449 -0
- tamfis_code/tasks.py +42 -0
- tamfis_code/workspace.py +329 -0
- tamfis_code-0.2.3.dist-info/METADATA +78 -0
- tamfis_code-0.2.3.dist-info/RECORD +32 -0
- tamfis_code-0.2.3.dist-info/WHEEL +5 -0
- tamfis_code-0.2.3.dist-info/entry_points.txt +4 -0
- tamfis_code-0.2.3.dist-info/top_level.txt +1 -0
tamfis_code/indexer.py
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""Code indexing and semantic search for TAMFIS-CODE"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Any, Optional, Set
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CodeSymbol:
|
|
13
|
+
"""A code symbol (function, class, variable)"""
|
|
14
|
+
name: str
|
|
15
|
+
kind: str
|
|
16
|
+
file_path: str
|
|
17
|
+
line_start: int
|
|
18
|
+
line_end: int
|
|
19
|
+
signature: Optional[str] = None
|
|
20
|
+
docstring: Optional[str] = None
|
|
21
|
+
parent: Optional[str] = None
|
|
22
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CodeFile:
|
|
26
|
+
"""Indexed code file"""
|
|
27
|
+
path: str
|
|
28
|
+
language: str
|
|
29
|
+
symbols: List[CodeSymbol] = field(default_factory=list)
|
|
30
|
+
imports: List[str] = field(default_factory=list)
|
|
31
|
+
exports: List[str] = field(default_factory=list)
|
|
32
|
+
size: int = 0
|
|
33
|
+
lines: int = 0
|
|
34
|
+
|
|
35
|
+
class CodeIndexer:
|
|
36
|
+
"""Index and search code files"""
|
|
37
|
+
|
|
38
|
+
SUPPORTED_LANGUAGES = {
|
|
39
|
+
'.py': 'python',
|
|
40
|
+
'.js': 'javascript',
|
|
41
|
+
'.ts': 'typescript',
|
|
42
|
+
'.tsx': 'typescript',
|
|
43
|
+
'.jsx': 'javascript',
|
|
44
|
+
'.go': 'go',
|
|
45
|
+
'.rs': 'rust',
|
|
46
|
+
'.c': 'c',
|
|
47
|
+
'.cpp': 'cpp',
|
|
48
|
+
'.h': 'c',
|
|
49
|
+
'.hpp': 'cpp',
|
|
50
|
+
'.java': 'java',
|
|
51
|
+
'.rb': 'ruby',
|
|
52
|
+
'.php': 'php',
|
|
53
|
+
'.swift': 'swift',
|
|
54
|
+
'.kt': 'kotlin',
|
|
55
|
+
'.sh': 'shell',
|
|
56
|
+
'.bash': 'shell',
|
|
57
|
+
'.zsh': 'shell',
|
|
58
|
+
'.fish': 'shell',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def __init__(self, root_path: Path, index_path: Optional[Path] = None):
|
|
62
|
+
self.root_path = Path(root_path)
|
|
63
|
+
# Workspace-scoped by default (hash of the resolved root) -- a
|
|
64
|
+
# single shared ~/.tamfis/index/code_index.json used to silently
|
|
65
|
+
# clobber across every project this CLI ever indexed, since no
|
|
66
|
+
# caller passed an explicit index_path.
|
|
67
|
+
default_index_path = Path.home() / '.tamfis' / 'index' / hashlib.sha256(str(self.root_path.resolve()).encode()).hexdigest()[:16]
|
|
68
|
+
self.index_path = index_path or default_index_path
|
|
69
|
+
self.files: Dict[str, CodeFile] = {}
|
|
70
|
+
self.index_path.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
self.ignore_patterns = [
|
|
72
|
+
'*.pyc', '__pycache__', '.git', '.env', 'venv',
|
|
73
|
+
'node_modules', '.idea', '.vscode', '*.log', '*.tmp',
|
|
74
|
+
'.DS_Store', 'Thumbs.db', '.pytest_cache', '.mypy_cache',
|
|
75
|
+
'.coverage', 'htmlcov', '.tox', '.eggs', '*.egg-info'
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
def index(self, paths: List[str] = None, force: bool = False):
|
|
79
|
+
"""Index files in the workspace"""
|
|
80
|
+
if not paths:
|
|
81
|
+
paths = [str(self.root_path)]
|
|
82
|
+
|
|
83
|
+
indexed_count = 0
|
|
84
|
+
for path_str in paths:
|
|
85
|
+
path = Path(path_str)
|
|
86
|
+
if path.is_file():
|
|
87
|
+
if self._index_file(path):
|
|
88
|
+
indexed_count += 1
|
|
89
|
+
elif path.is_dir():
|
|
90
|
+
indexed_count += self._index_directory(path)
|
|
91
|
+
|
|
92
|
+
self._save_index()
|
|
93
|
+
return indexed_count
|
|
94
|
+
|
|
95
|
+
def _index_directory(self, directory: Path) -> int:
|
|
96
|
+
"""Recursively index a directory"""
|
|
97
|
+
count = 0
|
|
98
|
+
# Check for .gitignore patterns
|
|
99
|
+
ignore_patterns = self._load_gitignore(directory)
|
|
100
|
+
|
|
101
|
+
for path in directory.rglob('*'):
|
|
102
|
+
if path.is_file():
|
|
103
|
+
# Skip ignored files
|
|
104
|
+
if self._is_ignored(path, ignore_patterns):
|
|
105
|
+
continue
|
|
106
|
+
ext = path.suffix.lower()
|
|
107
|
+
if ext in self.SUPPORTED_LANGUAGES:
|
|
108
|
+
if self._index_file(path):
|
|
109
|
+
count += 1
|
|
110
|
+
return count
|
|
111
|
+
|
|
112
|
+
def _load_gitignore(self, directory: Path) -> List[str]:
|
|
113
|
+
"""Load .gitignore patterns"""
|
|
114
|
+
gitignore = directory / '.gitignore'
|
|
115
|
+
patterns = []
|
|
116
|
+
if gitignore.exists():
|
|
117
|
+
try:
|
|
118
|
+
with open(gitignore, 'r') as f:
|
|
119
|
+
for line in f:
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if line and not line.startswith('#'):
|
|
122
|
+
patterns.append(line)
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
return patterns
|
|
126
|
+
|
|
127
|
+
def _is_ignored(self, path: Path, patterns: List[str]) -> bool:
|
|
128
|
+
"""Check if path matches ignore patterns"""
|
|
129
|
+
path_str = str(path)
|
|
130
|
+
# Check built-in patterns
|
|
131
|
+
for pattern in self.ignore_patterns:
|
|
132
|
+
if pattern in path_str or path_str.endswith(pattern):
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Check .gitignore patterns
|
|
136
|
+
for pattern in patterns:
|
|
137
|
+
if pattern.endswith('/'):
|
|
138
|
+
if pattern in path_str:
|
|
139
|
+
return True
|
|
140
|
+
else:
|
|
141
|
+
if pattern in path_str:
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def _index_file(self, file_path: Path) -> bool:
|
|
146
|
+
"""Index a single file"""
|
|
147
|
+
ext = file_path.suffix.lower()
|
|
148
|
+
if ext not in self.SUPPORTED_LANGUAGES:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
language = self.SUPPORTED_LANGUAGES[ext]
|
|
152
|
+
try:
|
|
153
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
154
|
+
except Exception:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
# Parse symbols based on language
|
|
158
|
+
if language == 'python':
|
|
159
|
+
symbols = self._parse_python(content, file_path)
|
|
160
|
+
imports = self._parse_python_imports(content)
|
|
161
|
+
elif language in ('javascript', 'typescript'):
|
|
162
|
+
symbols = self._parse_javascript(content, file_path)
|
|
163
|
+
imports = self._parse_js_imports(content)
|
|
164
|
+
elif language == 'go':
|
|
165
|
+
symbols = self._parse_go(content, file_path)
|
|
166
|
+
imports = self._parse_go_imports(content)
|
|
167
|
+
else:
|
|
168
|
+
symbols = self._parse_generic(content, file_path)
|
|
169
|
+
imports = []
|
|
170
|
+
|
|
171
|
+
code_file = CodeFile(
|
|
172
|
+
path=str(file_path),
|
|
173
|
+
language=language,
|
|
174
|
+
symbols=symbols,
|
|
175
|
+
imports=imports,
|
|
176
|
+
size=file_path.stat().st_size,
|
|
177
|
+
lines=len(content.split('\n'))
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self.files[str(file_path)] = code_file
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def _parse_python(self, content: str, file_path: Path) -> List[CodeSymbol]:
|
|
184
|
+
"""Parse Python file for symbols"""
|
|
185
|
+
symbols = []
|
|
186
|
+
lines = content.split('\n')
|
|
187
|
+
|
|
188
|
+
func_pattern = re.compile(r'^\s*def\s+(\w+)\s*\(([^)]*)\)')
|
|
189
|
+
class_pattern = re.compile(r'^\s*class\s+(\w+)(?:\s*\(([^)]*)\))?')
|
|
190
|
+
|
|
191
|
+
for i, line in enumerate(lines, 1):
|
|
192
|
+
func_match = func_pattern.search(line)
|
|
193
|
+
if func_match:
|
|
194
|
+
symbols.append(CodeSymbol(
|
|
195
|
+
name=func_match.group(1),
|
|
196
|
+
kind='function',
|
|
197
|
+
file_path=str(file_path),
|
|
198
|
+
line_start=i,
|
|
199
|
+
line_end=self._find_function_end(lines, i),
|
|
200
|
+
signature=func_match.group(2),
|
|
201
|
+
))
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
class_match = class_pattern.search(line)
|
|
205
|
+
if class_match:
|
|
206
|
+
symbols.append(CodeSymbol(
|
|
207
|
+
name=class_match.group(1),
|
|
208
|
+
kind='class',
|
|
209
|
+
file_path=str(file_path),
|
|
210
|
+
line_start=i,
|
|
211
|
+
line_end=self._find_class_end(lines, i),
|
|
212
|
+
signature=class_match.group(2) if class_match.group(2) else None,
|
|
213
|
+
))
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
return symbols
|
|
217
|
+
|
|
218
|
+
def _find_function_end(self, lines: List[str], start: int) -> int:
|
|
219
|
+
"""Find the end of a function definition"""
|
|
220
|
+
if start >= len(lines):
|
|
221
|
+
return start
|
|
222
|
+
|
|
223
|
+
indent = len(lines[start-1]) - len(lines[start-1].lstrip())
|
|
224
|
+
for i in range(start, len(lines)):
|
|
225
|
+
if i == len(lines) - 1:
|
|
226
|
+
return i + 1
|
|
227
|
+
stripped = lines[i].strip()
|
|
228
|
+
if stripped and not stripped.startswith('#'):
|
|
229
|
+
current_indent = len(lines[i]) - len(lines[i].lstrip())
|
|
230
|
+
if current_indent <= indent and stripped:
|
|
231
|
+
return i
|
|
232
|
+
return start + 1
|
|
233
|
+
|
|
234
|
+
def _find_class_end(self, lines: List[str], start: int) -> int:
|
|
235
|
+
"""Find the end of a class definition"""
|
|
236
|
+
if start >= len(lines):
|
|
237
|
+
return start
|
|
238
|
+
|
|
239
|
+
indent = len(lines[start-1]) - len(lines[start-1].lstrip())
|
|
240
|
+
for i in range(start, len(lines)):
|
|
241
|
+
if i == len(lines) - 1:
|
|
242
|
+
return i + 1
|
|
243
|
+
stripped = lines[i].strip()
|
|
244
|
+
if stripped and not stripped.startswith('#'):
|
|
245
|
+
current_indent = len(lines[i]) - len(lines[i].lstrip())
|
|
246
|
+
if current_indent <= indent and stripped and not lines[i].strip().startswith('def '):
|
|
247
|
+
return i
|
|
248
|
+
return start + 1
|
|
249
|
+
|
|
250
|
+
def _parse_python_imports(self, content: str) -> List[str]:
|
|
251
|
+
"""Parse Python imports"""
|
|
252
|
+
imports = []
|
|
253
|
+
for line in content.split('\n'):
|
|
254
|
+
if line.startswith('import ') or line.startswith('from '):
|
|
255
|
+
parts = line.split()
|
|
256
|
+
if len(parts) > 1:
|
|
257
|
+
imports.append(parts[1].split('.')[0])
|
|
258
|
+
return imports
|
|
259
|
+
|
|
260
|
+
def _parse_javascript(self, content: str, file_path: Path) -> List[CodeSymbol]:
|
|
261
|
+
"""Parse JavaScript/TypeScript for symbols"""
|
|
262
|
+
symbols = []
|
|
263
|
+
lines = content.split('\n')
|
|
264
|
+
|
|
265
|
+
func_pattern = re.compile(r'^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)')
|
|
266
|
+
arrow_pattern = re.compile(r'^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>')
|
|
267
|
+
class_pattern = re.compile(r'^\s*(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?')
|
|
268
|
+
interface_pattern = re.compile(r'^\s*(?:export\s+)?interface\s+(\w+)')
|
|
269
|
+
type_pattern = re.compile(r'^\s*(?:export\s+)?type\s+(\w+)')
|
|
270
|
+
|
|
271
|
+
for i, line in enumerate(lines, 1):
|
|
272
|
+
func_match = func_pattern.search(line)
|
|
273
|
+
if func_match:
|
|
274
|
+
symbols.append(CodeSymbol(
|
|
275
|
+
name=func_match.group(1),
|
|
276
|
+
kind='function',
|
|
277
|
+
file_path=str(file_path),
|
|
278
|
+
line_start=i,
|
|
279
|
+
line_end=i,
|
|
280
|
+
signature=func_match.group(2),
|
|
281
|
+
))
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
arrow_match = arrow_pattern.search(line)
|
|
285
|
+
if arrow_match:
|
|
286
|
+
symbols.append(CodeSymbol(
|
|
287
|
+
name=arrow_match.group(1),
|
|
288
|
+
kind='function',
|
|
289
|
+
file_path=str(file_path),
|
|
290
|
+
line_start=i,
|
|
291
|
+
line_end=i,
|
|
292
|
+
))
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
class_match = class_pattern.search(line)
|
|
296
|
+
if class_match:
|
|
297
|
+
symbols.append(CodeSymbol(
|
|
298
|
+
name=class_match.group(1),
|
|
299
|
+
kind='class',
|
|
300
|
+
file_path=str(file_path),
|
|
301
|
+
line_start=i,
|
|
302
|
+
line_end=self._find_js_class_end(lines, i),
|
|
303
|
+
))
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
interface_match = interface_pattern.search(line)
|
|
307
|
+
if interface_match:
|
|
308
|
+
symbols.append(CodeSymbol(
|
|
309
|
+
name=interface_match.group(1),
|
|
310
|
+
kind='interface',
|
|
311
|
+
file_path=str(file_path),
|
|
312
|
+
line_start=i,
|
|
313
|
+
line_end=i,
|
|
314
|
+
))
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
type_match = type_pattern.search(line)
|
|
318
|
+
if type_match:
|
|
319
|
+
symbols.append(CodeSymbol(
|
|
320
|
+
name=type_match.group(1),
|
|
321
|
+
kind='type',
|
|
322
|
+
file_path=str(file_path),
|
|
323
|
+
line_start=i,
|
|
324
|
+
line_end=i,
|
|
325
|
+
))
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
return symbols
|
|
329
|
+
|
|
330
|
+
def _find_js_class_end(self, lines: List[str], start: int) -> int:
|
|
331
|
+
"""Find the end of a JS class definition"""
|
|
332
|
+
brace_count = 0
|
|
333
|
+
for i in range(start-1, len(lines)):
|
|
334
|
+
line = lines[i]
|
|
335
|
+
brace_count += line.count('{') - line.count('}')
|
|
336
|
+
if brace_count == 0 and i > start:
|
|
337
|
+
return i + 1
|
|
338
|
+
return len(lines)
|
|
339
|
+
|
|
340
|
+
def _parse_js_imports(self, content: str) -> List[str]:
|
|
341
|
+
"""Parse JavaScript imports"""
|
|
342
|
+
imports = []
|
|
343
|
+
import_pattern = re.compile(r'import\s+(?:{[^}]*}\s+from\s+)?[\'"]([^\'"]+)[\'"]')
|
|
344
|
+
require_pattern = re.compile(r'require\s*\(\s*[\'"]([^\'"]+)[\'"]')
|
|
345
|
+
|
|
346
|
+
for line in content.split('\n'):
|
|
347
|
+
for pattern in [import_pattern, require_pattern]:
|
|
348
|
+
match = pattern.search(line)
|
|
349
|
+
if match:
|
|
350
|
+
imports.append(match.group(1))
|
|
351
|
+
return imports
|
|
352
|
+
|
|
353
|
+
def _parse_go(self, content: str, file_path: Path) -> List[CodeSymbol]:
|
|
354
|
+
"""Parse Go file for symbols"""
|
|
355
|
+
symbols = []
|
|
356
|
+
lines = content.split('\n')
|
|
357
|
+
|
|
358
|
+
func_pattern = re.compile(r'^\s*func\s+(\w+)\s*\(([^)]*)\)')
|
|
359
|
+
type_pattern = re.compile(r'^\s*type\s+(\w+)\s+(?:struct|interface)')
|
|
360
|
+
const_pattern = re.compile(r'^\s*const\s+(\w+)')
|
|
361
|
+
var_pattern = re.compile(r'^\s*var\s+(\w+)')
|
|
362
|
+
|
|
363
|
+
for i, line in enumerate(lines, 1):
|
|
364
|
+
func_match = func_pattern.search(line)
|
|
365
|
+
if func_match:
|
|
366
|
+
symbols.append(CodeSymbol(
|
|
367
|
+
name=func_match.group(1),
|
|
368
|
+
kind='function',
|
|
369
|
+
file_path=str(file_path),
|
|
370
|
+
line_start=i,
|
|
371
|
+
line_end=i,
|
|
372
|
+
))
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
type_match = type_pattern.search(line)
|
|
376
|
+
if type_match:
|
|
377
|
+
symbols.append(CodeSymbol(
|
|
378
|
+
name=type_match.group(1),
|
|
379
|
+
kind='type',
|
|
380
|
+
file_path=str(file_path),
|
|
381
|
+
line_start=i,
|
|
382
|
+
line_end=self._find_go_type_end(lines, i),
|
|
383
|
+
))
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
return symbols
|
|
387
|
+
|
|
388
|
+
def _find_go_type_end(self, lines: List[str], start: int) -> int:
|
|
389
|
+
"""Find the end of a Go type definition"""
|
|
390
|
+
brace_count = 0
|
|
391
|
+
for i in range(start-1, len(lines)):
|
|
392
|
+
line = lines[i]
|
|
393
|
+
brace_count += line.count('{') - line.count('}')
|
|
394
|
+
if brace_count == 0 and i > start:
|
|
395
|
+
return i + 1
|
|
396
|
+
return len(lines)
|
|
397
|
+
|
|
398
|
+
def _parse_go_imports(self, content: str) -> List[str]:
|
|
399
|
+
"""Parse Go imports"""
|
|
400
|
+
imports = []
|
|
401
|
+
import_pattern = re.compile(r'^\s*import\s+[\'"]?([^\'"\s]+)')
|
|
402
|
+
import_block = re.compile(r'^\s*import\s*\(([^)]+)\)', re.DOTALL)
|
|
403
|
+
|
|
404
|
+
# Simple import
|
|
405
|
+
for line in content.split('\n'):
|
|
406
|
+
match = import_pattern.search(line)
|
|
407
|
+
if match:
|
|
408
|
+
imports.append(match.group(1))
|
|
409
|
+
|
|
410
|
+
# Import block
|
|
411
|
+
block_match = import_block.search(content)
|
|
412
|
+
if block_match:
|
|
413
|
+
for line in block_match.group(1).split('\n'):
|
|
414
|
+
if line.strip():
|
|
415
|
+
imports.append(line.strip().strip('"').strip("'"))
|
|
416
|
+
|
|
417
|
+
return imports
|
|
418
|
+
|
|
419
|
+
def _parse_generic(self, content: str, file_path: Path) -> List[CodeSymbol]:
|
|
420
|
+
"""Generic fallback parsing"""
|
|
421
|
+
symbols = []
|
|
422
|
+
lines = content.split('\n')
|
|
423
|
+
|
|
424
|
+
patterns = [
|
|
425
|
+
(r'^\s*function\s+(\w+)', 'function'),
|
|
426
|
+
(r'^\s*class\s+(\w+)', 'class'),
|
|
427
|
+
(r'^\s*def\s+(\w+)', 'function'),
|
|
428
|
+
(r'^\s*(?:export\s+)?interface\s+(\w+)', 'interface'),
|
|
429
|
+
(r'^\s*(?:export\s+)?type\s+(\w+)', 'type'),
|
|
430
|
+
(r'^\s*(?:public|private|protected)?\s+(?:static\s+)?(\w+)\s*\(', 'method'),
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
for i, line in enumerate(lines, 1):
|
|
434
|
+
for pattern, kind in patterns:
|
|
435
|
+
match = re.search(pattern, line)
|
|
436
|
+
if match:
|
|
437
|
+
symbols.append(CodeSymbol(
|
|
438
|
+
name=match.group(1),
|
|
439
|
+
kind=kind,
|
|
440
|
+
file_path=str(file_path),
|
|
441
|
+
line_start=i,
|
|
442
|
+
line_end=i,
|
|
443
|
+
))
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
return symbols
|
|
447
|
+
|
|
448
|
+
def _save_index(self):
|
|
449
|
+
"""Save index to disk"""
|
|
450
|
+
index_data = {}
|
|
451
|
+
for path, code_file in self.files.items():
|
|
452
|
+
index_data[path] = {
|
|
453
|
+
'language': code_file.language,
|
|
454
|
+
'size': code_file.size,
|
|
455
|
+
'lines': code_file.lines,
|
|
456
|
+
'imports': code_file.imports,
|
|
457
|
+
'symbols': [
|
|
458
|
+
{
|
|
459
|
+
'name': s.name,
|
|
460
|
+
'kind': s.kind,
|
|
461
|
+
'line_start': s.line_start,
|
|
462
|
+
'line_end': s.line_end,
|
|
463
|
+
'signature': s.signature,
|
|
464
|
+
'docstring': s.docstring,
|
|
465
|
+
}
|
|
466
|
+
for s in code_file.symbols
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
index_file = self.index_path / 'code_index.json'
|
|
471
|
+
with open(index_file, 'w') as f:
|
|
472
|
+
json.dump(index_data, f, indent=2)
|
|
473
|
+
|
|
474
|
+
def load_index(self):
|
|
475
|
+
"""Load index from disk"""
|
|
476
|
+
index_file = self.index_path / 'code_index.json'
|
|
477
|
+
if not index_file.exists():
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
with open(index_file, 'r') as f:
|
|
481
|
+
index_data = json.load(f)
|
|
482
|
+
|
|
483
|
+
self.files = {}
|
|
484
|
+
for path, data in index_data.items():
|
|
485
|
+
symbols = [
|
|
486
|
+
CodeSymbol(
|
|
487
|
+
name=s['name'],
|
|
488
|
+
kind=s['kind'],
|
|
489
|
+
file_path=path,
|
|
490
|
+
line_start=s.get('line_start', 0),
|
|
491
|
+
line_end=s.get('line_end', 0),
|
|
492
|
+
signature=s.get('signature'),
|
|
493
|
+
docstring=s.get('docstring'),
|
|
494
|
+
)
|
|
495
|
+
for s in data.get('symbols', [])
|
|
496
|
+
]
|
|
497
|
+
self.files[path] = CodeFile(
|
|
498
|
+
path=path,
|
|
499
|
+
language=data.get('language', 'unknown'),
|
|
500
|
+
symbols=symbols,
|
|
501
|
+
imports=data.get('imports', []),
|
|
502
|
+
size=data.get('size', 0),
|
|
503
|
+
lines=data.get('lines', 0),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def search_symbol(self, query: str, kind: Optional[str] = None) -> List[CodeSymbol]:
|
|
507
|
+
"""Search for symbols by name"""
|
|
508
|
+
results = []
|
|
509
|
+
query_lower = query.lower()
|
|
510
|
+
|
|
511
|
+
for file_path, code_file in self.files.items():
|
|
512
|
+
for symbol in code_file.symbols:
|
|
513
|
+
if query_lower in symbol.name.lower():
|
|
514
|
+
if kind is None or symbol.kind == kind:
|
|
515
|
+
results.append(symbol)
|
|
516
|
+
|
|
517
|
+
return sorted(results, key=lambda x: x.file_path)
|
|
518
|
+
|
|
519
|
+
def search_imports(self, module: str) -> List[CodeFile]:
|
|
520
|
+
"""Find files that import a module"""
|
|
521
|
+
results = []
|
|
522
|
+
module_lower = module.lower()
|
|
523
|
+
|
|
524
|
+
for file_path, code_file in self.files.items():
|
|
525
|
+
for imp in code_file.imports:
|
|
526
|
+
if module_lower in imp.lower():
|
|
527
|
+
results.append(code_file)
|
|
528
|
+
break
|
|
529
|
+
|
|
530
|
+
return results
|
|
531
|
+
|
|
532
|
+
def get_file_summary(self, file_path: str) -> Optional[Dict[str, Any]]:
|
|
533
|
+
"""Get summary for a specific file"""
|
|
534
|
+
if file_path not in self.files:
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
code_file = self.files[file_path]
|
|
538
|
+
return {
|
|
539
|
+
'path': code_file.path,
|
|
540
|
+
'language': code_file.language,
|
|
541
|
+
'lines': code_file.lines,
|
|
542
|
+
'size': code_file.size,
|
|
543
|
+
'symbols_count': len(code_file.symbols),
|
|
544
|
+
'imports_count': len(code_file.imports),
|
|
545
|
+
'symbols': [
|
|
546
|
+
{'name': s.name, 'kind': s.kind}
|
|
547
|
+
for s in code_file.symbols
|
|
548
|
+
],
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
552
|
+
"""Get index statistics"""
|
|
553
|
+
total_symbols = sum(len(f.symbols) for f in self.files.values())
|
|
554
|
+
symbol_kinds = {}
|
|
555
|
+
languages = {}
|
|
556
|
+
|
|
557
|
+
for code_file in self.files.values():
|
|
558
|
+
languages[code_file.language] = languages.get(code_file.language, 0) + 1
|
|
559
|
+
for symbol in code_file.symbols:
|
|
560
|
+
symbol_kinds[symbol.kind] = symbol_kinds.get(symbol.kind, 0) + 1
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
'files': len(self.files),
|
|
564
|
+
'total_symbols': total_symbols,
|
|
565
|
+
'languages': languages,
|
|
566
|
+
'symbol_kinds': symbol_kinds,
|
|
567
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Instruction file management and reference processing"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Any, Optional, Union
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
# Import the new reference system
|
|
10
|
+
from .references import (
|
|
11
|
+
InstructionManager,
|
|
12
|
+
ReferenceResolver,
|
|
13
|
+
FileReference,
|
|
14
|
+
FolderReference,
|
|
15
|
+
process_references
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Re-export for backward compatibility
|
|
19
|
+
__all__ = [
|
|
20
|
+
'InstructionManager',
|
|
21
|
+
'ReferenceResolver',
|
|
22
|
+
'FileReference',
|
|
23
|
+
'FolderReference',
|
|
24
|
+
'process_references',
|
|
25
|
+
'get_instruction_context',
|
|
26
|
+
'resolve_file_references',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def get_instruction_context(workspace_root: Union[str, Path]) -> str:
|
|
30
|
+
"""Get instruction context for the workspace (backward compatible)"""
|
|
31
|
+
mgr = InstructionManager(workspace_root)
|
|
32
|
+
return mgr.get_combined_instructions()
|
|
33
|
+
|
|
34
|
+
def resolve_file_references(text: str, workspace_root: Union[str, Path]) -> Dict[str, Any]:
|
|
35
|
+
"""Resolve file references in text (backward compatible)"""
|
|
36
|
+
resolver = ReferenceResolver(workspace_root)
|
|
37
|
+
return resolver.resolve_references(text)
|
|
38
|
+
|
|
39
|
+
# Keep the existing functionality for backward compatibility
|
|
40
|
+
# The original parse_instruction_file function is preserved
|
|
41
|
+
|
|
42
|
+
def parse_instruction_file(file_path: Union[str, Path]) -> Dict[str, str]:
|
|
43
|
+
"""Parse an instruction file into sections"""
|
|
44
|
+
path = Path(file_path)
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
content = path.read_text(encoding='utf-8', errors='ignore')
|
|
49
|
+
sections = {}
|
|
50
|
+
|
|
51
|
+
# Parse markdown sections
|
|
52
|
+
section_pattern = r'^##+\s+(.+)$'
|
|
53
|
+
current_section = None
|
|
54
|
+
current_content = []
|
|
55
|
+
|
|
56
|
+
for line in content.split('\n'):
|
|
57
|
+
match = re.match(section_pattern, line)
|
|
58
|
+
if match:
|
|
59
|
+
if current_section:
|
|
60
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
61
|
+
current_section = match.group(1).strip()
|
|
62
|
+
current_content = []
|
|
63
|
+
else:
|
|
64
|
+
current_content.append(line)
|
|
65
|
+
|
|
66
|
+
if current_section:
|
|
67
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
68
|
+
|
|
69
|
+
return sections
|
|
70
|
+
|
|
71
|
+
def create_instruction_template(path: Union[str, Path] = 'TAMFIS.md') -> str:
|
|
72
|
+
"""Create a template instruction file"""
|
|
73
|
+
template = """# TAMFIS-CODE Instructions
|
|
74
|
+
|
|
75
|
+
## Project Overview
|
|
76
|
+
<!-- Describe your project, its purpose, and main components -->
|
|
77
|
+
|
|
78
|
+
## Coding Standards
|
|
79
|
+
<!-- Define coding standards and style guide -->
|
|
80
|
+
|
|
81
|
+
### Python Standards
|
|
82
|
+
- Use PEP 8 for Python code
|
|
83
|
+
- Maximum line length: 100 characters
|
|
84
|
+
- Use type hints for all function definitions
|
|
85
|
+
|
|
86
|
+
### JavaScript/TypeScript Standards
|
|
87
|
+
- Use ESLint with standard config
|
|
88
|
+
- Prefer async/await over callbacks
|
|
89
|
+
|
|
90
|
+
## Project Structure
|
|
91
|
+
<!-- Document your project structure -->
|
|
92
|
+
.
|
|
93
|
+
├── src/
|
|
94
|
+
│ └── ...
|
|
95
|
+
├── tests/
|
|
96
|
+
│ └── ...
|
|
97
|
+
└── docs/
|
|
98
|
+
└── ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## Common Patterns
|
|
102
|
+
<!-- Document common patterns used in the project -->
|
|
103
|
+
|
|
104
|
+
### Error Handling
|
|
105
|
+
<!-- How errors are handled -->
|
|
106
|
+
|
|
107
|
+
### Testing
|
|
108
|
+
<!-- Testing strategy and tools -->
|
|
109
|
+
|
|
110
|
+
## Important Notes
|
|
111
|
+
<!-- Any additional important information -->
|
|
112
|
+
|
|
113
|
+
## Dependencies
|
|
114
|
+
<!-- Key dependencies and their versions -->
|
|
115
|
+
|
|
116
|
+
## Environment Variables
|
|
117
|
+
<!-- Required environment variables -->
|
|
118
|
+
"""
|
|
119
|
+
path = Path(path)
|
|
120
|
+
path.write_text(template, encoding='utf-8')
|
|
121
|
+
return template
|