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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/npm-wrapper.js +235 -0
  4. package/bin/rg +0 -0
  5. package/bin/rg.exe +0 -0
  6. package/config.yaml.example +133 -0
  7. package/package.json +53 -0
  8. package/requirements.txt +9 -0
  9. package/src/__init__.py +11 -0
  10. package/src/core/__init__.py +1 -0
  11. package/src/core/agentic.py +1054 -0
  12. package/src/core/chat_manager.py +1552 -0
  13. package/src/core/config_manager.py +247 -0
  14. package/src/core/cron.py +527 -0
  15. package/src/core/cron_allowlist.py +118 -0
  16. package/src/core/memory.py +232 -0
  17. package/src/core/retry.py +71 -0
  18. package/src/core/sub_agent.py +326 -0
  19. package/src/core/tool_approval.py +220 -0
  20. package/src/core/tool_feedback.py +778 -0
  21. package/src/exceptions.py +79 -0
  22. package/src/llm/__init__.py +1 -0
  23. package/src/llm/client.py +171 -0
  24. package/src/llm/config.py +466 -0
  25. package/src/llm/prompts.py +735 -0
  26. package/src/llm/providers.py +417 -0
  27. package/src/llm/streaming.py +163 -0
  28. package/src/llm/token_tracker.py +368 -0
  29. package/src/tools/__init__.py +212 -0
  30. package/src/tools/constants.py +59 -0
  31. package/src/tools/create_file.py +136 -0
  32. package/src/tools/directory.py +389 -0
  33. package/src/tools/edit.py +543 -0
  34. package/src/tools/file_reader.py +322 -0
  35. package/src/tools/helpers/__init__.py +105 -0
  36. package/src/tools/helpers/base.py +550 -0
  37. package/src/tools/helpers/converters.py +44 -0
  38. package/src/tools/helpers/file_helpers.py +189 -0
  39. package/src/tools/helpers/formatters.py +411 -0
  40. package/src/tools/helpers/loader.py +231 -0
  41. package/src/tools/helpers/parallel_executor.py +231 -0
  42. package/src/tools/helpers/path_resolver.py +226 -0
  43. package/src/tools/helpers/plugin_manifest.py +156 -0
  44. package/src/tools/obsidian.py +96 -0
  45. package/src/tools/review_sub_agent.py +189 -0
  46. package/src/tools/rg_search.py +393 -0
  47. package/src/tools/search_plugins.py +109 -0
  48. package/src/tools/select_option.py +593 -0
  49. package/src/tools/shell.py +302 -0
  50. package/src/tools/sub_agent.py +139 -0
  51. package/src/tools/task_list.py +269 -0
  52. package/src/tools/web_search.py +61 -0
  53. package/src/ui/__init__.py +1 -0
  54. package/src/ui/banner.py +87 -0
  55. package/src/ui/commands.py +2694 -0
  56. package/src/ui/displays.py +213 -0
  57. package/src/ui/loader.py +284 -0
  58. package/src/ui/main.py +646 -0
  59. package/src/ui/prompt_utils.py +113 -0
  60. package/src/ui/setting_selector.py +590 -0
  61. package/src/ui/setup_wizard.py +294 -0
  62. package/src/ui/sub_agent_panel.py +234 -0
  63. package/src/ui/tool_confirmation.py +215 -0
  64. package/src/utils/__init__.py +1 -0
  65. package/src/utils/citation_parser.py +199 -0
  66. package/src/utils/editor.py +158 -0
  67. package/src/utils/gitignore_filter.py +149 -0
  68. package/src/utils/logger.py +254 -0
  69. package/src/utils/paths.py +30 -0
  70. package/src/utils/result_parsers.py +108 -0
  71. package/src/utils/safe_commands.py +243 -0
  72. package/src/utils/settings.py +174 -0
  73. package/src/utils/validation.py +191 -0
  74. 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
+ )