bone-agent 1.4.0 → 2.0.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.
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,302 +0,0 @@
1
- """Shell command execution tool with core command execution logic and @tool decorator."""
2
-
3
- import subprocess
4
- import shlex
5
- import os
6
- from pathlib import Path
7
- from typing import Optional
8
- from rich.panel import Panel
9
- from llm.config import TOOLS_REQUIRE_CONFIRMATION
10
- from utils.settings import tool_settings
11
- from exceptions import CommandExecutionError
12
- from utils.validation import CHAINING_OPERATORS
13
-
14
- from .helpers.base import tool
15
- from .helpers.formatters import format_tool_result
16
-
17
-
18
- def normalize_command(command, rg_exe_path):
19
- """Parse command and return (executable, args_list, needs_shell).
20
-
21
- Returns:
22
- tuple: (executable_path, args_list, needs_shell)
23
- - executable_path: Path object for rg.exe, or None for shell commands
24
- - args_list: List of arguments for direct execution, or command string for shell
25
- - needs_shell: Boolean indicating if command should run through shell
26
- """
27
- command = command.strip()
28
-
29
- # Handle rg commands
30
- if command.startswith("rg ") or command == "rg":
31
- if command == "rg":
32
- return rg_exe_path, [], False
33
-
34
- args_str = command[3:].strip() # Everything after "rg "
35
- # Parse only the arguments, not the full command string.
36
- # On Windows, posix=False preserves backslashes in paths.
37
- use_posix = os.name != "nt"
38
- args = shlex.split(args_str, posix=use_posix) if args_str else []
39
- args = [arg[1:-1] if len(arg) >= 2 and arg[0] == arg[-1] and arg[0] in ("'", '"') else arg for arg in args]
40
- return rg_exe_path, args, False
41
-
42
- # Other commands go through shell
43
- return None, command, True
44
-
45
-
46
- def confirm_tool(command, console, reason=None, requires_approval=True, approve_mode="safe", use_panel=True, is_edit_tool=False, cycle_approve_mode=None):
47
- """Prompt user for tool execution confirmation.
48
-
49
- Args:
50
- command: Command to execute
51
- console: Rich console for output (currently unused, kept for compatibility)
52
- reason: Optional reason for requiring confirmation
53
- requires_approval: Whether this command specifically requires approval (overrides global flag when True)
54
- approve_mode: Approval mode setting - "safe" requires confirmation, "accept_edits" auto-approves edits, "danger" auto-approves all
55
- use_panel: When True, display interactive confirmation panel. When False, auto-cancel without UI (default: True)
56
- is_edit_tool: Whether this is an edit tool (shows extra toggle option in panel)
57
- cycle_approve_mode: Optional callback to cycle approve_mode
58
-
59
- Returns:
60
- tuple: (action, guidance_text) where action is "accept", "advise", or "cancel"
61
- and guidance_text contains the user's input when action is "advise"
62
- """
63
- # Skip confirmation only if: global flag is off AND command doesn't require approval
64
- if not TOOLS_REQUIRE_CONFIRMATION and not requires_approval:
65
- return ("accept", None)
66
-
67
- # In danger mode, auto-approve everything except unsafe commands.
68
- # Safe commands (per SAFE_COMMAND_RULES) auto-approve.
69
- # Unsafe commands (e.g., git push) still require confirmation.
70
- if approve_mode == "danger":
71
- from utils.safe_commands import is_safe_command
72
- if is_safe_command(command):
73
- return ("accept", None)
74
- # Auto-accept all edit operations in danger mode
75
- if is_edit_tool or (requires_approval and "edit_file" in command):
76
- return ("accept", None)
77
- # Fall through to normal confirmation for unsafe commands
78
-
79
- # Skip confirmation for edit operations in accept_edits mode
80
- # This only applies to file edits, not execute_command
81
- if approve_mode == "accept_edits" and requires_approval and "edit_file" in command:
82
- return ("accept", None)
83
-
84
- # Handle case where console is None (e.g., parallel execution)
85
- if console is None:
86
- # Cancel by default when console is unavailable
87
- return ("cancel", None)
88
-
89
- # Try to use interactive panel
90
- if use_panel:
91
- from ui.tool_confirmation import ToolConfirmationPanel
92
- panel = ToolConfirmationPanel(command, reason, is_edit_tool=is_edit_tool, cycle_approve_mode=cycle_approve_mode)
93
- return panel.run()
94
-
95
- # Cancel by default if panel is disabled or fails
96
- return ("cancel", None)
97
-
98
-
99
- def _prepare_execution_environment(repo_root, rg_exe_path):
100
- """Prepare environment variables for command execution.
101
-
102
- Returns:
103
- dict: Environment variables with updated PATH
104
- """
105
- env = os.environ.copy()
106
- rg_parent = Path(rg_exe_path).parent if rg_exe_path else None
107
-
108
- if rg_parent and rg_parent.exists():
109
- bin_path = str(rg_parent)
110
- else:
111
- bin_path = str(repo_root / "bin")
112
-
113
- env["PATH"] = f"{bin_path}{os.pathsep}{env.get('PATH', '')}"
114
- return env
115
-
116
-
117
- def _execute_direct_command(cmd_list, repo_root, env, debug_mode, console):
118
- """Execute command directly (rg.exe) without PowerShell.
119
-
120
- Returns:
121
- subprocess.CompletedProcess
122
- """
123
- if debug_mode and console:
124
- console.print(f"[dim]→ Executing: {cmd_list}[/dim]")
125
- console.print(f"[dim]→ Working dir: {repo_root}[/dim]")
126
-
127
- result = subprocess.run(
128
- cmd_list,
129
- capture_output=True,
130
- text=True,
131
- encoding='utf-8',
132
- errors='replace',
133
- timeout=tool_settings.command_timeout_sec,
134
- cwd=str(repo_root),
135
- env=env,
136
- )
137
-
138
- if debug_mode and console:
139
- console.print(f"[dim]→ Exit code: {result.returncode}[/dim]")
140
-
141
- return result
142
-
143
-
144
- def _execute_shell_command(command, repo_root, env, debug_mode, console):
145
- """Execute command via shell (PowerShell on Windows, /bin/sh on Unix/Linux).
146
-
147
- Returns:
148
- subprocess.CompletedProcess
149
- """
150
- # Detect platform and use appropriate shell
151
- is_windows = os.name == 'nt'
152
-
153
- if is_windows:
154
- shell_cmd = ["powershell", "-NoProfile", "-NonInteractive", "-Command", str(command)]
155
- shell_name = "PowerShell"
156
- else:
157
- shell_cmd = ["/bin/sh", "-c", str(command)]
158
- shell_name = "/bin/sh"
159
-
160
- if debug_mode and console:
161
- console.print(f"[dim]→ Executing via {shell_name}: {command}[/dim]")
162
- console.print(f"[dim]→ Working dir: {repo_root}[/dim]")
163
-
164
- result = subprocess.run(
165
- shell_cmd,
166
- capture_output=True,
167
- text=True,
168
- encoding='utf-8',
169
- errors='replace',
170
- timeout=tool_settings.command_timeout_sec,
171
- cwd=str(repo_root),
172
- env=env,
173
- )
174
-
175
- if debug_mode and console:
176
- console.print(f"[dim]→ Exit code: {result.returncode}[/dim]")
177
-
178
- return result
179
-
180
-
181
- def run_shell_command(command, repo_root, rg_exe_path, console, debug_mode, gitignore_spec=None, max_matches=None):
182
- """Execute command via rg (direct) or shell (PowerShell on Windows, /bin/sh on Unix/Linux).
183
-
184
- Args:
185
- command: Command string to execute
186
- repo_root: Path to repository root
187
- rg_exe_path: Path to rg.exe
188
- console: Rich console for output
189
- debug_mode: Whether to show debug output
190
-
191
- Returns:
192
- str: Formatted tool result
193
-
194
- Raises:
195
- CommandExecutionError: If command execution fails
196
- """
197
- try:
198
- env = _prepare_execution_environment(repo_root, rg_exe_path)
199
- executable, args, needs_shell = normalize_command(command, rg_exe_path)
200
-
201
- if not needs_shell:
202
- # Direct execution (rg)
203
- cmd_list = [str(executable)] + args
204
- result = _execute_direct_command(cmd_list, repo_root, env, debug_mode, console)
205
- # AI gets truncated results (via format_tool_result); user sees summary via _display_tool_feedback
206
- formatted_result = format_tool_result(result, command=command, is_rg=True, debug_mode=True, max_matches=max_matches)
207
- else:
208
- # Shell execution (PowerShell on Windows, /bin/sh on Unix/Linux)
209
- result = _execute_shell_command(args, repo_root, env, debug_mode, console)
210
- # AI gets full results; user sees summary via _display_tool_feedback
211
- formatted_result = format_tool_result(result, command=command, debug_mode=True)
212
-
213
- if debug_mode and console:
214
- console.print()
215
- console.print(f"[dim]→ AI receives:\n{formatted_result}[/dim]")
216
-
217
- return formatted_result
218
- except CommandExecutionError:
219
- # Re-raise our custom exceptions
220
- raise
221
- except Exception as exc:
222
- raise CommandExecutionError(
223
- f"Command execution failed",
224
- details={"command": command, "original_error": str(exc)}
225
- )
226
-
227
-
228
- # =============================================================================
229
- # @tool decorated function
230
- # =============================================================================
231
-
232
- @tool(
233
- name="execute_command",
234
- description="Execute shell commands for git, system tasks, file ops, network, and package management. Runs from repo root. Multi-line shell commands are supported and preserved exactly, including heredocs. Use for git, ps, systemctl, rm, mv, cp, mkdir, ping, curl, wget, ssh, pacman, pip, npm, apt. Disallowed: rg, cat, ls, grep, find, head, tail, sed, awk, sort, uniq, wc, echo, touch, get-content, type, get-childitem, dir, new-item, set-content, add-content, tee. Use native tools instead.",
235
- parameters={
236
- "type": "object",
237
- "properties": {
238
- "command": {
239
- "type": "string",
240
- "description": "Shell command to execute from the repo root. May be a single-line command or a multi-line shell script/heredoc; newlines are preserved exactly."
241
- },
242
- "reason": {
243
- "type": "string",
244
- "description": "Brief explanation (shown during confirmation)"
245
- }
246
- },
247
- "required": ["command"]
248
- },
249
- requires_approval=True,
250
- terminal_policy="stop"
251
- )
252
- def execute_command(
253
- command: str,
254
- repo_root: Path,
255
- rg_exe_path: str,
256
- console,
257
- chat_manager,
258
- debug_mode: bool = False,
259
- gitignore_spec = None,
260
- reason: str = None
261
- ) -> str:
262
- """Execute a shell command.
263
-
264
- Args:
265
- command: Command string to execute. May contain newlines/heredocs; preserved exactly for shell execution.
266
- repo_root: Repository root directory (injected by context)
267
- rg_exe_path: Path to rg executable (injected by context)
268
- console: Rich console for output (injected by context)
269
- chat_manager: ChatManager instance (injected by context)
270
- debug_mode: Whether debug mode is enabled (injected by context)
271
- gitignore_spec: PathSpec for .gitignore filtering (injected by context)
272
-
273
- Returns:
274
- Command output with exit code
275
- """
276
- # Import validation functions here to avoid circular dependency
277
- from utils.validation import check_command, check_for_silent_blocked_command
278
-
279
- if not isinstance(command, str) or not command.strip():
280
- return "exit_code=1\nerror: 'command' argument must be a non-empty string."
281
-
282
- # Check for commands that should use native tools instead (silent blocking)
283
- is_blocked, reprompt_msg = check_for_silent_blocked_command(command)
284
- if is_blocked:
285
- # Return reprompt message to guide the AI to use the native tool
286
- # This is not shown to the user - the AI sees it and can retry
287
- if debug_mode and console:
288
- console.print(f"[dim]Silently blocked command: {command.split()[0]}[/dim]")
289
- return f"exit_code=1\n{reprompt_msg}"
290
-
291
- # Validate command
292
- is_safe, reason = check_command(command)
293
- if not is_safe:
294
- return reason
295
-
296
- # Execute command (approval workflow handled by orchestrator)
297
- try:
298
- return run_shell_command(
299
- command, repo_root, rg_exe_path, console, debug_mode, gitignore_spec
300
- )
301
- except Exception as e:
302
- return f"exit_code=1\nCommand execution failed: {str(e)}"
@@ -1,139 +0,0 @@
1
- """Sub-agent tool for complex multi-file exploration."""
2
-
3
- from pathlib import Path
4
-
5
- from .helpers.base import tool
6
- from core.sub_agent import run_sub_agent
7
- from utils.citation_parser import inject_file_contents
8
-
9
-
10
- class SimplePanelUpdater:
11
- """Simple panel updater for non-parallel tool execution.
12
-
13
- This is a fallback implementation used when panel_updater is None,
14
- typically in sequential mode where live updates aren't needed.
15
- """
16
-
17
- def __init__(self, console):
18
- """Initialize the simple panel updater.
19
-
20
- Args:
21
- console: Rich console for output
22
- """
23
- self.console = console
24
- self.total_tool_calls = 0
25
-
26
- def __enter__(self):
27
- """Enter context manager."""
28
- return self
29
-
30
- def __exit__(self, *args):
31
- """Exit context manager."""
32
- pass
33
-
34
- def append(self, text):
35
- """Append text to panel (no-op in simple mode)."""
36
- pass # No live updates in sequential mode
37
-
38
- def add_tool_call(self, tool_name, tool_result=None, command=None):
39
- """Track a tool call."""
40
- self.total_tool_calls += 1
41
-
42
- def set_complete(self, usage=None):
43
- """Mark panel as complete."""
44
- pass
45
-
46
- def set_error(self, message):
47
- """Display error message."""
48
- self.console.print(f"[red]Sub-Agent Error: {message}[/red]")
49
-
50
-
51
- @tool(
52
- name="sub_agent",
53
- description="Required: Call this first before any rg or read_file when answering 'how something works', architecture, patterns, multi-file flows, or broad exploration. Do not search manually — this tool is 10x faster. Examples: 'How does authentication work?', 'Explain the data flow', 'Where is X handled?'",
54
- parameters={
55
- "type": "object",
56
- "properties": {
57
- "query": {
58
- "type": "string",
59
- "description": "Task query, e.g. 'How does the chat manager handle history?'"
60
- }
61
- },
62
- "required": ["query"]
63
- },
64
- requires_approval=False,
65
- terminal_policy="yield"
66
- )
67
- def sub_agent(
68
- query: str,
69
- repo_root: Path,
70
- rg_exe_path: str,
71
- console,
72
- chat_manager,
73
- gitignore_spec = None,
74
- panel_updater = None
75
- ) -> str:
76
- """Run sub-agent for complex multi-file exploration.
77
-
78
- Args:
79
- query: Task query for the sub-agent
80
- repo_root: Repository root directory (injected by context)
81
- rg_exe_path: Path to rg executable (injected by context)
82
- console: Rich console for output (injected by context)
83
- chat_manager: ChatManager instance (injected by context)
84
- gitignore_spec: PathSpec for .gitignore filtering (injected by context)
85
- panel_updater: Optional SubAgentPanel for live updates (injected by context)
86
-
87
- Returns:
88
- Sub-agent result with injected file contents
89
- """
90
- if not query or not isinstance(query, str) or not query.strip():
91
- return "exit_code=1\nsub_agent requires a non-empty 'query' argument."
92
-
93
- # Import SimplePanelUpdater if not provided
94
- if panel_updater is None:
95
- # If running in sequential mode, create a simple panel updater
96
- panel_updater = SimplePanelUpdater(console)
97
-
98
- # Use panel for streaming tool output
99
- with panel_updater as panel:
100
- sub_agent_data = run_sub_agent(
101
- task_query=query,
102
- repo_root=repo_root,
103
- rg_exe_path=rg_exe_path,
104
- console=console,
105
- panel_updater=panel,
106
- )
107
-
108
- # Check for errors
109
- if sub_agent_data.get('error'):
110
- panel.set_error(sub_agent_data['error'])
111
- return f"exit_code=1\n{sub_agent_data['error']}"
112
-
113
- # Track usage
114
- usage = sub_agent_data.get('usage', {})
115
- if usage:
116
- chat_manager.token_tracker.add_usage(usage, model_name=sub_agent_data.get("model", ""))
117
- panel.set_complete({
118
- 'prompt_tokens': usage.get('prompt_tokens', 0),
119
- 'completion_tokens': usage.get('completion_tokens', 0),
120
- 'total_tokens': usage.get('total_tokens', 0),
121
- 'context_tokens': usage.get('context_tokens', 0),
122
- })
123
-
124
- # Display sub-agent result summary (used for context)
125
- raw_result = sub_agent_data.get('result', '')
126
-
127
- # If hard limit was exceeded, skip injection and return raw dump
128
- if sub_agent_data.get('hard_limit_exceeded'):
129
- return raw_result
130
-
131
- # Parse and inject file contents
132
- injected_result = inject_file_contents(
133
- raw_result, repo_root, gitignore_spec, console
134
- )
135
-
136
- return injected_result
137
-
138
-
139
-