moelove commited on
Commit
8a173aa
·
1 Parent(s): 422129b

add build-in tools and easy to quit

Browse files

Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>

.kiro/specs/ctrl-d-quit/design.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Design Document
2
+
3
+ ## Overview
4
+
5
+ This feature adds support for gracefully handling Ctrl+D (EOF signal) in the `amcp agent` interactive mode. Currently, users can only exit using explicit commands like 'exit', 'quit', or 'q'. Adding Ctrl+D support provides a more natural terminal experience consistent with standard CLI tools and REPL environments.
6
+
7
+ The implementation will catch the `EOFError` exception raised when users press Ctrl+D and perform the same cleanup and exit sequence as the existing exit commands.
8
+
9
+ ## Architecture
10
+
11
+ The change is localized to the interactive loop in `src/amcp/cli.py`. The architecture follows the existing pattern:
12
+
13
+ 1. **Input Layer**: The `console.input()` call in the interactive loop
14
+ 2. **Exception Handling**: New `EOFError` catch block
15
+ 3. **Exit Sequence**: Reuse existing goodbye message and cleanup logic
16
+
17
+ No new components or modules are required. The change integrates seamlessly with the existing conversation history persistence and session management.
18
+
19
+ ## Components and Interfaces
20
+
21
+ ### Modified Component: Interactive Loop (`src/amcp/cli.py`)
22
+
23
+ **Current Implementation:**
24
+ ```python
25
+ while True:
26
+ try:
27
+ user_input = console.input("[bold]You:[/bold] ").strip()
28
+
29
+ if user_input.lower() in ['exit', 'quit', 'q']:
30
+ console.print("[green]Goodbye! 👋[/green]")
31
+ break
32
+ # ... rest of loop
33
+ except KeyboardInterrupt:
34
+ console.print("\\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
35
+ except Exception as e:
36
+ console.print(f"[red]Error: {e}[/red]")
37
+ ```
38
+
39
+ **Modified Implementation:**
40
+ The `EOFError` exception will be caught at the same level as `KeyboardInterrupt`, providing consistent exception handling for terminal signals.
41
+
42
+ ### Interface Changes
43
+
44
+ No public API changes. The modification is internal to the CLI interactive loop. The behavior change is:
45
+
46
+ - **Before**: Ctrl+D causes an unhandled `EOFError`, potentially crashing or displaying an error
47
+ - **After**: Ctrl+D triggers graceful exit with goodbye message and proper cleanup
48
+
49
+ ## Data Models
50
+
51
+ No data model changes required. The existing session management and conversation history structures remain unchanged.
52
+
53
+ ## Correctness Properties
54
+
55
+ *A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
56
+
57
+ ### Property 1: EOF signal triggers graceful exit
58
+ *For any* interactive session, when an EOF signal (Ctrl+D) is received, the system should exit the interactive loop and display a goodbye message.
59
+ **Validates: Requirements 1.1, 1.2**
60
+
61
+ ### Property 2: Conversation history persists on EOF exit
62
+ *For any* interactive session with conversation history, when an EOF signal triggers exit, the conversation history should be saved before the program terminates.
63
+ **Validates: Requirements 1.3**
64
+
65
+ ### Property 3: EOF exit performs same cleanup as explicit exit
66
+ *For any* interactive session, the cleanup operations performed when exiting via EOF signal should be equivalent to the cleanup operations performed when exiting via explicit 'exit' command.
67
+ **Validates: Requirements 1.4**
68
+
69
+ ### Property 4: Backward compatibility maintained
70
+ *For any* existing exit command ('exit', 'quit', 'q'), the command should continue to function identically after EOF handling is added.
71
+ **Validates: Requirements 1.5**
72
+
73
+ ## Error Handling
74
+
75
+ ### EOFError Handling
76
+
77
+ The `EOFError` exception is raised by Python's `input()` function when it encounters an EOF signal:
78
+ - **Unix/Linux/macOS**: Ctrl+D
79
+ - **Windows**: Ctrl+Z followed by Enter
80
+
81
+ **Handling Strategy:**
82
+ 1. Catch `EOFError` in the same try-except block as other exceptions
83
+ 2. Print goodbye message using the same format as explicit exit commands
84
+ 3. Break from the interactive loop
85
+ 4. Allow normal program termination (which triggers existing cleanup via Agent's `_save_conversation_history()`)
86
+
87
+ ### Edge Cases
88
+
89
+ 1. **EOF on empty input**: Should exit gracefully (standard behavior)
90
+ 2. **EOF mid-input**: Should exit gracefully, discarding partial input
91
+ 3. **Multiple EOF signals**: First EOF should exit; subsequent signals are irrelevant
92
+ 4. **EOF during agent processing**: Handled by existing `KeyboardInterrupt` logic (Ctrl+C), not EOF
93
+
94
+ ## Testing Strategy
95
+
96
+ ### Unit Tests
97
+
98
+ Unit tests will verify the core EOF handling logic:
99
+
100
+ 1. **Test EOF triggers exit**: Simulate `EOFError` and verify the loop exits
101
+ 2. **Test goodbye message displayed**: Verify correct message is printed on EOF
102
+ 3. **Test backward compatibility**: Verify existing exit commands still work
103
+ 4. **Test conversation history saved**: Verify `_save_conversation_history()` is called before exit
104
+
105
+ ### Property-Based Tests
106
+
107
+ Property-based tests will verify the universal behaviors:
108
+
109
+ 1. **Property 1 Test**: Generate random interactive sessions, inject EOF signal, verify graceful exit with goodbye message
110
+ 2. **Property 2 Test**: Generate random sessions with conversation history, inject EOF, verify history is persisted
111
+ 3. **Property 3 Test**: Compare cleanup operations between EOF exit and explicit exit command, verify equivalence
112
+ 4. **Property 4 Test**: Generate random exit commands from the set {'exit', 'quit', 'q'}, verify all still function correctly
113
+
114
+ ### Testing Framework
115
+
116
+ - **Unit Testing**: pytest
117
+ - **Property-Based Testing**: Hypothesis (Python PBT library)
118
+ - **Mocking**: unittest.mock for simulating user input and EOF signals
119
+
120
+ ### Test Configuration
121
+
122
+ - Property-based tests will run a minimum of 100 iterations
123
+ - Tests will mock `console.input()` to simulate EOF without requiring actual terminal interaction
124
+ - Tests will verify both stdout output and internal state changes
125
+
126
+ ## Implementation Notes
127
+
128
+ ### Design Decisions
129
+
130
+ **Decision 1: Reuse existing exit logic**
131
+ - **Rationale**: The existing exit commands already handle cleanup correctly. By catching `EOFError` and breaking from the loop, we leverage the same cleanup path.
132
+ - **Alternative considered**: Implement separate cleanup function. Rejected due to code duplication and maintenance burden.
133
+
134
+ **Decision 2: Same goodbye message for all exit methods**
135
+ - **Rationale**: Consistency in user experience. Users should see the same friendly goodbye regardless of how they exit.
136
+ - **Alternative considered**: Different message for EOF. Rejected as it adds complexity without user benefit.
137
+
138
+ **Decision 3: Place EOFError handler alongside KeyboardInterrupt**
139
+ - **Rationale**: Both are terminal signal exceptions. Grouping them improves code readability and maintainability.
140
+ - **Alternative considered**: Separate try-except block. Rejected as it would duplicate exception handling structure.
141
+
142
+ ### Minimal Change Principle
143
+
144
+ This design follows the principle of minimal change:
145
+ - Single exception handler addition
146
+ - No new functions or classes
147
+ - No changes to data structures
148
+ - No changes to existing exit logic
149
+ - Estimated change: ~4 lines of code
150
+
151
+ ### Platform Compatibility
152
+
153
+ The implementation is platform-agnostic:
154
+ - Python's `EOFError` is raised consistently across platforms
155
+ - The specific key combination (Ctrl+D vs Ctrl+Z) is handled by the OS and Python runtime
156
+ - No platform-specific code required
.kiro/specs/ctrl-d-quit/requirements.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Requirements Document
2
+
3
+ ## Introduction
4
+
5
+ This feature adds support for quitting the `amcp agent` interactive mode using Ctrl+D (EOF signal) in addition to the existing `exit` command. This provides a more natural terminal experience consistent with other CLI tools.
6
+
7
+ ## Glossary
8
+
9
+ - **Agent Interactive Mode**: The REPL (Read-Eval-Print Loop) interface where users interact with the agent through a command-line prompt
10
+ - **EOF Signal**: End-of-File signal generated by pressing Ctrl+D on Unix-like systems or Ctrl+Z on Windows
11
+ - **EOFError**: Python exception raised when the input() function encounters an EOF signal
12
+
13
+ ## Requirements
14
+
15
+ ### Requirement 1
16
+
17
+ **User Story:** As a user, I want to quit the agent interactive mode using Ctrl+D, so that I can exit quickly using standard terminal conventions.
18
+
19
+ #### Acceptance Criteria
20
+
21
+ 1. WHEN a user presses Ctrl+D in the agent interactive mode THEN the system SHALL catch the EOFError exception and exit gracefully
22
+ 2. WHEN the system exits via Ctrl+D THEN the system SHALL display a goodbye message to the user
23
+ 3. WHEN the system exits via Ctrl+D THEN the system SHALL save the conversation history before exiting
24
+ 4. WHEN the system exits via Ctrl+D THEN the system SHALL perform the same cleanup as the existing 'exit' command
25
+ 5. THE system SHALL maintain backward compatibility with existing exit commands ('exit', 'quit', 'q')
.kiro/specs/ctrl-d-quit/tasks.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Plan
2
+
3
+ - [x] 1. Add EOFError handling to interactive loop
4
+ - Modify the interactive loop in `src/amcp/cli.py` to catch `EOFError` exception
5
+ - Add exception handler alongside existing `KeyboardInterrupt` handler
6
+ - Display goodbye message using same format as explicit exit commands
7
+ - Break from loop to trigger normal cleanup sequence
8
+ - _Requirements: 1.1, 1.2, 1.3, 1.4_
9
+
10
+ - [ ] 2. Set up testing infrastructure
11
+ - Add pytest and hypothesis to project dependencies in `pyproject.toml`
12
+ - Create `tests/` directory structure
13
+ - Create `tests/test_cli.py` for CLI tests
14
+ - Set up test fixtures for mocking console input
15
+ - _Requirements: All (testing infrastructure)_
16
+
17
+ - [ ] 3. Write property-based tests for EOF handling
18
+ - [ ] 3.1 Write property test for graceful exit on EOF
19
+ - **Property 1: EOF signal triggers graceful exit**
20
+ - **Validates: Requirements 1.1, 1.2**
21
+
22
+ - [ ] 3.2 Write property test for conversation history persistence
23
+ - **Property 2: Conversation history persists on EOF exit**
24
+ - **Validates: Requirements 1.3**
25
+
26
+ - [ ] 3.3 Write property test for cleanup equivalence
27
+ - **Property 3: EOF exit performs same cleanup as explicit exit**
28
+ - **Validates: Requirements 1.4**
29
+
30
+ - [ ] 3.4 Write property test for backward compatibility
31
+ - **Property 4: Backward compatibility maintained**
32
+ - **Validates: Requirements 1.5**
33
+
34
+ - [ ] 4. Write unit tests for EOF handling
35
+ - Test EOF exception is caught and handled gracefully
36
+ - Test goodbye message is displayed on EOF
37
+ - Test existing exit commands ('exit', 'quit', 'q') still work
38
+ - Test conversation history is saved before exit
39
+ - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
40
+
41
+ - [ ] 5. Manual verification
42
+ - Run the agent in interactive mode and test Ctrl+D on Unix/Linux/macOS
43
+ - Verify goodbye message appears
44
+ - Verify conversation history is saved
45
+ - Verify existing exit commands still work
46
+ - _Requirements: All_
README.md CHANGED
@@ -75,6 +75,8 @@ amcp mcp call --server exa --tool web_search_exa --args '{"query":"rust async"}'
75
  - **grep**: Search for patterns in files using ripgrep
76
  - **bash**: Execute bash commands for file operations and system tasks
77
  - **think**: Internal reasoning and planning
 
 
78
 
79
  ## Config
80
 
@@ -101,6 +103,8 @@ base_url = "https://api.sambanova.ai/v1"
101
  model = "DeepSeek-V3.1-Terminus"
102
  api_key = "your-api-key"
103
  mcp_tools_enabled = true
 
 
104
  ```
105
 
106
  ## Notes
 
75
  - **grep**: Search for patterns in files using ripgrep
76
  - **bash**: Execute bash commands for file operations and system tasks
77
  - **think**: Internal reasoning and planning
78
+ - **write_file**: Write content to files (can be disabled via config)
79
+ - **edit_file**: Edit files with search and replace (can be disabled via config)
80
 
81
  ## Config
82
 
 
103
  model = "DeepSeek-V3.1-Terminus"
104
  api_key = "your-api-key"
105
  mcp_tools_enabled = true
106
+ write_tool_enabled = true # Enable/disable built-in write_file tool
107
+ edit_tool_enabled = true # Enable/disable built-in edit_file tool
108
  ```
109
 
110
  ## Notes
src/amcp/cli.py CHANGED
@@ -229,6 +229,9 @@ def main(
229
  console.print(f"[dim]Steps: {summary['steps_taken']}/{summary['max_steps']} | Tools called: {summary['tools_called']} | Session: {agent.session_id}[/dim]")
230
  console.print()
231
 
 
 
 
232
  except KeyboardInterrupt:
233
  console.print("\\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
234
  except Exception as e:
 
229
  console.print(f"[dim]Steps: {summary['steps_taken']}/{summary['max_steps']} | Tools called: {summary['tools_called']} | Session: {agent.session_id}[/dim]")
230
  console.print()
231
 
232
+ except EOFError:
233
+ console.print("[green]Goodbye! 👋[/green]")
234
+ break
235
  except KeyboardInterrupt:
236
  console.print("\\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
237
  except Exception as e:
src/amcp/config.py CHANGED
@@ -40,6 +40,9 @@ class ChatConfig:
40
  # MCP tool exposure
41
  mcp_tools_enabled: Optional[bool] = None
42
  mcp_servers: Optional[list[str]] = None # which servers' tools to expose; if unset, expose all configured servers
 
 
 
43
 
44
 
45
  @dataclass
@@ -61,8 +64,10 @@ _DEFAULT = {
61
  "tool_loop_limit": 5,
62
  "default_max_lines": 400,
63
  # "read_roots": ["."] # optional; defaults to current working directory when unset
64
- "mcp_tools_enabled": True,
65
  # "mcp_servers": ["exa"] # optional; if unset, expose all configured servers
 
 
66
  }
67
  }
68
 
@@ -89,6 +94,8 @@ def _decode_chat(raw: Mapping[str, object] | None) -> Optional[ChatConfig]:
89
  read_roots = raw.get("read_roots")
90
  mcp_tools_enabled = raw.get("mcp_tools_enabled")
91
  mcp_servers = raw.get("mcp_servers")
 
 
92
  return ChatConfig(
93
  base_url=str(base_url) if base_url is not None else None,
94
  model=str(model) if model is not None else None,
@@ -98,6 +105,8 @@ def _decode_chat(raw: Mapping[str, object] | None) -> Optional[ChatConfig]:
98
  read_roots=[str(p) for p in (read_roots or [])] if read_roots is not None else None,
99
  mcp_tools_enabled=bool(mcp_tools_enabled) if mcp_tools_enabled is not None else None,
100
  mcp_servers=[str(s) for s in (mcp_servers or [])] if mcp_servers is not None else None,
 
 
101
  )
102
 
103
 
@@ -147,6 +156,10 @@ def _encode_chat(c: ChatConfig | None) -> dict | None:
147
  out["mcp_tools_enabled"] = bool(c.mcp_tools_enabled)
148
  if c.mcp_servers is not None:
149
  out["mcp_servers"] = list(c.mcp_servers)
 
 
 
 
150
  return out
151
 
152
 
 
40
  # MCP tool exposure
41
  mcp_tools_enabled: Optional[bool] = None
42
  mcp_servers: Optional[list[str]] = None # which servers' tools to expose; if unset, expose all configured servers
43
+ # Built-in file modification tools
44
+ write_tool_enabled: Optional[bool] = None
45
+ edit_tool_enabled: Optional[bool] = None
46
 
47
 
48
  @dataclass
 
64
  "tool_loop_limit": 5,
65
  "default_max_lines": 400,
66
  # "read_roots": ["."] # optional; defaults to current working directory when unset
67
+ "mcp_tools_enabled": True,
68
  # "mcp_servers": ["exa"] # optional; if unset, expose all configured servers
69
+ "write_tool_enabled": True,
70
+ "edit_tool_enabled": True,
71
  }
72
  }
73
 
 
94
  read_roots = raw.get("read_roots")
95
  mcp_tools_enabled = raw.get("mcp_tools_enabled")
96
  mcp_servers = raw.get("mcp_servers")
97
+ write_tool_enabled = raw.get("write_tool_enabled")
98
+ edit_tool_enabled = raw.get("edit_tool_enabled")
99
  return ChatConfig(
100
  base_url=str(base_url) if base_url is not None else None,
101
  model=str(model) if model is not None else None,
 
105
  read_roots=[str(p) for p in (read_roots or [])] if read_roots is not None else None,
106
  mcp_tools_enabled=bool(mcp_tools_enabled) if mcp_tools_enabled is not None else None,
107
  mcp_servers=[str(s) for s in (mcp_servers or [])] if mcp_servers is not None else None,
108
+ write_tool_enabled=bool(write_tool_enabled) if write_tool_enabled is not None else None,
109
+ edit_tool_enabled=bool(edit_tool_enabled) if edit_tool_enabled is not None else None,
110
  )
111
 
112
 
 
156
  out["mcp_tools_enabled"] = bool(c.mcp_tools_enabled)
157
  if c.mcp_servers is not None:
158
  out["mcp_servers"] = list(c.mcp_servers)
159
+ if c.write_tool_enabled is not None:
160
+ out["write_tool_enabled"] = bool(c.write_tool_enabled)
161
+ if c.edit_tool_enabled is not None:
162
+ out["edit_tool_enabled"] = bool(c.edit_tool_enabled)
163
  return out
164
 
165
 
src/amcp/tools.py CHANGED
@@ -480,8 +480,129 @@ class GrepTool(BaseTool):
480
  }
481
 
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  # Initialize default tool registry
484
- def create_default_tool_registry() -> ToolRegistry:
485
  """Create a tool registry with default tools."""
486
  registry = ToolRegistry()
487
 
@@ -491,6 +612,11 @@ def create_default_tool_registry() -> ToolRegistry:
491
  registry.register(ThinkTool())
492
  registry.register(BashTool())
493
 
 
 
 
 
 
494
  return registry
495
 
496
 
@@ -498,11 +624,22 @@ def create_default_tool_registry() -> ToolRegistry:
498
  _default_registry: Optional[ToolRegistry] = None
499
 
500
 
501
- def get_tool_registry() -> ToolRegistry:
502
  """Get the global tool registry instance."""
503
  global _default_registry
504
  if _default_registry is None:
505
- _default_registry = create_default_tool_registry()
 
 
 
 
 
 
 
 
 
 
 
506
  return _default_registry
507
 
508
 
 
480
  }
481
 
482
 
483
+ class WriteFileTool(BaseTool):
484
+ """Tool for writing content to files."""
485
+
486
+ @property
487
+ def name(self) -> str:
488
+ return "write_file"
489
+
490
+ @property
491
+ def description(self) -> str:
492
+ return "Write content to a file. Creates new file or overwrites existing file."
493
+
494
+ def execute(self, path: str, content: str) -> ToolResult:
495
+ """Execute the write file tool."""
496
+ from pathlib import Path
497
+
498
+ try:
499
+ file_path = Path(path).expanduser().resolve()
500
+ file_path.parent.mkdir(parents=True, exist_ok=True)
501
+ file_path.write_text(content, encoding="utf-8")
502
+
503
+ return ToolResult(
504
+ success=True,
505
+ content=f"Successfully wrote {len(content)} characters to {file_path}",
506
+ metadata={"file_path": str(file_path), "size": len(content)}
507
+ )
508
+ except Exception as e:
509
+ return ToolResult(
510
+ success=False,
511
+ content="",
512
+ error=f"Failed to write file: {type(e).__name__}: {e}"
513
+ )
514
+
515
+ def get_parameters_schema(self) -> Dict[str, Any]:
516
+ return {
517
+ "type": "object",
518
+ "properties": {
519
+ "path": {
520
+ "type": "string",
521
+ "description": "Path to the file to write"
522
+ },
523
+ "content": {
524
+ "type": "string",
525
+ "description": "Content to write to the file"
526
+ }
527
+ },
528
+ "required": ["path", "content"],
529
+ "additionalProperties": False,
530
+ }
531
+
532
+
533
+ class EditFileTool(BaseTool):
534
+ """Tool for editing files with search and replace."""
535
+
536
+ @property
537
+ def name(self) -> str:
538
+ return "edit_file"
539
+
540
+ @property
541
+ def description(self) -> str:
542
+ return "Edit a file by replacing old_text with new_text. The old_text must match exactly."
543
+
544
+ def execute(self, path: str, old_text: str, new_text: str) -> ToolResult:
545
+ """Execute the edit file tool."""
546
+ from pathlib import Path
547
+
548
+ try:
549
+ file_path = Path(path).expanduser().resolve()
550
+
551
+ if not file_path.exists():
552
+ return ToolResult(
553
+ success=False,
554
+ content="",
555
+ error=f"File not found: {file_path}"
556
+ )
557
+
558
+ content = file_path.read_text(encoding="utf-8")
559
+
560
+ if old_text not in content:
561
+ return ToolResult(
562
+ success=False,
563
+ content="",
564
+ error="old_text not found in file"
565
+ )
566
+
567
+ new_content = content.replace(old_text, new_text, 1)
568
+ file_path.write_text(new_content, encoding="utf-8")
569
+
570
+ return ToolResult(
571
+ success=True,
572
+ content=f"Successfully edited {file_path}",
573
+ metadata={"file_path": str(file_path)}
574
+ )
575
+ except Exception as e:
576
+ return ToolResult(
577
+ success=False,
578
+ content="",
579
+ error=f"Failed to edit file: {type(e).__name__}: {e}"
580
+ )
581
+
582
+ def get_parameters_schema(self) -> Dict[str, Any]:
583
+ return {
584
+ "type": "object",
585
+ "properties": {
586
+ "path": {
587
+ "type": "string",
588
+ "description": "Path to the file to edit"
589
+ },
590
+ "old_text": {
591
+ "type": "string",
592
+ "description": "Text to search for (must match exactly)"
593
+ },
594
+ "new_text": {
595
+ "type": "string",
596
+ "description": "Text to replace with"
597
+ }
598
+ },
599
+ "required": ["path", "old_text", "new_text"],
600
+ "additionalProperties": False,
601
+ }
602
+
603
+
604
  # Initialize default tool registry
605
+ def create_default_tool_registry(enable_write: bool = True, enable_edit: bool = True) -> ToolRegistry:
606
  """Create a tool registry with default tools."""
607
  registry = ToolRegistry()
608
 
 
612
  registry.register(ThinkTool())
613
  registry.register(BashTool())
614
 
615
+ if enable_write:
616
+ registry.register(WriteFileTool())
617
+ if enable_edit:
618
+ registry.register(EditFileTool())
619
+
620
  return registry
621
 
622
 
 
624
  _default_registry: Optional[ToolRegistry] = None
625
 
626
 
627
+ def get_tool_registry(enable_write: Optional[bool] = None, enable_edit: Optional[bool] = None) -> ToolRegistry:
628
  """Get the global tool registry instance."""
629
  global _default_registry
630
  if _default_registry is None:
631
+ # Load config to determine defaults
632
+ from .config import load_config
633
+ cfg = load_config()
634
+ chat_cfg = cfg.chat
635
+
636
+ # Use config values if not explicitly provided
637
+ if enable_write is None:
638
+ enable_write = chat_cfg.write_tool_enabled if chat_cfg and chat_cfg.write_tool_enabled is not None else True
639
+ if enable_edit is None:
640
+ enable_edit = chat_cfg.edit_tool_enabled if chat_cfg and chat_cfg.edit_tool_enabled is not None else True
641
+
642
+ _default_registry = create_default_tool_registry(enable_write=enable_write, enable_edit=enable_edit)
643
  return _default_registry
644
 
645