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
@@ -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)
@@ -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)}"
@@ -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
-