bone-agent 1.3.3 → 2.0.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 (121) 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 -184
  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 -141
  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 -36
  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/targeted_searching.md +0 -10
  25. package/prompts/main/task_lists_pattern.md +0 -8
  26. package/prompts/main/temp_folder.md +0 -9
  27. package/prompts/main/think_before_acting.md +0 -10
  28. package/prompts/main/tone_and_style.md +0 -4
  29. package/prompts/main/tool_preferences.md +0 -24
  30. package/prompts/main/trust_subagent_context.md +0 -21
  31. package/prompts/main/when_to_use_sub_agent.md +0 -7
  32. package/prompts/micro/ask_questions.md +0 -1
  33. package/prompts/micro/batch_independent_calls.md +0 -1
  34. package/prompts/micro/casual_interactions.md +0 -1
  35. package/prompts/micro/code_references.md +0 -1
  36. package/prompts/micro/communication_style.md +0 -1
  37. package/prompts/micro/context_reliability.md +0 -1
  38. package/prompts/micro/conversational_tool_calling.md +0 -1
  39. package/prompts/micro/editing_pattern.md +0 -1
  40. package/prompts/micro/error_handling.md +0 -1
  41. package/prompts/micro/exploration_pattern.md +0 -1
  42. package/prompts/micro/intro.md +0 -1
  43. package/prompts/micro/obsidian.md +0 -4
  44. package/prompts/micro/obsidian_project.md +0 -5
  45. package/prompts/micro/professional_objectivity.md +0 -1
  46. package/prompts/micro/targeted_searching.md +0 -1
  47. package/prompts/micro/task_lists_pattern.md +0 -1
  48. package/prompts/micro/temp_folder.md +0 -1
  49. package/prompts/micro/think_before_acting.md +0 -5
  50. package/prompts/micro/tone_and_style.md +0 -1
  51. package/prompts/micro/tool_preferences.md +0 -1
  52. package/prompts/micro/trust_subagent_context.md +0 -1
  53. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  54. package/requirements.txt +0 -9
  55. package/src/__init__.py +0 -11
  56. package/src/core/__init__.py +0 -1
  57. package/src/core/agentic.py +0 -985
  58. package/src/core/chat_manager.py +0 -1564
  59. package/src/core/config_manager.py +0 -253
  60. package/src/core/cron.py +0 -582
  61. package/src/core/cron_allowlist.py +0 -118
  62. package/src/core/memory.py +0 -145
  63. package/src/core/retry.py +0 -71
  64. package/src/core/sub_agent.py +0 -326
  65. package/src/core/tool_approval.py +0 -220
  66. package/src/core/tool_feedback.py +0 -778
  67. package/src/exceptions.py +0 -79
  68. package/src/llm/__init__.py +0 -1
  69. package/src/llm/client.py +0 -171
  70. package/src/llm/config.py +0 -492
  71. package/src/llm/prompts.py +0 -489
  72. package/src/llm/providers.py +0 -436
  73. package/src/llm/streaming.py +0 -163
  74. package/src/llm/token_tracker.py +0 -384
  75. package/src/tools/__init__.py +0 -212
  76. package/src/tools/constants.py +0 -59
  77. package/src/tools/create_file.py +0 -136
  78. package/src/tools/directory.py +0 -389
  79. package/src/tools/edit.py +0 -545
  80. package/src/tools/file_reader.py +0 -322
  81. package/src/tools/helpers/__init__.py +0 -105
  82. package/src/tools/helpers/base.py +0 -550
  83. package/src/tools/helpers/converters.py +0 -44
  84. package/src/tools/helpers/file_helpers.py +0 -189
  85. package/src/tools/helpers/formatters.py +0 -411
  86. package/src/tools/helpers/loader.py +0 -231
  87. package/src/tools/helpers/parallel_executor.py +0 -231
  88. package/src/tools/helpers/path_resolver.py +0 -232
  89. package/src/tools/helpers/plugin_manifest.py +0 -156
  90. package/src/tools/obsidian.py +0 -96
  91. package/src/tools/review_sub_agent.py +0 -189
  92. package/src/tools/rg_search.py +0 -460
  93. package/src/tools/search_plugins.py +0 -109
  94. package/src/tools/select_option.py +0 -600
  95. package/src/tools/shell.py +0 -302
  96. package/src/tools/sub_agent.py +0 -139
  97. package/src/tools/task_list.py +0 -269
  98. package/src/tools/web_search.py +0 -61
  99. package/src/ui/__init__.py +0 -1
  100. package/src/ui/banner.py +0 -87
  101. package/src/ui/commands.py +0 -2809
  102. package/src/ui/displays.py +0 -214
  103. package/src/ui/loader.py +0 -284
  104. package/src/ui/main.py +0 -647
  105. package/src/ui/prompt_utils.py +0 -113
  106. package/src/ui/setting_selector.py +0 -590
  107. package/src/ui/setup_wizard.py +0 -294
  108. package/src/ui/sub_agent_panel.py +0 -234
  109. package/src/ui/tool_confirmation.py +0 -215
  110. package/src/utils/__init__.py +0 -1
  111. package/src/utils/citation_parser.py +0 -199
  112. package/src/utils/editor.py +0 -158
  113. package/src/utils/gitignore_filter.py +0 -149
  114. package/src/utils/logger.py +0 -254
  115. package/src/utils/paths.py +0 -30
  116. package/src/utils/result_parsers.py +0 -108
  117. package/src/utils/safe_commands.py +0 -243
  118. package/src/utils/settings.py +0 -191
  119. package/src/utils/user_message_logger.py +0 -120
  120. package/src/utils/validation.py +0 -191
  121. package/src/utils/web_search.py +0 -173
@@ -1,156 +0,0 @@
1
- """Plugin manifest for on-demand tool discovery.
2
-
3
- Plugin-tier tools are registered here instead of ToolRegistry at import time.
4
- This keeps plugin schemas out of the LLM context window until explicitly
5
- activated via the search_plugins core tool.
6
- """
7
-
8
- import logging
9
- from typing import Dict, List, Optional
10
-
11
- from .base import ToolDefinition
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class PluginManifest:
17
- """Index of plugin-tier tools available for on-demand activation.
18
-
19
- Tools are registered here when modules with @tool(tier="plugin") are
20
- imported. The search_plugins core tool queries this index to find and
21
- activate matching plugins.
22
- """
23
-
24
- def __init__(self):
25
- self._plugins: Dict[str, ToolDefinition] = {}
26
-
27
- def register(self, tool_def: ToolDefinition) -> None:
28
- """Register a plugin tool definition.
29
-
30
- Args:
31
- tool_def: ToolDefinition with tier="plugin"
32
- """
33
- if tool_def.name in self._plugins:
34
- logger.warning(
35
- f"Plugin '{tool_def.name}' is being overwritten. "
36
- f"Previous: {self._plugins[tool_def.name].handler}, "
37
- f"New: {tool_def.handler}"
38
- )
39
- self._plugins[tool_def.name] = tool_def
40
- logger.debug(f"Plugin registered in manifest: {tool_def.name}")
41
-
42
- def get(self, name: str) -> Optional[ToolDefinition]:
43
- """Get a plugin tool definition by name.
44
-
45
- Args:
46
- name: Plugin tool name
47
-
48
- Returns:
49
- ToolDefinition or None if not found
50
- """
51
- return self._plugins.get(name)
52
-
53
- def get_all(self) -> List[ToolDefinition]:
54
- """Get all registered plugin definitions.
55
-
56
- Returns:
57
- List of all ToolDefinitions in the manifest
58
- """
59
- return list(self._plugins.values())
60
-
61
- def search(self, query: str, category: str = None, max_results: int = 5) -> List[ToolDefinition]:
62
- """Search the manifest for plugins matching a query.
63
-
64
- Args:
65
- query: Search query (matched against name, description, tags, category)
66
- category: Optional category filter
67
- max_results: Maximum number of results to return
68
-
69
- Returns:
70
- List of matching ToolDefinitions, sorted by relevance score
71
- """
72
- query_lower = query.lower()
73
- query_terms = query_lower.split()
74
-
75
- scored = []
76
- for tool_def in self._plugins.values():
77
- # Apply category filter if specified
78
- if category and tool_def.category != category:
79
- continue
80
-
81
- # Calculate relevance score
82
- score = 0.0
83
-
84
- # Exact name match (highest priority)
85
- if query_lower == tool_def.name.lower():
86
- score += 100.0
87
-
88
- # Name contains query
89
- if query_lower in tool_def.name.lower():
90
- score += 50.0
91
-
92
- # Name contains individual terms
93
- for term in query_terms:
94
- if term in tool_def.name.lower():
95
- score += 20.0
96
-
97
- # Description match
98
- if query_lower in tool_def.description.lower():
99
- score += 30.0
100
- for term in query_terms:
101
- if term in tool_def.description.lower():
102
- score += 10.0
103
-
104
- # Tag match
105
- for tag in tool_def.tags:
106
- if query_lower in tag.lower():
107
- score += 15.0
108
- for term in query_terms:
109
- if term in tag.lower():
110
- score += 5.0
111
-
112
- # Category match
113
- if category and tool_def.category == category:
114
- score += 10.0
115
- elif not category and query_lower in tool_def.category.lower():
116
- score += 15.0
117
-
118
- if score > 0:
119
- scored.append((score, tool_def))
120
-
121
- # Sort by score descending
122
- scored.sort(key=lambda x: x[0], reverse=True)
123
-
124
- return [tool_def for _, tool_def in scored[:max_results]]
125
-
126
- def get_categories(self) -> List[str]:
127
- """Get all unique categories in the manifest.
128
-
129
- Returns:
130
- Sorted list of category strings
131
- """
132
- categories = {td.category for td in self._plugins.values() if td.category}
133
- return sorted(categories)
134
-
135
- def plugin_count(self) -> int:
136
- """Get the number of registered plugins.
137
-
138
- Returns:
139
- Number of plugins in the manifest
140
- """
141
- return len(self._plugins)
142
-
143
- def has_plugin(self, name: str) -> bool:
144
- """Check if a plugin exists in the manifest.
145
-
146
- Args:
147
- name: Plugin tool name
148
-
149
- Returns:
150
- True if plugin is in the manifest
151
- """
152
- return name in self._plugins
153
-
154
-
155
- # Singleton instance
156
- plugin_manifest = PluginManifest()
@@ -1,96 +0,0 @@
1
- """Obsidian vault integration utilities.
2
-
3
- Provides vault session management for project note routing.
4
- """
5
-
6
- import logging
7
- import os
8
- from pathlib import Path
9
- from dataclasses import dataclass
10
- from typing import Optional
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- @dataclass(frozen=True)
16
- class VaultSession:
17
- """Immutable snapshot of vault pairing state.
18
-
19
- Built once when the vault is activated (register()) and shared everywhere.
20
- Eliminates scattered derivation of vault_root, project_folder, and path prefixes.
21
-
22
- Attributes:
23
- vault_root: Absolute validated vault root Path.
24
- project_folder: Absolute project folder (vault_root / project_base / repo_name).
25
- project_folder_relative: Vault-relative project folder string (e.g. "Dev/myrepo").
26
- """
27
- vault_root: Path
28
- project_folder: Path
29
- project_folder_relative: str
30
-
31
-
32
- # Module-level session — set by register(), cleared by unregister()
33
- _session: Optional[VaultSession] = None
34
-
35
-
36
- def get_vault_session() -> Optional[VaultSession]:
37
- """Return the active VaultSession, or None if vault is not paired."""
38
- return _session
39
-
40
-
41
- def build_vault_session(repo_root: Path = None) -> Optional[VaultSession]:
42
- """Build a VaultSession from current settings.
43
-
44
- Args:
45
- repo_root: Repository root used to derive project folder name.
46
- Falls back to os.getcwd() if not provided.
47
-
48
- Returns:
49
- VaultSession if vault is configured and valid, None otherwise.
50
- """
51
- from utils.settings import obsidian_settings
52
-
53
- if not obsidian_settings.is_active():
54
- return None
55
-
56
- root = obsidian_settings.vault_path # Already validated by is_active()
57
- # Re-resolve to absolute (is_active checks existence but returns raw string)
58
- root = Path(root).resolve()
59
-
60
- project_base = obsidian_settings.project_base or "Dev"
61
- repo_name = repo_root.name if repo_root else os.path.basename(os.getcwd())
62
- if not repo_name:
63
- return None
64
-
65
- project_folder = root / project_base / repo_name
66
- pf_relative = str(project_base) + "/" + repo_name
67
-
68
- return VaultSession(
69
- vault_root=root,
70
- project_folder=project_folder,
71
- project_folder_relative=pf_relative,
72
- )
73
-
74
-
75
- def init_session(repo_root: Path = None) -> Optional[VaultSession]:
76
- """Build and cache the VaultSession.
77
-
78
- Should be called once with the actual repo_root when available.
79
- Falls back to os.getcwd() if repo_root is None (acceptable for prompt
80
- building before the orchestrator starts).
81
-
82
- Returns:
83
- The active VaultSession, or None if vault is not configured.
84
- """
85
- global _session
86
- session = build_vault_session(repo_root=repo_root)
87
- if session:
88
- # Only cache if we have a real repo_root — otherwise the orchestrator
89
- # will call us again with the correct path and that should win.
90
- if repo_root is not None or _session is None:
91
- _session = session
92
- logger.info(f"Vault session initialized: {session.project_folder_relative}")
93
- return _session
94
-
95
-
96
-
@@ -1,189 +0,0 @@
1
- """Review sub-agent tool for analyzing git diffs."""
2
-
3
- import re
4
- from pathlib import Path
5
-
6
- from core.sub_agent import run_sub_agent
7
- from tools.sub_agent import SimplePanelUpdater
8
- from utils.citation_parser import inject_file_contents
9
-
10
- # Approximate chars per token (conservative heuristic, avoids tokenizer dependency)
11
- _CHARS_PER_TOKEN = 4
12
-
13
- # Max tokens for the diff context (leaves room for agent conversation within budget)
14
- _MAX_DIFF_TOKENS = 50_000
15
-
16
-
17
- def _estimate_tokens(text: str) -> int:
18
- """Rough token estimate based on character count."""
19
- return len(text) // _CHARS_PER_TOKEN
20
-
21
-
22
- def _truncate_diff(diff: str, max_chars: int) -> tuple:
23
- """Truncate a diff to fit within a character budget, keeping whole files.
24
-
25
- Returns:
26
- Tuple of (truncated_diff, list_of_skipped_file_names)
27
- """
28
- # Split on diff headers: each file starts with "diff --git"
29
- file_blocks = re.split(r'(?=^diff --git )', diff, flags=re.MULTILINE)
30
- # First element may be empty if diff starts with "diff --git"
31
- if file_blocks and file_blocks[0].strip() == '':
32
- file_blocks = file_blocks[1:]
33
-
34
- result_blocks = []
35
- skipped_files = []
36
- current_chars = 0
37
-
38
- for block in file_blocks:
39
- block_chars = len(block)
40
- if current_chars + block_chars > max_chars:
41
- # Extract file name from the block header for the skip list
42
- match = re.search(r'^diff --git a/(.*?) b/', block, re.MULTILINE)
43
- if match:
44
- skipped_files.append(match.group(1))
45
- else:
46
- result_blocks.append(block)
47
- current_chars += block_chars
48
-
49
- truncated = '\n'.join(result_blocks)
50
- return truncated, skipped_files
51
-
52
-
53
- def _count_binary_files(diff: str) -> int:
54
- """Count binary file entries in a diff."""
55
- return len(re.findall(r'^Binary files ', diff, re.MULTILINE))
56
-
57
-
58
- def _build_review_context(diff: str) -> str:
59
- """Build the initial context message for the review sub-agent.
60
-
61
- Handles truncation and binary file counting.
62
-
63
- Returns:
64
- Tuple of (context_string, warning_string_or_None)
65
- """
66
- warning = None
67
- max_chars = _MAX_DIFF_TOKENS * _CHARS_PER_TOKEN
68
-
69
- # Check for binary files
70
- binary_count = _count_binary_files(diff)
71
- binary_note = ""
72
- if binary_count > 0:
73
- binary_note = f"\n\nNote: {binary_count} binary file(s) were skipped in this diff."
74
-
75
- # Check if truncation is needed
76
- estimated_tokens = _estimate_tokens(diff)
77
- if estimated_tokens > _MAX_DIFF_TOKENS:
78
- truncated_diff, skipped_files = _truncate_diff(diff, max_chars)
79
- skipped_note = ""
80
- if skipped_files:
81
- skipped_note = (
82
- f"\n\nWARNING: Diff was too large and has been truncated. "
83
- f"The following {len(skipped_files)} file(s) were omitted from review:\n"
84
- + "\n".join(f" - {f}" for f in skipped_files)
85
- )
86
- warning = f"Diff truncated: {len(skipped_files)} file(s) omitted."
87
- context = f"Review this git diff:\n\n{truncated_diff}{binary_note}{skipped_note}"
88
- else:
89
- context = f"Review this git diff:\n\n{diff}{binary_note}"
90
-
91
- return context, warning
92
-
93
-
94
- def review_changes(
95
- diff_output: str,
96
- repo_root: Path,
97
- rg_exe_path: str,
98
- console,
99
- chat_manager,
100
- gitignore_spec=None,
101
- panel_updater=None,
102
- user_intent: str = None,
103
- ) -> dict:
104
- """Run review sub-agent on a git diff.
105
-
106
- Args:
107
- diff_output: Git diff string to review
108
- repo_root: Repository root directory
109
- rg_exe_path: Path to rg executable
110
- console: Rich console for output
111
- chat_manager: ChatManager instance
112
- gitignore_spec: PathSpec for .gitignore filtering
113
- panel_updater: Optional SubAgentPanel for live updates
114
- user_intent: Optional description of what the user was trying to do
115
-
116
- Returns:
117
- Dict with keys:
118
- display: Clean review text (no injected file contents)
119
- history: Review text with file contents injected from citations
120
- """
121
- if not diff_output or not isinstance(diff_output, str) or not diff_output.strip():
122
- result = "No changes to review."
123
- return {"display": result, "history": result}
124
-
125
- # Build context (handles truncation and binary file notes)
126
- review_context, truncation_warning = _build_review_context(diff_output)
127
-
128
- if truncation_warning:
129
- console.print(f"[yellow]{truncation_warning}[/yellow]")
130
-
131
- # Set up panel updater
132
- if panel_updater is None:
133
- panel_updater = SimplePanelUpdater(console)
134
-
135
- # Task query for the review agent
136
- if user_intent:
137
- task_query = (
138
- "Analyze the diff provided above. "
139
- "For each changed file, use read_file to get surrounding context. "
140
- "Then provide a structured code review with findings, issues, and suggestions.\n\n"
141
- f"The user's intent for these changes was: {user_intent}\n"
142
- "Use this context to better understand the changes and focus your review accordingly."
143
- )
144
- else:
145
- task_query = (
146
- "Analyze the diff provided above. "
147
- "For each changed file, use read_file to get surrounding context. "
148
- "Then provide a structured code review with findings, issues, and suggestions."
149
- )
150
-
151
- with panel_updater as panel:
152
- sub_agent_data = run_sub_agent(
153
- task_query=task_query,
154
- repo_root=repo_root,
155
- rg_exe_path=rg_exe_path,
156
- console=console,
157
- panel_updater=panel,
158
- sub_agent_type="review",
159
- initial_context=review_context,
160
- )
161
-
162
- # Check for errors
163
- if sub_agent_data.get('error'):
164
- panel.set_error(sub_agent_data['error'])
165
- result = f"exit_code=1\n{sub_agent_data['error']}"
166
- return {"display": result, "history": result}
167
-
168
- # Track usage
169
- usage = sub_agent_data.get('usage', {})
170
- if usage:
171
- chat_manager.token_tracker.add_usage(usage, model_name=sub_agent_data.get("model", ""))
172
- panel.set_complete({
173
- 'prompt_tokens': usage.get('prompt_tokens', 0),
174
- 'completion_tokens': usage.get('completion_tokens', 0),
175
- 'total_tokens': usage.get('total_tokens', 0)
176
- })
177
-
178
- raw_result = sub_agent_data.get('result', '')
179
-
180
- # If hard limit was exceeded, skip injection and return raw dump for both
181
- if sub_agent_data.get('hard_limit_exceeded'):
182
- return {"display": raw_result, "history": raw_result}
183
-
184
- # Always inject file contents from citations into the history version
185
- injected_result = inject_file_contents(
186
- raw_result, repo_root, gitignore_spec, console
187
- )
188
-
189
- return {"display": raw_result, "history": injected_result}