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,399 +0,0 @@
1
- """Token usage tracking for chat sessions."""
2
-
3
- from llm.config import get_model_cost
4
-
5
-
6
- def usage_with_cost(response: dict) -> dict:
7
- """Extract usage dict from an LLM response, optionally including cost.
8
-
9
- Copies 'usage' (which may contain 'cost' for OpenRouter-style responses) and
10
- promotes a top-level 'cost' field (some providers) into the usage dict.
11
- This ensures any upstream-reported cost is captured regardless of location.
12
-
13
- Reduces repeated copy-and-merge boilerplate across call sites.
14
-
15
- Args:
16
- response: LLM response dict containing 'usage' (and optionally 'cost').
17
-
18
- Returns:
19
- dict with usage fields; empty dict if response has no 'usage'.
20
- """
21
- usage = dict(response.get("usage", {}))
22
- # Top-level cost takes precedence (some providers place it here)
23
- if "cost" in response:
24
- usage["cost"] = response["cost"]
25
- return usage
26
-
27
-
28
- class TokenTracker:
29
- """Tracks token usage across a chat session."""
30
-
31
- def __init__(self):
32
- self.total_prompt_tokens = 0 # Cumulative input tokens (never reset by compaction)
33
- self.total_completion_tokens = 0 # Cumulative output tokens (never reset by compaction)
34
- self.total_tokens = 0 # Cumulative total tokens (never reset by compaction)
35
-
36
- # Conversation tokens: per-conversation billing (reset on /new)
37
- self.conv_prompt_tokens = 0 # Current conversation input tokens
38
- self.conv_completion_tokens = 0 # Current conversation output tokens
39
- self.conv_total_tokens = 0 # Current conversation total tokens
40
-
41
- # Context tokens: current conversation length (all messages in context)
42
- self.current_context_tokens = 0 # Updated via set_context_tokens()
43
-
44
- # Upstream-reported cost (e.g. OpenRouter's actual cost per request)
45
- self.total_actual_cost = 0.0 # Cumulative upstream-reported cost (never reset by compaction)
46
- self.conv_actual_cost = 0.0 # Per-conversation upstream-reported cost (reset on /new)
47
-
48
- # Config-estimated cost (fallback when upstream cost is absent)
49
- self.total_estimated_cost = 0.0 # Cumulative estimated cost (never reset by compaction)
50
- self.conv_estimated_cost = 0.0 # Per-conversation estimated cost (reset on /new)
51
-
52
- # Cache tokens: tracked when providers return cache breakdowns
53
- # Only input tokens can be cached (no output caching in any known API)
54
- self.total_cache_read_tokens = 0 # Cumulative input tokens read from cache
55
- self.total_cache_creation_tokens = 0 # Cumulative input tokens written to cache
56
- self.conv_cache_read_tokens = 0 # Per-conversation cache read tokens
57
- self.conv_cache_creation_tokens = 0 # Per-conversation cache creation tokens
58
-
59
- # Last usage payload diagnostics (useful for debugging provider reporting gaps)
60
- self.last_usage_snapshot = None
61
- self.last_usage_keys = []
62
- self.last_cache_metrics_reported = None
63
-
64
- # Active prompt variant (loaded from prompts/ directory)
65
- self.current_variant = "main"
66
-
67
- def add_usage(self, usage_data, model_name: str = ""):
68
- """Add token usage from an API response.
69
-
70
- Accepts either a full LLM response dict (non-streaming) or a pre-extracted
71
- usage dict (streaming). Full responses are normalized internally via
72
- usage_with_cost() to extract usage fields and promote top-level cost.
73
-
74
- Cost is resolved internally:
75
- 1. Upstream-reported cost (e.g. OpenRouter's response['usage']['cost']) — most accurate
76
- 2. Config-based fallback (tokens × rates from MODEL_PRICES) — used when upstream cost is absent
77
-
78
- Args:
79
- usage_data: Full LLM response dict (with 'usage' key) or pre-extracted
80
- usage dict (with 'prompt_tokens', 'completion_tokens').
81
- May also contain 'cost' (upstream-reported actual cost).
82
- model_name: Model name for config-based cost lookup (used as fallback).
83
- """
84
- if not usage_data or not isinstance(usage_data, dict):
85
- return
86
-
87
- # Normalize: full response dicts (non-streaming) have usage nested under
88
- # a 'usage' key with cost possibly at the top level. Extract and merge.
89
- # Pre-extracted usage dicts (streaming) pass through unchanged.
90
- if "usage" in usage_data:
91
- usage_data = usage_with_cost(usage_data)
92
-
93
- self.last_usage_snapshot = dict(usage_data)
94
- self.last_usage_keys = sorted(usage_data.keys())
95
- details = usage_data.get('prompt_tokens_details')
96
- self.last_cache_metrics_reported = (
97
- usage_data.get('cache_read_input_tokens') is not None
98
- or usage_data.get('cache_creation_input_tokens') is not None
99
- or usage_data.get('cached_tokens') is not None
100
- or (isinstance(details, dict) and details.get('cached_tokens') is not None)
101
- )
102
-
103
- # Update cumulative token counts (accumulated for billing, never reset by compaction)
104
- prompt_tokens = usage_data.get('prompt_tokens', 0)
105
- completion_tokens = usage_data.get('completion_tokens', 0)
106
- self.total_prompt_tokens += prompt_tokens
107
- self.total_completion_tokens += completion_tokens
108
- self.total_tokens += prompt_tokens + completion_tokens
109
-
110
- # Update conversation token counts (reset on /new)
111
- self.conv_prompt_tokens += prompt_tokens
112
- self.conv_completion_tokens += completion_tokens
113
- self.conv_total_tokens += prompt_tokens + completion_tokens
114
-
115
- # Extract cache tokens from provider responses (if available)
116
- # Anthropic: cache_read_input_tokens, cache_creation_input_tokens
117
- # OpenAI: prompt_tokens_details.cached_tokens
118
- # Use explicit is-not-None checks to avoid treating 0 as falsy
119
- cache_read = usage_data.get('cache_read_input_tokens')
120
- if cache_read is None:
121
- cache_read = usage_data.get('cached_tokens')
122
- if cache_read is None:
123
- details = usage_data.get('prompt_tokens_details')
124
- cache_read = details.get('cached_tokens') if details else None
125
- cache_read = cache_read or 0
126
-
127
- cache_creation = usage_data.get('cache_creation_input_tokens', 0)
128
- self.total_cache_read_tokens += cache_read
129
- self.total_cache_creation_tokens += cache_creation
130
- self.conv_cache_read_tokens += cache_read
131
- self.conv_cache_creation_tokens += cache_creation
132
-
133
- # Record cost: upstream-reported takes priority; compute from config as fallback
134
- upstream_cost = usage_data.get('cost')
135
- if upstream_cost is not None:
136
- try:
137
- self.add_actual_cost(float(upstream_cost))
138
- except (ValueError, TypeError):
139
- pass
140
- else:
141
- # Fallback: look up cost rates from config
142
- cost_in, cost_out = get_model_cost(model_name)
143
- if cost_in > 0 or cost_out > 0:
144
- # Compute the billable (non-cache) input token count for cost
145
- # estimation. Providers normalize `prompt_tokens` differently:
146
- # - Anthropic handler: sums input + cache_read + cache_creation
147
- # - OpenAI: prompt_tokens natively includes cached_tokens
148
- # - Future providers: may exclude cache tokens from prompt_tokens
149
- # Use the explicit `input_tokens` field (Anthropic native,
150
- # non-cache portion) when available; otherwise subtract cache
151
- # tokens from prompt_tokens (assumes prompt_tokens includes
152
- # cache counts).
153
- base_prompt = usage_data.get('input_tokens')
154
- if base_prompt is None:
155
- base_prompt = max(0, prompt_tokens - cache_read - cache_creation)
156
- computed = self._calculate_cost(base_prompt, completion_tokens, cost_in, cost_out)
157
- self.add_estimated_cost(computed['total_cost'])
158
-
159
- def add_actual_cost(self, cost_usd: float):
160
- """Add upstream-reported actual cost for a request.
161
-
162
- Used when providers like OpenRouter return the exact cost in the response,
163
- which is more accurate than estimating from token counts × static rates.
164
-
165
- Args:
166
- cost_usd: Actual cost in USD for a single request
167
- """
168
- self.total_actual_cost += cost_usd
169
- self.conv_actual_cost += cost_usd
170
-
171
- def add_estimated_cost(self, cost_usd: float):
172
- """Add config-estimated cost for a request.
173
-
174
- Used as a fallback when providers do not return cost in the response.
175
- Estimated costs are tracked separately from upstream-reported actual costs
176
- so they remain distinguishable.
177
-
178
- Args:
179
- cost_usd: Estimated cost in USD for a single request
180
- """
181
- self.total_estimated_cost += cost_usd
182
- self.conv_estimated_cost += cost_usd
183
-
184
- def has_actual_cost(self) -> bool:
185
- """Whether any upstream-reported actual cost has been recorded."""
186
- return self.total_actual_cost > 0.0
187
-
188
- def has_estimated_cost(self) -> bool:
189
- """Whether any config-estimated cost has been recorded."""
190
- return self.total_estimated_cost > 0.0
191
-
192
- def has_cost(self) -> bool:
193
- """Whether any cost (actual or estimated) has been recorded."""
194
- return self.total_actual_cost > 0.0 or self.total_estimated_cost > 0.0
195
-
196
- def get_session_summary(self):
197
- """Return formatted session usage summary string."""
198
- parts = (
199
- f"Session Input: [#5F9EA0]{self.current_context_tokens:,}[/#5F9EA0] | "
200
- f"Session Total: [#5F9EA0]{self.conv_total_tokens:,}[/#5F9EA0]"
201
- )
202
- total_cost = self.total_actual_cost + self.total_estimated_cost
203
- if total_cost > 0:
204
- parts += f" | Cost: [green]${total_cost:.4f}[/green]"
205
- return parts
206
-
207
- def get_all_token_counts(self):
208
- """Return all token counts as a dictionary for UI display.
209
-
210
- Returns:
211
- dict with keys: prompt_in, completion_out, total
212
- """
213
- return {
214
- 'prompt_in': self.total_prompt_tokens,
215
- 'completion_out': self.total_completion_tokens,
216
- 'total': self.total_tokens
217
- }
218
-
219
- def reset(self, prompt_tokens=None, completion_tokens=None, total_tokens=None):
220
- """Reset token counters to zero or to specified values.
221
-
222
- Used by /clear to reset conversation context while preserving cumulative
223
- billing costs across the session.
224
-
225
- Args:
226
- prompt_tokens: If provided, set total_prompt_tokens to this value
227
- completion_tokens: If provided, set total_completion_tokens to this value
228
- total_tokens: If provided, set total_tokens to this value
229
- """
230
- self.total_prompt_tokens = prompt_tokens if prompt_tokens is not None else 0
231
- self.total_completion_tokens = completion_tokens if completion_tokens is not None else 0
232
- if total_tokens is None:
233
- self.total_tokens = self.total_prompt_tokens + self.total_completion_tokens
234
- else:
235
- self.total_tokens = total_tokens
236
- self.current_context_tokens = 0 # Reset context tokens
237
- self.total_cache_read_tokens = 0
238
- self.total_cache_creation_tokens = 0
239
- # Note: total_actual_cost and total_estimated_cost are preserved across resets (cumulative billing)
240
-
241
- def reset_all(self):
242
- """Full reset of all counters including cost accumulators.
243
-
244
- Used on provider switch to clear stale cost state from the previous
245
- provider. Unlike reset(), this zeros actual/estimated costs so the
246
- new provider starts with a clean billing slate.
247
- """
248
- self.reset()
249
- self.total_actual_cost = 0.0
250
- self.total_estimated_cost = 0.0
251
- self.total_cache_read_tokens = 0
252
- self.total_cache_creation_tokens = 0
253
-
254
- @staticmethod
255
- def estimate_tokens(text, model=""):
256
- """Estimate token count using tiktoken.
257
-
258
- Args:
259
- text: String to estimate tokens for
260
- model: Optional model name for encoding selection (uses cl100k_base if empty)
261
-
262
- Returns:
263
- Estimated token count (int)
264
- """
265
- if not text:
266
- return 0
267
-
268
- try:
269
- import tiktoken
270
- try:
271
- enc = tiktoken.encoding_for_model(model) if model else tiktoken.get_encoding("cl100k_base")
272
- except Exception:
273
- enc = tiktoken.get_encoding("cl100k_base")
274
- return len(enc.encode(text))
275
- except ImportError:
276
- # Fallback to character-based approximation if tiktoken not available
277
- return len(text) // 4
278
-
279
- def set_context_tokens(self, token_count):
280
- """Set the current context token count.
281
-
282
- Args:
283
- token_count: Actual token count of the current message list
284
- """
285
- self.current_context_tokens = token_count
286
-
287
- @staticmethod
288
- def _calculate_cost(prompt_tokens: int, completion_tokens: int, cost_in: float, cost_out: float) -> dict:
289
- """Core cost formula: (tokens / 1M) * rate."""
290
- input_cost = (prompt_tokens / 1_000_000) * cost_in
291
- output_cost = (completion_tokens / 1_000_000) * cost_out
292
- return {
293
- 'input_cost': input_cost,
294
- 'output_cost': output_cost,
295
- 'total_cost': input_cost + output_cost,
296
- }
297
-
298
- def reset_conversation(self):
299
- """Reset conversation token counters (called on /new).
300
-
301
- Session totals (total_prompt_tokens, total_completion_tokens) are preserved.
302
- """
303
- self.conv_prompt_tokens = 0
304
- self.conv_completion_tokens = 0
305
- self.conv_total_tokens = 0
306
- self.conv_actual_cost = 0.0
307
- self.conv_estimated_cost = 0.0
308
- self.conv_cache_read_tokens = 0
309
- self.conv_cache_creation_tokens = 0
310
-
311
- def get_usage_for_prompt(self, context_limit: int = 200_000) -> str:
312
- """Get formatted usage information for inclusion in agent prompts.
313
-
314
- This provides agents with awareness of their current context window
315
- usage to help them work within context limits. Urgency is based on
316
- actual context length (current_context_tokens), not cumulative billing.
317
-
318
- Args:
319
- context_limit: The context window limit to compare against (default: 200k)
320
-
321
- Returns:
322
- Formatted string with usage statistics and guidance
323
- """
324
- context_used = self.current_context_tokens
325
- total_burned = self.total_tokens
326
- remaining = context_limit - context_used
327
- percentage = (context_used / context_limit) * 100
328
-
329
- # Determine urgency level
330
- if percentage >= 90:
331
- urgency = "CRITICAL"
332
- guidance = "You have nearly exhausted your token budget. Be extremely concise and limit exploration."
333
- elif percentage >= 75:
334
- urgency = "HIGH"
335
- guidance = "You are approaching your token limit. Prioritize focused exploration over breadth."
336
- elif percentage >= 50:
337
- urgency = "MODERATE"
338
- guidance = "You have used half your token budget. Be mindful of exploration scope."
339
- else:
340
- urgency = "LOW"
341
- guidance = "Token usage is within normal bounds."
342
-
343
- return (
344
- f"## Token Usage Awareness\n\n"
345
- f"**Status:** {urgency} | **Context:** {context_used:,} / {context_limit:,} ({percentage:.1f}%)\n"
346
- f"**Remaining:** {remaining:,} tokens | **Session total burned:** {total_burned:,}\n\n"
347
- f"**Guidance:** {guidance}\n\n"
348
- f"**Note:** Context shows current conversation length; session total is cumulative across all LLM calls."
349
- )
350
-
351
- def get_context_summary(self) -> str:
352
- """Get a brief summary of current context usage.
353
-
354
- Returns:
355
- Concise string with context and session totals
356
- """
357
- return (
358
- f"Context: {self.current_context_tokens:,} tokens | "
359
- f"Session total burned: {self.total_tokens:,} tokens"
360
- )
361
-
362
- def get_display_cost(self, model_name: str = "") -> float:
363
- """Get the cost to display in UI (session-level).
364
-
365
- Priority:
366
- 1. Upstream-reported actual cost (most accurate, e.g. OpenRouter)
367
- 2. Config-based fallback (tokens x rates from MODEL_PRICES)
368
-
369
- Args:
370
- model_name: Model name for config-based cost lookup (fallback).
371
-
372
- Returns:
373
- Total cost in USD, or 0.0 if neither source is available
374
- """
375
- # If we have upstream-reported cost, use it (most accurate)
376
- if self.has_actual_cost():
377
- return self.total_actual_cost + self.total_estimated_cost
378
- # Fallback: full config-based recalculation for all tokens
379
- cost_in, cost_out = get_model_cost(model_name)
380
- if cost_in > 0 or cost_out > 0:
381
- return self._calculate_cost(
382
- self.total_prompt_tokens, self.total_completion_tokens,
383
- cost_in, cost_out
384
- )['total_cost']
385
- return 0.0
386
-
387
- def get_conversation_display_cost(self, cost_in: float, cost_out: float) -> float:
388
- """Get the cost to display for conversation-level (reset on /new).
389
-
390
- For callers that already have cost rates (e.g. config_manager), this
391
- computes directly.
392
-
393
- Returns:
394
- Conversation cost in USD
395
- """
396
- return self._calculate_cost(
397
- self.conv_prompt_tokens, self.conv_completion_tokens,
398
- cost_in, cost_out
399
- )['total_cost']
@@ -1,151 +0,0 @@
1
- """Tool execution utilities.
2
-
3
- This package provides command execution, file editing, and result formatting
4
- capabilities for the bone-agent AI assistant.
5
- """
6
-
7
- import logging
8
- import sys
9
- from pathlib import Path
10
-
11
- _logger = logging.getLogger(__name__)
12
-
13
- # Command execution (now in shell.py)
14
- from .shell import (
15
- confirm_tool,
16
- run_shell_command,
17
- )
18
-
19
- # UI components
20
- from ui.tool_confirmation import ToolConfirmationPanel
21
-
22
- # File editing (now in edit.py)
23
- from .edit import (
24
- _resolve_repo_path,
25
- preview_edit_file,
26
- run_edit_file,
27
- )
28
-
29
- # Result formatting (now in helpers/)
30
- from .helpers.formatters import (
31
- format_tool_result,
32
- format_file_result,
33
- _build_diff,
34
- _detect_newline,
35
- )
36
-
37
- # File operations
38
- from .directory import list_directory
39
- from .create_file import create_file
40
- from .file_reader import read_file
41
-
42
- # Constants
43
- from . import constants
44
-
45
- # Tool definitions
46
- # Import tool modules to trigger @tool decorator registration
47
- # These modules register themselves when imported
48
- from . import file_reader
49
- from . import directory
50
- from . import create_file
51
- from . import edit # edit.py now contains both core logic and @tool decorators
52
- from . import rg_search
53
- from . import shell # shell.py now contains both core logic and @tool decorators
54
- from . import web_search
55
- from . import sub_agent
56
- # review_sub_agent is not an LLM tool — used as a /review slash command in ui.commands
57
-
58
- from . import task_list
59
- from . import select_option
60
-
61
- # search_plugins — core meta-tool for capability discovery and loading
62
- from . import search_plugins
63
-
64
- # Obsidian tools — conditional registration (register() pattern, NOT @tool at import)
65
- # Only imported and registered when vault is configured and enabled.
66
- # This ensures zero token cost when no vault is linked.
67
- try:
68
- from utils.settings import obsidian_settings
69
- if obsidian_settings.is_active():
70
- from . import obsidian as _obsidian_mod
71
- _obsidian_mod.register()
72
- except Exception as e:
73
- _logger.debug("Obsidian tools not loaded: %s", e)
74
-
75
- # Tool schema exports (now in helpers/base.py, merged from definitions.py)
76
- from .helpers.base import TOOLS
77
-
78
- __all__ = [
79
- # Command execution
80
- 'confirm_tool',
81
- 'run_shell_command',
82
- # UI components
83
- 'ToolConfirmationPanel',
84
- # File editing
85
- '_resolve_repo_path',
86
- 'preview_edit_file',
87
- 'run_edit_file',
88
- # Formatters
89
- 'format_tool_result',
90
- 'format_file_result',
91
- '_build_diff',
92
- '_detect_newline',
93
- # File operations
94
- 'read_file',
95
- 'list_directory',
96
- 'create_file',
97
- # Constants
98
- 'constants',
99
- # Tool definitions
100
- 'TOOLS',
101
- ]
102
-
103
- # =============================================================================
104
- # Re-export helpers at package level
105
- # =============================================================================
106
- from .helpers import (
107
- ToolDefinition,
108
- ToolRegistry,
109
- tool,
110
- build_context,
111
- get_tool_schemas,
112
- get_terminal_policy,
113
- TERMINAL_NONE,
114
- TERMINAL_YIELD,
115
- TERMINAL_STOP,
116
- )
117
-
118
- # Apply disabled tools from settings (after all tools are registered)
119
- try:
120
- from utils.settings import tool_settings
121
- for tool_name in tool_settings.disabled_tools:
122
- ToolRegistry.disable(tool_name)
123
- except Exception as e:
124
- _logger.debug("Failed to apply disabled tools: %s", e)
125
-
126
- # Load plugin tools into the PluginManifest (not ToolRegistry).
127
- # Plugin modules with @tool(tier="plugin") register into the manifest
128
- # and are only activated in ToolRegistry on-demand via search_plugins.
129
- try:
130
- from .helpers.loader import discover_tools
131
- from .helpers.plugin_manifest import plugin_manifest
132
-
133
- repo_root = Path(__file__).resolve().parents[2]
134
- src_dir = str(repo_root / "src")
135
- if src_dir not in sys.path:
136
- sys.path.insert(0, src_dir)
137
-
138
- discover_tools([str(repo_root / "tool_plugins")])
139
-
140
- _logger.info(
141
- "Plugin manifest: %s plugins available (categories: %s)",
142
- plugin_manifest.plugin_count(),
143
- plugin_manifest.get_categories(),
144
- )
145
-
146
- # Re-apply disabled_tools now that plugins are in the manifest
147
- for tool_name in tool_settings.disabled_tools:
148
- if plugin_manifest.has_plugin(tool_name):
149
- ToolRegistry.disable(tool_name)
150
- except Exception as e:
151
- _logger.debug("Failed to load plugin tools: %s", e)
@@ -1,59 +0,0 @@
1
- """Centralized constants for tools.
2
-
3
- This module contains all magic numbers and configuration values used across
4
- the tools infrastructure. Centralizing constants makes the code more
5
- maintainable and self-documenting.
6
- """
7
-
8
- # ============================================================================
9
- # Directory Listing Constants
10
- # ============================================================================
11
-
12
- # Total items that trigger truncation in directory listings
13
- TRUNCATION_THRESHOLD = 100
14
-
15
- # Maximum files to show per folder when truncating directory listings
16
- MAX_FILES_PER_FOLDER = 10
17
-
18
- # Hard upper limit for total items to collect in directory listings
19
- # Prevents context explosion on very large directories
20
- MAX_TOTAL_ITEMS = 500
21
-
22
-
23
- # ============================================================================
24
- # File Reading Constants
25
- # ============================================================================
26
-
27
- # Chunk size for streaming file reads (8KB)
28
- # Balances memory usage with read performance
29
- FILE_READ_CHUNK_SIZE = 8192
30
-
31
- # Maximum buffer size for file reading (10MB)
32
- # Handles pathological files with very long single lines
33
- FILE_READ_MAX_BUFFER_SIZE = 10_000_000
34
-
35
- # Maximum lines to show in file output formatting
36
- # Prevents overwhelming context with excessive output
37
- FORMATTER_MAX_LINES = 100
38
-
39
-
40
- # ============================================================================
41
- # Task List Constants
42
- # ============================================================================
43
-
44
- # Maximum number of tasks allowed in a task list
45
- MAX_TASKS = 50
46
-
47
- # Maximum length for individual task descriptions
48
- MAX_TASK_LEN = 200
49
-
50
- # Maximum length for task list titles
51
- MAX_TASK_TITLE_LEN = 80
52
-
53
-
54
- # ============================================================================
55
- # UI/Display Constants
56
- # ============================================================================
57
-
58
- # Default terminal width fallback for non-TTY environments
59
- DEFAULT_TERMINAL_WIDTH = 80