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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/tools/task_list.py
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
"""Task list management tools.
|
|
2
|
-
|
|
3
|
-
These tools provide in-session task tracking for long EDIT workflows.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import textwrap
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Optional, List
|
|
9
|
-
|
|
10
|
-
from .helpers.base import tool
|
|
11
|
-
from .helpers.converters import coerce_int
|
|
12
|
-
from . import constants
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _escape_rich(text):
|
|
16
|
-
"""Escape square brackets in text so Rich renders them literally."""
|
|
17
|
-
return text.replace("[", "\\[").replace("]", "\\]")
|
|
18
|
-
def _strip_rich_markup(text):
|
|
19
|
-
"""Remove Rich console markup tags from text for plain-text comparison.
|
|
20
|
-
|
|
21
|
-
Handles [tag]...[/tag], [/tag], and standalone [tag] forms.
|
|
22
|
-
Also un-escapes literal bracket sequences (\\[ \\]).
|
|
23
|
-
"""
|
|
24
|
-
import re
|
|
25
|
-
# Un-escape literal brackets first
|
|
26
|
-
text = text.replace("\\[", "[").replace("\\]", "]")
|
|
27
|
-
# Remove [/tag] closing tags
|
|
28
|
-
text = re.sub(r'\[/\w+\]', '', text)
|
|
29
|
-
# Remove [tag] opening tags (but not [x], [ ], or [N] patterns)
|
|
30
|
-
text = re.sub(r'\[(?!x\]|\s?\]|\d+\])/?\w+\]', '', text)
|
|
31
|
-
return text
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _format_task_list(task_list, title=None):
|
|
35
|
-
"""Format task list for display with Rich markup.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
task_list: List of task dicts with 'description' and 'completed' keys
|
|
39
|
-
title: Optional title for the task list
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
Formatted task list string with Rich markup
|
|
43
|
-
"""
|
|
44
|
-
if not task_list:
|
|
45
|
-
return "exit_code=1\nerror: No task list exists. Use create_task_list first.\n\n"
|
|
46
|
-
|
|
47
|
-
safe_title = (title or "").strip() if isinstance(title, str) else ""
|
|
48
|
-
safe_title = safe_title[:constants.MAX_TASK_TITLE_LEN] if safe_title else "untitled"
|
|
49
|
-
|
|
50
|
-
done_count = sum(1 for t in task_list if t.get("completed"))
|
|
51
|
-
total = len(task_list)
|
|
52
|
-
all_done = done_count == total
|
|
53
|
-
|
|
54
|
-
# Escape user-provided text to prevent Rich markup injection
|
|
55
|
-
escaped_title = _escape_rich(safe_title)
|
|
56
|
-
|
|
57
|
-
# Header with progress
|
|
58
|
-
if all_done:
|
|
59
|
-
header = f"[bold green]\u2713[/bold green] [bold]{escaped_title}[/bold] [green]({done_count}/{total} done)[/green]"
|
|
60
|
-
else:
|
|
61
|
-
header = f"[bold]{escaped_title}[/bold] [dim]({done_count}/{total} done)[/dim]"
|
|
62
|
-
|
|
63
|
-
lines = [header]
|
|
64
|
-
|
|
65
|
-
# Indent for task lines: 2 spaces + bullet + space = 4 visible chars before description
|
|
66
|
-
TASK_INDENT = " "
|
|
67
|
-
BULLET_DONE = "[dim green]\u2713[/dim green]"
|
|
68
|
-
BULLET_PENDING = "[dim white]\u25cb[/dim white]"
|
|
69
|
-
# Visible width of bullet prefix: " ✓ " = 4 chars
|
|
70
|
-
# Continuation indent must match for alignment
|
|
71
|
-
DESC_INDENT = " " # 4 spaces, aligns with description after bullet
|
|
72
|
-
TASK_WRAP_WIDTH = 60 # Reasonable width for wrapped task descriptions
|
|
73
|
-
|
|
74
|
-
for i, task in enumerate(task_list):
|
|
75
|
-
is_done = bool(task.get("completed"))
|
|
76
|
-
desc = str(task.get("description", ""))
|
|
77
|
-
if len(desc) > constants.MAX_TASK_LEN:
|
|
78
|
-
desc = desc[:constants.MAX_TASK_LEN - 3] + "..."
|
|
79
|
-
escaped_desc = _escape_rich(desc)
|
|
80
|
-
|
|
81
|
-
if is_done:
|
|
82
|
-
bullet = BULLET_DONE
|
|
83
|
-
# Wrap description text only (no bullet), apply markup separately
|
|
84
|
-
desc_lines = textwrap.wrap(escaped_desc, width=TASK_WRAP_WIDTH - 4, break_long_words=True, break_on_hyphens=True)
|
|
85
|
-
if not desc_lines:
|
|
86
|
-
desc_lines = [escaped_desc]
|
|
87
|
-
# First line: bullet + struck-through description
|
|
88
|
-
first_line = f"{TASK_INDENT}{bullet} [dim strike]{desc_lines[0]}[/dim strike]"
|
|
89
|
-
lines.append(first_line)
|
|
90
|
-
# Continuation lines: indent + struck-through text (no bullet)
|
|
91
|
-
for dline in desc_lines[1:]:
|
|
92
|
-
lines.append(f"{DESC_INDENT}[dim strike]{dline}[/dim strike]")
|
|
93
|
-
else:
|
|
94
|
-
bullet = BULLET_PENDING
|
|
95
|
-
# Wrap long pending descriptions with proper indentation
|
|
96
|
-
desc_lines = textwrap.wrap(escaped_desc, width=TASK_WRAP_WIDTH - 4, break_long_words=True, break_on_hyphens=True)
|
|
97
|
-
if not desc_lines:
|
|
98
|
-
desc_lines = [escaped_desc]
|
|
99
|
-
first_line = f"{TASK_INDENT}{bullet} {desc_lines[0]}"
|
|
100
|
-
lines.append(first_line)
|
|
101
|
-
for dline in desc_lines[1:]:
|
|
102
|
-
lines.append(f"{DESC_INDENT}{dline}")
|
|
103
|
-
|
|
104
|
-
return "\n".join(lines) + "\n\n"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@tool(
|
|
108
|
-
name="create_task_list",
|
|
109
|
-
description="Create or replace an in-session task list for tracking long edit workflows.",
|
|
110
|
-
parameters={
|
|
111
|
-
"type": "object",
|
|
112
|
-
"properties": {
|
|
113
|
-
"tasks": {
|
|
114
|
-
"type": "array",
|
|
115
|
-
"items": {"type": "string"},
|
|
116
|
-
"description": "Task descriptions (non-empty after trimming)"
|
|
117
|
-
},
|
|
118
|
-
"title": {
|
|
119
|
-
"type": "string",
|
|
120
|
-
"description": "Short title summarizing the workflow (e.g. 'Add pagination to user API'). Always provide a meaningful title."
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"required": ["tasks", "title"]
|
|
124
|
-
},
|
|
125
|
-
requires_approval=False
|
|
126
|
-
)
|
|
127
|
-
def create_task_list(
|
|
128
|
-
tasks: List[str],
|
|
129
|
-
chat_manager,
|
|
130
|
-
title: str,
|
|
131
|
-
) -> str:
|
|
132
|
-
"""Create or replace an in-session task list.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
tasks: List of task descriptions
|
|
136
|
-
chat_manager: ChatManager instance (injected by context)
|
|
137
|
-
title: Title for the task list
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
Formatted task list result
|
|
141
|
-
"""
|
|
142
|
-
# Validate title
|
|
143
|
-
if not isinstance(title, str):
|
|
144
|
-
return "exit_code=1\nerror: 'title' must be a string.\n\n"
|
|
145
|
-
title = title.strip()
|
|
146
|
-
if not title:
|
|
147
|
-
return "exit_code=1\nerror: 'title' must be non-empty.\n\n"
|
|
148
|
-
title = title[:constants.MAX_TASK_TITLE_LEN]
|
|
149
|
-
|
|
150
|
-
# Normalize tasks
|
|
151
|
-
normalized = []
|
|
152
|
-
for i, task in enumerate(tasks):
|
|
153
|
-
if not isinstance(task, str):
|
|
154
|
-
return f"exit_code=1\nerror: Task at index {i} must be a string.\n\n"
|
|
155
|
-
trimmed = task.strip()
|
|
156
|
-
if not trimmed:
|
|
157
|
-
return f"exit_code=1\nerror: Task at index {i} must be non-empty.\n\n"
|
|
158
|
-
if len(trimmed) > constants.MAX_TASK_LEN:
|
|
159
|
-
return (
|
|
160
|
-
f"exit_code=1\nerror: Task at index {i} exceeds MAX_TASK_LEN={constants.MAX_TASK_LEN}.\n\n"
|
|
161
|
-
)
|
|
162
|
-
normalized.append(trimmed)
|
|
163
|
-
|
|
164
|
-
if len(normalized) == 0:
|
|
165
|
-
return "exit_code=1\nerror: Provide at least one non-empty task.\n\n"
|
|
166
|
-
if len(normalized) > constants.MAX_TASKS:
|
|
167
|
-
return f"exit_code=1\nerror: Too many tasks (max {constants.MAX_TASKS}).\n\n"
|
|
168
|
-
|
|
169
|
-
# Set task list on chat_manager
|
|
170
|
-
chat_manager.task_list = [
|
|
171
|
-
{"description": t, "completed": False}
|
|
172
|
-
for t in normalized
|
|
173
|
-
]
|
|
174
|
-
chat_manager.task_list_title = title or None
|
|
175
|
-
|
|
176
|
-
return _format_task_list(chat_manager.task_list, chat_manager.task_list_title)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
@tool(
|
|
180
|
-
name="complete_task",
|
|
181
|
-
description="Mark one or more tasks complete in the current task list.",
|
|
182
|
-
parameters={
|
|
183
|
-
"type": "object",
|
|
184
|
-
"properties": {
|
|
185
|
-
"task_id": {
|
|
186
|
-
"type": "integer",
|
|
187
|
-
"description": "Zero-based index of a single task to complete"
|
|
188
|
-
},
|
|
189
|
-
"task_ids": {
|
|
190
|
-
"type": "array",
|
|
191
|
-
"items": {"type": "integer"},
|
|
192
|
-
"description": "Zero-based task indices to complete"
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
requires_approval=False
|
|
197
|
-
)
|
|
198
|
-
def complete_task(
|
|
199
|
-
chat_manager,
|
|
200
|
-
task_id: Optional[int] = None,
|
|
201
|
-
task_ids: Optional[List[int]] = None
|
|
202
|
-
) -> str:
|
|
203
|
-
"""Mark one or more tasks as complete.
|
|
204
|
-
|
|
205
|
-
Args:
|
|
206
|
-
chat_manager: ChatManager instance (injected by context)
|
|
207
|
-
task_id: Single task index to mark complete
|
|
208
|
-
task_ids: Multiple task indices to mark complete
|
|
209
|
-
|
|
210
|
-
Returns:
|
|
211
|
-
Formatted task list result
|
|
212
|
-
"""
|
|
213
|
-
# Normalize to list: prefer task_ids if both provided
|
|
214
|
-
if task_ids is not None:
|
|
215
|
-
ids_raw = task_ids
|
|
216
|
-
elif task_id is not None:
|
|
217
|
-
ids_raw = [task_id]
|
|
218
|
-
else:
|
|
219
|
-
return "exit_code=1\nerror: Either 'task_id' or 'task_ids' must be provided.\n\n"
|
|
220
|
-
|
|
221
|
-
if not isinstance(ids_raw, list):
|
|
222
|
-
return "exit_code=1\nerror: IDs must be an array of integers.\n\n"
|
|
223
|
-
|
|
224
|
-
task_list = getattr(chat_manager, "task_list", None) or []
|
|
225
|
-
if not task_list:
|
|
226
|
-
return "exit_code=1\nerror: No task list exists. Use create_task_list first.\n\n"
|
|
227
|
-
|
|
228
|
-
# Validate all IDs
|
|
229
|
-
valid_ids = []
|
|
230
|
-
for i, tid in enumerate(ids_raw):
|
|
231
|
-
tid_int, error = coerce_int(tid)
|
|
232
|
-
if error:
|
|
233
|
-
return f"exit_code=1\nerror: ID at index {i}: {error}\n\n"
|
|
234
|
-
if tid_int < 0:
|
|
235
|
-
return f"exit_code=1\nerror: ID at index {i} must be non-negative.\n\n"
|
|
236
|
-
if tid_int >= len(task_list):
|
|
237
|
-
return (
|
|
238
|
-
f"exit_code=1\nerror: ID {tid_int} (index {i}) is out of range (0-{len(task_list) - 1}).\n\n"
|
|
239
|
-
)
|
|
240
|
-
valid_ids.append(tid_int)
|
|
241
|
-
|
|
242
|
-
# Mark tasks as complete
|
|
243
|
-
for tid in valid_ids:
|
|
244
|
-
task_list[tid]["completed"] = True
|
|
245
|
-
|
|
246
|
-
return _format_task_list(task_list, chat_manager.task_list_title)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
@tool(
|
|
250
|
-
name="show_task_list",
|
|
251
|
-
description="Show the current task list without modifying it.",
|
|
252
|
-
parameters={"type": "object", "properties": {}},
|
|
253
|
-
requires_approval=False
|
|
254
|
-
)
|
|
255
|
-
def show_task_list(
|
|
256
|
-
chat_manager
|
|
257
|
-
) -> str:
|
|
258
|
-
"""Display the current task list.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
chat_manager: ChatManager instance (injected by context)
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
Formatted task list result
|
|
265
|
-
"""
|
|
266
|
-
task_list = getattr(chat_manager, "task_list", None) or []
|
|
267
|
-
title = getattr(chat_manager, "task_list_title", None)
|
|
268
|
-
|
|
269
|
-
return _format_task_list(task_list, title)
|
package/src/tools/web_search.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""Web search tool using DuckDuckGo."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
from .helpers.base import tool
|
|
7
|
-
from utils.web_search import run_web_search
|
|
8
|
-
from exceptions import LLMConnectionError
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@tool(
|
|
12
|
-
name="web_search",
|
|
13
|
-
description="Search web for info, docs, and current events using DuckDuckGo (no API key needed). Automatically fetches and extracts full article content from top results.",
|
|
14
|
-
parameters={
|
|
15
|
-
"type": "object",
|
|
16
|
-
"properties": {
|
|
17
|
-
"query": {
|
|
18
|
-
"type": "string",
|
|
19
|
-
"description": "Search query to execute"
|
|
20
|
-
},
|
|
21
|
-
"num_results": {
|
|
22
|
-
"type": "integer",
|
|
23
|
-
"description": "Results to return (default: 5, max 10)"
|
|
24
|
-
},
|
|
25
|
-
"fetch_content": {
|
|
26
|
-
"type": "boolean",
|
|
27
|
-
"description": "Fetch full page content from top results (default: true). Set to false for URL/snippet only."
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"required": ["query"]
|
|
31
|
-
},
|
|
32
|
-
requires_approval=False
|
|
33
|
-
)
|
|
34
|
-
def web_search(
|
|
35
|
-
query: str,
|
|
36
|
-
console,
|
|
37
|
-
num_results: Optional[int] = None,
|
|
38
|
-
fetch_content: bool = True
|
|
39
|
-
) -> str:
|
|
40
|
-
"""Search the web using DuckDuckGo with optional full content extraction.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
query: Search query to execute
|
|
44
|
-
console: Rich console for output (injected by context)
|
|
45
|
-
num_results: Number of results to return (default: 5, max: 10)
|
|
46
|
-
fetch_content: Whether to fetch full page content (default: true)
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
Formatted search results with content
|
|
50
|
-
"""
|
|
51
|
-
arguments = {"query": query}
|
|
52
|
-
if num_results is not None:
|
|
53
|
-
arguments["num_results"] = num_results
|
|
54
|
-
arguments["fetch_content"] = fetch_content
|
|
55
|
-
|
|
56
|
-
try:
|
|
57
|
-
return run_web_search(arguments, console)
|
|
58
|
-
except LLMConnectionError as e:
|
|
59
|
-
return f"exit_code=1\nWeb search failed: {e}"
|
|
60
|
-
except Exception as e:
|
|
61
|
-
return f"exit_code=1\nWeb search failed: {str(e)}"
|
package/src/ui/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""User interface layer for bone-agent."""
|
package/src/ui/banner.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Startup banner display - separated from main.py to avoid circular imports."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from rich.console import Console
|
|
6
|
-
from rich.panel import Panel
|
|
7
|
-
from rich.text import Text
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
import json
|
|
10
|
-
from llm import config
|
|
11
|
-
|
|
12
|
-
console = Console()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def format_directory_path(path: str) -> str:
|
|
16
|
-
"""Format directory path to show first and last parts with ellipsis.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
path: Full directory path.
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
Shortened path like 'c:/.../bone-agent' or full path if short enough.
|
|
23
|
-
"""
|
|
24
|
-
parts = path.split(os.sep)
|
|
25
|
-
if len(parts) > 2:
|
|
26
|
-
return f"{parts[0]}.../{parts[-1]}"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _get_version() -> str:
|
|
30
|
-
"""Read version from package.json (single source of truth)."""
|
|
31
|
-
try:
|
|
32
|
-
pkg_path = Path(__file__).resolve().parent.parent.parent / "package.json"
|
|
33
|
-
with open(pkg_path) as f:
|
|
34
|
-
return json.load(f)["version"]
|
|
35
|
-
except Exception:
|
|
36
|
-
return "?.?.?"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def display_startup_banner(approve_mode: str, *, clear_screen: bool = False):
|
|
40
|
-
"""Ultra-minimalist startup screen for bone-agent.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
approve_mode: Current approval mode setting.
|
|
44
|
-
clear_screen: If True, clear the terminal before rendering.
|
|
45
|
-
"""
|
|
46
|
-
if clear_screen:
|
|
47
|
-
console.clear()
|
|
48
|
-
# Get model name based on provider
|
|
49
|
-
provider_config = config.get_provider_config(config.LLM_PROVIDER)
|
|
50
|
-
if config.LLM_PROVIDER == "local":
|
|
51
|
-
model_path = provider_config.get("model") or ""
|
|
52
|
-
model_name = os.path.basename(model_path) if model_path else "None"
|
|
53
|
-
else:
|
|
54
|
-
model_name = provider_config.get("model") or "None"
|
|
55
|
-
|
|
56
|
-
# Get and format current directory
|
|
57
|
-
current_dir = os.getcwd()
|
|
58
|
-
formatted_dir = format_directory_path(current_dir)
|
|
59
|
-
|
|
60
|
-
# Create grid for 2-column layout
|
|
61
|
-
grid = Table.grid(expand=True)
|
|
62
|
-
grid.add_column(justify="left", ratio=1)
|
|
63
|
-
grid.add_column(justify="right", ratio=1)
|
|
64
|
-
|
|
65
|
-
# Add content
|
|
66
|
-
grid.add_row(
|
|
67
|
-
Text("bone", style="bold white"),
|
|
68
|
-
Text(f"v{_get_version()}", style="dim white")
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
model_info = Text.assemble(
|
|
72
|
-
(f"{config.LLM_PROVIDER.upper()} ", "bold #5F9EA0"),
|
|
73
|
-
(f"{model_name}", "grey70")
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
grid.add_row(
|
|
77
|
-
model_info,
|
|
78
|
-
Text(formatted_dir, style="dim grey50")
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
# Display in panel
|
|
82
|
-
console.print(Panel(
|
|
83
|
-
grid,
|
|
84
|
-
border_style="grey23",
|
|
85
|
-
padding=(0, 2)
|
|
86
|
-
))
|
|
87
|
-
|