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,247 @@
1
+
2
+
3
+ from pathlib import Path
4
+ import shutil
5
+ from typing import Dict, Any, Optional
6
+ import logging
7
+ import yaml
8
+ from llm import config as llm_config
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ConfigManager:
14
+
15
+ def __init__(self, config_path: Optional[Path] = None):
16
+ self.config_path = config_path or llm_config.CONFIG_PATH
17
+ self._cached_data = None
18
+
19
+ def load(self, force_reload: bool = False) -> Dict[str, Any]:
20
+ """Load configuration from file, using cache if available.
21
+
22
+ Args:
23
+ force_reload: If True, bypass cache and reload from disk
24
+
25
+ Returns:
26
+ Configuration dictionary
27
+ """
28
+ if not force_reload and self._cached_data is not None:
29
+ return self._cached_data
30
+
31
+ if not self.config_path.exists():
32
+ self._cached_data = llm_config.generate_config_template()
33
+ return self._cached_data
34
+
35
+ try:
36
+ with open(self.config_path, 'r', encoding='utf-8-sig') as f:
37
+ self._cached_data = yaml.safe_load(f) or {}
38
+
39
+ # Migrate: rename old provider IDs -> bone (provider ID rename)
40
+ old_provider = self._cached_data.get('LAST_PROVIDER')
41
+ if old_provider in ('vmcode_proxy', 'vmcode_free', 'vmcode'):
42
+ logger.info("Migrating provider name '%s' -> 'bone'", old_provider)
43
+ self._cached_data['LAST_PROVIDER'] = 'bone'
44
+ self.save(self._cached_data, create_backup=True)
45
+
46
+ return self._cached_data
47
+ except yaml.YAMLError as e:
48
+ logger.error(f"Failed to parse config file {self.config_path}: {e}")
49
+ logger.warning("Using default configuration template")
50
+ self._cached_data = llm_config.generate_config_template()
51
+ return self._cached_data
52
+
53
+ def save(self, config_data: Dict[str, Any], create_backup: bool = False):
54
+ if create_backup and self.config_path.exists():
55
+ backup_path = self.config_path.with_suffix('.backup')
56
+ shutil.copy2(self.config_path, backup_path)
57
+
58
+ with open(self.config_path, 'w', encoding='utf-8-sig') as f:
59
+ yaml.dump(config_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
60
+
61
+ self._cached_data = config_data
62
+ # Note: Config is read from disk on reload. Call reload_config() after changes.
63
+
64
+ def update_field(self, key: str, value: Any, create_backup: bool = False) -> Optional[Path]:
65
+ """Update a single configuration field.
66
+
67
+ Args:
68
+ key: Configuration key to update
69
+ value: New value for the key
70
+ create_backup: If True, create a backup before saving
71
+
72
+ Returns:
73
+ Backup path if backup was created, None otherwise
74
+ """
75
+ config_data = self.load(force_reload=True)
76
+ config_data[key] = value
77
+
78
+ backup_path = None
79
+ if create_backup and self.config_path.exists():
80
+ backup_path = self.config_path.with_suffix('.backup')
81
+
82
+ self.save(config_data, create_backup=create_backup)
83
+ return backup_path
84
+
85
+ def set_provider(self, provider_name: str) -> Optional[Path]:
86
+ return self.update_field('LAST_PROVIDER', provider_name)
87
+
88
+ def _extract_model_pricing(self, model_name: str, config: Dict[str, Any] | None = None) -> Dict[str, float]:
89
+ """Extract pricing for a model from MODEL_PRICES.
90
+
91
+ Delegates to llm_config.get_model_cost() — single source of truth.
92
+ """
93
+ cost_in, cost_out = llm_config.get_model_cost(model_name, config=config)
94
+ return {'in': cost_in, 'out': cost_out}
95
+
96
+ def get_usage_costs(self, provider: str = None, model: str = None) -> Dict[str, float]:
97
+ """Get usage costs for a specific model.
98
+
99
+ Args:
100
+ provider: Provider name (e.g., 'openrouter', 'glm', 'openai').
101
+ If None, uses the last provider from config.
102
+ model: Model name (e.g., 'minimax/minimax-m2.5', 'GLM-4.7').
103
+ If None, uses the current model from the provider.
104
+
105
+ Returns:
106
+ Dict with 'in' and 'out' cost values per 1M tokens
107
+ """
108
+ config_data = self.load()
109
+
110
+ if provider is None:
111
+ provider = config_data.get('LAST_PROVIDER', 'glm')
112
+
113
+ # Get model name from config if not provided
114
+ if model is None:
115
+ provider_model_map = {
116
+ 'bone': 'BONE_PROXY_MODEL',
117
+ 'openrouter': 'OPENROUTER_MODEL',
118
+ 'glm': 'GLM_MODEL',
119
+ 'openai': 'OPENAI_MODEL',
120
+ 'gemini': 'GEMINI_MODEL',
121
+ 'minimax': 'MINIMAX_MODEL',
122
+ 'anthropic': 'ANTHROPIC_MODEL',
123
+ 'kimi': 'KIMI_MODEL'
124
+ }
125
+ model_key = provider_model_map.get(provider.lower())
126
+ if model_key:
127
+ model = config_data.get(model_key, '')
128
+
129
+ return self._extract_model_pricing(model, config=config_data)
130
+
131
+ def set_model(self, provider_name: str, model: str) -> Optional[Path]:
132
+ """Set model for a specific provider.
133
+
134
+ Args:
135
+ provider_name: Provider name (e.g., 'openrouter', 'glm', 'local', 'openai')
136
+ model: Model name/path to set
137
+
138
+ Returns:
139
+ Backup path if backup was created, None otherwise
140
+ """
141
+ # Map provider names to their config keys
142
+ provider_keys = {
143
+ 'local': 'LOCAL_MODEL_PATH',
144
+ 'bone': 'BONE_PROXY_MODEL',
145
+ 'openrouter': 'OPENROUTER_MODEL',
146
+ 'glm': 'GLM_MODEL',
147
+ 'openai': 'OPENAI_MODEL',
148
+ 'gemini': 'GEMINI_MODEL',
149
+ 'minimax': 'MINIMAX_MODEL',
150
+ 'anthropic': 'ANTHROPIC_MODEL',
151
+ 'kimi': 'KIMI_MODEL'
152
+ }
153
+
154
+ if provider_name not in provider_keys:
155
+ raise ValueError(f"Unknown provider: {provider_name}")
156
+
157
+ key = provider_keys[provider_name]
158
+ return self.update_field(key, model)
159
+
160
+ def set_api_key(self, provider_name: str, api_key: str) -> Optional[Path]:
161
+ """Set API key for a specific provider.
162
+
163
+ Args:
164
+ provider_name: Provider name (e.g., 'openrouter', 'glm', 'openai')
165
+ api_key: API key to set
166
+
167
+ Returns:
168
+ Backup path if backup was created, None otherwise
169
+ """
170
+ # Map provider names to their config keys
171
+ provider_keys = {
172
+ 'openrouter': 'OPENROUTER_API_KEY',
173
+ 'bone': 'BONE_PROXY_API_KEY',
174
+ 'glm': 'GLM_API_KEY',
175
+ 'openai': 'OPENAI_API_KEY',
176
+ 'gemini': 'GEMINI_API_KEY',
177
+ 'minimax': 'MINIMAX_API_KEY',
178
+ 'anthropic': 'ANTHROPIC_API_KEY',
179
+ 'kimi': 'KIMI_API_KEY'
180
+ }
181
+
182
+ if provider_name not in provider_keys:
183
+ raise ValueError(f"Unknown provider: {provider_name}")
184
+
185
+ key = provider_keys[provider_name]
186
+ return self.update_field(key, api_key)
187
+
188
+ def get_model_price(self, model_name: str) -> Dict[str, float]:
189
+ """Get pricing for a specific model.
190
+
191
+ Args:
192
+ model_name: Model name (e.g., 'minimax/minimax-m2.5', 'GLM-4.7')
193
+
194
+ Returns:
195
+ Dict with 'in' and 'out' cost values per 1M tokens
196
+ """
197
+ config_data = self.load()
198
+ return self._extract_model_pricing(model_name, config=config_data)
199
+
200
+ def set_model_price(self, model_name: str, cost_in: float, cost_out: float) -> Optional[Path]:
201
+ """Set pricing for a specific model.
202
+
203
+ Args:
204
+ model_name: Model name (e.g., 'minimax/minimax-m2.5', 'GLM-4.7')
205
+ cost_in: Cost per 1M input tokens
206
+ cost_out: Cost per 1M output tokens
207
+
208
+ Returns:
209
+ Backup path if backup was created, None otherwise
210
+ """
211
+ config_data = self.load(force_reload=True)
212
+
213
+ if 'MODEL_PRICES' not in config_data:
214
+ config_data['MODEL_PRICES'] = {}
215
+
216
+ config_data['MODEL_PRICES'][model_name] = {
217
+ 'cost_in': cost_in,
218
+ 'cost_out': cost_out
219
+ }
220
+
221
+ return self.save(config_data, create_backup=False)
222
+
223
+ def list_model_prices(self) -> Dict[str, Dict[str, float]]:
224
+ """Get all model-specific pricing.
225
+
226
+ Returns:
227
+ Dict mapping model names to their pricing (cost_in/cost_out per 1M tokens)
228
+ """
229
+ config_data = self.load()
230
+ return config_data.get('MODEL_PRICES', {})
231
+
232
+ def delete_model_price(self, model_name: str) -> Optional[Path]:
233
+ """Delete pricing for a specific model.
234
+
235
+ Args:
236
+ model_name: Model name to remove from pricing
237
+
238
+ Returns:
239
+ Backup path if backup was created, None otherwise
240
+ """
241
+ config_data = self.load(force_reload=True)
242
+
243
+ if 'MODEL_PRICES' in config_data and model_name in config_data['MODEL_PRICES']:
244
+ del config_data['MODEL_PRICES'][model_name]
245
+ return self.save(config_data, create_backup=False)
246
+
247
+ return None