bone-agent 1.4.0 → 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 (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,185 +0,0 @@
1
- """Capability manifest for on-demand plugin and skill 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 dataclasses import dataclass
10
- from typing import Dict, Iterable, List, Optional
11
-
12
- from core.skills import (
13
- SearchCandidate,
14
- iter_skill_summaries,
15
- search_candidates,
16
- )
17
- from utils.settings import tool_settings
18
-
19
- from .base import ToolDefinition
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- @dataclass
25
- class CapabilityMatch:
26
- kind: str
27
- name: str
28
- description: str
29
- category: str | None = None
30
- tags: list[str] | None = None
31
- tool_def: ToolDefinition | None = None
32
- preview: str | None = None
33
- activated: bool = False
34
- already_active: bool = False
35
-
36
-
37
- class PluginManifest:
38
- """Index of plugin-tier tools and stored skills for discovery surfaces.
39
-
40
- Plugin tools are registered here when modules with @tool(tier="plugin")
41
- are imported. Stored skills are discovered from disk on demand. The
42
- search_plugins core tool queries this manifest to find and activate or
43
- load capabilities.
44
- """
45
-
46
- def __init__(self):
47
- self._plugins: Dict[str, ToolDefinition] = {}
48
-
49
- def register(self, tool_def: ToolDefinition) -> None:
50
- """Register a plugin tool definition.
51
-
52
- Args:
53
- tool_def: ToolDefinition with tier="plugin"
54
- """
55
- if tool_def.name in self._plugins:
56
- logger.warning(
57
- f"Plugin '{tool_def.name}' is being overwritten. "
58
- f"Previous: {self._plugins[tool_def.name].handler}, "
59
- f"New: {tool_def.handler}"
60
- )
61
- self._plugins[tool_def.name] = tool_def
62
- logger.debug(f"Plugin registered in manifest: {tool_def.name}")
63
-
64
- def get(self, name: str) -> Optional[ToolDefinition]:
65
- """Get a plugin tool definition by name.
66
-
67
- Args:
68
- name: Plugin tool name
69
-
70
- Returns:
71
- ToolDefinition or None if not found
72
- """
73
- return self._plugins.get(name)
74
-
75
- def get_all(self) -> List[ToolDefinition]:
76
- """Get all registered plugin definitions.
77
-
78
- Returns:
79
- List of all ToolDefinitions in the manifest
80
- """
81
- return list(self._plugins.values())
82
-
83
- def _iter_capabilities(self, category: str = None) -> Iterable[CapabilityMatch]:
84
- """Yield available plugin and skill capabilities for discovery surfaces."""
85
- include_plugins = category in (None, "plugin")
86
- include_skills = category in (None, "skill")
87
- disabled_tools = set(tool_settings.disabled_tools or [])
88
- hidden_skills = set(tool_settings.hidden_skills or [])
89
-
90
- if include_plugins:
91
- for tool_def in self._plugins.values():
92
- if tool_def.name in disabled_tools:
93
- continue
94
- if category not in (None, "plugin") and tool_def.category != category:
95
- continue
96
- yield CapabilityMatch(
97
- kind="plugin",
98
- name=tool_def.name,
99
- description=tool_def.description,
100
- category=tool_def.category,
101
- tags=list(tool_def.tags or []),
102
- tool_def=tool_def,
103
- )
104
-
105
- if include_skills:
106
- for summary in iter_skill_summaries():
107
- if summary.name in hidden_skills:
108
- continue
109
- yield CapabilityMatch(
110
- kind="skill",
111
- name=summary.name,
112
- description=summary.description or summary.preview,
113
- category="skill",
114
- tags=summary.tags or ["skill"],
115
- preview=summary.preview,
116
- )
117
-
118
- def search_capabilities(
119
- self,
120
- query: str,
121
- category: str = None,
122
- max_results: int = 5,
123
- ) -> List[CapabilityMatch]:
124
- """Search plugins and skills through one shared discovery path."""
125
- combined_candidates = [
126
- SearchCandidate(
127
- item=capability,
128
- text=" ".join(
129
- part
130
- for part in [
131
- capability.name,
132
- capability.description,
133
- capability.category or "",
134
- " ".join(capability.tags or []),
135
- ]
136
- if part
137
- ),
138
- compact_text="",
139
- exact_text=capability.name,
140
- )
141
- for capability in self._iter_capabilities(category=category)
142
- ]
143
- combined_matches = search_candidates(
144
- query,
145
- combined_candidates,
146
- max_results=max_results,
147
- item_key=lambda capability: f"{capability.kind}:{capability.name}",
148
- )
149
- return [match.item for match in combined_matches]
150
-
151
- def list_all_capabilities(self, category: str = None) -> List[CapabilityMatch]:
152
- """Return all available capabilities without fuzzy scoring."""
153
- return list(self._iter_capabilities(category=category))
154
-
155
- def get_categories(self) -> List[str]:
156
- """Get all unique categories in the manifest.
157
-
158
- Returns:
159
- Sorted list of category strings
160
- """
161
- categories = {td.category for td in self._plugins.values() if td.category}
162
- return sorted(categories)
163
-
164
- def plugin_count(self) -> int:
165
- """Get the number of registered plugins.
166
-
167
- Returns:
168
- Number of plugins in the manifest
169
- """
170
- return len(self._plugins)
171
-
172
- def has_plugin(self, name: str) -> bool:
173
- """Check if a plugin exists in the manifest.
174
-
175
- Args:
176
- name: Plugin tool name
177
-
178
- Returns:
179
- True if plugin is in the manifest
180
- """
181
- return name in self._plugins
182
-
183
-
184
- # Singleton instance
185
- 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,190 +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
- 'context_tokens': usage.get('context_tokens', 0),
177
- })
178
-
179
- raw_result = sub_agent_data.get('result', '')
180
-
181
- # If hard limit was exceeded, skip injection and return raw dump for both
182
- if sub_agent_data.get('hard_limit_exceeded'):
183
- return {"display": raw_result, "history": raw_result}
184
-
185
- # Always inject file contents from citations into the history version
186
- injected_result = inject_file_contents(
187
- raw_result, repo_root, gitignore_spec, console
188
- )
189
-
190
- return {"display": raw_result, "history": injected_result}