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.
@@ -0,0 +1,336 @@
1
+ """File and folder reference support for TAMFIS-CODE"""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import List, Dict, Any, Optional, Set, Union
7
+ from dataclasses import dataclass, field
8
+ import fnmatch
9
+
10
+ @dataclass
11
+ class FileReference:
12
+ """A file referenced in a conversation"""
13
+ path: str
14
+ content: str
15
+ line_start: Optional[int] = None
16
+ line_end: Optional[int] = None
17
+ is_selected: bool = False
18
+
19
+ def get_lines(self) -> List[str]:
20
+ """Get specific lines if line range is specified"""
21
+ if self.line_start is None:
22
+ return self.content.split('\n')
23
+
24
+ lines = self.content.split('\n')
25
+ end = self.line_end or self.line_start # If no end, use start (single line)
26
+ return lines[self.line_start-1:end]
27
+
28
+ def get_context(self, context_lines: int = 3) -> str:
29
+ """Get content with context lines around selection"""
30
+ if self.line_start is None:
31
+ return self.content
32
+
33
+ lines = self.content.split('\n')
34
+ start = max(0, self.line_start - 1 - context_lines)
35
+ end = min(len(lines), (self.line_end or self.line_start) + context_lines)
36
+
37
+ result = []
38
+ for i in range(start, end):
39
+ if i >= self.line_start - 1 and i < (self.line_end or self.line_start):
40
+ result.append(f"> {lines[i]}")
41
+ else:
42
+ result.append(f" {lines[i]}")
43
+
44
+ return '\n'.join(result)
45
+
46
+ @dataclass
47
+ class FolderReference:
48
+ """A folder referenced in a conversation"""
49
+ path: str
50
+ files: List[FileReference] = field(default_factory=list)
51
+ pattern: Optional[str] = None
52
+ recursive: bool = True
53
+
54
+ def get_content(self) -> str:
55
+ """Get content of all files in the folder"""
56
+ result = []
57
+ for f in self.files:
58
+ result.append(f"=== {f.path} ===\n{f.content}")
59
+ return '\n\n'.join(result)
60
+
61
+ class ReferenceResolver:
62
+ """Resolve @file and @folder references in prompts"""
63
+
64
+ def __init__(self, workspace_root: Union[str, Path]):
65
+ self.workspace_root = Path(workspace_root)
66
+ self.references: Dict[str, Union[FileReference, FolderReference]] = {}
67
+
68
+ # Common ignore patterns
69
+ self.ignore_patterns = [
70
+ '*.pyc', '__pycache__', '.git', '.env', 'venv',
71
+ 'node_modules', '.idea', '.vscode', '*.log', '*.tmp',
72
+ '.DS_Store', 'Thumbs.db'
73
+ ]
74
+
75
+ def resolve_references(self, text: str) -> Dict[str, Any]:
76
+ """Find and resolve all @references in text"""
77
+ # Pattern for @file references with optional line range
78
+ # Matches: @path/to/file.py:10-20 or @path/to/file.py:10 or @path/to/file.py
79
+ file_pattern = r'@([^\s@:]+(?:\.[^\s@:]+)?)(?::(\d+)(?:-(\d+))?)?'
80
+
81
+ references = {
82
+ 'files': [],
83
+ 'folders': [],
84
+ 'resolved_content': text
85
+ }
86
+
87
+ # First, find folder references (ending with /)
88
+ folder_pattern = r'@([^\s@:]+)/'
89
+ for match in re.finditer(folder_pattern, text):
90
+ ref_path = match.group(1)
91
+ folder_ref = self._resolve_folder(ref_path)
92
+ if folder_ref:
93
+ references['folders'].append(folder_ref)
94
+ references['resolved_content'] = references['resolved_content'].replace(
95
+ f'@{ref_path}/', f'[Folder: {ref_path}/]'
96
+ )
97
+
98
+ # Then find file references
99
+ for match in re.finditer(file_pattern, text):
100
+ # Skip if the match is part of a folder reference (ends with /)
101
+ if match.group(0).endswith('/'):
102
+ continue
103
+
104
+ ref_path = match.group(1)
105
+ line_start = match.group(2)
106
+ line_end = match.group(3)
107
+
108
+ # Skip if it's actually a folder (has trailing slash in original)
109
+ full_match = match.group(0)
110
+ end_pos = match.end()
111
+ if end_pos < len(text) and text[end_pos] == '/':
112
+ continue
113
+
114
+ # Resolve the file
115
+ file_ref = self._resolve_file(ref_path, line_start, line_end)
116
+ if file_ref:
117
+ references['files'].append(file_ref)
118
+ references['resolved_content'] = references['resolved_content'].replace(
119
+ match.group(0), f'[File: {ref_path}]'
120
+ )
121
+
122
+ return references
123
+
124
+ def _resolve_file(self, ref_path: str, line_start: Optional[str] = None, line_end: Optional[str] = None) -> Optional[FileReference]:
125
+ """Resolve a file reference"""
126
+ # Try to find the file
127
+ full_path = self._find_file(ref_path)
128
+
129
+ if not full_path or not full_path.exists() or not full_path.is_file():
130
+ return None
131
+
132
+ try:
133
+ content = full_path.read_text(encoding='utf-8', errors='ignore')
134
+
135
+ # Parse line range if specified
136
+ ls = int(line_start) if line_start else None
137
+ # If only line_start is specified, line_end defaults to line_start (single line)
138
+ le = int(line_end) if line_end else ls
139
+
140
+ return FileReference(
141
+ path=str(full_path.relative_to(self.workspace_root)),
142
+ content=content,
143
+ line_start=ls,
144
+ line_end=le,
145
+ )
146
+ except Exception:
147
+ return None
148
+
149
+ def _find_file(self, ref_path: str) -> Optional[Path]:
150
+ """Find a file by path, trying multiple strategies"""
151
+ # Strategy 1: Direct path
152
+ full_path = self.workspace_root / ref_path
153
+ if full_path.exists() and full_path.is_file():
154
+ return full_path
155
+
156
+ # Strategy 2: In tamfis_code directory
157
+ full_path = self.workspace_root / 'tamfis_code' / Path(ref_path).name
158
+ if full_path.exists() and full_path.is_file():
159
+ return full_path
160
+
161
+ # Strategy 3: With common extensions
162
+ extensions = ['.py', '.js', '.ts', '.go', '.rs', '.c', '.cpp', '.h', '.java',
163
+ '.rb', '.php', '.swift', '.kt', '.json', '.yaml', '.yml', '.toml',
164
+ '.md', '.txt', '.sh', '.bash', '.zsh', '.fish', '.html', '.css']
165
+ for ext in extensions:
166
+ # Try with extension in root
167
+ full_path = self.workspace_root / f"{ref_path}{ext}"
168
+ if full_path.exists() and full_path.is_file():
169
+ return full_path
170
+ # Try with extension in tamfis_code
171
+ full_path = self.workspace_root / 'tamfis_code' / f"{Path(ref_path).name}{ext}"
172
+ if full_path.exists() and full_path.is_file():
173
+ return full_path
174
+
175
+ # Strategy 4: Search recursively
176
+ for path in self.workspace_root.rglob(f"*{Path(ref_path).name}"):
177
+ if path.is_file():
178
+ return path
179
+
180
+ return None
181
+
182
+ def _resolve_folder(self, ref_path: str) -> Optional[FolderReference]:
183
+ """Resolve a folder reference"""
184
+ full_path = self.workspace_root / ref_path
185
+
186
+ if not full_path.exists() or not full_path.is_dir():
187
+ return None
188
+
189
+ files = []
190
+ for file_path in self._get_files_in_folder(full_path):
191
+ try:
192
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
193
+ files.append(FileReference(
194
+ path=str(file_path.relative_to(self.workspace_root)),
195
+ content=content,
196
+ ))
197
+ except Exception:
198
+ continue
199
+
200
+ return FolderReference(
201
+ path=str(full_path.relative_to(self.workspace_root)),
202
+ files=files,
203
+ )
204
+
205
+ def _get_files_in_folder(self, folder: Path) -> List[Path]:
206
+ """Get all files in a folder (recursive)"""
207
+ files = []
208
+ for item in folder.rglob('*'):
209
+ if item.is_file():
210
+ # Check ignore patterns
211
+ if not self._is_ignored(item):
212
+ files.append(item)
213
+ return files
214
+
215
+ def _is_ignored(self, path: Path) -> bool:
216
+ """Check if a path should be ignored"""
217
+ path_str = str(path)
218
+ for pattern in self.ignore_patterns:
219
+ if fnmatch.fnmatch(path_str, f"*{pattern}*"):
220
+ return True
221
+ return False
222
+
223
+ def get_context_summary(self, references: Dict[str, Any]) -> str:
224
+ """Generate a summary of resolved references for the AI context"""
225
+ parts = []
226
+
227
+ if references['files']:
228
+ parts.append("## Referenced Files\n")
229
+ for f in references['files']:
230
+ parts.append(f"### {f.path}")
231
+ parts.append(f.get_context())
232
+ parts.append("")
233
+
234
+ if references['folders']:
235
+ parts.append("## Referenced Folders\n")
236
+ for folder in references['folders']:
237
+ parts.append(f"### {folder.path}")
238
+ parts.append(f"({len(folder.files)} files)")
239
+ parts.append("")
240
+
241
+ return '\n'.join(parts) if parts else ""
242
+
243
+ class InstructionManager:
244
+ """Manages instruction files (TAMFIS.md, etc.)"""
245
+
246
+ INSTRUCTION_FILES = ['TAMFIS.md', 'CLAUDE.md', 'CODEX.md', '.tamfis', '.claude']
247
+
248
+ def __init__(self, workspace_root: Union[str, Path]):
249
+ self.workspace_root = Path(workspace_root)
250
+ self.instructions: Dict[str, str] = {}
251
+ self._load_instructions()
252
+
253
+ def _load_instructions(self):
254
+ """Load all instruction files from the workspace"""
255
+ for filename in self.INSTRUCTION_FILES:
256
+ # Check root
257
+ root_file = self.workspace_root / filename
258
+ if root_file.exists() and root_file.is_file():
259
+ self.instructions[filename] = root_file.read_text(encoding='utf-8', errors='ignore')
260
+
261
+ # Check .tamfis directory
262
+ config_file = self.workspace_root / '.tamfis' / filename
263
+ if config_file.exists() and config_file.is_file():
264
+ self.instructions[f".tamfis/{filename}"] = config_file.read_text(encoding='utf-8', errors='ignore')
265
+
266
+ # Check .claude directory
267
+ config_file = self.workspace_root / '.claude' / filename
268
+ if config_file.exists() and config_file.is_file():
269
+ self.instructions[f".claude/{filename}"] = config_file.read_text(encoding='utf-8', errors='ignore')
270
+
271
+ def get_instruction(self, name: str = None) -> Optional[str]:
272
+ """Get a specific instruction file content"""
273
+ if name:
274
+ return self.instructions.get(name)
275
+
276
+ # Return the first found instruction
277
+ for filename in self.INSTRUCTION_FILES:
278
+ if filename in self.instructions:
279
+ return self.instructions[filename]
280
+ if f".tamfis/{filename}" in self.instructions:
281
+ return self.instructions[f".tamfis/{filename}"]
282
+
283
+ return None
284
+
285
+ def get_all_instructions(self) -> Dict[str, str]:
286
+ """Get all instruction files"""
287
+ return self.instructions
288
+
289
+ def get_combined_instructions(self) -> str:
290
+ """Get combined instruction content"""
291
+ parts = []
292
+ for name, content in self.instructions.items():
293
+ parts.append(f"## {name}\n{content}")
294
+ return '\n\n'.join(parts)
295
+
296
+ def create_instruction_file(self, filename: str = 'TAMFIS.md', content: str = None) -> Path:
297
+ """Create a new instruction file"""
298
+ file_path = self.workspace_root / filename
299
+
300
+ if content is None:
301
+ content = """# TAMFIS-CODE Instructions
302
+
303
+ ## Project Context
304
+ <!-- Describe your project context here -->
305
+
306
+ ## Coding Standards
307
+ <!-- Specify coding standards and style guide -->
308
+
309
+ ## Common Patterns
310
+ <!-- Document common patterns used in the project -->
311
+
312
+ ## Important Notes
313
+ <!-- Any additional important information -->
314
+ """
315
+
316
+ file_path.write_text(content, encoding='utf-8')
317
+ self.instructions[filename] = content
318
+ return file_path
319
+
320
+ def process_references(text: str, workspace_root: Union[str, Path]) -> Dict[str, Any]:
321
+ """Process all @references in text"""
322
+ resolver = ReferenceResolver(workspace_root)
323
+ references = resolver.resolve_references(text)
324
+
325
+ # Get instruction context
326
+ instruction_mgr = InstructionManager(workspace_root)
327
+ instruction_context = instruction_mgr.get_combined_instructions()
328
+
329
+ return {
330
+ 'references': references,
331
+ 'instruction_context': instruction_context,
332
+ 'enhanced_text': '\n\n'.join(filter(None, [
333
+ instruction_context if instruction_context else '',
334
+ references['resolved_content']
335
+ ]))
336
+ }