bone-agent 1.4.0 → 2.0.1

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.
Files changed (126) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -201
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -144
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -50
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/skills.md +0 -3
  25. package/prompts/main/targeted_searching.md +0 -10
  26. package/prompts/main/task_lists_pattern.md +0 -8
  27. package/prompts/main/temp_folder.md +0 -9
  28. package/prompts/main/think_before_acting.md +0 -10
  29. package/prompts/main/tone_and_style.md +0 -4
  30. package/prompts/main/tool_preferences.md +0 -24
  31. package/prompts/main/trust_subagent_context.md +0 -21
  32. package/prompts/main/when_to_use_sub_agent.md +0 -7
  33. package/prompts/micro/ask_questions.md +0 -1
  34. package/prompts/micro/batch_independent_calls.md +0 -1
  35. package/prompts/micro/casual_interactions.md +0 -1
  36. package/prompts/micro/code_references.md +0 -1
  37. package/prompts/micro/communication_style.md +0 -1
  38. package/prompts/micro/context_reliability.md +0 -1
  39. package/prompts/micro/conversational_tool_calling.md +0 -1
  40. package/prompts/micro/editing_pattern.md +0 -1
  41. package/prompts/micro/error_handling.md +0 -1
  42. package/prompts/micro/exploration_pattern.md +0 -1
  43. package/prompts/micro/intro.md +0 -1
  44. package/prompts/micro/obsidian.md +0 -4
  45. package/prompts/micro/obsidian_project.md +0 -5
  46. package/prompts/micro/professional_objectivity.md +0 -1
  47. package/prompts/micro/skills.md +0 -1
  48. package/prompts/micro/targeted_searching.md +0 -1
  49. package/prompts/micro/task_lists_pattern.md +0 -1
  50. package/prompts/micro/temp_folder.md +0 -1
  51. package/prompts/micro/think_before_acting.md +0 -5
  52. package/prompts/micro/tone_and_style.md +0 -1
  53. package/prompts/micro/tool_preferences.md +0 -1
  54. package/prompts/micro/trust_subagent_context.md +0 -1
  55. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  56. package/requirements.txt +0 -9
  57. package/src/__init__.py +0 -11
  58. package/src/core/__init__.py +0 -1
  59. package/src/core/agentic.py +0 -1085
  60. package/src/core/chat_manager.py +0 -1577
  61. package/src/core/config_manager.py +0 -260
  62. package/src/core/cron.py +0 -578
  63. package/src/core/cron_allowlist.py +0 -118
  64. package/src/core/memory.py +0 -145
  65. package/src/core/metadata.py +0 -75
  66. package/src/core/retry.py +0 -71
  67. package/src/core/skills.py +0 -463
  68. package/src/core/sub_agent.py +0 -376
  69. package/src/core/tool_approval.py +0 -220
  70. package/src/core/tool_feedback.py +0 -789
  71. package/src/exceptions.py +0 -79
  72. package/src/llm/__init__.py +0 -1
  73. package/src/llm/client.py +0 -176
  74. package/src/llm/codex_provider.py +0 -350
  75. package/src/llm/config.py +0 -536
  76. package/src/llm/prompts.py +0 -494
  77. package/src/llm/providers.py +0 -438
  78. package/src/llm/streaming.py +0 -163
  79. package/src/llm/token_tracker.py +0 -399
  80. package/src/tools/__init__.py +0 -151
  81. package/src/tools/constants.py +0 -59
  82. package/src/tools/create_file.py +0 -136
  83. package/src/tools/directory.py +0 -389
  84. package/src/tools/edit.py +0 -549
  85. package/src/tools/file_reader.py +0 -322
  86. package/src/tools/helpers/__init__.py +0 -99
  87. package/src/tools/helpers/base.py +0 -599
  88. package/src/tools/helpers/converters.py +0 -44
  89. package/src/tools/helpers/file_helpers.py +0 -189
  90. package/src/tools/helpers/formatters.py +0 -411
  91. package/src/tools/helpers/loader.py +0 -145
  92. package/src/tools/helpers/parallel_executor.py +0 -231
  93. package/src/tools/helpers/path_resolver.py +0 -283
  94. package/src/tools/helpers/plugin_manifest.py +0 -185
  95. package/src/tools/obsidian.py +0 -96
  96. package/src/tools/review_sub_agent.py +0 -190
  97. package/src/tools/rg_search.py +0 -477
  98. package/src/tools/search_plugins.py +0 -177
  99. package/src/tools/select_option.py +0 -600
  100. package/src/tools/shell.py +0 -302
  101. package/src/tools/sub_agent.py +0 -139
  102. package/src/tools/task_list.py +0 -269
  103. package/src/tools/web_search.py +0 -61
  104. package/src/ui/__init__.py +0 -1
  105. package/src/ui/banner.py +0 -87
  106. package/src/ui/commands.py +0 -3131
  107. package/src/ui/displays.py +0 -239
  108. package/src/ui/loader.py +0 -284
  109. package/src/ui/main.py +0 -643
  110. package/src/ui/prompt_utils.py +0 -113
  111. package/src/ui/setting_selector.py +0 -590
  112. package/src/ui/setup_wizard.py +0 -294
  113. package/src/ui/sub_agent_panel.py +0 -234
  114. package/src/ui/tool_confirmation.py +0 -226
  115. package/src/utils/__init__.py +0 -1
  116. package/src/utils/citation_parser.py +0 -199
  117. package/src/utils/editor.py +0 -207
  118. package/src/utils/gitignore_filter.py +0 -149
  119. package/src/utils/logger.py +0 -254
  120. package/src/utils/paths.py +0 -30
  121. package/src/utils/result_parsers.py +0 -108
  122. package/src/utils/safe_commands.py +0 -243
  123. package/src/utils/settings.py +0 -195
  124. package/src/utils/user_message_logger.py +0 -120
  125. package/src/utils/validation.py +0 -201
  126. package/src/utils/web_search.py +0 -173
@@ -1,136 +0,0 @@
1
- """File creation operations."""
2
-
3
- import os
4
- from pathlib import Path
5
- from typing import Optional, Tuple
6
-
7
- from utils.settings import MAX_FILE_PREVIEW_LINES
8
- from .helpers.base import tool
9
- from .helpers.path_resolver import PathResolver
10
- from .helpers.formatters import format_file_result
11
-
12
-
13
- def _validate_create_path(
14
- path_str: str,
15
- repo_root: Path,
16
- gitignore_spec,
17
- vault_root: Path = None,
18
- ) -> Tuple[Optional[Path], Optional[str]]:
19
- """Validate and resolve path for file creation.
20
-
21
- Args:
22
- path_str: Path string to validate
23
- repo_root: Repository root directory
24
- gitignore_spec: Optional PathSpec for .gitignore filtering
25
- vault_root: Optional Obsidian vault root path
26
-
27
- Returns:
28
- (resolved_path, error_message) - error_message is None if valid
29
- """
30
- vault_path = Path(vault_root) if vault_root else None
31
- resolver = PathResolver(repo_root=repo_root, gitignore_spec=gitignore_spec, vault_path=vault_path)
32
- return resolver.resolve_and_validate(
33
- path_str,
34
- check_gitignore=True,
35
- must_exist=False, # File doesn't need to exist yet
36
- enforce_boundary=True,
37
- )
38
-
39
-
40
- @tool(
41
- name="create_file",
42
- description="Create a new file with optional initial content. File must not exist.",
43
- parameters={
44
- "type": "object",
45
- "properties": {
46
- "path_str": {"type": "string", "description": "Path to create"},
47
- "content": {"type": "string", "description": "Initial content (omit for empty file)"}
48
- },
49
- "required": ["path_str"]
50
- },
51
- )
52
- def create_file(
53
- path_str: str,
54
- repo_root: Path,
55
- content: Optional[str] = None,
56
- gitignore_spec = None,
57
- vault_root: str = None,
58
- ) -> str:
59
- """Create a new file with optional initial content.
60
-
61
- Creates a new file at the specified path, creating parent directories
62
- if needed. The file must not already exist. Respects .gitignore.
63
-
64
- Args:
65
- path_str: Path string to the file to create
66
- repo_root: Repository root directory (for path resolution)
67
- content: Optional initial content for the file. If omitted, creates empty file.
68
- gitignore_spec: Optional PathSpec for .gitignore filtering
69
- vault_root: Optional Obsidian vault root path
70
-
71
- Returns:
72
- str: Formatted result with exit_code and status, including preview
73
- """
74
- try:
75
- # Validate path
76
- resolved, error = _validate_create_path(path_str, repo_root, gitignore_spec, vault_root=vault_root)
77
- if error:
78
- return format_file_result(exit_code=1, error=error, path=path_str)
79
-
80
- # Check if already exists
81
- if resolved.exists():
82
- try:
83
- rel_path = resolved.relative_to(repo_root)
84
- except ValueError:
85
- rel_path = resolved
86
- return format_file_result(
87
- exit_code=1,
88
- error="File already exists",
89
- path=str(rel_path)
90
- )
91
-
92
- # Create parent directories if needed
93
- parent_dir = resolved.parent
94
- if parent_dir != repo_root and not parent_dir.exists():
95
- parent_dir.mkdir(parents=True, exist_ok=True)
96
-
97
- # Write content or create empty file
98
- if content is not None:
99
- resolved.write_text(content, encoding="utf-8", newline="")
100
- else:
101
- content = ""
102
- resolved.touch()
103
-
104
- # Build result with content for display (truncate preview if needed)
105
- result_lines = []
106
- result_lines.append(f"exit_code=0")
107
- try:
108
- rel_path = resolved.relative_to(repo_root)
109
- except ValueError:
110
- rel_path = resolved
111
- result_lines.append(f"path={rel_path}")
112
- result_lines.append(f"content=File created successfully")
113
- result_lines.append("")
114
- result_lines.append(f"=== FILE_CONTENT ===")
115
-
116
- # Truncate content for preview if it exceeds max lines
117
- if content:
118
- content_lines = content.splitlines(keepends=True)
119
- if len(content_lines) > MAX_FILE_PREVIEW_LINES:
120
- truncated_content = "".join(content_lines[:MAX_FILE_PREVIEW_LINES])
121
- omitted = len(content_lines) - MAX_FILE_PREVIEW_LINES
122
- result_lines.append(truncated_content)
123
- result_lines.append(f"\n... ({omitted} more lines omitted from preview)")
124
- else:
125
- result_lines.append(content)
126
-
127
- result_lines.append("=== END_FILE_CONTENT ===")
128
-
129
- return "\n".join(result_lines) + "\n\n"
130
-
131
- except PermissionError:
132
- return format_file_result(exit_code=1, error="Permission denied", path=path_str)
133
- except OSError as e:
134
- return format_file_result(exit_code=1, error=f"Invalid filename: {e}", path=path_str)
135
- except Exception as e:
136
- return format_file_result(exit_code=1, error=str(e), path=path_str)
@@ -1,389 +0,0 @@
1
- """Directory listing operations."""
2
-
3
- import fnmatch
4
- import os
5
- from pathlib import Path
6
- from typing import Optional, Tuple, Dict, List
7
-
8
- from .helpers.base import tool
9
- from .helpers.path_resolver import PathResolver
10
- from .helpers.file_helpers import GitignoreFilter
11
- from .helpers.formatters import format_file_result
12
- from . import constants
13
-
14
-
15
- def _group_items_by_directory(items, show_files, show_dirs) -> Dict:
16
- """Group items by their parent directory for smart truncation.
17
-
18
- Args:
19
- items: List of (kind, rel_path, size, raw_path, line_count) tuples
20
- show_files: Whether files are included in results
21
- show_dirs: Whether directories are included in results
22
-
23
- Returns:
24
- Dict mapping parent_dir -> {'dirs': [dir_items], 'files': [file_items]}
25
- """
26
- groups = {}
27
-
28
- for kind, rel_path, size, raw_path, line_count in items:
29
- # Get parent directory
30
- if '/' in rel_path:
31
- parent_dir = Path(rel_path).parent
32
- else:
33
- parent_dir = Path('.') # Root level
34
-
35
- if parent_dir not in groups:
36
- groups[parent_dir] = {'dirs': [], 'files': []}
37
-
38
- if kind == 'DIR ':
39
- groups[parent_dir]['dirs'].append((kind, rel_path, size, raw_path, line_count))
40
- else: # FILE
41
- groups[parent_dir]['files'].append((kind, rel_path, size, raw_path, line_count))
42
-
43
- return groups
44
-
45
-
46
- def _apply_smart_truncation(items, show_files, show_dirs, hit_limit=False) -> Tuple[List, Optional[Dict]]:
47
- """Apply smart truncation to directory listing results.
48
-
49
- Truncation Strategy:
50
- - If total items < TRUNCATION_THRESHOLD: No truncation
51
- - If total items >= TRUNCATION_THRESHOLD:
52
- * ALL directories are shown (preserve structure)
53
- * Files are sampled: top MAX_FILES_PER_FOLDER per directory
54
- * Truncation metadata returned for message generation
55
-
56
- Args:
57
- items: List of (kind, rel_path, size, raw_path) tuples
58
- show_files: Whether files are included in results
59
- show_dirs: Whether directories are included in results
60
- hit_limit: Whether we hit MAX_TOTAL_ITEMS during collection
61
-
62
- Returns:
63
- Tuple of:
64
- - List of items after truncation
65
- - None if no truncation, or dict with truncation metadata
66
- """
67
- total_count = len(items)
68
-
69
- # Fast path: no truncation needed
70
- if total_count < constants.TRUNCATION_THRESHOLD:
71
- return items, None
72
-
73
- # Group items by parent directory
74
- groups = _group_items_by_directory(items, show_files, show_dirs)
75
-
76
- # Apply truncation per directory
77
- truncated_items = []
78
- total_dirs_shown = 0
79
- total_files_shown = 0
80
- total_files_omitted = 0
81
-
82
- # Sort directories alphabetically for consistent output
83
- for parent_dir in sorted(groups.keys(), key=lambda p: str(p)):
84
- group = groups[parent_dir]
85
-
86
- # Always include all directories
87
- if show_dirs:
88
- truncated_items.extend(group['dirs'])
89
- total_dirs_shown += len(group['dirs'])
90
-
91
- # Truncate files if needed
92
- if show_files and group['files']:
93
- # Sort files alphabetically and take top N
94
- sorted_files = sorted(group['files'], key=lambda x: x[1]) # Sort by rel_path
95
- files_to_show = sorted_files[:constants.MAX_FILES_PER_FOLDER]
96
- truncated_items.extend(files_to_show)
97
-
98
- total_files_shown += len(files_to_show)
99
- total_files_omitted += len(group['files']) - len(files_to_show)
100
-
101
- # Build truncation info
102
- truncation_info = {
103
- 'total': total_count,
104
- 'shown': len(truncated_items),
105
- 'omitted': total_count - len(truncated_items),
106
- 'dirs_shown': total_dirs_shown,
107
- 'files_shown': total_files_shown,
108
- 'files_omitted': total_files_omitted,
109
- 'all_dirs_shown': True
110
- }
111
-
112
- return truncated_items, truncation_info
113
-
114
-
115
- def _validate_directory_path(
116
- path_str: str,
117
- repo_root: Path,
118
- vault_root: str = None
119
- ) -> Tuple[Optional[Path], Optional[str]]:
120
- """Validate and resolve path for directory listing.
121
-
122
- This function wraps PathResolver.resolve_and_validate() for the directory
123
- tool's specific needs, ensuring the path is a directory.
124
-
125
- Args:
126
- path_str: Path string to validate
127
- repo_root: Repository root directory
128
- vault_root: Optional Obsidian vault root path
129
-
130
- Returns:
131
- (resolved_path, error_message) - error_message is None if valid
132
- """
133
- from pathlib import Path as P
134
- vault_path = P(vault_root) if vault_root else None
135
- # Use PathResolver for centralized validation
136
- resolver = PathResolver(repo_root=repo_root, gitignore_spec=None, vault_path=vault_path)
137
- resolved, error = resolver.resolve_and_validate(
138
- path_str,
139
- check_gitignore=False, # Directory listing shows everything
140
- must_exist=True,
141
- must_be_dir=True, # Must be a directory
142
- enforce_boundary=True
143
- )
144
-
145
- if error:
146
- return None, error
147
-
148
- return resolved, None
149
-
150
-
151
- @tool(
152
- name="list_directory",
153
- description="List directory contents (preferred over PowerShell).",
154
- parameters={
155
- "type": "object",
156
- "properties": {
157
- "path_str": {"type": "string", "description": "Path to list (default: '.')"},
158
- "recursive": {"type": "boolean", "description": "List recursively (default: false)"},
159
- "show_files": {"type": "boolean", "description": "Include files (default: true)"},
160
- "show_dirs": {"type": "boolean", "description": "Include directories (default: true)"},
161
- "pattern": {"type": "string", "description": "Glob filter (e.g., \"*.py\")"}
162
- },
163
- "required": ["path_str"]
164
- },
165
- )
166
- def list_directory(
167
- path_str: str,
168
- repo_root: Path,
169
- recursive: bool = False,
170
- show_files: bool = True,
171
- show_dirs: bool = True,
172
- pattern: Optional[str] = None,
173
- gitignore_spec = None,
174
- vault_root: str = None
175
- ) -> str:
176
- """List directory contents.
177
-
178
- Directory listing that respects .gitignore and shows file sizes in a
179
- consistent format.
180
-
181
- Args:
182
- path_str: Path string to the directory to list
183
- repo_root: Repository root directory (for path resolution)
184
- recursive: List recursively
185
- show_files: Include files in output
186
- show_dirs: Include directories in output
187
- pattern: Optional glob pattern to filter results (e.g., "*.py")
188
- gitignore_spec: Optional PathSpec for .gitignore filtering
189
- vault_root: Optional Obsidian vault root path
190
-
191
- Returns:
192
- str: Formatted result with exit_code, items_count, and directory listing
193
- """
194
- try:
195
- # Validate path
196
- resolved, error = _validate_directory_path(path_str, repo_root, vault_root=vault_root)
197
- if error:
198
- return format_file_result(
199
- exit_code=1,
200
- error=error,
201
- path=path_str
202
- )
203
-
204
- def _match_pattern(rel_path: Path) -> bool:
205
- if not pattern:
206
- return True
207
- return fnmatch.fnmatch(rel_path.as_posix(), pattern)
208
-
209
- # Create gitignore filter if spec is provided
210
- gitignore_filter = GitignoreFilter(repo_root=repo_root, gitignore_spec=gitignore_spec) if gitignore_spec else None
211
-
212
- def _is_ignored(path: Path) -> bool:
213
- if path.name == ".gitignore":
214
- return True
215
- if ".git" in path.parts:
216
- return True
217
- if gitignore_filter is None:
218
- return False
219
- # Use GitignoreFilter for consistent behavior
220
- return gitignore_filter.is_ignored(path)
221
-
222
- # Collect items
223
- items = []
224
- base_dir = resolved
225
- hit_limit = False # Track if we hit constants.MAX_TOTAL_ITEMS
226
-
227
- def _count_lines(file_path: Path) -> int:
228
- """Count lines in a file efficiently."""
229
- try:
230
- with open(file_path, 'rb') as f:
231
- return sum(1 for _ in f) - 1 # Subtract 1 for last newline, but handle empty files
232
- except (OSError, IOError):
233
- return 0
234
-
235
- def _add_item(kind, rel_path, size_str, raw_path, line_count=None):
236
- nonlocal hit_limit
237
- if kind == "FILE" and not show_files:
238
- return
239
- if kind == "DIR " and not show_dirs:
240
- return
241
- if not _match_pattern(rel_path):
242
- return
243
- if hit_limit:
244
- return
245
- if len(items) >= constants.MAX_TOTAL_ITEMS:
246
- hit_limit = True
247
- return
248
- items.append((kind, str(rel_path), size_str, raw_path, line_count))
249
-
250
- if recursive:
251
- stack = [resolved]
252
- while stack and not hit_limit:
253
- current = stack.pop()
254
- try:
255
- with os.scandir(current) as it:
256
- for entry in it:
257
- try:
258
- if entry.is_symlink():
259
- continue
260
- except OSError:
261
- continue
262
-
263
- is_dir = entry.is_dir(follow_symlinks=False)
264
- is_file = entry.is_file(follow_symlinks=False)
265
-
266
- if not is_dir and not is_file:
267
- continue
268
-
269
- entry_path = Path(entry.path)
270
-
271
- if _is_ignored(entry_path):
272
- continue
273
-
274
- rel_path = entry_path.relative_to(base_dir)
275
-
276
- if is_file:
277
- try:
278
- size = f"{entry.stat(follow_symlinks=False).st_size:>10}"
279
- line_count = _count_lines(entry_path)
280
- except OSError:
281
- size = " ?"
282
- line_count = 0
283
- _add_item("FILE", rel_path, size, entry_path, line_count)
284
- else:
285
- _add_item("DIR ", rel_path, " ", entry_path, line_count=0)
286
- stack.append(entry_path)
287
- except PermissionError:
288
- continue
289
- else:
290
- with os.scandir(resolved) as it:
291
- for entry in it:
292
- try:
293
- if entry.is_symlink():
294
- continue
295
- except OSError:
296
- continue
297
-
298
- is_dir = entry.is_dir(follow_symlinks=False)
299
- is_file = entry.is_file(follow_symlinks=False)
300
-
301
- if not is_dir and not is_file:
302
- continue
303
-
304
- entry_path = Path(entry.path)
305
-
306
- if _is_ignored(entry_path):
307
- continue
308
-
309
- rel_path = entry_path.relative_to(base_dir)
310
-
311
- if is_file:
312
- try:
313
- size = f"{entry.stat(follow_symlinks=False).st_size:>10}"
314
- line_count = _count_lines(entry_path)
315
- except OSError:
316
- size = " ?"
317
- line_count = 0
318
- _add_item("FILE", rel_path, size, entry_path, line_count)
319
- else:
320
- _add_item("DIR ", rel_path, " ", entry_path, line_count=0)
321
-
322
- # Sort: directories first, then alphabetically
323
- items.sort(key=lambda x: (0 if x[0] == "DIR " else 1, x[1]))
324
-
325
- # Format output
326
- if not items:
327
- try:
328
- rel_path = resolved.relative_to(repo_root)
329
- except ValueError:
330
- rel_path = resolved
331
- return format_file_result(
332
- exit_code=0,
333
- content="(empty directory)",
334
- path=str(rel_path),
335
- items_count=0
336
- )
337
-
338
- # Apply smart truncation if needed
339
- truncated_items, truncation_info = _apply_smart_truncation(items, show_files, show_dirs, hit_limit)
340
-
341
- # Build lines with truncation message
342
- lines = []
343
- for kind, rel_path, size, _, line_count in truncated_items:
344
- if kind == "FILE":
345
- lines.append(f"{kind} {rel_path} {line_count:6} lines {size} bytes")
346
- else:
347
- lines.append(f"{kind} {rel_path} {line_count:6} lines")
348
-
349
- # Add truncation message at end
350
- if truncation_info:
351
- lines.append("")
352
- msg = f"[{truncation_info['files_omitted']} file(s) omitted ({truncation_info['shown']} shown from {truncation_info['total']} total items)]"
353
- if hit_limit:
354
- msg += f"\n[WARNING: Listing stopped at {constants.MAX_TOTAL_ITEMS} items to prevent context overflow. Use filters or specific paths to explore further.]"
355
- lines.append(msg)
356
-
357
- content = "\n".join(lines)
358
-
359
- # Update truncation info to indicate if we hit the hard limit
360
- if truncation_info and hit_limit:
361
- truncation_info['hit_limit'] = True
362
- truncation_info['max_items'] = constants.MAX_TOTAL_ITEMS
363
-
364
- try:
365
- rel_path = resolved.relative_to(repo_root)
366
- except ValueError:
367
- rel_path = resolved
368
-
369
- return format_file_result(
370
- exit_code=0,
371
- content=content,
372
- path=str(rel_path),
373
- items_count=truncation_info['total'] if truncation_info else len(items),
374
- truncated=(truncation_info is not None),
375
- truncation_info=truncation_info
376
- )
377
-
378
- except PermissionError:
379
- return format_file_result(
380
- exit_code=1,
381
- error="Permission denied",
382
- path=path_str
383
- )
384
- except Exception as e:
385
- return format_file_result(
386
- exit_code=1,
387
- error=str(e),
388
- path=path_str
389
- )