bone-agent 1.3.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/npm-wrapper.js +235 -0
  4. package/bin/rg +0 -0
  5. package/bin/rg.exe +0 -0
  6. package/config.yaml.example +133 -0
  7. package/package.json +53 -0
  8. package/requirements.txt +9 -0
  9. package/src/__init__.py +11 -0
  10. package/src/core/__init__.py +1 -0
  11. package/src/core/agentic.py +1054 -0
  12. package/src/core/chat_manager.py +1552 -0
  13. package/src/core/config_manager.py +247 -0
  14. package/src/core/cron.py +527 -0
  15. package/src/core/cron_allowlist.py +118 -0
  16. package/src/core/memory.py +232 -0
  17. package/src/core/retry.py +71 -0
  18. package/src/core/sub_agent.py +326 -0
  19. package/src/core/tool_approval.py +220 -0
  20. package/src/core/tool_feedback.py +778 -0
  21. package/src/exceptions.py +79 -0
  22. package/src/llm/__init__.py +1 -0
  23. package/src/llm/client.py +171 -0
  24. package/src/llm/config.py +466 -0
  25. package/src/llm/prompts.py +735 -0
  26. package/src/llm/providers.py +417 -0
  27. package/src/llm/streaming.py +163 -0
  28. package/src/llm/token_tracker.py +368 -0
  29. package/src/tools/__init__.py +212 -0
  30. package/src/tools/constants.py +59 -0
  31. package/src/tools/create_file.py +136 -0
  32. package/src/tools/directory.py +389 -0
  33. package/src/tools/edit.py +543 -0
  34. package/src/tools/file_reader.py +322 -0
  35. package/src/tools/helpers/__init__.py +105 -0
  36. package/src/tools/helpers/base.py +550 -0
  37. package/src/tools/helpers/converters.py +44 -0
  38. package/src/tools/helpers/file_helpers.py +189 -0
  39. package/src/tools/helpers/formatters.py +411 -0
  40. package/src/tools/helpers/loader.py +231 -0
  41. package/src/tools/helpers/parallel_executor.py +231 -0
  42. package/src/tools/helpers/path_resolver.py +226 -0
  43. package/src/tools/helpers/plugin_manifest.py +156 -0
  44. package/src/tools/obsidian.py +96 -0
  45. package/src/tools/review_sub_agent.py +189 -0
  46. package/src/tools/rg_search.py +393 -0
  47. package/src/tools/search_plugins.py +109 -0
  48. package/src/tools/select_option.py +593 -0
  49. package/src/tools/shell.py +302 -0
  50. package/src/tools/sub_agent.py +139 -0
  51. package/src/tools/task_list.py +269 -0
  52. package/src/tools/web_search.py +61 -0
  53. package/src/ui/__init__.py +1 -0
  54. package/src/ui/banner.py +87 -0
  55. package/src/ui/commands.py +2694 -0
  56. package/src/ui/displays.py +213 -0
  57. package/src/ui/loader.py +284 -0
  58. package/src/ui/main.py +646 -0
  59. package/src/ui/prompt_utils.py +113 -0
  60. package/src/ui/setting_selector.py +590 -0
  61. package/src/ui/setup_wizard.py +294 -0
  62. package/src/ui/sub_agent_panel.py +234 -0
  63. package/src/ui/tool_confirmation.py +215 -0
  64. package/src/utils/__init__.py +1 -0
  65. package/src/utils/citation_parser.py +199 -0
  66. package/src/utils/editor.py +158 -0
  67. package/src/utils/gitignore_filter.py +149 -0
  68. package/src/utils/logger.py +254 -0
  69. package/src/utils/paths.py +30 -0
  70. package/src/utils/result_parsers.py +108 -0
  71. package/src/utils/safe_commands.py +243 -0
  72. package/src/utils/settings.py +174 -0
  73. package/src/utils/validation.py +191 -0
  74. package/src/utils/web_search.py +173 -0
@@ -0,0 +1,220 @@
1
+ """Tool approval workflows for edit_file and execute_command."""
2
+
3
+ from rich.text import Text
4
+
5
+ from tools import confirm_tool
6
+
7
+
8
+ def handle_edit_approval(preview, file_path, args_dict, console, thinking_indicator,
9
+ approve_mode, cycle_approve_mode, repo_root, gitignore_spec,
10
+ vault_root_str):
11
+ """Handle edit_file approval workflow.
12
+
13
+ Args:
14
+ preview: Either a rich Text object or a plain string to display.
15
+ file_path: The file path being edited (for the confirm prompt).
16
+ args_dict: Tool arguments dict (path, search, replace, context_lines).
17
+ console: Rich console for display.
18
+ thinking_indicator: ThinkingIndicator instance (may be None).
19
+ approve_mode: Current approval mode string.
20
+ cycle_approve_mode: Callable to cycle approval mode.
21
+ repo_root: Repository root path string.
22
+ gitignore_spec: Gitignore spec object.
23
+ vault_root_str: Callable returning vault root path string.
24
+
25
+ Returns:
26
+ (result_str, should_exit) tuple where should_exit=True means cancel the agentic loop.
27
+ """
28
+ # Display preview
29
+ console.print(preview)
30
+ console.print()
31
+
32
+ # Stop thinking indicator while waiting for user input
33
+ if thinking_indicator:
34
+ thinking_indicator.stop()
35
+
36
+ action, guidance = confirm_tool(
37
+ f"edit_file: {file_path}",
38
+ console,
39
+ reason=args_dict.get('reason', 'Apply file edit with above changes'),
40
+ requires_approval=True,
41
+ approve_mode=approve_mode,
42
+ is_edit_tool=True,
43
+ cycle_approve_mode=cycle_approve_mode
44
+ )
45
+
46
+ if action == "accept":
47
+ from tools.edit import _execute_edit_file
48
+ final_result = _execute_edit_file(
49
+ path=args_dict.get('path'),
50
+ search=args_dict.get('search'),
51
+ replace=args_dict.get('replace'),
52
+ repo_root=repo_root,
53
+ console=console,
54
+ gitignore_spec=gitignore_spec,
55
+ context_lines=args_dict.get('context_lines', 3),
56
+ vault_root=vault_root_str()
57
+ )
58
+ # Strip exit_code line from final result before displaying
59
+ if final_result and isinstance(final_result, str):
60
+ result_lines = [line for line in final_result.split('\n') if not line.startswith('exit_code=')]
61
+ final_result = '\n'.join(result_lines).strip()
62
+ result_str, should_exit = final_result, False
63
+ elif action == "advise":
64
+ console.print(f"[dim]Edit not applied. User advice: {guidance}[/dim]")
65
+ result_str = f"exit_code=1\nEdit not applied. User advice: {guidance}"
66
+ should_exit = False
67
+ else: # cancel
68
+ console.print("[dim]Operation canceled by user.[/dim]")
69
+ result_str = "exit_code=1\nOperation canceled by user. Do not retry this operation."
70
+ should_exit = True
71
+
72
+ # Restart thinking indicator after user input
73
+ if thinking_indicator:
74
+ thinking_indicator.start()
75
+
76
+ return result_str, should_exit
77
+
78
+
79
+ def resolve_edit_preview(result):
80
+ """Extract a displayable preview from an edit_file tool result.
81
+
82
+ Handles both Rich Text objects (new format) and legacy string format.
83
+
84
+ Args:
85
+ result: Either a rich Text object or a string.
86
+
87
+ Returns:
88
+ (preview, is_valid) tuple.
89
+ - preview: Text object, plain string, or None if error.
90
+ - is_valid: False if the result is an error (non-zero exit_code).
91
+ """
92
+ if isinstance(result, Text):
93
+ return result, True
94
+ elif isinstance(result, str) and result.startswith("exit_code=0"):
95
+ lines = result.split('\n')
96
+ preview_lines = [line for line in lines if not line.startswith("exit_code=")]
97
+ preview = '\n'.join(preview_lines).strip()
98
+ return preview, True
99
+ else:
100
+ # Error occurred during preview - don't show to user
101
+ return None, False
102
+
103
+
104
+ def handle_command_approval(command, arguments, tool, context, console,
105
+ thinking_indicator, approve_mode, debug_mode,
106
+ cron_job_id=None, cron_allowlist=None,
107
+ cron_interactive=False):
108
+ """Handle execute_command approval workflow.
109
+
110
+ Checks for silent blocks, auto-approval, and prompts user if needed.
111
+ When a cron_job_id and cron_allowlist are provided, commands on the
112
+ job's allow list are auto-approved; unlisted commands are blocked
113
+ (in scheduled mode) or prompted interactively (in test-run mode).
114
+
115
+ Args:
116
+ command: The shell command string.
117
+ arguments: Tool arguments dict (includes 'reason').
118
+ tool: The tool object to execute on approval.
119
+ context: Tool execution context dict.
120
+ console: Rich console for display.
121
+ thinking_indicator: ThinkingIndicator instance (may be None).
122
+ approve_mode: Current approval mode string.
123
+ debug_mode: Whether debug mode is active (for silent block logging).
124
+ cron_job_id: Optional cron job ID for allow list checking.
125
+ cron_allowlist: Optional CronAllowlist instance for cron command gating.
126
+ cron_interactive: If True, cron job is in interactive test-run mode.
127
+
128
+ Returns:
129
+ (result, should_exit, command_executed) tuple.
130
+ - result: Tool result string.
131
+ - should_exit: True if the user canceled (break the agentic loop).
132
+ - command_executed: True if the command was actually executed (display output).
133
+ """
134
+ from utils.validation import is_auto_approved_command, check_for_silent_blocked_command
135
+
136
+ # Check if command should be silently blocked (redirect to native tool)
137
+ is_blocked, reprompt_msg = check_for_silent_blocked_command(command)
138
+ if is_blocked:
139
+ if debug_mode:
140
+ console.print(f"[dim]Silently blocked command: {command.split()[0]}[/dim]")
141
+ result = f"exit_code=1\n{reprompt_msg}"
142
+ return result, False, False
143
+
144
+ # Check if command should be auto-approved (global safe commands)
145
+ auto_approve = is_auto_approved_command(command)
146
+
147
+ # Check cron allow list
148
+ cron_auto_approved = False
149
+ if cron_job_id and cron_allowlist:
150
+ if cron_allowlist.is_allowed(cron_job_id, command):
151
+ cron_auto_approved = True
152
+ elif not auto_approve:
153
+ # Command not on allow list and not globally safe
154
+ # Determine if we're in interactive test-run or scheduled mode
155
+ if cron_interactive:
156
+ # Interactive test run (/cron run) — prompt the user
157
+ pass # Fall through to normal interactive approval below
158
+ else:
159
+ # Scheduled run — block the command, let agent adapt
160
+ allowed_cmds = cron_allowlist.get_commands(cron_job_id)
161
+ allowed_preview = ", ".join(f"'{c}'" for c in allowed_cmds[:5])
162
+ if len(allowed_cmds) > 5:
163
+ allowed_preview += f", ... ({len(allowed_cmds)} total)"
164
+ if not allowed_preview:
165
+ allowed_preview = "(none - run '/cron run <id>' to build the allow list)"
166
+ result = (
167
+ f"exit_code=1\n"
168
+ f"Command not in cron allow list for job '{cron_job_id}'.\n"
169
+ f"Command: {command}\n"
170
+ f"Allowed: {allowed_preview}\n"
171
+ f"Do not retry this command. Use only approved commands or "
172
+ f"ask the user to run '/cron run {cron_job_id}' to add it."
173
+ )
174
+ return result, False, False
175
+
176
+ if cron_auto_approved or auto_approve:
177
+ # Auto-approved command - execute without prompting
178
+ result = tool.execute(arguments, context)
179
+ command_executed = True
180
+
181
+ # In cron test-run mode, auto-save newly approved commands to allow list
182
+ # Skip globally-safe commands — they're auto-approved regardless of the allow list
183
+ if cron_job_id and cron_allowlist and cron_interactive and not auto_approve:
184
+ cron_allowlist.add_command(cron_job_id, command)
185
+
186
+ return result, False, command_executed
187
+
188
+ # Interactive approval (test-run mode or normal session)
189
+ # Stop thinking indicator while waiting for user input
190
+ if thinking_indicator:
191
+ thinking_indicator.stop()
192
+
193
+ action, guidance = confirm_tool(
194
+ f"execute_command: {command[:80]}{'...' if len(command) > 80 else ''}",
195
+ console,
196
+ reason=arguments.get('reason', 'Execute shell command'),
197
+ requires_approval=True,
198
+ approve_mode=approve_mode
199
+ )
200
+
201
+ if action == "accept":
202
+ result = tool.execute(arguments, context)
203
+ command_executed = True
204
+ # Auto-save approved command to cron allow list during test run
205
+ if cron_job_id and cron_allowlist:
206
+ cron_allowlist.add_command(cron_job_id, command)
207
+ elif action == "advise":
208
+ result = f"Command not executed. User advice: {guidance}"
209
+ command_executed = False
210
+ elif action == "cancel":
211
+ result = "Command canceled by user. Do not retry this operation."
212
+ if thinking_indicator:
213
+ thinking_indicator.start()
214
+ return result, True, False
215
+
216
+ # Restart thinking indicator after user input
217
+ if thinking_indicator:
218
+ thinking_indicator.start()
219
+
220
+ return result, False, command_executed