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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- 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 -50
- 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/skills.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/skills.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 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- 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 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- 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 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- 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 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- 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 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- 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 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/core/memory.py
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
"""Multi-layer memory system for the agent.
|
|
2
|
-
|
|
3
|
-
Two-layer persistent memory:
|
|
4
|
-
- User memory (global): ~/.bone/user_memory.md
|
|
5
|
-
- Project memory (per-repo): {repo_root}/.bone/agents.md
|
|
6
|
-
|
|
7
|
-
Memory files are read-only during conversations — loaded into the system prompt
|
|
8
|
-
for context but never written inline. All writes happen through the dream cron job,
|
|
9
|
-
which consolidates user messages into focused memories nightly.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import logging
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Optional
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
# Capacity constants (prompt-enforced, no code enforcement)
|
|
19
|
-
CHAR_LIMIT = 1500 # suggested chars per layer (~500 tokens)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class MemoryManager:
|
|
23
|
-
"""Manages two-layer memory: user-level (global) and project-level (per-repo).
|
|
24
|
-
|
|
25
|
-
Uses a lazy singleton pattern — first call with repo_root bootstraps the
|
|
26
|
-
instance, subsequent calls reuse it. Call reset() when switching repos.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
_instance: Optional["MemoryManager"] = None
|
|
30
|
-
|
|
31
|
-
def __init__(self, repo_root: Path):
|
|
32
|
-
self.repo_root = repo_root
|
|
33
|
-
self.user_memory_path = Path.home() / ".bone" / "user_memory.md"
|
|
34
|
-
self.project_memory_path = repo_root / ".bone" / "agents.md"
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def get_instance(cls, repo_root: Path = None) -> Optional["MemoryManager"]:
|
|
38
|
-
"""Lazy singleton. First call sets repo_root, subsequent calls reuse instance.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
repo_root: Path to repository root. Required on first call,
|
|
42
|
-
ignored on subsequent calls (until reset()).
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
MemoryManager instance, or None if no repo_root provided and
|
|
46
|
-
no instance has been initialized yet.
|
|
47
|
-
"""
|
|
48
|
-
if cls._instance is not None:
|
|
49
|
-
return cls._instance
|
|
50
|
-
if repo_root is None:
|
|
51
|
-
return None
|
|
52
|
-
cls._instance = cls(repo_root)
|
|
53
|
-
return cls._instance
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def reset(cls) -> None:
|
|
57
|
-
"""Clear singleton. Called when switching repos via /cd."""
|
|
58
|
-
cls._instance = None
|
|
59
|
-
|
|
60
|
-
def ensure_exists(self) -> None:
|
|
61
|
-
"""Create user-level directory and memory file only.
|
|
62
|
-
|
|
63
|
-
Project-level .bone/agents.md is created lazily on first write,
|
|
64
|
-
not at startup. This prevents creating .bone/ directories in
|
|
65
|
-
non-project locations (e.g. when running from ~/.bone/ itself).
|
|
66
|
-
"""
|
|
67
|
-
self._ensure_dir_and_file(
|
|
68
|
-
self.user_memory_path,
|
|
69
|
-
"# User Memory\n\n",
|
|
70
|
-
)
|
|
71
|
-
# Add .bone/ to .gitignore if repo_root has a git repo
|
|
72
|
-
self._ensure_gitignore()
|
|
73
|
-
|
|
74
|
-
def load_user_memory(self) -> str:
|
|
75
|
-
"""Read and return user memory file content. Returns empty string if missing."""
|
|
76
|
-
return self._read_file(self.user_memory_path)
|
|
77
|
-
|
|
78
|
-
def load_project_memory(self) -> str:
|
|
79
|
-
"""Read and return project memory file content. Returns empty string if missing."""
|
|
80
|
-
return self._read_file(self.project_memory_path)
|
|
81
|
-
|
|
82
|
-
def get_user_usage(self) -> dict:
|
|
83
|
-
"""Return {chars_used, chars_limit} for user memory."""
|
|
84
|
-
content = self.load_user_memory()
|
|
85
|
-
return {"chars_used": len(content), "chars_limit": CHAR_LIMIT}
|
|
86
|
-
|
|
87
|
-
def get_project_usage(self) -> dict:
|
|
88
|
-
"""Return {chars_used, chars_limit} for project memory."""
|
|
89
|
-
content = self.load_project_memory()
|
|
90
|
-
return {"chars_used": len(content), "chars_limit": CHAR_LIMIT}
|
|
91
|
-
|
|
92
|
-
# ---- Private helpers ----
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
def _has_entries(content: str) -> bool:
|
|
96
|
-
"""Check if memory file has entries beyond just the header.
|
|
97
|
-
|
|
98
|
-
A file with only "# User Memory\\n\\n" is considered empty.
|
|
99
|
-
"""
|
|
100
|
-
stripped = content.strip()
|
|
101
|
-
# Remove the H1 header line and blank lines
|
|
102
|
-
for line in stripped.split("\n"):
|
|
103
|
-
line = line.strip()
|
|
104
|
-
if not line or line.startswith("#"):
|
|
105
|
-
continue
|
|
106
|
-
# Found a non-header, non-blank line — has entries
|
|
107
|
-
return True
|
|
108
|
-
return False
|
|
109
|
-
|
|
110
|
-
@staticmethod
|
|
111
|
-
def _ensure_dir_and_file(path: Path, default_content: str) -> None:
|
|
112
|
-
"""Create parent directory and file with default content if missing."""
|
|
113
|
-
try:
|
|
114
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
if not path.exists():
|
|
116
|
-
path.write_text(default_content, encoding="utf-8")
|
|
117
|
-
logger.debug("Created memory file: %s", path)
|
|
118
|
-
except Exception as e:
|
|
119
|
-
logger.warning("Failed to create memory file %s: %s", path, e)
|
|
120
|
-
|
|
121
|
-
@staticmethod
|
|
122
|
-
def _read_file(path: Path) -> str:
|
|
123
|
-
"""Read file content, return empty string on any error."""
|
|
124
|
-
try:
|
|
125
|
-
if path.exists():
|
|
126
|
-
return path.read_text(encoding="utf-8")
|
|
127
|
-
except Exception as e:
|
|
128
|
-
logger.warning("Failed to read memory file %s: %s", path, e)
|
|
129
|
-
return ""
|
|
130
|
-
|
|
131
|
-
def _ensure_gitignore(self) -> None:
|
|
132
|
-
"""Add .bone/ to .gitignore if not already present."""
|
|
133
|
-
gitignore = self.repo_root / ".gitignore"
|
|
134
|
-
if not self.repo_root.is_dir() or not (self.repo_root / ".git").is_dir():
|
|
135
|
-
return # Not a git repo
|
|
136
|
-
try:
|
|
137
|
-
if not gitignore.exists():
|
|
138
|
-
gitignore.write_text(".bone/\n", encoding="utf-8")
|
|
139
|
-
return
|
|
140
|
-
content = gitignore.read_text(encoding="utf-8")
|
|
141
|
-
if ".bone" not in content:
|
|
142
|
-
with open(gitignore, "a", encoding="utf-8") as f:
|
|
143
|
-
f.write("\n.bone/\n")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
logger.warning("Failed to update .gitignore: %s", e)
|
package/src/core/metadata.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""AI-powered metadata generation for skills and plugins.
|
|
2
|
-
|
|
3
|
-
Generates description and tags from content when not provided manually.
|
|
4
|
-
Shared between skill frontmatter and plugin registration.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import logging
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
_SYSTEM_PROMPT = """\
|
|
14
|
-
You generate concise metadata for code tools and prompts.
|
|
15
|
-
|
|
16
|
-
Given the content below, return a JSON object with exactly two fields:
|
|
17
|
-
- "description": a one-sentence summary (max 120 chars) of what this does
|
|
18
|
-
- "tags": a list of 3-7 lowercase single-word tags for discovery
|
|
19
|
-
|
|
20
|
-
Return ONLY the JSON object, no other text."""
|
|
21
|
-
|
|
22
|
-
_MAX_CONTENT_CHARS = 3000
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def generate_metadata(content: str, name: str = "") -> dict:
|
|
26
|
-
"""Generate description and tags from content using the LLM.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
content: The skill prompt or plugin source to describe.
|
|
30
|
-
name: Optional name for context.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
Dict with 'description' (str) and 'tags' (list[str]).
|
|
34
|
-
Returns defaults on failure.
|
|
35
|
-
"""
|
|
36
|
-
truncated = content[:_MAX_CONTENT_CHARS]
|
|
37
|
-
if len(content) > _MAX_CONTENT_CHARS:
|
|
38
|
-
truncated += "\n..."
|
|
39
|
-
|
|
40
|
-
user_msg = f"Name: {name}\n\n{truncated}" if name else truncated
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
from llm.client import LLMClient
|
|
44
|
-
|
|
45
|
-
client = LLMClient()
|
|
46
|
-
response = client.chat_completion(
|
|
47
|
-
messages=[
|
|
48
|
-
{"role": "system", "content": _SYSTEM_PROMPT},
|
|
49
|
-
{"role": "user", "content": user_msg},
|
|
50
|
-
],
|
|
51
|
-
stream=False,
|
|
52
|
-
tools=None,
|
|
53
|
-
)
|
|
54
|
-
raw = response["choices"][0]["message"]["content"].strip()
|
|
55
|
-
# Strip markdown code fences if present
|
|
56
|
-
raw = re.sub(r"^```(?:json)?\s*\n?", "", raw)
|
|
57
|
-
raw = re.sub(r"\n?```\s*$", "", raw)
|
|
58
|
-
parsed = json.loads(raw)
|
|
59
|
-
|
|
60
|
-
description = str(parsed.get("description", ""))[:120]
|
|
61
|
-
tags = [str(t).lower() for t in parsed.get("tags", []) if t]
|
|
62
|
-
|
|
63
|
-
return {"description": description, "tags": tags}
|
|
64
|
-
|
|
65
|
-
except Exception:
|
|
66
|
-
logger.debug("Metadata generation failed for '%s'", name, exc_info=True)
|
|
67
|
-
return _fallback_metadata(content, name)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _fallback_metadata(content: str, name: str) -> dict:
|
|
71
|
-
"""Simple heuristic metadata when LLM is unavailable."""
|
|
72
|
-
# Use first line or first ~100 chars as description
|
|
73
|
-
first_line = content.strip().split("\n", 1)[0].strip()
|
|
74
|
-
desc = first_line[:120] if first_line else name
|
|
75
|
-
return {"description": desc, "tags": [name] if name else []}
|
package/src/core/retry.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Retry logic for LLM connection and timeout errors."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from exceptions import LLMResponseError
|
|
6
|
-
|
|
7
|
-
# Timeout retry constants
|
|
8
|
-
RETRY_MAX_ATTEMPTS = 3
|
|
9
|
-
RETRY_DELAYS = (2, 4) # exponential backoff per attempt
|
|
10
|
-
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
11
|
-
RETRYABLE_ERROR_KEYWORDS = (
|
|
12
|
-
"timeout", "timed out", "connectionerror", "connection refused",
|
|
13
|
-
"connection reset", "connection aborted", "name or service not known",
|
|
14
|
-
"network unreachable", "no route to host", "eof occurred",
|
|
15
|
-
)
|
|
16
|
-
NON_RETRYABLE_STATUS_CODES = {400, 401, 403, 405, 422}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def is_retryable_error(error):
|
|
20
|
-
"""Check if an LLMConnectionError is retryable.
|
|
21
|
-
|
|
22
|
-
Retryable conditions:
|
|
23
|
-
- Timeout or connection-level errors (network unreachable, DNS failure, etc.)
|
|
24
|
-
- HTTP 429 (rate limited), 502, 503, 504 (server errors)
|
|
25
|
-
|
|
26
|
-
Non-retryable conditions:
|
|
27
|
-
- HTTP 400, 401, 403, 405, 422 (client/auth errors)
|
|
28
|
-
- LLMResponseError (malformed response data)
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
error: Exception instance (typically LLMConnectionError)
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
bool: True if the error is retryable
|
|
35
|
-
"""
|
|
36
|
-
# Never retry response parsing errors
|
|
37
|
-
if isinstance(error, LLMResponseError):
|
|
38
|
-
return False
|
|
39
|
-
|
|
40
|
-
# Check HTTP status code first (most reliable signal)
|
|
41
|
-
details = getattr(error, 'details', {}) or {}
|
|
42
|
-
status_code = details.get("status_code")
|
|
43
|
-
if status_code is not None:
|
|
44
|
-
if status_code in NON_RETRYABLE_STATUS_CODES:
|
|
45
|
-
return False
|
|
46
|
-
if status_code in RETRYABLE_STATUS_CODES:
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
# For network-level errors, check the original error message
|
|
50
|
-
original_error = details.get("original_error", "")
|
|
51
|
-
original_lower = original_error.lower()
|
|
52
|
-
return any(keyword in original_lower for keyword in RETRYABLE_ERROR_KEYWORDS)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def wait_with_cancel_message(console, delay_seconds):
|
|
56
|
-
"""Wait briefly before retrying, showing a dim status line.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
console: Rich console for output
|
|
60
|
-
delay_seconds: Seconds to wait
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
bool: True if wait completed, False if interrupted by KeyboardInterrupt
|
|
64
|
-
"""
|
|
65
|
-
console.print(f"[dim]Connection issue, retrying in {delay_seconds}s... (Ctrl+C to cancel)[/dim]")
|
|
66
|
-
try:
|
|
67
|
-
time.sleep(delay_seconds)
|
|
68
|
-
except KeyboardInterrupt:
|
|
69
|
-
console.print("[dim]Retry cancelled.[/dim]")
|
|
70
|
-
return False
|
|
71
|
-
return True
|