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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
"""Interactive tool confirmation panel with arrow key navigation."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from html import escape
|
|
5
|
-
from threading import Timer
|
|
6
|
-
from typing import Optional, Tuple
|
|
7
|
-
from prompt_toolkit import HTML
|
|
8
|
-
from prompt_toolkit.application import Application
|
|
9
|
-
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
-
from prompt_toolkit.keys import Keys
|
|
11
|
-
from prompt_toolkit.layout import Layout, HSplit, Window
|
|
12
|
-
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ToolConfirmationPanel:
|
|
18
|
-
"""Interactive panel for tool execution confirmation with arrow key navigation."""
|
|
19
|
-
|
|
20
|
-
# Public constants
|
|
21
|
-
SUMMARY_DISPLAY_DELAY = 0.5 # Seconds to show summary before auto-exit
|
|
22
|
-
CURSOR = "> "
|
|
23
|
-
STANDARD_OPTIONS = [
|
|
24
|
-
{"value": "accept", "text": "Accept"},
|
|
25
|
-
{"value": "advise", "text": "Advise"},
|
|
26
|
-
{"value": "cancel", "text": "Cancel"},
|
|
27
|
-
]
|
|
28
|
-
EDIT_OPTIONS = [
|
|
29
|
-
{"value": "accept", "text": "Accept"},
|
|
30
|
-
{"value": "accept_all_edits", "text": "Accept All Edits"},
|
|
31
|
-
{"value": "advise", "text": "Advise"},
|
|
32
|
-
{"value": "cancel", "text": "Cancel"},
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
def __init__(self, tool_command: str, reason: Optional[str] = None, is_edit_tool: bool = False, cycle_approve_mode=None):
|
|
36
|
-
"""Initialize the tool confirmation panel.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
tool_command: Command/tool being executed
|
|
40
|
-
reason: Optional reason/details about the tool execution
|
|
41
|
-
is_edit_tool: Whether this is an edit tool (shows extra toggle option)
|
|
42
|
-
cycle_approve_mode: Optional callback to cycle approve_mode
|
|
43
|
-
"""
|
|
44
|
-
self.tool_command = tool_command
|
|
45
|
-
self.reason = reason
|
|
46
|
-
self.is_edit_tool = is_edit_tool
|
|
47
|
-
self.cycle_approve_mode = cycle_approve_mode
|
|
48
|
-
self.selected_index = 0
|
|
49
|
-
self._showing_summary = False
|
|
50
|
-
self._selected_value = None
|
|
51
|
-
# Use appropriate options based on tool type
|
|
52
|
-
self._options = self.EDIT_OPTIONS if is_edit_tool else self.STANDARD_OPTIONS
|
|
53
|
-
|
|
54
|
-
def _append_field(self, lines: list[str], label: str, value: object, *, formatted: bool = False) -> None:
|
|
55
|
-
"""Append a field to the panel, escaping untrusted values by default."""
|
|
56
|
-
value_text = str(value)
|
|
57
|
-
if not formatted:
|
|
58
|
-
value_text = escape(value_text)
|
|
59
|
-
value_lines = value_text.splitlines() or [""]
|
|
60
|
-
lines.append(f"<b>{escape(label)}:</b> {value_lines[0]}")
|
|
61
|
-
for continuation in value_lines[1:]:
|
|
62
|
-
lines.append(f" {continuation}")
|
|
63
|
-
|
|
64
|
-
def _get_display_text(self) -> HTML:
|
|
65
|
-
"""Get the formatted text to display.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
HTML formatted text with current selection state
|
|
69
|
-
"""
|
|
70
|
-
lines = []
|
|
71
|
-
|
|
72
|
-
# Check if showing summary
|
|
73
|
-
if self._showing_summary:
|
|
74
|
-
# Find the option text for the selected value
|
|
75
|
-
selected_opt = next((opt for opt in self._options if opt.get("value") == self._selected_value), None)
|
|
76
|
-
selected_text = selected_opt.get("text", self._selected_value) if selected_opt else self._selected_value
|
|
77
|
-
|
|
78
|
-
lines.append("<b>Selection Summary</b>")
|
|
79
|
-
lines.append("")
|
|
80
|
-
self._append_field(lines, "Tool", self.tool_command)
|
|
81
|
-
lines.append(f'<style fg="gray"> Selected: {escape(str(selected_text))}</style>')
|
|
82
|
-
lines.append("")
|
|
83
|
-
else:
|
|
84
|
-
# Tool information
|
|
85
|
-
self._append_field(lines, "Tool", self.tool_command)
|
|
86
|
-
if self.reason:
|
|
87
|
-
self._append_field(lines, "Reason", self.reason)
|
|
88
|
-
lines.append("")
|
|
89
|
-
|
|
90
|
-
# Render options
|
|
91
|
-
for idx, opt in enumerate(self._options):
|
|
92
|
-
text = opt.get("text", "")
|
|
93
|
-
|
|
94
|
-
if idx == self.selected_index:
|
|
95
|
-
# Selected option - show cursor and highlight in bold white
|
|
96
|
-
lines.append(f'<style fg="white" bold="true">{self.CURSOR}{text}</style>')
|
|
97
|
-
else:
|
|
98
|
-
# Unselected option - dark grey
|
|
99
|
-
lines.append(f'<style fg="gray"> {text}</style>')
|
|
100
|
-
|
|
101
|
-
return HTML("\n".join(lines))
|
|
102
|
-
|
|
103
|
-
def _exit_with_summary(self, event, result: str) -> None:
|
|
104
|
-
"""Show summary screen and exit application after delay.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
event: PromptToolkit event object
|
|
108
|
-
result: Result value to return when application exits
|
|
109
|
-
"""
|
|
110
|
-
if self._showing_summary:
|
|
111
|
-
return
|
|
112
|
-
self._showing_summary = True
|
|
113
|
-
self._selected_value = result
|
|
114
|
-
event.app.invalidate()
|
|
115
|
-
Timer(self.SUMMARY_DISPLAY_DELAY, lambda: event.app.exit(result=result)).start()
|
|
116
|
-
|
|
117
|
-
def _create_key_bindings(self) -> KeyBindings:
|
|
118
|
-
"""Create key bindings for navigation and selection.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
KeyBindings object with Up/Down/Enter/Esc handlers
|
|
122
|
-
"""
|
|
123
|
-
bindings = KeyBindings()
|
|
124
|
-
|
|
125
|
-
@bindings.add(Keys.Up)
|
|
126
|
-
def move_up(event):
|
|
127
|
-
"""Move selection up."""
|
|
128
|
-
if self.selected_index > 0:
|
|
129
|
-
self.selected_index -= 1
|
|
130
|
-
event.app.invalidate()
|
|
131
|
-
|
|
132
|
-
@bindings.add(Keys.Down)
|
|
133
|
-
def move_down(event):
|
|
134
|
-
"""Move selection down."""
|
|
135
|
-
if self.selected_index < len(self._options) - 1:
|
|
136
|
-
self.selected_index += 1
|
|
137
|
-
event.app.invalidate()
|
|
138
|
-
|
|
139
|
-
@bindings.add(Keys.Enter)
|
|
140
|
-
def select(event):
|
|
141
|
-
"""Confirm selection."""
|
|
142
|
-
selected_value = self._options[self.selected_index].get("value")
|
|
143
|
-
self._selected_value = selected_value
|
|
144
|
-
|
|
145
|
-
# Handle accept_all_edits option
|
|
146
|
-
if selected_value == "accept_all_edits":
|
|
147
|
-
# Call the callback to cycle approve_mode
|
|
148
|
-
if self.cycle_approve_mode:
|
|
149
|
-
self.cycle_approve_mode()
|
|
150
|
-
# Return "accept" so the current edit proceeds
|
|
151
|
-
self._exit_with_summary(event, "accept")
|
|
152
|
-
else:
|
|
153
|
-
# Show summary then auto-exit
|
|
154
|
-
self._exit_with_summary(event, selected_value)
|
|
155
|
-
|
|
156
|
-
@bindings.add(Keys.Escape)
|
|
157
|
-
def cancel(event):
|
|
158
|
-
"""Cancel selection."""
|
|
159
|
-
self._exit_with_summary(event, "cancel")
|
|
160
|
-
|
|
161
|
-
return bindings
|
|
162
|
-
|
|
163
|
-
def _create_layout(self) -> Layout:
|
|
164
|
-
"""Create the panel layout.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
Layout object configured for the confirmation panel
|
|
168
|
-
"""
|
|
169
|
-
def get_content():
|
|
170
|
-
return self._get_display_text()
|
|
171
|
-
|
|
172
|
-
content_control = FormattedTextControl(get_content)
|
|
173
|
-
|
|
174
|
-
root_container = HSplit([
|
|
175
|
-
Window(content=content_control, height=None),
|
|
176
|
-
])
|
|
177
|
-
|
|
178
|
-
return Layout(root_container)
|
|
179
|
-
|
|
180
|
-
def _handle_result(self, result: str) -> Tuple[str, Optional[str]]:
|
|
181
|
-
"""Handle the result from the confirmation panel.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
result: The action selected by the user
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
Tuple of (action, guidance_text):
|
|
188
|
-
- action: "accept", "advise", "accept_all_edits", or "cancel"
|
|
189
|
-
- guidance_text: User's advice if action is "advise", None otherwise
|
|
190
|
-
"""
|
|
191
|
-
# Handle advise input separately
|
|
192
|
-
if result == "advise":
|
|
193
|
-
from prompt_toolkit import PromptSession
|
|
194
|
-
from prompt_toolkit.formatted_text import HTML
|
|
195
|
-
|
|
196
|
-
guidance_session = PromptSession()
|
|
197
|
-
guidance = guidance_session.prompt(HTML("<b>Enter your advice: </b>")).strip()
|
|
198
|
-
if not guidance:
|
|
199
|
-
# Empty advise treated as cancel
|
|
200
|
-
return ("cancel", None)
|
|
201
|
-
return ("advise", guidance)
|
|
202
|
-
|
|
203
|
-
return (result, None)
|
|
204
|
-
|
|
205
|
-
def run(self) -> Tuple[str, Optional[str]]:
|
|
206
|
-
"""Display the confirmation panel and wait for user input.
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
Tuple of (action, guidance_text):
|
|
210
|
-
- action: "accept", "advise", "accept_all_edits", or "cancel"
|
|
211
|
-
- guidance_text: User's advice if action is "advise", None otherwise
|
|
212
|
-
"""
|
|
213
|
-
# Create and run the application
|
|
214
|
-
bindings = self._create_key_bindings()
|
|
215
|
-
layout = self._create_layout()
|
|
216
|
-
|
|
217
|
-
application = Application(
|
|
218
|
-
layout=layout,
|
|
219
|
-
key_bindings=bindings,
|
|
220
|
-
full_screen=False,
|
|
221
|
-
mouse_support=False,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Use run_async with asyncio to properly await coroutines
|
|
225
|
-
result = asyncio.run(application.run_async())
|
|
226
|
-
return self._handle_result(result)
|
package/src/utils/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Utility modules for bone-agent."""
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""Citation parser for sub-agent results.
|
|
2
|
-
|
|
3
|
-
Parses bracketed citation patterns from sub-agent output and injects
|
|
4
|
-
the actual file contents. This module is the single source of truth
|
|
5
|
-
for the citation format contract between sub-agent and main agent.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import re
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import List, Optional
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class Citation:
|
|
16
|
-
"""A parsed file citation from sub-agent output."""
|
|
17
|
-
rel_path: str
|
|
18
|
-
start_line: int
|
|
19
|
-
end_line: Optional[int] = None # None means full file
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# Regex to find explicit citation patterns (bracketed notation only for safety)
|
|
23
|
-
CITATION_PATTERN = re.compile(
|
|
24
|
-
r"(?:-\s+\[(.*?)\]\s+\((?:lines\s+)?(\d+)-(\d+)(?:\s*lines)?|full)\)|"
|
|
25
|
-
r"(?:lines\s+(\d+)-(\d+)\s+in\s+\[(.*?)\])|"
|
|
26
|
-
r"(?:\[(.*?)\]:(\d+)-(\d+))|"
|
|
27
|
-
r"(?:\[(.*?)\]:(\d+))|"
|
|
28
|
-
r"(?:\[([^\]]*?/[^\]]*?)\](?![:(]))"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def parse_citations(text: str) -> List[Citation]:
|
|
33
|
-
"""Parse bracketed citation patterns from sub-agent output.
|
|
34
|
-
|
|
35
|
-
Supports these formats:
|
|
36
|
-
- [path/to/file] (lines N-M) or (full)
|
|
37
|
-
- lines N-M in [path/to/file]
|
|
38
|
-
- [path/to/file]:N-M
|
|
39
|
-
- [path/to/file]:N
|
|
40
|
-
- [path/to/file] (full file)
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
text: Sub-agent result text to parse
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
List of Citation instances found in the text
|
|
47
|
-
"""
|
|
48
|
-
citations = []
|
|
49
|
-
|
|
50
|
-
for line in text.split('\n'):
|
|
51
|
-
match = CITATION_PATTERN.search(line)
|
|
52
|
-
if not match:
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
if match.group(1):
|
|
56
|
-
# Pattern 1: - [file] (N-M) or (full)
|
|
57
|
-
rel_path = match.group(1).strip()
|
|
58
|
-
if match.group(2) and match.group(3):
|
|
59
|
-
start_line = int(match.group(2))
|
|
60
|
-
end_line = int(match.group(3))
|
|
61
|
-
else:
|
|
62
|
-
start_line = 1
|
|
63
|
-
end_line = None
|
|
64
|
-
elif match.group(4) and match.group(5) and match.group(6):
|
|
65
|
-
# Pattern 2: lines N-M in [file]
|
|
66
|
-
start_line = int(match.group(4))
|
|
67
|
-
end_line = int(match.group(5))
|
|
68
|
-
rel_path = match.group(6).strip()
|
|
69
|
-
elif match.group(7) and match.group(8) and match.group(9):
|
|
70
|
-
# Pattern 3: [file]:N-M
|
|
71
|
-
rel_path = match.group(7).strip()
|
|
72
|
-
start_line = int(match.group(8))
|
|
73
|
-
end_line = int(match.group(9))
|
|
74
|
-
elif match.group(10) and match.group(11):
|
|
75
|
-
# Pattern 4: [file]:N (single line)
|
|
76
|
-
rel_path = match.group(10).strip()
|
|
77
|
-
start_line = int(match.group(11))
|
|
78
|
-
end_line = start_line
|
|
79
|
-
elif match.group(12):
|
|
80
|
-
# Pattern 5: [file] (full file)
|
|
81
|
-
rel_path = match.group(12).strip()
|
|
82
|
-
start_line = 1
|
|
83
|
-
end_line = None
|
|
84
|
-
else:
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
citations.append(Citation(
|
|
88
|
-
rel_path=rel_path,
|
|
89
|
-
start_line=start_line,
|
|
90
|
-
end_line=end_line,
|
|
91
|
-
))
|
|
92
|
-
|
|
93
|
-
return citations
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _format_header(citation: Citation, lines_read: Optional[int], actual_start_line: Optional[int]) -> str:
|
|
97
|
-
"""Format a citation header string for injected content.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
citation: The Citation being formatted
|
|
101
|
-
lines_read: Actual number of lines read (from metadata)
|
|
102
|
-
actual_start_line: Actual start line (from metadata)
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
Formatted header string like "lines 45-78 (34 lines)"
|
|
106
|
-
"""
|
|
107
|
-
if lines_read is not None:
|
|
108
|
-
actual_start = actual_start_line or citation.start_line
|
|
109
|
-
if actual_start > 1:
|
|
110
|
-
end = actual_start + lines_read - 1
|
|
111
|
-
else:
|
|
112
|
-
end = lines_read
|
|
113
|
-
line_label = "line" if lines_read == 1 else "lines"
|
|
114
|
-
return f"lines {actual_start}-{end} ({lines_read} {line_label})"
|
|
115
|
-
return "full"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def inject_file_contents(
|
|
119
|
-
raw_result: str,
|
|
120
|
-
repo_root: Path,
|
|
121
|
-
gitignore_spec=None,
|
|
122
|
-
console=None,
|
|
123
|
-
) -> str:
|
|
124
|
-
"""Parse sub-agent result and inject actual file contents.
|
|
125
|
-
|
|
126
|
-
Extracts citations from the sub-agent output, reads the referenced
|
|
127
|
-
files, and appends the content in a structured format that the main
|
|
128
|
-
agent can use directly.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
raw_result: Sub-agent result text containing citations
|
|
132
|
-
repo_root: Repository root directory
|
|
133
|
-
gitignore_spec: PathSpec for .gitignore filtering
|
|
134
|
-
console: Rich console for output (unused, kept for API compat)
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
Combined string with original result + injected file contents,
|
|
138
|
-
or just the original result if no citations were found
|
|
139
|
-
"""
|
|
140
|
-
from tools.file_reader import read_file as read_file_with_bypass
|
|
141
|
-
from utils.result_parsers import extract_multiple_metadata
|
|
142
|
-
|
|
143
|
-
citations = parse_citations(raw_result)
|
|
144
|
-
if not citations:
|
|
145
|
-
return raw_result
|
|
146
|
-
|
|
147
|
-
injected_files_content = []
|
|
148
|
-
|
|
149
|
-
for citation in citations:
|
|
150
|
-
max_lines = None
|
|
151
|
-
if citation.end_line is not None:
|
|
152
|
-
max_lines = citation.end_line - citation.start_line + 1
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
tool_result = read_file_with_bypass(
|
|
156
|
-
citation.rel_path,
|
|
157
|
-
repo_root,
|
|
158
|
-
max_lines=max_lines,
|
|
159
|
-
start_line=citation.start_line,
|
|
160
|
-
gitignore_spec=gitignore_spec,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Check for exit code
|
|
164
|
-
first_line = tool_result.split('\n')[0] if tool_result else ""
|
|
165
|
-
if first_line.startswith("exit_code="):
|
|
166
|
-
exit_code = first_line.split("=")[1].split()[0]
|
|
167
|
-
if exit_code != "0":
|
|
168
|
-
injected_files_content.append(
|
|
169
|
-
f"### {citation.rel_path} (Blocked or unavailable)"
|
|
170
|
-
)
|
|
171
|
-
injected_files_content.append(tool_result.strip())
|
|
172
|
-
injected_files_content.append("")
|
|
173
|
-
continue
|
|
174
|
-
|
|
175
|
-
# Strip metadata line and extract content
|
|
176
|
-
content_lines = tool_result.splitlines()[1:] if isinstance(tool_result, str) else []
|
|
177
|
-
content = "\n".join(content_lines).rstrip()
|
|
178
|
-
|
|
179
|
-
# Parse actual lines_read and start_line from metadata
|
|
180
|
-
metadata = extract_multiple_metadata(tool_result, 'lines_read', 'start_line')
|
|
181
|
-
lines_read = metadata.get('lines_read')
|
|
182
|
-
actual_start = metadata.get('start_line')
|
|
183
|
-
|
|
184
|
-
header_info = _format_header(citation, lines_read, actual_start)
|
|
185
|
-
|
|
186
|
-
injected_files_content.append(f"### {citation.rel_path} ({header_info})")
|
|
187
|
-
injected_files_content.append("```")
|
|
188
|
-
injected_files_content.append(content)
|
|
189
|
-
injected_files_content.append("```\n")
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
injected_files_content.append(
|
|
193
|
-
f"### {citation.rel_path} (Error reading file: {e})"
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
if not injected_files_content:
|
|
197
|
-
return raw_result
|
|
198
|
-
|
|
199
|
-
return raw_result + "\n\n## Injected File Contents\n\n" + "\n".join(injected_files_content)
|
package/src/utils/editor.py
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""External editor integration for bone-agent."""
|
|
2
|
-
import os
|
|
3
|
-
import platform
|
|
4
|
-
import shlex
|
|
5
|
-
import subprocess
|
|
6
|
-
import tempfile
|
|
7
|
-
import shutil
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Tuple, Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def get_editor() -> str:
|
|
13
|
-
"""Get editor command with OS-specific defaults.
|
|
14
|
-
|
|
15
|
-
Priority:
|
|
16
|
-
1. Windows: notepad.exe
|
|
17
|
-
2. Linux: nvim, then EDITOR, then nano/vi/vim
|
|
18
|
-
3. Other Unix: EDITOR, then nano/vi/vim
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
str: Editor command or path
|
|
22
|
-
"""
|
|
23
|
-
system = platform.system()
|
|
24
|
-
|
|
25
|
-
if system == "Windows":
|
|
26
|
-
return "notepad.exe"
|
|
27
|
-
|
|
28
|
-
if system == "Linux" and shutil.which("nvim"):
|
|
29
|
-
return "nvim"
|
|
30
|
-
|
|
31
|
-
editor = os.environ.get("EDITOR")
|
|
32
|
-
if editor and editor.strip():
|
|
33
|
-
return editor.strip()
|
|
34
|
-
|
|
35
|
-
for cmd in ["nano", "vi", "vim"]:
|
|
36
|
-
if shutil.which(cmd):
|
|
37
|
-
return cmd
|
|
38
|
-
|
|
39
|
-
return "nano"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _create_temp_file() -> Tuple[Path, object]:
|
|
43
|
-
"""Create temporary file for editing.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
tuple: (Path object, file handle)
|
|
47
|
-
"""
|
|
48
|
-
# Create with .md extension for better syntax highlighting
|
|
49
|
-
temp_fd = tempfile.NamedTemporaryFile(
|
|
50
|
-
mode='w+',
|
|
51
|
-
suffix='.md',
|
|
52
|
-
prefix='bone_agent_edit_',
|
|
53
|
-
delete=False,
|
|
54
|
-
encoding='utf-8'
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
temp_fd.flush()
|
|
58
|
-
|
|
59
|
-
return Path(temp_fd.name), temp_fd
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _strip_comment_lines(content: str) -> str:
|
|
63
|
-
"""Remove comment lines (starting with #) from content.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
content: Raw content from editor
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
str: Content with comment lines removed
|
|
70
|
-
"""
|
|
71
|
-
lines = []
|
|
72
|
-
for line in content.split('\n'):
|
|
73
|
-
stripped = line.strip()
|
|
74
|
-
# Keep empty lines and non-comment lines
|
|
75
|
-
if not stripped.startswith('#'):
|
|
76
|
-
lines.append(line)
|
|
77
|
-
|
|
78
|
-
return '\n'.join(lines).strip()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _build_editor_command(editor_cmd: str, temp_path: Path) -> Tuple[bool, str | list[str]]:
|
|
82
|
-
"""Build a subprocess command for launching the configured editor."""
|
|
83
|
-
use_shell = platform.system() == "Windows"
|
|
84
|
-
if use_shell:
|
|
85
|
-
return use_shell, f'{editor_cmd} "{temp_path}"'
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
command_parts = shlex.split(editor_cmd)
|
|
89
|
-
except ValueError as e:
|
|
90
|
-
raise ValueError(f"Invalid EDITOR command: {e}") from e
|
|
91
|
-
|
|
92
|
-
if not command_parts:
|
|
93
|
-
raise FileNotFoundError(editor_cmd)
|
|
94
|
-
|
|
95
|
-
return use_shell, [*command_parts, str(temp_path)]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _open_editor_with_temp_file(
|
|
99
|
-
console,
|
|
100
|
-
editor_cmd: str,
|
|
101
|
-
debug_mode: bool = False,
|
|
102
|
-
initial_content: str = "",
|
|
103
|
-
post_process=None,
|
|
104
|
-
) -> Tuple[bool, Optional[str]]:
|
|
105
|
-
"""Open the configured editor for a temporary file and return the saved content."""
|
|
106
|
-
temp_path = None
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
temp_path, temp_fd = _create_temp_file()
|
|
110
|
-
if initial_content:
|
|
111
|
-
temp_fd.write(initial_content)
|
|
112
|
-
temp_fd.flush()
|
|
113
|
-
temp_fd.close()
|
|
114
|
-
|
|
115
|
-
if debug_mode:
|
|
116
|
-
console.print(f"[dim]Temp file: {temp_path}[/dim]")
|
|
117
|
-
console.print(f"[dim]Editor: {editor_cmd}[/dim]")
|
|
118
|
-
|
|
119
|
-
console.print("[#5F9EA0]Opening editor...[/#5F9EA0]")
|
|
120
|
-
console.print("[dim]Save and close the editor when done[/dim]")
|
|
121
|
-
|
|
122
|
-
use_shell, command = _build_editor_command(editor_cmd, temp_path)
|
|
123
|
-
result = subprocess.run(
|
|
124
|
-
command,
|
|
125
|
-
shell=use_shell,
|
|
126
|
-
check=False,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
if result.returncode != 0 and debug_mode:
|
|
130
|
-
console.print(f"[yellow]Editor exited with code {result.returncode}[/yellow]")
|
|
131
|
-
|
|
132
|
-
content = temp_path.read_text(encoding="utf-8")
|
|
133
|
-
if post_process is not None:
|
|
134
|
-
content = post_process(content)
|
|
135
|
-
|
|
136
|
-
if debug_mode:
|
|
137
|
-
console.print(f"[dim]Read {len(content)} characters[/dim]")
|
|
138
|
-
|
|
139
|
-
return (True, content)
|
|
140
|
-
|
|
141
|
-
except FileNotFoundError:
|
|
142
|
-
console.print(f"[red]Editor '{editor_cmd}' not found[/red]", markup=False)
|
|
143
|
-
console.print("[dim]Set EDITOR as environment variable[/dim]")
|
|
144
|
-
if debug_mode:
|
|
145
|
-
console.print(f"[dim]Tried to run: {editor_cmd}[/dim]")
|
|
146
|
-
return (False, None)
|
|
147
|
-
|
|
148
|
-
except PermissionError as e:
|
|
149
|
-
console.print(f"[red]Permission denied: {e}[/red]", markup=False)
|
|
150
|
-
console.print("[dim]Check permissions on temporary directory[/dim]")
|
|
151
|
-
return (False, None)
|
|
152
|
-
|
|
153
|
-
except Exception as e:
|
|
154
|
-
console.print(f"[red]Failed to open editor: {e}[/red]", markup=False)
|
|
155
|
-
if debug_mode:
|
|
156
|
-
import traceback
|
|
157
|
-
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
158
|
-
return (False, None)
|
|
159
|
-
|
|
160
|
-
finally:
|
|
161
|
-
if temp_path and temp_path.exists():
|
|
162
|
-
try:
|
|
163
|
-
temp_path.unlink()
|
|
164
|
-
if debug_mode:
|
|
165
|
-
console.print("[dim]Cleaned up temp file[/dim]")
|
|
166
|
-
except Exception as e:
|
|
167
|
-
console.print(f"[yellow]Warning: Failed to delete temp file: {e}[/yellow]")
|
|
168
|
-
if debug_mode:
|
|
169
|
-
console.print(f"[dim]Temp file may remain at: {temp_path}[/dim]")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def open_editor_for_input(console, debug_mode: bool = False) -> Tuple[bool, Optional[str]]:
|
|
173
|
-
"""Open external editor and return user input.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
console: Rich console for output
|
|
177
|
-
debug_mode: Whether to show debug information
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
tuple: (success: bool, content: str or None)
|
|
181
|
-
- (True, content) if successful
|
|
182
|
-
- (False, None) if failed or cancelled
|
|
183
|
-
"""
|
|
184
|
-
return _open_editor_with_temp_file(
|
|
185
|
-
console,
|
|
186
|
-
editor_cmd=get_editor(),
|
|
187
|
-
debug_mode=debug_mode,
|
|
188
|
-
post_process=_strip_comment_lines,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def open_editor_for_content(
|
|
193
|
-
console,
|
|
194
|
-
initial_content: str = "",
|
|
195
|
-
debug_mode: bool = False,
|
|
196
|
-
) -> Tuple[bool, Optional[str]]:
|
|
197
|
-
"""Open external editor with initial content and return the saved file.
|
|
198
|
-
|
|
199
|
-
Unlike open_editor_for_input, this preserves markdown comment lines so it can
|
|
200
|
-
edit existing markdown files without dropping headings or notes.
|
|
201
|
-
"""
|
|
202
|
-
return _open_editor_with_temp_file(
|
|
203
|
-
console,
|
|
204
|
-
editor_cmd=get_editor(),
|
|
205
|
-
debug_mode=debug_mode,
|
|
206
|
-
initial_content=initial_content or "",
|
|
207
|
-
)
|