bone-agent 1.3.3 → 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 (121) 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 -184
  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 -141
  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 -36
  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/targeted_searching.md +0 -10
  25. package/prompts/main/task_lists_pattern.md +0 -8
  26. package/prompts/main/temp_folder.md +0 -9
  27. package/prompts/main/think_before_acting.md +0 -10
  28. package/prompts/main/tone_and_style.md +0 -4
  29. package/prompts/main/tool_preferences.md +0 -24
  30. package/prompts/main/trust_subagent_context.md +0 -21
  31. package/prompts/main/when_to_use_sub_agent.md +0 -7
  32. package/prompts/micro/ask_questions.md +0 -1
  33. package/prompts/micro/batch_independent_calls.md +0 -1
  34. package/prompts/micro/casual_interactions.md +0 -1
  35. package/prompts/micro/code_references.md +0 -1
  36. package/prompts/micro/communication_style.md +0 -1
  37. package/prompts/micro/context_reliability.md +0 -1
  38. package/prompts/micro/conversational_tool_calling.md +0 -1
  39. package/prompts/micro/editing_pattern.md +0 -1
  40. package/prompts/micro/error_handling.md +0 -1
  41. package/prompts/micro/exploration_pattern.md +0 -1
  42. package/prompts/micro/intro.md +0 -1
  43. package/prompts/micro/obsidian.md +0 -4
  44. package/prompts/micro/obsidian_project.md +0 -5
  45. package/prompts/micro/professional_objectivity.md +0 -1
  46. package/prompts/micro/targeted_searching.md +0 -1
  47. package/prompts/micro/task_lists_pattern.md +0 -1
  48. package/prompts/micro/temp_folder.md +0 -1
  49. package/prompts/micro/think_before_acting.md +0 -5
  50. package/prompts/micro/tone_and_style.md +0 -1
  51. package/prompts/micro/tool_preferences.md +0 -1
  52. package/prompts/micro/trust_subagent_context.md +0 -1
  53. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  54. package/requirements.txt +0 -9
  55. package/src/__init__.py +0 -11
  56. package/src/core/__init__.py +0 -1
  57. package/src/core/agentic.py +0 -985
  58. package/src/core/chat_manager.py +0 -1564
  59. package/src/core/config_manager.py +0 -253
  60. package/src/core/cron.py +0 -582
  61. package/src/core/cron_allowlist.py +0 -118
  62. package/src/core/memory.py +0 -145
  63. package/src/core/retry.py +0 -71
  64. package/src/core/sub_agent.py +0 -326
  65. package/src/core/tool_approval.py +0 -220
  66. package/src/core/tool_feedback.py +0 -778
  67. package/src/exceptions.py +0 -79
  68. package/src/llm/__init__.py +0 -1
  69. package/src/llm/client.py +0 -171
  70. package/src/llm/config.py +0 -492
  71. package/src/llm/prompts.py +0 -489
  72. package/src/llm/providers.py +0 -436
  73. package/src/llm/streaming.py +0 -163
  74. package/src/llm/token_tracker.py +0 -384
  75. package/src/tools/__init__.py +0 -212
  76. package/src/tools/constants.py +0 -59
  77. package/src/tools/create_file.py +0 -136
  78. package/src/tools/directory.py +0 -389
  79. package/src/tools/edit.py +0 -545
  80. package/src/tools/file_reader.py +0 -322
  81. package/src/tools/helpers/__init__.py +0 -105
  82. package/src/tools/helpers/base.py +0 -550
  83. package/src/tools/helpers/converters.py +0 -44
  84. package/src/tools/helpers/file_helpers.py +0 -189
  85. package/src/tools/helpers/formatters.py +0 -411
  86. package/src/tools/helpers/loader.py +0 -231
  87. package/src/tools/helpers/parallel_executor.py +0 -231
  88. package/src/tools/helpers/path_resolver.py +0 -232
  89. package/src/tools/helpers/plugin_manifest.py +0 -156
  90. package/src/tools/obsidian.py +0 -96
  91. package/src/tools/review_sub_agent.py +0 -189
  92. package/src/tools/rg_search.py +0 -460
  93. package/src/tools/search_plugins.py +0 -109
  94. package/src/tools/select_option.py +0 -600
  95. package/src/tools/shell.py +0 -302
  96. package/src/tools/sub_agent.py +0 -139
  97. package/src/tools/task_list.py +0 -269
  98. package/src/tools/web_search.py +0 -61
  99. package/src/ui/__init__.py +0 -1
  100. package/src/ui/banner.py +0 -87
  101. package/src/ui/commands.py +0 -2809
  102. package/src/ui/displays.py +0 -214
  103. package/src/ui/loader.py +0 -284
  104. package/src/ui/main.py +0 -647
  105. package/src/ui/prompt_utils.py +0 -113
  106. package/src/ui/setting_selector.py +0 -590
  107. package/src/ui/setup_wizard.py +0 -294
  108. package/src/ui/sub_agent_panel.py +0 -234
  109. package/src/ui/tool_confirmation.py +0 -215
  110. package/src/utils/__init__.py +0 -1
  111. package/src/utils/citation_parser.py +0 -199
  112. package/src/utils/editor.py +0 -158
  113. package/src/utils/gitignore_filter.py +0 -149
  114. package/src/utils/logger.py +0 -254
  115. package/src/utils/paths.py +0 -30
  116. package/src/utils/result_parsers.py +0 -108
  117. package/src/utils/safe_commands.py +0 -243
  118. package/src/utils/settings.py +0 -191
  119. package/src/utils/user_message_logger.py +0 -120
  120. package/src/utils/validation.py +0 -191
  121. package/src/utils/web_search.py +0 -173
package/src/ui/main.py DELETED
@@ -1,647 +0,0 @@
1
- """Main entry point for bone-agent chatbot."""
2
-
3
- import os
4
- import sys
5
- import time
6
- import random
7
- import threading
8
- import warnings
9
- import atexit
10
- from pathlib import Path
11
-
12
- # Suppress prompt_toolkit RuntimeWarning about unawaited coroutines during cleanup
13
- warnings.filterwarnings("ignore", category=RuntimeWarning)
14
-
15
- # Add src directory to Python path so we can import llm, core, utils modules
16
- src_dir = Path(__file__).resolve().parent.parent
17
- if str(src_dir) not in sys.path:
18
- sys.path.insert(0, str(src_dir))
19
-
20
- from rich.console import Console
21
- from rich.theme import Theme
22
- from rich.text import Text
23
- from prompt_toolkit import PromptSession
24
- from prompt_toolkit.key_binding import KeyBindings
25
- from prompt_toolkit.formatted_text import ANSI
26
- from prompt_toolkit.styles import Style
27
-
28
- from llm import config
29
- from llm.config import TOOLS_ENABLED
30
- from core.chat_manager import ChatManager
31
- from ui.commands import process_command
32
- from ui.banner import display_startup_banner
33
- from ui.prompt_utils import get_bottom_toolbar_text, setup_common_bindings, TOOLBAR_STYLE
34
- from core.agentic import agentic_answer
35
- from utils.settings import MonokaiDarkBGStyle, left_align_headings
36
- from utils.paths import REPO_ROOT, RG_EXE_PATH
37
- from exceptions import BoneAgentError
38
- from tools.loader import load_all_tools
39
-
40
- # Console setup
41
- console = Console(theme=Theme({
42
- "markdown.hr": "grey50",
43
- "markdown.heading": "default",
44
- "markdown.h1": "default",
45
- "markdown.h2": "default",
46
- "markdown.h3": "default",
47
- "markdown.h4": "default",
48
- "markdown.h5": "default",
49
- "markdown.h6": "default",
50
- "markdown.paragraph_text": "default",
51
- "markdown.text": "default",
52
- "markdown.item": "default",
53
- "markdown.list_item": "default",
54
- "markdown.code": "default",
55
- "markdown.code_block": "default",
56
- "markdown.link": "default",
57
- "markdown.link_url": "default",
58
- }))
59
-
60
- # Debug mode container (used as mutable reference)
61
- DEBUG_MODE_CONTAINER = {'debug': False}
62
-
63
- # Ctrl+C exit tracking (for double Ctrl+C to exit)
64
- CTRL_C_TRACKER = {
65
- 'last_time': 0,
66
- 'exit_window': 2.0, # 2 second window for double Ctrl+C
67
- 'exit_requested': False
68
- }
69
-
70
- # Block input during thinking/agentic processing (prevents key presses from being queued)
71
- INPUT_BLOCKED = {'blocked': False}
72
-
73
-
74
- class ThinkingIndicator:
75
- """Simple spinner wrapper that always cleans up."""
76
-
77
- def __init__(self, console, message="Thinking ...", spinner="dots"):
78
- self.console = console
79
- self.message = message
80
- self.spinner = spinner
81
- self._last_word_change = 0
82
- self._word_change_interval = 15.0 # Change word every 15 seconds
83
-
84
- self._common_words = [
85
- "Thinking ...",
86
- "Chunking ...",
87
- "Completing ...",
88
- "Computing ...",
89
- "Programming ...",
90
- "Understanding ...",
91
- "Vibing ...",
92
- "Perpetuating ...",
93
- "Analyzing ...",
94
- "Evaluating ...",
95
- "Synthesizing ...",
96
- "Working ...",
97
- "Debugging ...",
98
- "Scrutinizing ...",
99
- "Formulating ...",
100
- "Predicting next token ...",
101
- "Outsourcing ...",
102
- "Checking vitals ...",
103
- "Scanning fingerprints ...",
104
- "Rerouting ...",
105
- "Refactoring ...",
106
- "Burning tokens ...",
107
- "Conjuring ...",
108
- "Recalculating ...",
109
- "Spinning ...",
110
- "Pointing ...",
111
- "Dematerializing ...",
112
- "Compiling ...",
113
- "Fetching ...",
114
- "Buffering ...",
115
- "Syncing ...",
116
- "Caching ...",
117
- "Connecting ...",
118
- "Indexing ...",
119
- "Authenticating ...",
120
- "Validating ...",
121
- ]
122
-
123
- self._rare_words = [
124
- '"Engineering" ...',
125
- "Deleting (jk) ...",
126
- "Computer... Fix my program ...",
127
- "Exiting VIM ...",
128
- "Rolling for perception ...",
129
- "Pinging ...",
130
- "Ponging ...",
131
- "Programming HTML ...",
132
- "Leaking memory ...",
133
- "Cooking ...",
134
- "Mining ...",
135
- "Crafting ...",
136
- "Pushing to prod ...",
137
- "Checking with Altman ...",
138
- "Collecting 200 ...",
139
- "Rebooting...",
140
- "Wasting water ...",
141
- "Asking Stack Overflow ...",
142
- "Reading the docs ...",
143
- "Asking ChatGPT ...",
144
- "Binging it ...",
145
- "Googling it ...",
146
- "Dockerizing ...",
147
- "Forking it ...",
148
- "Checking the logs ...",
149
- "Checking the backup ...",
150
- "Performing vLookup ...",
151
- "Downloading more RAM ...",
152
- "Performing SumIf ...",
153
- "Spinning up servers ...",
154
- "Getting chat completion ...",
155
- "Merging conflicts ...",
156
- "Feature creeping ...",
157
- ]
158
-
159
- self._legendary_words = [
160
- "I'm confused ...",
161
- "Running in O(n²) ...",
162
- "Checking Jira ...",
163
- "Gaining consciousness ...",
164
- "Mining Bitcoin ...",
165
- "Accessing null pointer ...",
166
- "FIXING ME ...",
167
- "READING ME ...",
168
- "Converting to PDF and back ...",
169
- "Rewriting in Rust ...",
170
- "Rewriting in JavaScript ...",
171
- "Recursively calling myself ...",
172
- "Contacting AWS Support ...",
173
- "Reviewing footage ...",
174
- "Dedotating wam ...",
175
- "Pondering the orb ...",
176
- "Computer... ENHANCE ...",
177
- "Consulting council ...",
178
- "Releasing the files ...",
179
- "Redacting the files ...",
180
- "Uhhhh ...",
181
- "Selling data ...",
182
- "Okeyyy lets go ...",
183
- ]
184
- self._status = None
185
- self._active = False
186
- self._start_time = None
187
- self._timer_thread = None
188
- self._stop_timer = threading.Event()
189
- self._elapsed_before_pause = 0.0
190
- self._has_been_started = False
191
- self._saved_termios = None
192
-
193
- def _select_random_word(self):
194
- """Select a random word from weighted word lists."""
195
- roll = random.random()
196
-
197
- if roll < 0.80:
198
- return random.choice(self._common_words)
199
- elif roll < 0.95:
200
- return random.choice(self._rare_words)
201
- else:
202
- return random.choice(self._legendary_words)
203
-
204
- @staticmethod
205
- def _format_time(seconds):
206
- """Format seconds as whole seconds or minutes:seconds."""
207
- if seconds >= 60:
208
- mins = int(seconds // 60)
209
- secs = int(seconds % 60)
210
- return f"{mins}m {secs}s"
211
- else:
212
- return f"{int(seconds)}s"
213
-
214
- @staticmethod
215
- def _set_raw_mode():
216
- """Switch stdin to raw mode to prevent keystroke echoes during spinner."""
217
- if os.name == 'nt':
218
- return
219
- try:
220
- import termios
221
- fd = sys.stdin.fileno()
222
- old = termios.tcgetattr(fd)
223
- new = old.copy()
224
- # lflag: disable ECHO, ICANON (line buffering), IEXTEN
225
- new[3] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN)
226
- # iflag: disable ICRNL (map CR to NL) so Enter doesn't produce newline
227
- new[0] &= ~(termios.ICRNL)
228
- termios.tcsetattr(fd, termios.TCSANOW, new)
229
- return old
230
- except Exception:
231
- return None
232
-
233
- @staticmethod
234
- def _restore_terminal_mode(saved):
235
- """Restore terminal mode from saved termios attributes."""
236
- if os.name == 'nt' or saved is None:
237
- return
238
- try:
239
- import termios
240
- termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, saved)
241
- except Exception:
242
- pass
243
-
244
- def start(self):
245
- # Select initial word
246
- self.message = self._select_random_word()
247
-
248
- # Initialize timer (reset only on first start)
249
- if not self._has_been_started:
250
- self._elapsed_before_pause = 0.0
251
- self._has_been_started = True
252
- self._last_word_change = 0
253
-
254
- self._start_time = time.time()
255
- self._stop_timer.clear()
256
-
257
- # Always recreate and restart status with new message
258
- if self._status and self._active:
259
- self._status.stop()
260
- self._saved_termios = self._set_raw_mode()
261
- self._status = self.console.status(self.message, spinner=self.spinner, spinner_style="#5F9EA0")
262
- self._status.start()
263
- self._active = True
264
-
265
- # Start background timer thread
266
- self._timer_thread = threading.Thread(target=self._update_timer, daemon=True)
267
- self._timer_thread.start()
268
-
269
- def _update_timer(self):
270
- """Background thread: update status message with elapsed time."""
271
- while not self._stop_timer.is_set() and self._status and self._active:
272
- # Calculate elapsed time including previous pauses
273
- elapsed = self._elapsed_before_pause + (time.time() - self._start_time)
274
-
275
- # Change word every 15 seconds
276
- if elapsed - self._last_word_change >= self._word_change_interval:
277
- self.message = self._select_random_word()
278
- self._last_word_change = elapsed
279
-
280
- # Format elapsed time (e.g., "Thinking ... (1s)" or "Thinking ... (1m 30s)")
281
- time_str = f"({self._format_time(elapsed)})"
282
- updated_message = f"{self.message} {time_str}"
283
-
284
- # Update the status message
285
- if self._status:
286
- self._status.update(updated_message)
287
-
288
- self._stop_timer.wait(0.1) # Update every 100ms
289
-
290
- def stop(self, reset=False):
291
- """Stop the thinking indicator.
292
-
293
- Args:
294
- reset: If True, reset elapsed time and state for next use cycle.
295
- """
296
- # Calculate and store elapsed time (including accumulated pauses)
297
- elapsed_time = None
298
- if self._start_time:
299
- elapsed_time = self._elapsed_before_pause + (time.time() - self._start_time)
300
- self._elapsed_before_pause = elapsed_time
301
-
302
- # Stop timer thread first (close race window before stopping status)
303
- self._active = False
304
- self._stop_timer.set()
305
- if self._timer_thread:
306
- self._timer_thread.join(timeout=0.5)
307
-
308
- if self._status:
309
- self._status.stop()
310
- self._status = None
311
-
312
- # Restore terminal mode (must happen after status.stop() so Rich
313
- # cursor cleanup runs in raw mode, then we hand control back to ptk)
314
- self._restore_terminal_mode(self._saved_termios)
315
- self._saved_termios = None
316
-
317
- # Reset state for next use cycle
318
- if reset:
319
- self._has_been_started = False
320
- self._elapsed_before_pause = 0.0
321
-
322
- self._start_time = None
323
-
324
- def pause(self):
325
- # Stop without showing completion time (accumulates elapsed time)
326
- self.stop(reset=False)
327
-
328
- def resume(self):
329
- # Resume with timer continuing from accumulated time
330
- self.start()
331
-
332
-
333
- def check_double_ctrl_c() -> bool:
334
- """
335
- Check if this is a double Ctrl+C (within exit window).
336
- Returns True if should exit, False otherwise.
337
- Updates the tracker timestamp and exit_requested flag.
338
- """
339
- # Check if exit was already requested
340
- if CTRL_C_TRACKER['exit_requested']:
341
- return True
342
-
343
- current_time = time.time()
344
- time_since_last = current_time - CTRL_C_TRACKER['last_time']
345
-
346
- if time_since_last <= CTRL_C_TRACKER['exit_window']:
347
- # Double Ctrl+C detected - set exit flag and return True
348
- CTRL_C_TRACKER['exit_requested'] = True
349
- return True
350
- else:
351
- # First Ctrl+C or too much time passed - update timestamp and continue
352
- CTRL_C_TRACKER['last_time'] = current_time
353
- return False
354
-
355
-
356
- def _drain_stdin(session):
357
- """Drain buffered keystrokes and clear the prompt_toolkit buffer.
358
-
359
- Called after AI processing ends to discard any input the user
360
- typed while the thinking indicator was active.
361
- """
362
- try:
363
- if os.name != 'nt':
364
- import termios
365
- termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
366
- else:
367
- import msvcrt
368
- while msvcrt.kbhit():
369
- msvcrt.getch()
370
- except Exception:
371
- pass
372
-
373
- try:
374
- buf = session.default_buffer
375
- if buf and buf.text:
376
- buf.text = ""
377
- except Exception:
378
- pass
379
-
380
-
381
- def main():
382
- """Main interactive chat loop."""
383
-
384
- # Load all tools (built-in and user tools)
385
- # This populates the ToolRegistry with all decorated tools
386
- load_all_tools()
387
-
388
- # Check for config.yaml — run setup wizard on first run
389
- from ui.setup_wizard import is_first_run, run_wizard as _run_setup_wizard
390
-
391
- if is_first_run():
392
- console.print("\n[#5F9EA0]No config found — launching setup wizard.[/#5F9EA0]\n")
393
- _run_setup_wizard(console)
394
- # Reload config after wizard writes it
395
- try:
396
- from llm import config as llm_config
397
- llm_config.reload_config()
398
- except Exception:
399
- pass
400
-
401
- chat_manager = ChatManager()
402
- thinking_indicator = ThinkingIndicator(console)
403
- # Safety net: ensure terminal mode is restored even on unhandled exceptions
404
- def _safety_restore():
405
- ThinkingIndicator._restore_terminal_mode(thinking_indicator._saved_termios)
406
- atexit.register(_safety_restore)
407
- # Start server if needed
408
- console.print("[yellow]Initializing...[/yellow]")
409
- chat_manager.server_process = chat_manager.start_server_if_needed()
410
- if not chat_manager.server_process and chat_manager.client.provider == "local":
411
- console.print("[red]Failed to start local server![/red]")
412
- return
413
-
414
- display_startup_banner(chat_manager.approve_mode, clear_screen=True)
415
-
416
- # Start cron scheduler (background thread for scheduled jobs)
417
- cron_scheduler = None
418
- try:
419
- from core.cron import CronScheduler
420
- cron_scheduler = CronScheduler(console=console)
421
- cron_scheduler.start()
422
- except Exception as e:
423
- import logging as _log
424
- _log.warning("Cron scheduler failed to start: %s", e)
425
- console.print(f"[yellow]Cron scheduler unavailable: {e}[/yellow]")
426
-
427
- # First-run onboarding: check if active provider needs an API key but has none
428
- try:
429
- from llm import config as llm_config
430
- active_provider = chat_manager.client.provider
431
- provider_cfg = llm_config.get_provider_config(active_provider)
432
- if (
433
- provider_cfg.get("type") == "api"
434
- and not provider_cfg.get("api_key")
435
- ):
436
- console.print()
437
- console.print("[bold #5F9EA0]Welcome! Get started in two steps:[/bold #5F9EA0]")
438
- console.print()
439
- console.print(" [bold]1.[/bold] [bold white on grey23] /signup <email> [/bold white on grey23] [dim]— create a free account & API key[/dim]")
440
- console.print(" [bold]2.[/bold] [bold white on grey23] /provider[/bold white on grey23] [dim]— or pick another provider (OpenAI, Anthropic, ...)[/dim]")
441
- console.print()
442
- console.print("[dim]Tip: use [bold #5F9EA0]/key <your-key>[/bold #5F9EA0] to set a key for any provider.[/dim]")
443
- console.print()
444
- except Exception:
445
- pass # Best-effort; don't block startup on failure
446
-
447
- # Setup prompt_toolkit with Tab key binding
448
- bindings = setup_common_bindings(chat_manager)
449
-
450
- def get_prompt(chat_manager):
451
- """Return colored prompt."""
452
- prompt_text = Text.assemble(
453
- (" > ", "white")
454
- )
455
- with console.capture() as capture:
456
- console.print(prompt_text, end="")
457
- return ANSI(capture.get())
458
-
459
- @bindings.add('escape', 'escape')
460
- def clear_input(event):
461
- """Clear the current input line on double ESC press (blocked during thinking)."""
462
- if INPUT_BLOCKED.get('blocked', False):
463
- return
464
- buffer = event.app.current_buffer
465
- if buffer is not None:
466
- buffer.text = ""
467
- event.app.invalidate()
468
-
469
- session = PromptSession(key_bindings=bindings, style=TOOLBAR_STYLE)
470
-
471
- try:
472
- while True:
473
- # Check if exit was requested via double Ctrl+C
474
- if CTRL_C_TRACKER['exit_requested']:
475
- break
476
-
477
- try:
478
- # Use prompt_toolkit for input with Tab key binding and dynamic prompt
479
- prompt_kwargs = {
480
- "bottom_toolbar": lambda: get_bottom_toolbar_text(chat_manager),
481
- }
482
- raw_input = session.prompt(
483
- lambda: get_prompt(chat_manager),
484
- **prompt_kwargs,
485
- )
486
- user_input = raw_input.strip()
487
-
488
- if not user_input:
489
- # Clear the empty input line to avoid multiple prompts stacking
490
- import sys
491
- sys.stdout.write("\033[F\033[K") # Move up and clear line
492
- sys.stdout.flush()
493
- continue
494
-
495
- # Process commands
496
- cmd_result, modified_input = process_command(chat_manager, user_input, console, DEBUG_MODE_CONTAINER, cron_scheduler)
497
- if cmd_result == "exit":
498
- break
499
- elif cmd_result == "handled":
500
- continue
501
-
502
- # Use modified input if provided (from /edit command)
503
- final_input = modified_input if modified_input else user_input
504
-
505
- chat_manager.maybe_auto_compact(console)
506
-
507
- thinking_indicator.start()
508
- INPUT_BLOCKED['blocked'] = True
509
- try:
510
- console.print("─" * console.width, style="rgb(30,30,30)")
511
- console.print() # Extra newline after user input to separate from LLM response
512
- # Add user message
513
- if TOOLS_ENABLED:
514
- try:
515
- agentic_answer(
516
- chat_manager,
517
- final_input,
518
- console,
519
- REPO_ROOT,
520
- RG_EXE_PATH,
521
- DEBUG_MODE_CONTAINER['debug'],
522
- thinking_indicator=thinking_indicator,
523
- )
524
- chat_manager._update_context_tokens()
525
- except KeyboardInterrupt:
526
- if not check_double_ctrl_c():
527
- console.print("\n[yellow]Response interrupted (Ctrl+C). Press Ctrl+C again to exit.[/yellow]")
528
- console.print() # Extra spacing
529
- except BoneAgentError as e:
530
- # Handle all bone-agent custom exceptions gracefully
531
- console.print(f"[red]Error: {e}[/red]", markup=False)
532
- if hasattr(e, 'details') and e.details:
533
- console.print(f"[dim]Details: {e.details}[/dim]", markup=False)
534
- else:
535
- chat_manager.messages.append({"role": "user", "content": final_input})
536
-
537
- try:
538
- stream = chat_manager.client.chat_completion(
539
- chat_manager.messages, stream=True
540
- )
541
- if isinstance(stream, str):
542
- console.print(f"[red]Error: {stream}[/red]")
543
- continue
544
-
545
- try:
546
- # Stream response
547
- chunks = []
548
- usage_data = None
549
- for chunk in stream:
550
- # Check if this is usage data (final chunk)
551
- if isinstance(chunk, dict) and '__usage__' in chunk:
552
- usage_data = chunk['__usage__']
553
- else:
554
- chunks.append(chunk)
555
- full_response = "".join(chunks)
556
-
557
- # Clear thinking indicator before printing response
558
- thinking_indicator.stop(reset=True)
559
- INPUT_BLOCKED['blocked'] = False
560
- _drain_stdin(session)
561
-
562
- if full_response.strip():
563
- md = Markdown(left_align_headings(full_response), code_theme=MonokaiDarkBGStyle, justify="left")
564
- console.print(md)
565
-
566
- chat_manager.messages.append(
567
- {"role": "assistant", "content": full_response}
568
- )
569
-
570
- # Add usage tracking (resolves cost from config if
571
- # upstream-reported cost is absent in the usage dict)
572
- if usage_data:
573
- provider_cfg = llm.config.get_provider_config(chat_manager.client.provider)
574
- chat_manager.token_tracker.add_usage(
575
- usage_data,
576
- model_name=provider_cfg.get("model", ""),
577
- )
578
-
579
- chat_manager._update_context_tokens()
580
- except KeyboardInterrupt:
581
- # Ctrl+C pressed during streaming
582
- if not check_double_ctrl_c():
583
- console.print("\n[yellow]Response interrupted (Ctrl+C). Press Ctrl+C again to exit.[/yellow]")
584
- # Save partial response
585
- if chunks:
586
- partial = "".join(chunks)
587
- if partial.strip():
588
- partial_with_note = partial + "\n\n*[Response interrupted]*"
589
- md = Markdown(left_align_headings(partial_with_note), code_theme=MonokaiDarkBGStyle, justify="left")
590
- console.print(md)
591
- chat_manager.messages.append(
592
- {"role": "assistant", "content": partial}
593
- )
594
- console.print() # Extra spacing
595
- finally:
596
- # Ensure HTTP connection is closed
597
- if hasattr(stream, 'close'):
598
- stream.close()
599
-
600
- except BoneAgentError as e:
601
- # Handle all bone-agent custom exceptions gracefully
602
- console.print(f"[red]Error: {e}[/red]", markup=False)
603
- if hasattr(e, 'details') and e.details:
604
- console.print(f"[dim]Details: {e.details}[/dim]", markup=False)
605
- except Exception as e:
606
- console.print(f"[red]Error during generation: {e}[/red]", markup=False)
607
- finally:
608
- thinking_indicator.stop(reset=True)
609
- INPUT_BLOCKED['blocked'] = False
610
- _drain_stdin(session)
611
-
612
- except KeyboardInterrupt:
613
- # Ctrl+C pressed while waiting for input
614
- if check_double_ctrl_c():
615
- break
616
- else:
617
- console.print("\n[dim](Press Ctrl+C again to exit, or type 'exit' to quit)[/dim]")
618
- continue
619
- except EOFError:
620
- # stdin closed (Ctrl+D or piped input ended)
621
- break
622
-
623
- finally:
624
- # Display session summary before cleanup
625
- summary = chat_manager.token_tracker.get_session_summary()
626
- console.print(f"\n[white]Session Summary: {summary}[/white]")
627
-
628
- # Stop cron scheduler if running
629
- if cron_scheduler:
630
- cron_scheduler.stop()
631
-
632
- chat_manager.cleanup()
633
- console.print("[yellow]Goodbye![/yellow]")
634
-
635
-
636
- if __name__ == "__main__":
637
- import argparse
638
-
639
- parser = argparse.ArgumentParser(description="bone-agent CLI")
640
- parser.add_argument("--cron-run", metavar="JOB_ID", help="Run a cron job headlessly and exit")
641
- args = parser.parse_args()
642
-
643
- if args.cron_run:
644
- from core.cron import run_job_headless
645
- sys.exit(run_job_headless(args.cron_run))
646
-
647
- main()