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,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)
@@ -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