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
|
@@ -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
|
+
}
|