bone-agent 1.3.3 → 1.4.0
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/README.md +17 -0
- package/config.yaml.example +5 -2
- package/package.json +1 -1
- package/prompts/main/communication_style.md +1 -1
- package/prompts/main/dream.md +23 -9
- package/prompts/main/skills.md +3 -0
- package/prompts/micro/communication_style.md +1 -1
- package/prompts/micro/skills.md +1 -0
- package/src/core/agentic.py +138 -38
- package/src/core/chat_manager.py +19 -6
- package/src/core/config_manager.py +8 -1
- package/src/core/cron.py +0 -4
- package/src/core/metadata.py +75 -0
- package/src/core/skills.py +463 -0
- package/src/core/sub_agent.py +93 -43
- package/src/core/tool_feedback.py +87 -76
- package/src/llm/client.py +7 -2
- package/src/llm/codex_provider.py +350 -0
- package/src/llm/config.py +46 -2
- package/src/llm/prompts.py +12 -7
- package/src/llm/providers.py +3 -1
- package/src/llm/token_tracker.py +15 -0
- package/src/tools/__init__.py +24 -85
- package/src/tools/create_file.py +1 -1
- package/src/tools/directory.py +1 -1
- package/src/tools/edit.py +5 -1
- package/src/tools/file_reader.py +1 -1
- package/src/tools/helpers/__init__.py +1 -7
- package/src/tools/helpers/base.py +65 -16
- package/src/tools/helpers/loader.py +2 -88
- package/src/tools/helpers/path_resolver.py +54 -3
- package/src/tools/helpers/plugin_manifest.py +99 -70
- package/src/tools/review_sub_agent.py +2 -1
- package/src/tools/rg_search.py +24 -7
- package/src/tools/search_plugins.py +140 -72
- package/src/tools/shell.py +3 -3
- package/src/ui/commands.py +355 -33
- package/src/ui/displays.py +26 -1
- package/src/ui/main.py +0 -4
- package/src/ui/tool_confirmation.py +16 -5
- package/src/utils/editor.py +88 -39
- package/src/utils/settings.py +6 -2
- package/src/utils/validation.py +10 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Interactive tool confirmation panel with arrow key navigation."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from html import escape
|
|
4
5
|
from threading import Timer
|
|
5
6
|
from typing import Optional, Tuple
|
|
6
7
|
from prompt_toolkit import HTML
|
|
@@ -50,6 +51,16 @@ class ToolConfirmationPanel:
|
|
|
50
51
|
# Use appropriate options based on tool type
|
|
51
52
|
self._options = self.EDIT_OPTIONS if is_edit_tool else self.STANDARD_OPTIONS
|
|
52
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
|
+
|
|
53
64
|
def _get_display_text(self) -> HTML:
|
|
54
65
|
"""Get the formatted text to display.
|
|
55
66
|
|
|
@@ -66,14 +77,14 @@ class ToolConfirmationPanel:
|
|
|
66
77
|
|
|
67
78
|
lines.append("<b>Selection Summary</b>")
|
|
68
79
|
lines.append("")
|
|
69
|
-
|
|
70
|
-
lines.append(f'<style fg="gray"> Selected: {str(selected_text)}</style>')
|
|
80
|
+
self._append_field(lines, "Tool", self.tool_command)
|
|
81
|
+
lines.append(f'<style fg="gray"> Selected: {escape(str(selected_text))}</style>')
|
|
71
82
|
lines.append("")
|
|
72
83
|
else:
|
|
73
84
|
# Tool information
|
|
74
|
-
|
|
85
|
+
self._append_field(lines, "Tool", self.tool_command)
|
|
75
86
|
if self.reason:
|
|
76
|
-
|
|
87
|
+
self._append_field(lines, "Reason", self.reason)
|
|
77
88
|
lines.append("")
|
|
78
89
|
|
|
79
90
|
# Render options
|
|
@@ -87,7 +98,7 @@ class ToolConfirmationPanel:
|
|
|
87
98
|
# Unselected option - dark grey
|
|
88
99
|
lines.append(f'<style fg="gray"> {text}</style>')
|
|
89
100
|
|
|
90
|
-
|
|
101
|
+
return HTML("\n".join(lines))
|
|
91
102
|
|
|
92
103
|
def _exit_with_summary(self, event, result: str) -> None:
|
|
93
104
|
"""Show summary screen and exit application after delay.
|
package/src/utils/editor.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""External editor integration for bone-agent."""
|
|
2
2
|
import os
|
|
3
3
|
import platform
|
|
4
|
+
import shlex
|
|
4
5
|
import subprocess
|
|
5
6
|
import tempfile
|
|
6
7
|
import shutil
|
|
@@ -12,27 +13,30 @@ def get_editor() -> str:
|
|
|
12
13
|
"""Get editor command with OS-specific defaults.
|
|
13
14
|
|
|
14
15
|
Priority:
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
16
|
+
1. Windows: notepad.exe
|
|
17
|
+
2. Linux: nvim, then EDITOR, then nano/vi/vim
|
|
18
|
+
3. Other Unix: EDITOR, then nano/vi/vim
|
|
17
19
|
|
|
18
20
|
Returns:
|
|
19
21
|
str: Editor command or path
|
|
20
22
|
"""
|
|
21
|
-
|
|
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
|
+
|
|
22
31
|
editor = os.environ.get("EDITOR")
|
|
23
32
|
if editor and editor.strip():
|
|
24
33
|
return editor.strip()
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for cmd in ["nvim", "nano", "vi", "vim"]:
|
|
32
|
-
if shutil.which(cmd):
|
|
33
|
-
return cmd
|
|
34
|
-
# Fallback to nano even if not found (will error later)
|
|
35
|
-
return "nano"
|
|
35
|
+
for cmd in ["nano", "vi", "vim"]:
|
|
36
|
+
if shutil.which(cmd):
|
|
37
|
+
return cmd
|
|
38
|
+
|
|
39
|
+
return "nano"
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def _create_temp_file() -> Tuple[Path, object]:
|
|
@@ -74,24 +78,38 @@ def _strip_comment_lines(content: str) -> str:
|
|
|
74
78
|
return '\n'.join(lines).strip()
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
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}"'
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
try:
|
|
88
|
+
command_parts = shlex.split(editor_cmd)
|
|
89
|
+
except ValueError as e:
|
|
90
|
+
raise ValueError(f"Invalid EDITOR command: {e}") from e
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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."""
|
|
90
106
|
temp_path = None
|
|
91
107
|
|
|
92
108
|
try:
|
|
93
|
-
# Create temporary file
|
|
94
109
|
temp_path, temp_fd = _create_temp_file()
|
|
110
|
+
if initial_content:
|
|
111
|
+
temp_fd.write(initial_content)
|
|
112
|
+
temp_fd.flush()
|
|
95
113
|
temp_fd.close()
|
|
96
114
|
|
|
97
115
|
if debug_mode:
|
|
@@ -101,24 +119,19 @@ def open_editor_for_input(console, debug_mode: bool = False) -> Tuple[bool, Opti
|
|
|
101
119
|
console.print("[#5F9EA0]Opening editor...[/#5F9EA0]")
|
|
102
120
|
console.print("[dim]Save and close the editor when done[/dim]")
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
# Use shell=True on Windows for notepad, False for better security on Unix
|
|
106
|
-
use_shell = platform.system() == "Windows"
|
|
107
|
-
|
|
122
|
+
use_shell, command = _build_editor_command(editor_cmd, temp_path)
|
|
108
123
|
result = subprocess.run(
|
|
109
|
-
|
|
124
|
+
command,
|
|
110
125
|
shell=use_shell,
|
|
111
|
-
check=False
|
|
126
|
+
check=False,
|
|
112
127
|
)
|
|
113
128
|
|
|
114
129
|
if result.returncode != 0 and debug_mode:
|
|
115
130
|
console.print(f"[yellow]Editor exited with code {result.returncode}[/yellow]")
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# Strip comment lines
|
|
121
|
-
content = _strip_comment_lines(content)
|
|
132
|
+
content = temp_path.read_text(encoding="utf-8")
|
|
133
|
+
if post_process is not None:
|
|
134
|
+
content = post_process(content)
|
|
122
135
|
|
|
123
136
|
if debug_mode:
|
|
124
137
|
console.print(f"[dim]Read {len(content)} characters[/dim]")
|
|
@@ -145,14 +158,50 @@ def open_editor_for_input(console, debug_mode: bool = False) -> Tuple[bool, Opti
|
|
|
145
158
|
return (False, None)
|
|
146
159
|
|
|
147
160
|
finally:
|
|
148
|
-
# Cleanup temp file
|
|
149
161
|
if temp_path and temp_path.exists():
|
|
150
162
|
try:
|
|
151
163
|
temp_path.unlink()
|
|
152
164
|
if debug_mode:
|
|
153
|
-
console.print(
|
|
165
|
+
console.print("[dim]Cleaned up temp file[/dim]")
|
|
154
166
|
except Exception as e:
|
|
155
|
-
# Log but don't crash on cleanup failure
|
|
156
167
|
console.print(f"[yellow]Warning: Failed to delete temp file: {e}[/yellow]")
|
|
157
168
|
if debug_mode:
|
|
158
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
|
+
)
|
package/src/utils/settings.py
CHANGED
|
@@ -51,6 +51,7 @@ class ToolSettings:
|
|
|
51
51
|
max_shell_output_lines: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_shell_output_lines", 200))
|
|
52
52
|
max_file_preview_lines: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_file_preview_lines", 200))
|
|
53
53
|
disabled_tools: list = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("disabled_tools", []))
|
|
54
|
+
hidden_skills: list = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("hidden_skills", []))
|
|
54
55
|
|
|
55
56
|
@dataclass
|
|
56
57
|
class FileSettings:
|
|
@@ -80,11 +81,14 @@ class ToolCompactionSettings:
|
|
|
80
81
|
@dataclass
|
|
81
82
|
class SubAgentSettings:
|
|
82
83
|
"""Sub-agent token limits and behavior configuration."""
|
|
83
|
-
soft_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("soft_limit_tokens",
|
|
84
|
-
hard_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("hard_limit_tokens",
|
|
84
|
+
soft_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("soft_limit_tokens", 100_000))
|
|
85
|
+
hard_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("hard_limit_tokens", 150_000))
|
|
86
|
+
billed_warning_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("billed_warning_tokens", 200_000))
|
|
87
|
+
billed_hard_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("billed_hard_limit_tokens", 500_000))
|
|
85
88
|
enable_compaction: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("enable_compaction", True))
|
|
86
89
|
compact_trigger_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("compact_trigger_tokens", 50_000))
|
|
87
90
|
allowed_tools: list = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("allowed_tools", ["rg", "read_file", "list_directory", "web_search"]))
|
|
91
|
+
allow_active_plugins: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("allow_active_plugins", False))
|
|
88
92
|
dump_context_on_hard_limit: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("dump_context_on_hard_limit", True))
|
|
89
93
|
|
|
90
94
|
|
package/src/utils/validation.py
CHANGED
|
@@ -167,6 +167,16 @@ def check_command(command):
|
|
|
167
167
|
if command.lower().startswith("powershell"):
|
|
168
168
|
return False, "nested powershell invocation"
|
|
169
169
|
|
|
170
|
+
# Multi-line shell scripts/heredocs are valid command payloads.
|
|
171
|
+
# Do not tokenize the full script here: tokenization can reject valid shell
|
|
172
|
+
# syntax before the shell sees it. Safety/approval checks happen upstream.
|
|
173
|
+
if "\n" in command:
|
|
174
|
+
for line in command.splitlines():
|
|
175
|
+
line = line.strip()
|
|
176
|
+
if line and line.lower().startswith("powershell"):
|
|
177
|
+
return False, "nested powershell invocation"
|
|
178
|
+
return True, None
|
|
179
|
+
|
|
170
180
|
# Basic validation - ensure command has content
|
|
171
181
|
tokens = _tokenize_segment(command)
|
|
172
182
|
if not tokens:
|