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,243 +0,0 @@
1
- """Structured command safety system for auto-approval.
2
-
3
- Replaces the flat ALLOWED_COMMANDS whitelist with a command+subcommand
4
- granularity system that distinguishes read-only operations from mutations.
5
-
6
- Design principles:
7
- - No args = not safe (for commands with subcommand variants)
8
- - Gate anything that has potential to be unsafe
9
- - Deny-by-default: commands not in the dict require approval
10
- - Compound flags use longest-prefix matching
11
- """
12
-
13
- import os
14
- import shlex
15
- from utils.validation import CHAINING_OPERATORS
16
-
17
-
18
- # ---------------------------------------------------------------------------
19
- # SAFE_COMMAND_RULES
20
- # ---------------------------------------------------------------------------
21
- # Maps command names to their safety profile:
22
- # None → always safe (inherently read-only, e.g., ps, pwd)
23
- # set() → only safe for listed subcommands/flags
24
- #
25
- # Platform normalization strips .exe suffix and lowercases before lookup.
26
-
27
- SAFE_COMMAND_RULES: dict[str, frozenset | None] = {
28
- # --- Always safe (truly read-only, no mutating subcommands) ---
29
- "pwd": None,
30
- "which": None,
31
- "whereis": None,
32
- "uname": None,
33
- "hostname": None,
34
- "uptime": None,
35
- "date": None,
36
- "cal": None,
37
- "whoami": None,
38
- "id": None,
39
- "env": frozenset({"--version", "--help"}),
40
- "printenv": frozenset({"--version", "--help"}),
41
- "lscpu": None,
42
- "lsblk": None,
43
- "file": None,
44
- "stat": None,
45
- "md5sum": None,
46
- "sha256sum": None,
47
- "free": None,
48
- "df": None,
49
- "du": None,
50
- "dmesg": None,
51
- "ltrace": None,
52
- "ps": None,
53
- "pgrep": None,
54
- "pidof": None,
55
- "lsof": None,
56
- "ping": None,
57
- "nslookup": None,
58
- "dig": None,
59
- "ss": None,
60
- "ifconfig": None,
61
- "netstat": None,
62
- "journalctl": None,
63
- "apt-cache": None,
64
- "apt-show": None,
65
- "dpkg-query": None,
66
-
67
- # --- Subcommand-gated (safe only for specific read-only operations) ---
68
- "git": frozenset({
69
- "status", "log", "diff", "show", "branch",
70
- "remote", "tag",
71
- "rev-parse", "shortlog", "describe", "symbolic-ref",
72
- "reflog", "name-rev", "blame", "annotate",
73
- "for-each-ref", "ls-files", "ls-tree", "ls-remote",
74
- }),
75
- "pip": frozenset({"show", "list", "--version", "check", "debug", "index", "inspect"}),
76
- "pip3": frozenset({"show", "list", "--version", "check", "debug", "index", "inspect"}),
77
- "npm": frozenset({"list", "ls", "view", "version", "outdated", "pack", "info", "doctor", "audit"}),
78
- "node": frozenset({"--version"}),
79
- "python": frozenset({"--version"}),
80
- "python3": frozenset({"--version"}),
81
- "pacman": frozenset({
82
- "-Q", "-Qi", "-Ql", "-Qo", "-Qs", "-Qt",
83
- "-F", "-Si", "-Ss", "-Fl", "-G",
84
- }),
85
- "dpkg": frozenset({"-l", "-s", "-S", "-L", "-p", "--verify", "--audit"}),
86
- "rpm": frozenset({"-q", "-qa", "-qi", "-ql", "-qf", "--queryformat"}),
87
- "dnf": frozenset({"list", "info", "search", "check-update", "repoquery"}),
88
- "yum": frozenset({"list", "info", "search", "check-update"}),
89
- "systemctl": frozenset({
90
- "status", "list-units", "list-unit-files", "show",
91
- "is-active", "is-enabled", "cat", "list-timers",
92
- "list-sockets", "list-jobs",
93
- }),
94
- "service": frozenset({"--status-all"}), # "service <name> status" handled by _is_safe_service_command
95
- "ip": frozenset({"addr", "address", "link", "route", "neigh", "maddr", "rule", "netns"}),
96
-
97
- # --- Windows equivalents ---
98
- "where": None,
99
- "systeminfo": None,
100
- "Get-Process": None,
101
- "Get-Service": None,
102
- "Get-ChildItem": None,
103
- "Get-Content": None,
104
- "Get-Location": None,
105
- "Test-Connection": None,
106
- "Get-NetIPAddress": None,
107
- }
108
-
109
-
110
- # Sub-subcommand deny lists for commands where the first arg passes safety
111
- # but later args can be mutating. Checked AFTER first-arg matching.
112
- # If any token appears in the deny list, the command is rejected.
113
- _IP_MUTATING_VERBS = frozenset({
114
- "set", "add", "delete", "replace", "flush", "change",
115
- })
116
-
117
- # Commands that need deep token scanning mapped to their deny sets.
118
- _DEEP_SCAN_RULES: dict[str, frozenset] = {
119
- "ip": _IP_MUTATING_VERBS,
120
- }
121
-
122
-
123
- def _tokenize(command: str) -> list[str]:
124
- """Tokenize a command string using platform-appropriate splitting."""
125
- use_posix = os.name != "nt"
126
- try:
127
- return shlex.split(command, posix=use_posix)
128
- except ValueError:
129
- return command.split()
130
-
131
-
132
- def _normalize_command_name(name: str) -> str:
133
- """Normalize a command name for lookup.
134
-
135
- Strips .exe suffix and lowercases. Does NOT normalize PowerShell
136
- cmdlet casing (case-insensitive lookup handles that).
137
- """
138
- if name.lower().endswith(".exe"):
139
- name = name[:-4]
140
- return name.lower()
141
-
142
-
143
- def _matches_safe_subcommand(arg: str, safe_set: frozenset) -> bool:
144
- """Check if an argument matches any entry in the safe subcommand set.
145
-
146
- Uses longest-prefix matching for flag-style arguments:
147
- e.g., if '-Qi' is safe, then '-Qil' also matches.
148
- For word-style subcommands (e.g., git 'status'), exact match only.
149
-
150
- Comparison is case-insensitive.
151
- """
152
- arg_lower = arg.lower()
153
-
154
- # Build lowercase version of safe_set for case-insensitive comparison
155
- safe_lower = {s.lower() for s in safe_set}
156
-
157
- # Exact match
158
- if arg_lower in safe_lower:
159
- return True
160
-
161
- # Longest-prefix match for flags (arguments starting with -)
162
- if arg_lower.startswith("-"):
163
- # Try progressively shorter prefixes
164
- for length in range(len(arg_lower) - 1, 1, -1):
165
- prefix = arg_lower[:length]
166
- if prefix in safe_lower:
167
- return True
168
-
169
- return False
170
-
171
-
172
- def is_safe_command(command: str) -> bool:
173
- """Check if a command should be auto-approved (safe, read-only).
174
-
175
- A command is auto-approved when:
176
- 1. It contains no chaining/redirection operators
177
- 2. The command name is in SAFE_COMMAND_RULES
178
- 3. If gated (has a set of safe subcommands), the first argument
179
- matches an entry in the set
180
- 4. If always-safe (None), it's approved with or without args
181
-
182
- Args:
183
- command: Command string to validate
184
-
185
- Returns:
186
- bool: True if the command is safe to auto-approve
187
- """
188
- command = command.strip()
189
- if not command:
190
- return False
191
-
192
- # Strip "powershell " prefix if present (legacy support for Windows users)
193
- if command.lower().startswith("powershell "):
194
- command = command[len("powershell "):].strip()
195
-
196
- # Reject any command containing chaining/redirection operators
197
- if CHAINING_OPERATORS.search(command):
198
- return False
199
-
200
- # Tokenize and get command name
201
- tokens = _tokenize(command)
202
- if not tokens:
203
- return False
204
-
205
- cmd_name = _normalize_command_name(tokens[0])
206
-
207
- # Look up in rules (deny-by-default)
208
- if cmd_name not in SAFE_COMMAND_RULES:
209
- # Unknown command — require approval
210
- return False
211
-
212
- rule = SAFE_COMMAND_RULES[cmd_name]
213
- if rule is None:
214
- # Always-safe command (e.g., ps, pwd)
215
- return True
216
-
217
- if not rule:
218
- # Empty frozenset — no safe subcommands defined, deny
219
- return False
220
-
221
- # Gated command — need at least one subcommand arg
222
- if len(tokens) < 2:
223
- return False
224
-
225
- # Check first argument against safe subcommand set
226
- first_arg = tokens[1]
227
-
228
- # Special case: "service <name> status" — the safe subcommand is the LAST arg
229
- if cmd_name == "service" and len(tokens) >= 3 and tokens[-1].lower() == "status":
230
- return True
231
-
232
- if not _matches_safe_subcommand(first_arg, rule):
233
- return False
234
-
235
- # Deep scan: for commands with known mutating sub-subcommands,
236
- # reject if any remaining token matches the deny list.
237
- deny_set = _DEEP_SCAN_RULES.get(cmd_name)
238
- if deny_set and len(tokens) > 2:
239
- for tok in tokens[2:]:
240
- if tok.lower() in deny_set:
241
- return False
242
-
243
- return True
@@ -1,195 +0,0 @@
1
- """Centralized configuration for bone-agent."""
2
- import re
3
- from dataclasses import dataclass, field
4
- from pathlib import Path
5
- from typing import Set
6
-
7
- # Load config from llm.config
8
- # Note: src/ is added to sys.path in main.py, so we can import directly
9
- from llm.config import _CONFIG
10
-
11
- # Styles and themes
12
- from pygments.styles.monokai import MonokaiStyle
13
-
14
-
15
- class MonokaiDarkBGStyle(MonokaiStyle):
16
- """Monokai style with dark background for code highlighting."""
17
- background_color = "#141414"
18
-
19
-
20
- _HEADING_RE = re.compile(r'^(#{1,6})\s+(.+)$', re.MULTILINE)
21
-
22
-
23
- def left_align_headings(text: str) -> str:
24
- """Strip markdown heading markers to avoid Rich's centering."""
25
- return _HEADING_RE.sub(lambda m: m.group(2), text)
26
-
27
-
28
- @dataclass
29
- class ServerSettings:
30
- """Local llama-server configuration."""
31
- ngl_layers: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("ngl_layers", 99))
32
- ctx_size: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("ctx_size", 8192))
33
- n_predict: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("n_predict", 8192))
34
- rope_scale: float = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("rope_scale", 1.0))
35
- threads: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("threads", 4))
36
- batch_size: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("batch_size", 2048))
37
- ubatch_size: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("ubatch_size", 512))
38
- flash_attn: bool = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("flash_attn", True))
39
- health_check_timeout_sec: int = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("health_check_timeout_sec", 120))
40
- health_check_interval_sec: float = field(default_factory=lambda: _CONFIG.get("SERVER_SETTINGS", {}).get("health_check_interval_sec", 1.0))
41
-
42
-
43
- @dataclass
44
- class ToolSettings:
45
- """Tool execution limits and defaults."""
46
- max_tool_calls: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_tool_calls", 100))
47
- command_timeout_sec: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("command_timeout_sec", 30))
48
- enable_parallel_execution: bool = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("enable_parallel_execution", True))
49
- max_parallel_workers: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_parallel_workers", 10))
50
- max_command_output_lines: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_command_output_lines", 100))
51
- max_shell_output_lines: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_shell_output_lines", 200))
52
- max_file_preview_lines: int = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("max_file_preview_lines", 200))
53
- disabled_tools: list = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("disabled_tools", []))
54
- hidden_skills: list = field(default_factory=lambda: _CONFIG.get("TOOL_SETTINGS", {}).get("hidden_skills", []))
55
-
56
- @dataclass
57
- class FileSettings:
58
- """File scanning and reading limits."""
59
- max_file_bytes: int = field(default_factory=lambda: _CONFIG.get("FILE_SETTINGS", {}).get("max_file_bytes", 200_000))
60
- max_total_bytes: int = field(default_factory=lambda: _CONFIG.get("FILE_SETTINGS", {}).get("max_total_bytes", 1_500_000))
61
- exclude_dirs: Set[str] = None
62
-
63
- def __post_init__(self):
64
- if self.exclude_dirs is None:
65
- config_exclude = _CONFIG.get("FILE_SETTINGS", {}).get("exclude_dirs")
66
- if config_exclude:
67
- self.exclude_dirs = set(config_exclude)
68
- else:
69
- self.exclude_dirs = {".git", ".venv", "llama.cpp", "bin", "__pycache__"}
70
-
71
-
72
- @dataclass
73
- class ToolCompactionSettings:
74
- """Per-message tool result compaction settings."""
75
- enable_per_message_compaction: bool = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("tool_compaction", {}).get("enable_per_message_compaction", True))
76
- uncompacted_tail_tokens: int = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("tool_compaction", {}).get("uncompacted_tail_tokens", 40_000))
77
- min_tool_blocks: int = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("tool_compaction", {}).get("min_tool_blocks", 5))
78
- compact_failed_tools: bool = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("tool_compaction", {}).get("compact_failed_tools", True))
79
-
80
-
81
- @dataclass
82
- class SubAgentSettings:
83
- """Sub-agent token limits and behavior configuration."""
84
- soft_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("soft_limit_tokens", 100_000))
85
- hard_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("hard_limit_tokens", 150_000))
86
- billed_warning_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("billed_warning_tokens", 200_000))
87
- billed_hard_limit_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("billed_hard_limit_tokens", 500_000))
88
- enable_compaction: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("enable_compaction", True))
89
- compact_trigger_tokens: int = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("compact_trigger_tokens", 50_000))
90
- allowed_tools: list = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("allowed_tools", ["rg", "read_file", "list_directory", "web_search"]))
91
- allow_active_plugins: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("allow_active_plugins", False))
92
- dump_context_on_hard_limit: bool = field(default_factory=lambda: _CONFIG.get("SUB_AGENT_SETTINGS", {}).get("dump_context_on_hard_limit", True))
93
-
94
-
95
- # Context compaction settings
96
- @dataclass
97
- class ContextSettings:
98
- """Context compaction thresholds and defaults."""
99
- compact_trigger_tokens: int = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("compact_trigger_tokens", 100_000))
100
- max_context_window: int = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("max_context_window", 200_000))
101
- log_conversations: bool = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("log_conversations", False))
102
- conversations_dir: str = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("conversations_dir", "conversations"))
103
- notify_auto_compaction: bool = field(default_factory=lambda: _CONFIG.get("CONTEXT_SETTINGS", {}).get("notify_auto_compaction", True))
104
- tool_compaction: ToolCompactionSettings = field(default_factory=ToolCompactionSettings)
105
- hard_limit_tokens: int = field(init=False, repr=False)
106
-
107
- def __post_init__(self):
108
- _ctx = _CONFIG.get("CONTEXT_SETTINGS", {})
109
- if "hard_limit_tokens" in _ctx:
110
- self.hard_limit_tokens = _ctx["hard_limit_tokens"]
111
- else:
112
- self.hard_limit_tokens = int(self.max_context_window * 0.9)
113
-
114
-
115
- @dataclass
116
- class PromptSettings:
117
- """Prompt variant selection."""
118
- variant: str = field(default_factory=lambda: _CONFIG.get("PROMPT_SETTINGS", {}).get("variant", "micro"))
119
-
120
-
121
- @dataclass
122
- class DreamSettings:
123
- """Dream memory consolidation settings."""
124
- enabled: bool = field(default_factory=lambda: _CONFIG.get("DREAM_SETTINGS", {}).get("enabled", True))
125
-
126
-
127
- @dataclass
128
- class ObsidianSettings:
129
- """Obsidian vault integration settings.
130
-
131
- Supports runtime updates via update() method for /obsidian commands.
132
- """
133
- vault_path: str = field(default_factory=lambda: _CONFIG.get("OBSIDIAN_SETTINGS", {}).get("vault_path", ""))
134
- enabled: bool = field(default_factory=lambda: _CONFIG.get("OBSIDIAN_SETTINGS", {}).get("enabled", False))
135
- exclude_folders: str = field(default_factory=lambda: _CONFIG.get("OBSIDIAN_SETTINGS", {}).get("exclude_folders", ".obsidian,.trash,node_modules,.git,__pycache__"))
136
- project_base: str = field(default_factory=lambda: _CONFIG.get("OBSIDIAN_SETTINGS", {}).get("project_base", "Dev"))
137
-
138
- def update(self, **kwargs):
139
- """Update settings fields at runtime.
140
-
141
- Args:
142
- **kwargs: Field names and values to update
143
- """
144
- from dataclasses import fields
145
- valid_keys = {f.name for f in fields(self)}
146
- for key, value in kwargs.items():
147
- if key in valid_keys:
148
- setattr(self, key, value)
149
-
150
- def is_configured(self) -> bool:
151
- """Check if Obsidian integration is configured in settings.
152
-
153
- Returns:
154
- True if enabled and vault_path is set (does NOT validate disk)
155
- """
156
- return self.enabled and bool(self.vault_path)
157
-
158
- def is_active(self) -> bool:
159
- """Check if Obsidian integration is fully operational.
160
-
161
- Validates the vault path exists on disk and contains .obsidian/.
162
-
163
- Returns:
164
- True if enabled, vault_path is set, and vault is valid on disk
165
- """
166
- if not self.enabled or not self.vault_path:
167
- return False
168
- root = Path(self.vault_path).resolve()
169
- if not root.is_dir():
170
- return False
171
- return (root / ".obsidian").is_dir()
172
-
173
- @property
174
- def exclude_folders_list(self) -> list:
175
- """Return exclude_folders as a pre-parsed list of strings.
176
-
177
- Avoids repeated str.split(",") on every rg call.
178
- """
179
- return [f.strip() for f in self.exclude_folders.split(",") if f.strip()]
180
-
181
-
182
- # Global instances
183
- server_settings = ServerSettings()
184
- tool_settings = ToolSettings()
185
- file_settings = FileSettings()
186
- context_settings = ContextSettings()
187
- sub_agent_settings = SubAgentSettings()
188
- dream_settings = DreamSettings()
189
- obsidian_settings = ObsidianSettings()
190
- prompt_settings = PromptSettings()
191
- # Tool execution constants
192
- MAX_TOOL_CALLS = tool_settings.max_tool_calls
193
- MAX_COMMAND_OUTPUT_LINES = tool_settings.max_command_output_lines
194
- MAX_SHELL_OUTPUT_LINES = tool_settings.max_shell_output_lines
195
- MAX_FILE_PREVIEW_LINES = tool_settings.max_file_preview_lines
@@ -1,120 +0,0 @@
1
- """Lightweight user-message logger for the dream memory system.
2
-
3
- Appends one JSONL line per user message, one file per day per project.
4
- Always on by default — no toggle needed.
5
- """
6
-
7
- import hashlib
8
- import json
9
- import logging
10
- from datetime import datetime, timedelta
11
- from pathlib import Path
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- # Base directory for daily message logs
16
- CONVERSATIONS_DIR = Path.home() / ".bone" / "conversations"
17
- RETENTION_DAYS = 7
18
-
19
-
20
- def _project_suffix(project_dir: Path) -> str:
21
- """Generate a short suffix from a project directory path.
22
-
23
- Format: {dirname}_{first 6 chars of SHA256(path)}
24
- Avoids collisions between repos with the same folder name.
25
- """
26
- path_str = str(project_dir.resolve())
27
- h = hashlib.sha256(path_str.encode()).hexdigest()[:6]
28
- return f"{project_dir.name}_{h}"
29
-
30
-
31
- PROJECT_INDEX_FILE = CONVERSATIONS_DIR / ".project_index.jsonl"
32
-
33
-
34
- def _register_project(key: str, project_dir: Path) -> None:
35
- """Append a key→path mapping to the project index if not already present."""
36
- resolved = str(project_dir.resolve())
37
- # Check if this key already maps to this path
38
- if PROJECT_INDEX_FILE.exists():
39
- with open(PROJECT_INDEX_FILE, "r", encoding="utf-8") as f:
40
- for line in f:
41
- try:
42
- entry = json.loads(line)
43
- except json.JSONDecodeError:
44
- continue
45
- if entry.get("key") == key and entry.get("path") == resolved:
46
- return # Already indexed
47
- PROJECT_INDEX_FILE.parent.mkdir(parents=True, exist_ok=True)
48
- with open(PROJECT_INDEX_FILE, "a", encoding="utf-8") as f:
49
- f.write(json.dumps({"key": key, "path": resolved}) + "\n")
50
-
51
-
52
- class UserMessageLogger:
53
- """Logs user messages to daily JSONL files for later dream processing.
54
-
55
- When a project_dir is provided, messages go to a per-project file:
56
- {date}__{dirname}_{hash}.jsonl
57
- Without a project_dir, messages go to the catch-all:
58
- {date}.jsonl
59
- """
60
-
61
- def __init__(self, conversations_dir: Path | None = None):
62
- self._dir = conversations_dir or CONVERSATIONS_DIR
63
- self._dir.mkdir(parents=True, exist_ok=True)
64
-
65
- def log_user_message(self, content: str, project_dir: Path | None = None) -> None:
66
- """Append a single user message to today's JSONL file.
67
-
68
- Args:
69
- content: The user message text.
70
- project_dir: Optional project root directory. If provided,
71
- messages are written to a per-project file.
72
-
73
- Opens in append mode and flushes immediately for crash safety.
74
- Each message is one self-contained JSON line.
75
- """
76
- today = datetime.now().strftime("%Y-%m-%d")
77
- if project_dir:
78
- suffix = _project_suffix(project_dir)
79
- _register_project(suffix, project_dir)
80
- filepath = self._dir / f"{today}__{suffix}.jsonl"
81
- else:
82
- filepath = self._dir / f"{today}.jsonl"
83
- entry = {"ts": datetime.now().isoformat(), "msg": content}
84
- with open(filepath, "a", encoding="utf-8") as f:
85
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
86
-
87
- @staticmethod
88
- def cleanup_old_files(directory: Path | None = None, retention_days: int = RETENTION_DAYS) -> int:
89
- """Delete JSONL files older than retention_days. Returns count of files removed."""
90
- target_dir = directory or CONVERSATIONS_DIR
91
- if not target_dir.exists():
92
- return 0
93
-
94
- cutoff = datetime.now() - timedelta(days=retention_days)
95
- removed = 0
96
- surviving = set()
97
- for f in target_dir.glob("*.jsonl"):
98
- if f.stat().st_mtime < cutoff.timestamp():
99
- f.unlink()
100
- removed += 1
101
- logger.debug("Removed old conversation log: %s", f.name)
102
- else:
103
- surviving.add(f.name)
104
-
105
- # Prune stale entries from the project index
106
- index_file = target_dir / ".project_index.jsonl"
107
- if index_file.exists():
108
- kept: list[str] = []
109
- for line in index_file.read_text(encoding="utf-8").splitlines():
110
- try:
111
- entry = json.loads(line)
112
- except json.JSONDecodeError:
113
- continue
114
- key = entry.get("key", "")
115
- # Keep entry if any file matching its key still exists
116
- if any(key in name for name in surviving):
117
- kept.append(line)
118
- index_file.write_text("\n".join(kept) + ("\n" if kept else ""), encoding="utf-8")
119
-
120
- return removed