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