bone-agent 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/npm-wrapper.js +235 -0
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +133 -0
- package/package.json +53 -0
- package/requirements.txt +9 -0
- package/src/__init__.py +11 -0
- package/src/core/__init__.py +1 -0
- package/src/core/agentic.py +1054 -0
- package/src/core/chat_manager.py +1552 -0
- package/src/core/config_manager.py +247 -0
- package/src/core/cron.py +527 -0
- package/src/core/cron_allowlist.py +118 -0
- package/src/core/memory.py +232 -0
- package/src/core/retry.py +71 -0
- package/src/core/sub_agent.py +326 -0
- package/src/core/tool_approval.py +220 -0
- package/src/core/tool_feedback.py +778 -0
- package/src/exceptions.py +79 -0
- package/src/llm/__init__.py +1 -0
- package/src/llm/client.py +171 -0
- package/src/llm/config.py +466 -0
- package/src/llm/prompts.py +735 -0
- package/src/llm/providers.py +417 -0
- package/src/llm/streaming.py +163 -0
- package/src/llm/token_tracker.py +368 -0
- package/src/tools/__init__.py +212 -0
- package/src/tools/constants.py +59 -0
- package/src/tools/create_file.py +136 -0
- package/src/tools/directory.py +389 -0
- package/src/tools/edit.py +543 -0
- package/src/tools/file_reader.py +322 -0
- package/src/tools/helpers/__init__.py +105 -0
- package/src/tools/helpers/base.py +550 -0
- package/src/tools/helpers/converters.py +44 -0
- package/src/tools/helpers/file_helpers.py +189 -0
- package/src/tools/helpers/formatters.py +411 -0
- package/src/tools/helpers/loader.py +231 -0
- package/src/tools/helpers/parallel_executor.py +231 -0
- package/src/tools/helpers/path_resolver.py +226 -0
- package/src/tools/helpers/plugin_manifest.py +156 -0
- package/src/tools/obsidian.py +96 -0
- package/src/tools/review_sub_agent.py +189 -0
- package/src/tools/rg_search.py +393 -0
- package/src/tools/search_plugins.py +109 -0
- package/src/tools/select_option.py +593 -0
- package/src/tools/shell.py +302 -0
- package/src/tools/sub_agent.py +139 -0
- package/src/tools/task_list.py +269 -0
- package/src/tools/web_search.py +61 -0
- package/src/ui/__init__.py +1 -0
- package/src/ui/banner.py +87 -0
- package/src/ui/commands.py +2694 -0
- package/src/ui/displays.py +213 -0
- package/src/ui/loader.py +284 -0
- package/src/ui/main.py +646 -0
- package/src/ui/prompt_utils.py +113 -0
- package/src/ui/setting_selector.py +590 -0
- package/src/ui/setup_wizard.py +294 -0
- package/src/ui/sub_agent_panel.py +234 -0
- package/src/ui/tool_confirmation.py +215 -0
- package/src/utils/__init__.py +1 -0
- package/src/utils/citation_parser.py +199 -0
- package/src/utils/editor.py +158 -0
- package/src/utils/gitignore_filter.py +149 -0
- package/src/utils/logger.py +254 -0
- package/src/utils/paths.py +30 -0
- package/src/utils/result_parsers.py +108 -0
- package/src/utils/safe_commands.py +243 -0
- package/src/utils/settings.py +174 -0
- package/src/utils/validation.py +191 -0
- package/src/utils/web_search.py +173 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Web search using DuckDuckGo (no API key required)."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
from readability import Document
|
|
6
|
+
import html2text
|
|
7
|
+
|
|
8
|
+
from ddgs import DDGS
|
|
9
|
+
from exceptions import LLMConnectionError
|
|
10
|
+
|
|
11
|
+
# Number of top results to fetch full content from
|
|
12
|
+
_DEFAULT_FETCH_COUNT = 3
|
|
13
|
+
# Max characters per fetched page to avoid context bloat
|
|
14
|
+
_MAX_CONTENT_LENGTH = 8000
|
|
15
|
+
# HTTP timeout for page fetching (seconds)
|
|
16
|
+
_FETCH_TIMEOUT = 10
|
|
17
|
+
# Delay between page fetches to avoid rate limiting (seconds)
|
|
18
|
+
_FETCH_DELAY = 1.0
|
|
19
|
+
# User agent for page fetching
|
|
20
|
+
_USER_AGENT = "Mozilla/5.0 (compatible; bone-agent/1.0; +https://github.com/vincentm65/bone-agent-cli)"
|
|
21
|
+
|
|
22
|
+
def _fetch_page_content(url, console=None):
|
|
23
|
+
"""Fetch a URL and extract main article content as markdown.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
url: URL to fetch
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: Extracted markdown content, or empty string on failure
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
response = requests.get(
|
|
33
|
+
url,
|
|
34
|
+
headers={"User-Agent": _USER_AGENT},
|
|
35
|
+
timeout=_FETCH_TIMEOUT,
|
|
36
|
+
allow_redirects=True
|
|
37
|
+
)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
|
|
40
|
+
# Skip non-HTML content (PDFs, images, JSON APIs, etc.)
|
|
41
|
+
content_type = response.headers.get("content-type", "")
|
|
42
|
+
if "text/html" not in content_type and "text/plain" not in content_type:
|
|
43
|
+
if console:
|
|
44
|
+
console.print(f" [dim]Skipped {url} (non-HTML: {content_type})[/dim]")
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
# Check for empty response before parsing
|
|
48
|
+
if not response.text or not response.text.strip():
|
|
49
|
+
if console:
|
|
50
|
+
console.print(f" [dim]Empty response from {url}[/dim]")
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
# Use readability to extract the main article content
|
|
54
|
+
doc = Document(response.text)
|
|
55
|
+
summary_html = doc.summary()
|
|
56
|
+
|
|
57
|
+
# Convert cleaned HTML to markdown (per-call instance for thread safety)
|
|
58
|
+
md = html2text.HTML2Text()
|
|
59
|
+
md.ignore_links = False
|
|
60
|
+
md.ignore_images = True
|
|
61
|
+
md.body_width = 0
|
|
62
|
+
content = md.handle(summary_html).strip()
|
|
63
|
+
|
|
64
|
+
# Truncate at last newline/whitespace before limit to avoid mid-word splits
|
|
65
|
+
if len(content) > _MAX_CONTENT_LENGTH:
|
|
66
|
+
cutoff = content.rfind("\n", 0, _MAX_CONTENT_LENGTH)
|
|
67
|
+
if cutoff < _MAX_CONTENT_LENGTH * 0.8:
|
|
68
|
+
cutoff = _MAX_CONTENT_LENGTH
|
|
69
|
+
content = content[:cutoff] + "\n\n[... content truncated]"
|
|
70
|
+
|
|
71
|
+
return content
|
|
72
|
+
|
|
73
|
+
except requests.RequestException as e:
|
|
74
|
+
if console:
|
|
75
|
+
console.print(f" [dim]Failed to fetch {url}: {e}[/dim]")
|
|
76
|
+
return ""
|
|
77
|
+
except Exception as e:
|
|
78
|
+
if console:
|
|
79
|
+
console.print(f" [dim]Failed to parse {url}: {e}[/dim]")
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run_web_search(arguments, console):
|
|
84
|
+
"""Execute web search using DuckDuckGo and return formatted results.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
arguments: {
|
|
88
|
+
"query": "search terms to look for",
|
|
89
|
+
"num_results": 5, # optional, number of results (default: 5, max: 10)
|
|
90
|
+
"fetch_content": true # optional, fetch full page content (default: true)
|
|
91
|
+
}
|
|
92
|
+
console: Rich console for output
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: Formatted search results with metadata for model consumption
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
LLMConnectionError: If network search fails
|
|
99
|
+
"""
|
|
100
|
+
query = arguments.get("query")
|
|
101
|
+
num_results = arguments.get("num_results", 5)
|
|
102
|
+
fetch_content = arguments.get("fetch_content", True)
|
|
103
|
+
|
|
104
|
+
if not query:
|
|
105
|
+
raise LLMConnectionError(
|
|
106
|
+
"Missing required parameter: query",
|
|
107
|
+
details={"arguments": arguments}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Validate and clamp num_results between 1 and 10
|
|
111
|
+
try:
|
|
112
|
+
num_results = max(1, min(10, int(num_results)))
|
|
113
|
+
except (ValueError, TypeError):
|
|
114
|
+
num_results = 5
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with DDGS() as ddgs:
|
|
118
|
+
results = list(ddgs.text(query, max_results=num_results))
|
|
119
|
+
|
|
120
|
+
if not results:
|
|
121
|
+
return "results_found=0\nNo results found.\n\n"
|
|
122
|
+
|
|
123
|
+
# Determine how many results to fetch content from
|
|
124
|
+
fetch_count = min(_DEFAULT_FETCH_COUNT, len(results)) if fetch_content else 0
|
|
125
|
+
pages_fetched = 0
|
|
126
|
+
pages_failed = 0
|
|
127
|
+
|
|
128
|
+
# Format results for model
|
|
129
|
+
output_lines = []
|
|
130
|
+
for idx, result in enumerate(results, 1):
|
|
131
|
+
title = result.get("title", "Untitled")
|
|
132
|
+
url = result.get("href", "N/A")
|
|
133
|
+
body = result.get("body", "No content")
|
|
134
|
+
|
|
135
|
+
output_lines.append(f"[{idx}] {title}")
|
|
136
|
+
output_lines.append(f"URL: {url}")
|
|
137
|
+
output_lines.append(f"Snippet: {body}")
|
|
138
|
+
|
|
139
|
+
# Fetch full content for top results
|
|
140
|
+
if fetch_content and idx <= fetch_count:
|
|
141
|
+
content = _fetch_page_content(url, console)
|
|
142
|
+
if content:
|
|
143
|
+
output_lines.append(f"\n--- Content ---\n{content}")
|
|
144
|
+
pages_fetched += 1
|
|
145
|
+
else:
|
|
146
|
+
output_lines.append(f"\n[Failed to fetch page content]")
|
|
147
|
+
pages_failed += 1
|
|
148
|
+
|
|
149
|
+
# Rate limiting: delay between fetches
|
|
150
|
+
if idx < fetch_count:
|
|
151
|
+
time.sleep(_FETCH_DELAY)
|
|
152
|
+
|
|
153
|
+
if idx < len(results):
|
|
154
|
+
output_lines.append("")
|
|
155
|
+
|
|
156
|
+
# Build result string with metadata for model
|
|
157
|
+
result_content = "\n".join(output_lines)
|
|
158
|
+
meta = f"results_found={len(results)}"
|
|
159
|
+
if fetch_content:
|
|
160
|
+
meta += f", pages_fetched={pages_fetched}"
|
|
161
|
+
if pages_failed:
|
|
162
|
+
meta += f", pages_failed={pages_failed}"
|
|
163
|
+
return f"{meta}\n{result_content}\n\n"
|
|
164
|
+
|
|
165
|
+
except LLMConnectionError:
|
|
166
|
+
# Re-raise our custom exceptions
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
console.print(f"Web search failed: {e}", style="red")
|
|
170
|
+
raise LLMConnectionError(
|
|
171
|
+
f"Failed to perform web search",
|
|
172
|
+
details={"query": query, "original_error": str(e)}
|
|
173
|
+
)
|