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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -184
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -141
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -36
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -985
- package/src/core/chat_manager.py +0 -1564
- package/src/core/config_manager.py +0 -253
- package/src/core/cron.py +0 -582
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/retry.py +0 -71
- package/src/core/sub_agent.py +0 -326
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -778
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -171
- package/src/llm/config.py +0 -492
- package/src/llm/prompts.py +0 -489
- package/src/llm/providers.py +0 -436
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -384
- package/src/tools/__init__.py +0 -212
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -545
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -105
- package/src/tools/helpers/base.py +0 -550
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -231
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -232
- package/src/tools/helpers/plugin_manifest.py +0 -156
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -189
- package/src/tools/rg_search.py +0 -460
- package/src/tools/search_plugins.py +0 -109
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -2809
- package/src/ui/displays.py +0 -214
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -647
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -215
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -158
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -191
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -191
- package/src/utils/web_search.py +0 -173
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
"""Centralized .gitignore filtering using pathspec library."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional, Tuple
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
# Always allow .gitignore itself to be read/edited
|
|
10
|
-
ALWAYS_ALLOWED_FILES = {".gitignore"}
|
|
11
|
-
|
|
12
|
-
# Try to import pathspec at module level
|
|
13
|
-
try:
|
|
14
|
-
import pathspec
|
|
15
|
-
except ImportError:
|
|
16
|
-
pathspec = None
|
|
17
|
-
logger.warning("pathspec library not installed - .gitignore filtering disabled")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def load_gitignore_spec(repo_root: Path):
|
|
21
|
-
"""Load .gitignore patterns into a PathSpec object.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
repo_root: Repository root directory
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
pathspec.PathSpec or None if .gitignore doesn't exist
|
|
28
|
-
"""
|
|
29
|
-
# Return None early if pathspec is not available
|
|
30
|
-
if pathspec is None:
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
gitignore_path = repo_root / ".gitignore"
|
|
34
|
-
|
|
35
|
-
if not gitignore_path.exists():
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
# Read .gitignore patterns
|
|
40
|
-
patterns = gitignore_path.read_text(encoding="utf-8").splitlines()
|
|
41
|
-
|
|
42
|
-
# Create PathSpec with gitwildmatch (git's pattern matching)
|
|
43
|
-
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
44
|
-
return spec
|
|
45
|
-
|
|
46
|
-
except Exception as e:
|
|
47
|
-
logger.warning(f"Failed to load .gitignore: {e}")
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def is_path_ignored(
|
|
52
|
-
path: Path, repo_root: Path, gitignore_spec
|
|
53
|
-
) -> Tuple[bool, Optional[str]]:
|
|
54
|
-
"""Check if a path is ignored by .gitignore.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
path: Absolute path to check
|
|
58
|
-
repo_root: Repository root directory
|
|
59
|
-
gitignore_spec: pathspec.PathSpec object (or None)
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Tuple of (is_ignored, matched_pattern)
|
|
63
|
-
- is_ignored: True if path should be blocked
|
|
64
|
-
- matched_pattern: The pattern that matched (or None)
|
|
65
|
-
"""
|
|
66
|
-
# No filtering if no .gitignore
|
|
67
|
-
if gitignore_spec is None:
|
|
68
|
-
return False, None
|
|
69
|
-
|
|
70
|
-
# Always allow .gitignore itself
|
|
71
|
-
if path.name in ALWAYS_ALLOWED_FILES:
|
|
72
|
-
return False, None
|
|
73
|
-
|
|
74
|
-
# Get relative path from repo root (gitignore only applies within repo)
|
|
75
|
-
try:
|
|
76
|
-
rel_path = path.relative_to(repo_root)
|
|
77
|
-
except ValueError:
|
|
78
|
-
# Path is outside repo - gitignore doesn't apply
|
|
79
|
-
return False, None
|
|
80
|
-
|
|
81
|
-
# Convert to forward slashes (git convention)
|
|
82
|
-
rel_path_str = str(rel_path).replace("\\", "/")
|
|
83
|
-
|
|
84
|
-
# Check if path matches any .gitignore pattern
|
|
85
|
-
# pathspec.match_file() returns True if the file should be ignored
|
|
86
|
-
if gitignore_spec.match_file(rel_path_str):
|
|
87
|
-
# Find which pattern matched (for better error messages)
|
|
88
|
-
matched_pattern = _find_matching_pattern(rel_path_str, gitignore_spec)
|
|
89
|
-
return True, matched_pattern
|
|
90
|
-
|
|
91
|
-
return False, None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _find_matching_pattern(path_str: str, gitignore_spec) -> Optional[str]:
|
|
95
|
-
"""Find which .gitignore pattern matched a path.
|
|
96
|
-
|
|
97
|
-
This is for better error messages.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
path_str: Relative path string (with forward slashes)
|
|
101
|
-
gitignore_spec: pathspec.PathSpec object
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
The matching pattern string, or None
|
|
105
|
-
"""
|
|
106
|
-
try:
|
|
107
|
-
# PathSpec stores patterns internally
|
|
108
|
-
for pattern in gitignore_spec.patterns:
|
|
109
|
-
if pattern.match_file(path_str):
|
|
110
|
-
# Return the original pattern string
|
|
111
|
-
return pattern.pattern
|
|
112
|
-
except Exception:
|
|
113
|
-
pass
|
|
114
|
-
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def format_gitignore_error(
|
|
119
|
-
path: Path, repo_root: Path, matched_pattern: Optional[str]
|
|
120
|
-
) -> str:
|
|
121
|
-
"""Format a user-friendly error message for .gitignore blocked files.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
path: The blocked file path
|
|
125
|
-
repo_root: Repository root
|
|
126
|
-
matched_pattern: The .gitignore pattern that matched
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Formatted error message
|
|
130
|
-
"""
|
|
131
|
-
try:
|
|
132
|
-
rel_path = path.relative_to(repo_root)
|
|
133
|
-
except ValueError:
|
|
134
|
-
rel_path = path
|
|
135
|
-
|
|
136
|
-
error_msg = (
|
|
137
|
-
f"exit_code=ERROR_GITIGNORE_BLOCKED\n"
|
|
138
|
-
f"File blocked by .gitignore: {rel_path}\n\n"
|
|
139
|
-
)
|
|
140
|
-
error_msg += "This file matches patterns in .gitignore and cannot be accessed.\n"
|
|
141
|
-
|
|
142
|
-
if matched_pattern:
|
|
143
|
-
error_msg += f"Matched pattern: {matched_pattern}\n"
|
|
144
|
-
|
|
145
|
-
error_msg += "\nTo access this file:\n"
|
|
146
|
-
error_msg += "1. Remove it from .gitignore, or\n"
|
|
147
|
-
error_msg += "2. Use git commands directly (git show, git diff)\n"
|
|
148
|
-
|
|
149
|
-
return error_msg
|
package/src/utils/logger.py
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"""Markdown conversation logging module for saving chat history to readable markdown files."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional, Dict, Any, List
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class MarkdownConversationLogger:
|
|
13
|
-
"""Logs conversations to Markdown format with tool call details."""
|
|
14
|
-
|
|
15
|
-
MAX_CONTENT_LENGTH = 2000
|
|
16
|
-
|
|
17
|
-
def __init__(self, conversations_dir: str = "conversations"):
|
|
18
|
-
"""Initialize markdown conversation logger.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
conversations_dir: Directory to save conversation logs
|
|
22
|
-
"""
|
|
23
|
-
self.conversations_dir = Path(conversations_dir)
|
|
24
|
-
self.conversations_dir.mkdir(exist_ok=True)
|
|
25
|
-
self.current_file: Optional[Path] = None
|
|
26
|
-
|
|
27
|
-
def start_session(self):
|
|
28
|
-
"""Start a new conversation session."""
|
|
29
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
30
|
-
self.current_file = self.conversations_dir / f"conversation_{timestamp}.md"
|
|
31
|
-
logger.info(f"Started markdown conversation logging to {self.current_file}")
|
|
32
|
-
|
|
33
|
-
# Write header
|
|
34
|
-
with open(self.current_file, 'w', encoding='utf-8') as f:
|
|
35
|
-
f.write(f"# Conversation Log\n\n")
|
|
36
|
-
f.write(f"**Started:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
37
|
-
f.write("---\n\n")
|
|
38
|
-
|
|
39
|
-
def _format_tool_call(self, tool_call: Dict[str, Any]) -> str:
|
|
40
|
-
"""Format a tool call for markdown display.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
tool_call: Tool call dict with id, type, function
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
Formatted markdown string
|
|
47
|
-
"""
|
|
48
|
-
fn = tool_call.get("function", {})
|
|
49
|
-
name = fn.get("name", "unknown")
|
|
50
|
-
arguments = fn.get("arguments", "{}")
|
|
51
|
-
args_str = self._format_json_value(arguments)
|
|
52
|
-
|
|
53
|
-
return f"""### {name}
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{args_str}
|
|
57
|
-
```"""
|
|
58
|
-
|
|
59
|
-
def _format_json_value(self, value: Any) -> str:
|
|
60
|
-
"""Format a value as JSON for markdown display.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
value: Value to format (string, dict, or other)
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
Formatted JSON string, or string representation if not JSON-serializable
|
|
67
|
-
"""
|
|
68
|
-
try:
|
|
69
|
-
if isinstance(value, str):
|
|
70
|
-
parsed = json.loads(value)
|
|
71
|
-
else:
|
|
72
|
-
parsed = value
|
|
73
|
-
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
74
|
-
except (json.JSONDecodeError, TypeError):
|
|
75
|
-
return str(value)
|
|
76
|
-
|
|
77
|
-
def _format_tool_call_inline(self, arguments: Any) -> str:
|
|
78
|
-
"""Format tool call arguments as JSON for inline display.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
arguments: Tool call arguments (string or dict)
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
Formatted JSON string
|
|
85
|
-
"""
|
|
86
|
-
args_str = self._format_json_value(arguments)
|
|
87
|
-
return f"```json\n{args_str}\n```"
|
|
88
|
-
|
|
89
|
-
def _format_tool_result(self, message: Dict[str, Any]) -> str:
|
|
90
|
-
"""Format a tool result for markdown display.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
message: Tool result message with role, tool_call_id, content
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Formatted markdown string
|
|
97
|
-
"""
|
|
98
|
-
content = message.get("content", "")
|
|
99
|
-
|
|
100
|
-
# Truncate very long outputs
|
|
101
|
-
if len(content) > self.MAX_CONTENT_LENGTH:
|
|
102
|
-
content = content[:self.MAX_CONTENT_LENGTH] + "\n\n... (truncated)"
|
|
103
|
-
|
|
104
|
-
# Try to format as code if it looks like structured output
|
|
105
|
-
if content and (content.startswith("{") or content.startswith("[")):
|
|
106
|
-
try:
|
|
107
|
-
parsed = json.loads(content)
|
|
108
|
-
content = json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
109
|
-
return f"```\n{content}\n```\n"
|
|
110
|
-
except json.JSONDecodeError:
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
return f"```\n{content}\n```\n"
|
|
114
|
-
|
|
115
|
-
def _format_message(self, message: Dict[str, Any], skip_tool_calls: bool = False) -> str:
|
|
116
|
-
"""Convert a message dict to markdown format.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
message: Message dict with role, content, tool_calls, etc.
|
|
120
|
-
skip_tool_calls: If True, don't include tool_calls section
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
Formatted markdown string
|
|
124
|
-
"""
|
|
125
|
-
role = message.get("role", "unknown")
|
|
126
|
-
content = message.get("content", "")
|
|
127
|
-
|
|
128
|
-
if role == "user":
|
|
129
|
-
emoji = "👤"
|
|
130
|
-
title = "User"
|
|
131
|
-
elif role == "assistant":
|
|
132
|
-
emoji = "🤖"
|
|
133
|
-
title = "Assistant"
|
|
134
|
-
elif role == "tool":
|
|
135
|
-
# Tool results are handled separately with their tool calls
|
|
136
|
-
return None
|
|
137
|
-
elif role == "system":
|
|
138
|
-
emoji = "⚙️"
|
|
139
|
-
title = "System"
|
|
140
|
-
else:
|
|
141
|
-
emoji = "📝"
|
|
142
|
-
title = role.capitalize()
|
|
143
|
-
|
|
144
|
-
md = f"\n## {emoji} {title}\n\n"
|
|
145
|
-
|
|
146
|
-
# Add content if present
|
|
147
|
-
if content:
|
|
148
|
-
md += f"{content}\n\n"
|
|
149
|
-
|
|
150
|
-
# Add tool calls if present (skip when skip_tool_calls=True)
|
|
151
|
-
if not skip_tool_calls and message.get("tool_calls"):
|
|
152
|
-
md += "### 🔧 Tool Calls\n\n"
|
|
153
|
-
for tc in message["tool_calls"]:
|
|
154
|
-
md += self._format_tool_call(tc) + "\n"
|
|
155
|
-
|
|
156
|
-
return md
|
|
157
|
-
|
|
158
|
-
def log_message(self, message: Dict[str, Any]):
|
|
159
|
-
"""Append a message to the current markdown file.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
message: Message dict with 'role' and 'content' keys, optionally 'tool_calls'
|
|
163
|
-
"""
|
|
164
|
-
if not self.current_file:
|
|
165
|
-
self.start_session()
|
|
166
|
-
|
|
167
|
-
# Check if this is a tool result - we'll handle it differently
|
|
168
|
-
if message.get("role") == "tool":
|
|
169
|
-
# Find the associated tool call by tool_call_id
|
|
170
|
-
tool_call_id = message.get("tool_call_id")
|
|
171
|
-
formatted = f"\n### 📋 Tool Result (ID: `{tool_call_id}`)\n\n"
|
|
172
|
-
formatted += self._format_tool_result(message) + "\n"
|
|
173
|
-
else:
|
|
174
|
-
formatted = self._format_message(message)
|
|
175
|
-
|
|
176
|
-
if formatted:
|
|
177
|
-
with open(self.current_file, 'a', encoding='utf-8') as f:
|
|
178
|
-
f.write(formatted)
|
|
179
|
-
|
|
180
|
-
def rewrite_log(self, messages: List[Dict[str, Any]]):
|
|
181
|
-
"""Rewrite the current markdown log to match the provided messages.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
messages: Full message list to persist
|
|
185
|
-
"""
|
|
186
|
-
if not self.current_file:
|
|
187
|
-
self.start_session()
|
|
188
|
-
|
|
189
|
-
# Rewrite entire file
|
|
190
|
-
with open(self.current_file, 'w', encoding='utf-8') as f:
|
|
191
|
-
# Write header
|
|
192
|
-
f.write(f"# Conversation Log\n\n")
|
|
193
|
-
f.write(f"**Started:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
194
|
-
f.write(f"**Last Updated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
195
|
-
f.write("---\n\n")
|
|
196
|
-
|
|
197
|
-
# Track tool calls to pair with results
|
|
198
|
-
pending_tool_calls = {}
|
|
199
|
-
|
|
200
|
-
for message in messages:
|
|
201
|
-
role = message.get("role")
|
|
202
|
-
|
|
203
|
-
if role == "assistant" and message.get("tool_calls"):
|
|
204
|
-
# Store tool calls for later pairing with results
|
|
205
|
-
for tc in message["tool_calls"]:
|
|
206
|
-
pending_tool_calls[tc["id"]] = tc
|
|
207
|
-
|
|
208
|
-
# Write the assistant message (skip tool_calls section)
|
|
209
|
-
formatted = self._format_message(message, skip_tool_calls=True)
|
|
210
|
-
if formatted:
|
|
211
|
-
f.write(formatted)
|
|
212
|
-
|
|
213
|
-
elif role == "tool":
|
|
214
|
-
# This is a tool result, pair it with the call
|
|
215
|
-
tool_call_id = message.get("tool_call_id")
|
|
216
|
-
tc = pending_tool_calls.get(tool_call_id)
|
|
217
|
-
|
|
218
|
-
if tc:
|
|
219
|
-
# Write tool call with result together
|
|
220
|
-
fn = tc.get("function", {})
|
|
221
|
-
name = fn.get("name", "unknown")
|
|
222
|
-
arguments = fn.get("arguments", "{}")
|
|
223
|
-
|
|
224
|
-
f.write(f"\n### 🔧 Tool Call: {name}\n\n")
|
|
225
|
-
f.write(self._format_tool_call_inline(arguments) + "\n\n")
|
|
226
|
-
f.write(f"**Result:**\n\n")
|
|
227
|
-
else:
|
|
228
|
-
# Orphaned result (no matching call)
|
|
229
|
-
f.write(f"\n### 🔧 Tool Result (ID: `{tool_call_id}`)\n\n")
|
|
230
|
-
|
|
231
|
-
f.write(self._format_tool_result(message) + "\n")
|
|
232
|
-
|
|
233
|
-
else:
|
|
234
|
-
# Regular message (user, system)
|
|
235
|
-
formatted = self._format_message(message)
|
|
236
|
-
if formatted:
|
|
237
|
-
f.write(formatted)
|
|
238
|
-
|
|
239
|
-
# Write footer
|
|
240
|
-
f.write("\n---\n\n")
|
|
241
|
-
f.write(f"*End of conversation*\n")
|
|
242
|
-
|
|
243
|
-
def end_session(self):
|
|
244
|
-
"""End the current conversation logging session."""
|
|
245
|
-
if not self.current_file:
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
# Add footer
|
|
249
|
-
with open(self.current_file, 'a', encoding='utf-8') as f:
|
|
250
|
-
f.write("\n---\n\n")
|
|
251
|
-
f.write(f"**Ended:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
252
|
-
|
|
253
|
-
logger.info(f"Ended markdown conversation session: {self.current_file.name}")
|
|
254
|
-
self.current_file = None
|
package/src/utils/paths.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""Shared path constants for bone-agent.
|
|
2
|
-
|
|
3
|
-
Centralizes APP_ROOT, REPO_ROOT, and tool paths (e.g. ripgrep)
|
|
4
|
-
so they can be imported from both core and ui modules without
|
|
5
|
-
creating circular dependencies.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _resolve_app_root() -> Path:
|
|
14
|
-
"""Return the application root directory.
|
|
15
|
-
|
|
16
|
-
For frozen builds (PyInstaller), this is the directory containing
|
|
17
|
-
the executable. For source installs, it's two levels up from this file
|
|
18
|
-
(i.e. the repo root).
|
|
19
|
-
"""
|
|
20
|
-
if getattr(sys, "frozen", False):
|
|
21
|
-
return Path(sys.executable).resolve().parent
|
|
22
|
-
return Path(__file__).resolve().parents[2]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
APP_ROOT = _resolve_app_root()
|
|
26
|
-
REPO_ROOT = Path.cwd().resolve()
|
|
27
|
-
|
|
28
|
-
# Platform-agnostic ripgrep path: 'rg' on Unix/Linux, 'rg.exe' on Windows
|
|
29
|
-
_RG_EXE_NAME = "rg.exe" if os.name == "nt" else "rg"
|
|
30
|
-
RG_EXE_PATH = (APP_ROOT / "bin" / _RG_EXE_NAME).resolve()
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Result parsing utilities for tool outputs."""
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def extract_exit_code(tool_result: str) -> Optional[int]:
|
|
7
|
-
"""Parse exit_code from tool result string.
|
|
8
|
-
|
|
9
|
-
Args:
|
|
10
|
-
tool_result: Tool result content string
|
|
11
|
-
|
|
12
|
-
Returns:
|
|
13
|
-
Exit code as integer, or None if not found
|
|
14
|
-
"""
|
|
15
|
-
if not isinstance(tool_result, str):
|
|
16
|
-
return None
|
|
17
|
-
first_line = tool_result.splitlines()[0] if tool_result else ""
|
|
18
|
-
if first_line.startswith("exit_code="):
|
|
19
|
-
try:
|
|
20
|
-
value = first_line.split("=", 1)[1].strip()
|
|
21
|
-
value = value.split()[0] if value else value
|
|
22
|
-
return int(value)
|
|
23
|
-
except ValueError:
|
|
24
|
-
return None
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def extract_metadata_from_result(tool_result: str, key: str) -> Optional[int]:
|
|
29
|
-
"""Parse metadata like matches_found, lines_read, etc. from tool result.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
tool_result: Tool result content string
|
|
33
|
-
key: Metadata key to extract (e.g., "matches_found", "lines_read")
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Extracted value as int, or None if not found
|
|
37
|
-
"""
|
|
38
|
-
if not isinstance(tool_result, str):
|
|
39
|
-
return None
|
|
40
|
-
for line in tool_result.split('\n'):
|
|
41
|
-
if line.startswith(f'{key}='):
|
|
42
|
-
try:
|
|
43
|
-
return int(line.split('=')[1].split()[0])
|
|
44
|
-
except (ValueError, IndexError):
|
|
45
|
-
return None
|
|
46
|
-
return None
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def extract_all_metadata(tool_result: str, line_index: int = 0) -> dict:
|
|
50
|
-
"""Parse entire metadata line into a dictionary.
|
|
51
|
-
|
|
52
|
-
Parses space-separated key=value pairs from a specific line.
|
|
53
|
-
Follows format defined in src/tools/helpers/formatters.py.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
tool_result: Tool result string
|
|
57
|
-
line_index: Which line to parse (default: 0, use 1 for rg results)
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
dict with all parsed metadata (e.g., {'exit_code': 0, 'lines_read': 123, ...})
|
|
61
|
-
Returns empty dict if tool_result is invalid or line_index out of range
|
|
62
|
-
"""
|
|
63
|
-
if not isinstance(tool_result, str) or not tool_result:
|
|
64
|
-
return {}
|
|
65
|
-
|
|
66
|
-
lines = tool_result.split('\n')
|
|
67
|
-
if line_index >= len(lines):
|
|
68
|
-
return {}
|
|
69
|
-
|
|
70
|
-
line = lines[line_index].strip()
|
|
71
|
-
if not line:
|
|
72
|
-
return {}
|
|
73
|
-
|
|
74
|
-
metadata = {}
|
|
75
|
-
# Parse: exit_code=0 path=file.py lines_read=123 start_line=1
|
|
76
|
-
for pair in line.split():
|
|
77
|
-
if '=' in pair:
|
|
78
|
-
key, value = pair.split('=', 1)
|
|
79
|
-
# Skip empty values
|
|
80
|
-
if not value:
|
|
81
|
-
continue
|
|
82
|
-
# Try to parse as int, keep as string if fails
|
|
83
|
-
try:
|
|
84
|
-
value = int(value)
|
|
85
|
-
except ValueError:
|
|
86
|
-
# Keep as string (e.g., paths, error messages)
|
|
87
|
-
pass
|
|
88
|
-
metadata[key] = value
|
|
89
|
-
|
|
90
|
-
return metadata
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def extract_multiple_metadata(tool_result: str, *keys: str, line_index: int = 0) -> dict:
|
|
94
|
-
"""Extract specific metadata keys from a tool result line.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
tool_result: Tool result string
|
|
98
|
-
*keys: Metadata keys to extract (e.g., 'lines_read', 'start_line')
|
|
99
|
-
line_index: Which line to parse (default: 0)
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
dict mapping keys to their parsed values
|
|
103
|
-
Missing keys are not included in the result
|
|
104
|
-
"""
|
|
105
|
-
all_metadata = extract_all_metadata(tool_result, line_index=line_index)
|
|
106
|
-
|
|
107
|
-
# Return only requested keys that exist
|
|
108
|
-
return {key: all_metadata[key] for key in keys if key in all_metadata}
|