Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion gateway/platforms/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,15 @@ async def connect(self) -> bool:
if not (bridge_dir / "node_modules").exists():
print(f"[{self.name}] Installing WhatsApp bridge dependencies...")
try:
# Read timeout from environment variable, default to 300 seconds (5 minutes)
# to accommodate slower systems like Unraid NAS
npm_install_timeout = int(os.environ.get("WHATSAPP_NPM_INSTALL_TIMEOUT", "300"))
install_result = subprocess.run(
["npm", "install", "--silent"],
cwd=str(bridge_dir),
capture_output=True,
text=True,
timeout=60,
timeout=npm_install_timeout,
)
if install_result.returncode != 0:
print(f"[{self.name}] npm install failed: {install_result.stderr}")
Expand Down
37 changes: 37 additions & 0 deletions tests/tools/test_file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,41 @@ def test_truncated_hint_with_nonzero_offset(self, mock_get):
assert "offset=100" in raw


class TestPatchSchema:
"""Tests for PATCH_SCHEMA to ensure required parameters are properly declared."""

def test_patch_schema_includes_all_required_params(self):
"""PATCH_SCHEMA should include all parameters that are conditionally required."""
from tools.file_tools import PATCH_SCHEMA

# Verify schema structure
assert "parameters" in PATCH_SCHEMA
assert "required" in PATCH_SCHEMA["parameters"]

# All parameters that are mode-specific should be in required list
required = PATCH_SCHEMA["parameters"]["required"]
assert "mode" in required
assert "path" in required
assert "old_string" in required
assert "new_string" in required
assert "patch" in required

# replace_all is optional (has default), so it should NOT be in required
assert "replace_all" not in required

def test_patch_schema_description_mentions_mode_specific_requirements(self):
"""PATCH_SCHEMA description should explain mode-specific requirements."""
from tools.file_tools import PATCH_SCHEMA

description = PATCH_SCHEMA.get("description", "")

# Description should mention mode-specific requirements
assert "mode-specific" in description.lower() or "IMPORTANT:" in description

# Should mention both modes
assert "mode='replace'" in description
assert "mode='patch'" in description




11 changes: 6 additions & 5 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2275,7 +2275,7 @@ def _load_config() -> dict:
},
"acp_command": {
"type": "string",
"description": "Per-task ACP command override (e.g. 'claude'). Overrides the top-level acp_command for this task only.",
"description": "Per-task ACP command override (e.g. 'copilot'). Overrides the top-level acp_command for this task only.",
},
"acp_args": {
"type": "array",
Expand Down Expand Up @@ -2315,18 +2315,19 @@ def _load_config() -> dict:
"acp_command": {
"type": "string",
"description": (
"Override ACP command for child agents (e.g. 'claude', 'copilot'). "
"Override ACP command for child agents (e.g. 'copilot'). "
"When set, children use ACP subprocess transport instead of inheriting "
"the parent's transport. Enables spawning Claude Code (claude --acp --stdio) "
"or other ACP-capable agents from any parent, including Discord/Telegram/CLI."
"the parent's transport. Requires an ACP-compatible CLI "
"(currently GitHub Copilot CLI via 'copilot --acp --stdio'). "
"See agent/copilot_acp_client.py for the implementation."
),
},
"acp_args": {
"type": "array",
"items": {"type": "string"},
"description": (
"Arguments for the ACP command (default: ['--acp', '--stdio']). "
"Only used when acp_command is set. Example: ['--acp', '--stdio', '--model', 'claude-opus-4-6']"
"Only used when acp_command is set."
),
},
},
Expand Down
12 changes: 6 additions & 6 deletions tools/file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,18 +908,18 @@ def _check_file_reqs():

PATCH_SCHEMA = {
"name": "patch",
"description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nReplace mode (default): find a unique string and replace it.\nPatch mode: apply V4A multi-file patches for bulk changes.",
"description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nIMPORTANT: Parameters are mode-specific:\n- For mode='replace': provide path, old_string, new_string (optionally replace_all)\n- For mode='patch': provide patch content",
"parameters": {
"type": "object",
"properties": {
"mode": {"type": "string", "enum": ["replace", "patch"], "description": "Edit mode: 'replace' for targeted find-and-replace, 'patch' for V4A multi-file patches", "default": "replace"},
"path": {"type": "string", "description": "File path to edit (required for 'replace' mode)"},
"old_string": {"type": "string", "description": "Text to find in the file (required for 'replace' mode). Must be unique in the file unless replace_all=true. Include enough surrounding context to ensure uniqueness."},
"new_string": {"type": "string", "description": "Replacement text (required for 'replace' mode). Can be empty string to delete the matched text."},
"path": {"type": "string", "description": "File path to edit (required when mode='replace')"},
"old_string": {"type": "string", "description": "Text to find in file (required when mode='replace'). Must be unique in file unless replace_all=true. Include enough surrounding context to ensure uniqueness."},
"new_string": {"type": "string", "description": "Replacement text (required when mode='replace'). Can be empty string to delete the matched text."},
"replace_all": {"type": "boolean", "description": "Replace all occurrences instead of requiring a unique match (default: false)", "default": False},
"patch": {"type": "string", "description": "V4A format patch content (required for 'patch' mode). Format:\n*** Begin Patch\n*** Update File: path/to/file\n@@ context hint @@\n context line\n-removed line\n+added line\n*** End Patch"}
"patch": {"type": "string", "description": "V4A format patch content (required when mode='patch'). Format:\n*** Begin Patch\n*** Update File: path/to/file\n@@ context hint @@\n context line\n-removed line\n+added line\n*** End Patch"}
},
"required": ["mode"]
"required": ["mode", "path", "old_string", "new_string", "patch"]
}
}

Expand Down
Loading