Refactor: Remove zombie code, fix bugs, and clean documentation
This comprehensive refactoring removes dead code, fixes bugs, and deletes outdated documentation to make the codebase production-ready. ## Files Deleted (16 files) ### Temporary/zombie files (9 files): - nul (Windows artifact) - quick_start.bat (superseded by run.bat) - scripts/proxmox_ssh.py (hardcoded credentials - security risk) - scripts/proxmox_ssh.sh (hardcoded credentials - security risk) - scripts/collection_output.txt (one-time audit output) - scripts/collect-homelab-config.sh (one-off infrastructure script) - scripts/collect-remote.sh (one-off infrastructure script) - memory_workspace/MEMORY.md.old (backup file) - promtail-config-optimized.yaml (misplaced homelab config) ### Outdated documentation (7 files): - MCP_MIGRATION.md (migration complete - 2026-02-15) - QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup) - SETUP.md (duplicate of README.md quick start) - WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md) - SUB_AGENTS.md (design doc for unimplemented feature) - JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented) - OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc) - LOGGING.md (redundant with well-commented logging_config.py) - docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12) ## Critical Bug Fixes (2 bugs) 1. bot_runner.py line 122: Fixed wrong dict key reference - Changed send_to_platform → send_to - Bug caused scheduled task platform info to never print 2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6 - Model was default but had no pricing entry - Caused cost under-reporting in Direct API mode ## Code Removed (14 files modified, ~1200 lines deleted) ### Dead imports removed (9 imports): - bot_runner.py: sys - agent.py: time - adapters/runtime.py: re - adapters/skill_integration.py: subprocess - tools.py: redundant Path import - mcp_servers/loki/loki_server.py: json - google_tools/oauth_manager.py: Thread, Dict - google_tools/gmail_client.py: os - google_tools/utils.py: email ### Unused functions/methods removed (9 functions): - agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant - scheduled_tasks.py: integrate_scheduler_with_runtime() - adapters/runtime.py: command_preprocessor(), markdown_postprocessor() - adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block - tools.py: _extract_mcp_result() - google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization() - google_tools/people_client.py: update_contact(), delete_contact() ### Code quality improvements: - memory_system.py: Removed empty else: pass branch - calendar_client.py: Fixed bare except: → except Exception: - mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop() - calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc) ## Impact - ~1200 lines of dead code removed - 16 obsolete files deleted - 2 critical bugs fixed - 3 deprecated APIs updated - Zero functionality broken (all changes verified) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
207
LOGGING.md
207
LOGGING.md
@@ -1,207 +0,0 @@
|
|||||||
# Structured Logging System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Ajarbot now includes a comprehensive structured logging system to track errors, tool executions, and system behavior.
|
|
||||||
|
|
||||||
## Log Files
|
|
||||||
|
|
||||||
All logs are stored in the `logs/` directory (gitignored):
|
|
||||||
|
|
||||||
### 1. `ajarbot.log` - Main Application Log
|
|
||||||
- **Format**: JSON (one record per line)
|
|
||||||
- **Level**: DEBUG and above
|
|
||||||
- **Size**: Rotates at 10MB, keeps 5 backups
|
|
||||||
- **Contents**: All application events, tool executions, LLM calls
|
|
||||||
|
|
||||||
### 2. `errors.log` - Error-Only Log
|
|
||||||
- **Format**: JSON
|
|
||||||
- **Level**: ERROR and CRITICAL only
|
|
||||||
- **Size**: Rotates at 5MB, keeps 3 backups
|
|
||||||
- **Contents**: Only errors and critical issues for quick diagnosis
|
|
||||||
|
|
||||||
### 3. `tools.log` - Tool Execution Log
|
|
||||||
- **Format**: JSON
|
|
||||||
- **Level**: INFO and above
|
|
||||||
- **Size**: Rotates at 10MB, keeps 3 backups
|
|
||||||
- **Contents**: Every tool call with inputs, outputs, duration, and success/failure
|
|
||||||
|
|
||||||
## Log Format
|
|
||||||
|
|
||||||
### JSON Structure
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2026-02-16T12:34:56.789Z",
|
|
||||||
"level": "ERROR",
|
|
||||||
"logger": "tools",
|
|
||||||
"message": "Tool failed: permanent_note",
|
|
||||||
"module": "tools",
|
|
||||||
"function": "execute_tool",
|
|
||||||
"line": 500,
|
|
||||||
"extra": {
|
|
||||||
"tool_name": "permanent_note",
|
|
||||||
"inputs": {"title": "Test", "content": "..."},
|
|
||||||
"success": false,
|
|
||||||
"error": "Unknown tool error",
|
|
||||||
"duration_ms": 123.45
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Log Example
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2026-02-16T06:00:15.234Z",
|
|
||||||
"level": "INFO",
|
|
||||||
"logger": "tools",
|
|
||||||
"message": "Tool executed: get_weather",
|
|
||||||
"extra": {
|
|
||||||
"tool_name": "get_weather",
|
|
||||||
"inputs": {"location": "Centennial, CO"},
|
|
||||||
"success": true,
|
|
||||||
"result_length": 456,
|
|
||||||
"duration_ms": 1234.56
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage in Code
|
|
||||||
|
|
||||||
### Get a Logger
|
|
||||||
```python
|
|
||||||
from logging_config import get_logger, get_tool_logger
|
|
||||||
|
|
||||||
# General logger
|
|
||||||
logger = get_logger("my_module")
|
|
||||||
|
|
||||||
# Specialized tool logger
|
|
||||||
tool_logger = get_tool_logger()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging Methods
|
|
||||||
|
|
||||||
**Basic logging:**
|
|
||||||
```python
|
|
||||||
logger.debug("Detailed debug info", key="value")
|
|
||||||
logger.info("Informational message", user_id=123)
|
|
||||||
logger.warning("Warning message", issue="something")
|
|
||||||
logger.error("Error occurred", exc_info=True, error_code="E001")
|
|
||||||
logger.critical("Critical system failure", exc_info=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tool execution logging:**
|
|
||||||
```python
|
|
||||||
tool_logger.log_tool_call(
|
|
||||||
tool_name="permanent_note",
|
|
||||||
inputs={"title": "Test", "content": "..."},
|
|
||||||
success=True,
|
|
||||||
result="Created note successfully",
|
|
||||||
duration_ms=123.45
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Analyzing Logs
|
|
||||||
|
|
||||||
### View Recent Errors
|
|
||||||
```bash
|
|
||||||
# Last 20 errors
|
|
||||||
tail -20 logs/errors.log | jq .
|
|
||||||
|
|
||||||
# Errors from specific module
|
|
||||||
grep '"module":"tools"' logs/errors.log | jq .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Performance Analysis
|
|
||||||
```bash
|
|
||||||
# Average tool execution time
|
|
||||||
cat logs/tools.log | jq -r '.extra.duration_ms' | awk '{sum+=$1; count++} END {print sum/count}'
|
|
||||||
|
|
||||||
# Failed tools
|
|
||||||
grep '"success":false' logs/tools.log | jq -r '.extra.tool_name' | sort | uniq -c
|
|
||||||
|
|
||||||
# Slowest tool calls
|
|
||||||
cat logs/tools.log | jq -r '[.extra.tool_name, .extra.duration_ms] | @csv' | sort -t, -k2 -rn | head -10
|
|
||||||
```
|
|
||||||
|
|
||||||
### Find Specific Errors
|
|
||||||
```bash
|
|
||||||
# Max token errors
|
|
||||||
grep -i "max.*token" logs/errors.log | jq .
|
|
||||||
|
|
||||||
# Tool iteration limits
|
|
||||||
grep -i "iteration.*exceeded" logs/ajarbot.log | jq .
|
|
||||||
|
|
||||||
# MCP tool failures
|
|
||||||
grep '"tool_name":"permanent_note"' logs/tools.log | grep '"success":false' | jq .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Patterns to Watch
|
|
||||||
|
|
||||||
1. **Max Tool Iterations** - Search: `"iteration.*exceeded"`
|
|
||||||
2. **Max Tokens** - Search: `"max.*token"`
|
|
||||||
3. **MCP Tool Failures** - Search: `"Unknown tool"` or failed MCP tool names
|
|
||||||
4. **Slow Tools** - Tools taking > 5000ms
|
|
||||||
5. **Repeated Failures** - Same tool failing multiple times
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Log Rotation
|
|
||||||
Logs automatically rotate when they reach size limits:
|
|
||||||
- `ajarbot.log`: 10MB → keeps 5 old files (50MB total)
|
|
||||||
- `errors.log`: 5MB → keeps 3 old files (15MB total)
|
|
||||||
- `tools.log`: 10MB → keeps 3 old files (30MB total)
|
|
||||||
|
|
||||||
Total max disk usage: ~95MB
|
|
||||||
|
|
||||||
### Manual Cleanup
|
|
||||||
```bash
|
|
||||||
# Remove old logs
|
|
||||||
rm logs/*.log.*
|
|
||||||
|
|
||||||
# Clear all logs (careful!)
|
|
||||||
rm logs/*.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration
|
|
||||||
|
|
||||||
### Automatic Integration
|
|
||||||
The logging system is automatically integrated into:
|
|
||||||
- ✅ `tools.py` - All tool executions logged
|
|
||||||
- ✅ Console output - Human-readable format
|
|
||||||
- ✅ File logs - JSON format for parsing
|
|
||||||
|
|
||||||
### Adding Logging to New Modules
|
|
||||||
```python
|
|
||||||
from logging_config import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
def my_function():
|
|
||||||
logger.info("Starting operation", operation_id=123)
|
|
||||||
try:
|
|
||||||
# Do work
|
|
||||||
logger.debug("Step completed", step=1)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Operation failed", exc_info=True, operation_id=123)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Quick Error Diagnosis**: Separate `errors.log` for immediate issue identification
|
|
||||||
2. **Performance Tracking**: Tool execution times and success rates
|
|
||||||
3. **Historical Analysis**: JSON format enables programmatic analysis
|
|
||||||
4. **Debugging**: Full context with inputs, outputs, and stack traces
|
|
||||||
5. **Monitoring**: Easy to parse logs for alerting systems
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Web dashboard for log visualization
|
|
||||||
- [ ] Real-time log streaming via WebSocket
|
|
||||||
- [ ] Automatic error rate alerts (email/Telegram)
|
|
||||||
- [ ] Integration with external monitoring (Datadog, CloudWatch)
|
|
||||||
- [ ] Log aggregation for multi-instance deployments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2026-02-16
|
|
||||||
**Log System Version:** 1.0
|
|
||||||
152
MCP_MIGRATION.md
152
MCP_MIGRATION.md
@@ -1,152 +0,0 @@
|
|||||||
# MCP Tools Migration Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Successfully migrated file/system tools to MCP (Model Context Protocol) servers for better performance and integration with Claude Agent SDK.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### MCP Tools (In-Process - No API Costs)
|
|
||||||
**File**: `mcp_tools.py`
|
|
||||||
**Server**: `file_system` (v1.0.0)
|
|
||||||
|
|
||||||
These tools run directly in the Python process using the Claude Agent SDK:
|
|
||||||
- ✅ `read_file` - Read file contents
|
|
||||||
- ✅ `write_file` - Create/overwrite files
|
|
||||||
- ✅ `edit_file` - Replace text in files
|
|
||||||
- ✅ `list_directory` - List directory contents
|
|
||||||
- ✅ `run_command` - Execute shell commands
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Zero per-token API costs when using Agent SDK
|
|
||||||
- Better performance (no IPC overhead)
|
|
||||||
- Direct access to application state
|
|
||||||
- Simpler deployment (single process)
|
|
||||||
|
|
||||||
### Traditional Tools (API-Based - Consumes Tokens)
|
|
||||||
**File**: `tools.py`
|
|
||||||
|
|
||||||
These tools require external APIs and fall back to Direct API even in Agent SDK mode:
|
|
||||||
- 🌤️ `get_weather` - OpenWeatherMap API
|
|
||||||
- 📧 `send_email`, `read_emails`, `get_email` - Gmail API
|
|
||||||
- 📅 `read_calendar`, `create_calendar_event`, `search_calendar` - Google Calendar API
|
|
||||||
- 👤 `create_contact`, `list_contacts`, `get_contact` - Google People API
|
|
||||||
|
|
||||||
**Why not MCP?**: These tools need OAuth state, external API calls, and async HTTP clients that are better suited to the traditional tool execution model.
|
|
||||||
|
|
||||||
## Model Configuration
|
|
||||||
|
|
||||||
### Agent SDK Mode (DEFAULT)
|
|
||||||
```python
|
|
||||||
USE_AGENT_SDK=true # Default
|
|
||||||
```
|
|
||||||
|
|
||||||
**Model Configuration**:
|
|
||||||
- Default: **claude-sonnet-4-5-20250929** (all operations - chat, tools, coding)
|
|
||||||
- Optional: **claude-opus-4-6** (requires `USE_OPUS_FOR_TOOLS=true`, only for extremely intensive tasks)
|
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
- Regular chat: Uses Sonnet (flat-rate, no API costs)
|
|
||||||
- File operations: Uses Sonnet via MCP tools (flat-rate, no API costs)
|
|
||||||
- Google/Weather: Uses Sonnet via Direct API fallback (requires ANTHROPIC_API_KEY, consumes tokens)
|
|
||||||
- Intensive tasks: Optionally enable Opus with `USE_OPUS_FOR_TOOLS=true` (flat-rate, no extra cost)
|
|
||||||
|
|
||||||
**Cost Structure**:
|
|
||||||
- Chat + MCP tools: Flat-rate subscription (Pro plan)
|
|
||||||
- Traditional tools (Google/Weather): Pay-per-token at Sonnet rates (requires API key)
|
|
||||||
|
|
||||||
### Direct API Mode
|
|
||||||
```python
|
|
||||||
USE_DIRECT_API=true
|
|
||||||
Model: claude-sonnet-4-5-20250929 # Cost-effective (never uses Opus - too expensive)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
- All operations: Pay-per-token
|
|
||||||
- Requires: ANTHROPIC_API_KEY in .env
|
|
||||||
- All tools: Traditional execution (same token cost)
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### MCP Server Integration
|
|
||||||
|
|
||||||
**In `llm_interface.py`**:
|
|
||||||
```python
|
|
||||||
from mcp_tools import file_system_server
|
|
||||||
|
|
||||||
options = ClaudeAgentOptions(
|
|
||||||
mcp_servers={"file_system": file_system_server},
|
|
||||||
allowed_tools=[
|
|
||||||
"read_file", "write_file", "edit_file",
|
|
||||||
"list_directory", "run_command"
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await query(
|
|
||||||
messages=sdk_messages,
|
|
||||||
max_tokens=max_tokens,
|
|
||||||
options=options,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Definition Format
|
|
||||||
|
|
||||||
**MCP Tool Example**:
|
|
||||||
```python
|
|
||||||
@tool(
|
|
||||||
name="read_file",
|
|
||||||
description="Read the contents of a file.",
|
|
||||||
input_schema={"file_path": str},
|
|
||||||
)
|
|
||||||
async def read_file_tool(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"content": [{"type": "text", "text": "..."}],
|
|
||||||
"isError": False # Optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Traditional Tool Example**:
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"name": "send_email",
|
|
||||||
"description": "Send an email from the bot's Gmail account.",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"to": {"type": "string"}, ...},
|
|
||||||
"required": ["to", "subject", "body"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Potential MCP Candidates
|
|
||||||
- [ ] Weather tool (if we cache API responses in-process)
|
|
||||||
- [ ] Memory search tools (direct DB access)
|
|
||||||
- [ ] Configuration management tools
|
|
||||||
|
|
||||||
### Google Tools Migration (Optional)
|
|
||||||
To fully migrate Google tools to MCP, we would need to:
|
|
||||||
1. Embed OAuth manager in MCP server lifecycle
|
|
||||||
2. Handle async HTTP clients within MCP context
|
|
||||||
3. Manage token refresh in-process
|
|
||||||
|
|
||||||
**Recommendation**: Keep Google tools as traditional tools for now. The complexity of OAuth state management outweighs the token cost savings for infrequent API calls.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test MCP server creation
|
|
||||||
python -c "from mcp_tools import file_system_server; print(file_system_server)"
|
|
||||||
|
|
||||||
# Test Agent SDK with Opus
|
|
||||||
python -c "import os; os.environ['USE_AGENT_SDK']='true'; from llm_interface import LLMInterface; llm = LLMInterface(provider='claude'); print(f'Model: {llm.model}')"
|
|
||||||
|
|
||||||
# Expected: Model: claude-opus-4-6
|
|
||||||
```
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Claude Agent SDK Docs: https://github.com/anthropics/claude-agent-sdk
|
|
||||||
- MCP Protocol: https://modelcontextprotocol.io
|
|
||||||
- Tool Decorators: `claude_agent_sdk.tool`, `create_sdk_mcp_server`
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Quick Setup: Obsidian Local REST API Plugin
|
|
||||||
|
|
||||||
## Your Current Status
|
|
||||||
- ✅ Obsidian is running
|
|
||||||
- ✅ Config file is ready (`config/obsidian_mcp.yaml`)
|
|
||||||
- ❌ Local REST API plugin not responding on port 27123
|
|
||||||
|
|
||||||
## Setup Steps
|
|
||||||
|
|
||||||
### 1. Install the Local REST API Plugin in Obsidian
|
|
||||||
|
|
||||||
1. Open **Obsidian**
|
|
||||||
2. Go to **Settings** (gear icon) → **Community Plugins**
|
|
||||||
3. If you see "Safe mode is on", click **Turn off Safe Mode**
|
|
||||||
4. Click **Browse** button
|
|
||||||
5. Search for: **"Local REST API"**
|
|
||||||
6. Click **Install** on the "Local REST API" plugin by coddingtonbear
|
|
||||||
7. After installation, click **Enable**
|
|
||||||
|
|
||||||
### 2. Configure the Plugin
|
|
||||||
|
|
||||||
1. In Obsidian Settings, scroll down to **Plugin Options**
|
|
||||||
2. Find **Local REST API** in the left sidebar
|
|
||||||
3. Copy your API key shown in the plugin settings
|
|
||||||
4. Compare it with the key in your `config/obsidian_mcp.yaml`:
|
|
||||||
```
|
|
||||||
api_key: "ee625f06a778e3267a9219f9b8c1065a039375ea270e414a34436c6a3027f2da"
|
|
||||||
```
|
|
||||||
5. If they don't match, update the config file with the correct key
|
|
||||||
|
|
||||||
### 3. Verify the Plugin is Running
|
|
||||||
|
|
||||||
1. Check that the plugin shows as **enabled** in Obsidian
|
|
||||||
2. The plugin should show: "Server running on http://127.0.0.1:27123"
|
|
||||||
3. Restart Obsidian if needed
|
|
||||||
|
|
||||||
### 4. Test the Connection
|
|
||||||
|
|
||||||
Run this command in your project directory:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python -c "from obsidian_mcp import check_obsidian_health; print('Health Check:', check_obsidian_health(force=True))"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected output**: `Health Check: True`
|
|
||||||
|
|
||||||
### 5. Restart the Bot
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
venv\Scripts\activate
|
|
||||||
python bot_runner.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Look for this line in the startup logs:
|
|
||||||
```
|
|
||||||
[LLM] Obsidian MCP server registered (8 tools)
|
|
||||||
```
|
|
||||||
|
|
||||||
If you see this instead, the plugin isn't working yet:
|
|
||||||
```
|
|
||||||
[LLM] Obsidian MCP enabled but health check failed - using custom tools only
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternative: File-Based Access (Already Working)
|
|
||||||
|
|
||||||
If you don't want to use the Local REST API plugin, your bot can **already** access your Obsidian vault via the filesystem using these tools:
|
|
||||||
|
|
||||||
- `fleeting_note` - Quick capture with auto-ID
|
|
||||||
- `daily_note` - Timestamped journal entries
|
|
||||||
- `literature_note` - Save web articles
|
|
||||||
- `permanent_note` - Create refined notes with auto-linking
|
|
||||||
- `search_vault` - Hybrid semantic search
|
|
||||||
- `search_by_tags` - Find notes by tags
|
|
||||||
- `read_file` / `write_file` / `edit_file` - Direct file access
|
|
||||||
|
|
||||||
The **Obsidian MCP tools** add these extra capabilities:
|
|
||||||
- `obsidian_update_note` - Frontmatter-aware editing
|
|
||||||
- `obsidian_global_search` - Native Obsidian search
|
|
||||||
- `obsidian_manage_frontmatter` - Advanced metadata management
|
|
||||||
- `obsidian_manage_tags` - Bulk tag operations
|
|
||||||
- `obsidian_delete_note` - Safe deletion
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Plugin shows "Server not running"
|
|
||||||
- Click the **Restart Server** button in the plugin settings
|
|
||||||
- Check Windows Firewall isn't blocking port 27123
|
|
||||||
|
|
||||||
### API key mismatch
|
|
||||||
- Copy the EXACT key from Obsidian plugin settings
|
|
||||||
- Update `config/obsidian_mcp.yaml` → `connection.api_key`
|
|
||||||
|
|
||||||
### Wrong vault path
|
|
||||||
- Your current vault path: `C:/Users/fam1n/OneDrive/Documents/Remote-Mind-Vault`
|
|
||||||
- Verify this path exists and contains a `.obsidian` folder
|
|
||||||
|
|
||||||
### Health check still fails after setup
|
|
||||||
- Restart Obsidian
|
|
||||||
- Restart the bot
|
|
||||||
- Check port 27123 isn't used by another program:
|
|
||||||
```powershell
|
|
||||||
netstat -ano | findstr :27123
|
|
||||||
```
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# Agent SDK Quick Reference Card
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Run bot (Agent SDK is default)
|
|
||||||
python bot_runner.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Mode Selection
|
|
||||||
|
|
||||||
### Agent SDK (Default)
|
|
||||||
```env
|
|
||||||
# No config needed - this is the default!
|
|
||||||
# Or explicitly:
|
|
||||||
USE_AGENT_SDK=true
|
|
||||||
```
|
|
||||||
✅ Uses Claude Pro subscription (no API costs)
|
|
||||||
|
|
||||||
### Direct API
|
|
||||||
```env
|
|
||||||
USE_DIRECT_API=true
|
|
||||||
ANTHROPIC_API_KEY=sk-ant-...
|
|
||||||
```
|
|
||||||
✅ Pay-per-token, usage tracking enabled
|
|
||||||
|
|
||||||
### Legacy Server
|
|
||||||
```env
|
|
||||||
USE_CLAUDE_CODE_SERVER=true
|
|
||||||
CLAUDE_CODE_SERVER_URL=http://localhost:8000
|
|
||||||
```
|
|
||||||
⚠️ Deprecated, not recommended
|
|
||||||
|
|
||||||
## 🔍 Verify Mode
|
|
||||||
|
|
||||||
Check startup message:
|
|
||||||
```
|
|
||||||
[LLM] Using Claude Agent SDK (Pro subscription) ← Agent SDK ✅
|
|
||||||
[LLM] Using Direct API (pay-per-token) ← Direct API 💳
|
|
||||||
[LLM] Using Claude Code server at ... ← Legacy ⚠️
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 Test Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python test_agent_sdk.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: **5/5 tests passed** 🎉
|
|
||||||
|
|
||||||
## 🛠️ Troubleshooting
|
|
||||||
|
|
||||||
### Issue: Fallback to Direct API
|
|
||||||
```bash
|
|
||||||
pip install claude-agent-sdk anyio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: ModuleNotFoundError
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Still using old mode
|
|
||||||
```bash
|
|
||||||
# Edit .env and remove conflicting variables
|
|
||||||
USE_DIRECT_API=false # or remove line
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Priority Order
|
|
||||||
|
|
||||||
```
|
|
||||||
1. USE_DIRECT_API=true → Direct API
|
|
||||||
2. USE_CLAUDE_CODE_SERVER → Legacy
|
|
||||||
3. USE_AGENT_SDK (default) → Agent SDK
|
|
||||||
4. SDK unavailable → Fallback to Direct API
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💰 Cost Comparison
|
|
||||||
|
|
||||||
| Mode | Cost per 1M tokens |
|
|
||||||
|------|-------------------|
|
|
||||||
| Agent SDK | **$0** (Pro subscription) |
|
|
||||||
| Direct API (Haiku) | $0.25 - $1.25 |
|
|
||||||
| Direct API (Sonnet) | $3.00 - $15.00 |
|
|
||||||
|
|
||||||
## 🎯 Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `llm_interface.py` | Core implementation |
|
|
||||||
| `requirements.txt` | Dependencies |
|
|
||||||
| `test_agent_sdk.py` | Test suite |
|
|
||||||
| `.env` | Configuration |
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
- `AGENT_SDK_IMPLEMENTATION.md` - Full technical details
|
|
||||||
- `MIGRATION_GUIDE_AGENT_SDK.md` - Step-by-step migration
|
|
||||||
- `IMPLEMENTATION_SUMMARY.md` - Executive summary
|
|
||||||
- `QUICK_REFERENCE_AGENT_SDK.md` - This file
|
|
||||||
|
|
||||||
## ✅ Features Preserved
|
|
||||||
|
|
||||||
✅ All 17 tools (file ops, Gmail, Calendar)
|
|
||||||
✅ Scheduled tasks
|
|
||||||
✅ Memory system
|
|
||||||
✅ Self-healing system
|
|
||||||
✅ Telegram adapter
|
|
||||||
✅ Slack adapter
|
|
||||||
✅ Model switching (/sonnet, /haiku)
|
|
||||||
✅ Usage tracking (Direct API mode)
|
|
||||||
|
|
||||||
## 🔄 Rollback
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Quick rollback to Direct API
|
|
||||||
USE_DIRECT_API=true
|
|
||||||
ANTHROPIC_API_KEY=sk-ant-...
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart bot. Done! ✅
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
1. Check logs: Look for `[LLM]` messages
|
|
||||||
2. Run tests: `python test_agent_sdk.py`
|
|
||||||
3. Check mode: Verify startup message
|
|
||||||
4. Review docs: See files above
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Date**: 2026-02-15
|
|
||||||
**Status**: ✅ Production Ready
|
|
||||||
59
SETUP.md
59
SETUP.md
@@ -1,59 +0,0 @@
|
|||||||
# Ajarbot Setup Guide
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
1. **Clone the repository**
|
|
||||||
```bash
|
|
||||||
git clone https://vulcan.apophisnetworking.net/jramos/ajarbot.git
|
|
||||||
cd ajarbot
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up Python environment**
|
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Configure credentials**
|
|
||||||
```bash
|
|
||||||
# Copy example files
|
|
||||||
copy .env.example .env
|
|
||||||
copy config\scheduled_tasks.example.yaml config\scheduled_tasks.yaml
|
|
||||||
copy config\adapters.yaml config\adapters.local.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Add your API keys**
|
|
||||||
- Edit `.env` and add your `ANTHROPIC_API_KEY`
|
|
||||||
- Edit `config\adapters.local.yaml` with your Slack/Telegram tokens
|
|
||||||
- Edit `config\scheduled_tasks.yaml` with your user/channel IDs
|
|
||||||
|
|
||||||
5. **Run the bot**
|
|
||||||
```bash
|
|
||||||
python bot_runner.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Files (NOT in Git)
|
|
||||||
|
|
||||||
These files contain your secrets and are ignored by git:
|
|
||||||
- `.env` - Your API keys
|
|
||||||
- `config/adapters.local.yaml` - Your bot tokens
|
|
||||||
- `config/scheduled_tasks.yaml` - Your user IDs
|
|
||||||
- `memory_workspace/memory_index.db` - Your conversation history
|
|
||||||
- `memory_workspace/memory/*.md` - Your daily logs
|
|
||||||
|
|
||||||
## Model Switching Commands
|
|
||||||
|
|
||||||
Send these commands to your bot:
|
|
||||||
- `/haiku` - Switch to Haiku (cheap, fast)
|
|
||||||
- `/sonnet` - Switch to Sonnet (smart, caching enabled)
|
|
||||||
- `/status` - Check current model and settings
|
|
||||||
|
|
||||||
## Cost Optimization
|
|
||||||
|
|
||||||
- Default model: Haiku 4.5 (12x cheaper than Sonnet)
|
|
||||||
- Prompt caching: Automatic when using Sonnet (90% savings)
|
|
||||||
- Context optimized: 3 messages, 2 memory results
|
|
||||||
- Max tool iterations: 5
|
|
||||||
|
|
||||||
See [README.md](README.md) for full documentation.
|
|
||||||
205
SUB_AGENTS.md
205
SUB_AGENTS.md
@@ -1,205 +0,0 @@
|
|||||||
# Sub-Agent Orchestration System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Ajarbot now supports **dynamic sub-agent spawning** - the ability to create specialized agents on-demand for complex tasks. The main agent can delegate work to specialists with focused system prompts, reducing context window bloat and improving task efficiency.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Main Agent (Garvis)
|
|
||||||
├─> Handles general chat, memory, scheduling
|
|
||||||
├─> Can spawn sub-agents dynamically
|
|
||||||
└─> Sub-agents share tools and (optionally) memory
|
|
||||||
|
|
||||||
Sub-Agent (Specialist)
|
|
||||||
├─> Focused system prompt (no SOUL, user profile overhead)
|
|
||||||
├─> Own conversation history (isolated context)
|
|
||||||
├─> Can use all 24 tools
|
|
||||||
└─> Returns result to main agent
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Dynamic spawning**: Create specialists at runtime, no hardcoded definitions
|
|
||||||
- **Caching**: Reuse specialists across multiple calls (agent_id parameter)
|
|
||||||
- **Memory sharing**: Sub-agents can share memory workspace with main agent
|
|
||||||
- **Tool access**: All tools available to sub-agents (file, web, zettelkasten, Google)
|
|
||||||
- **Isolation**: Each sub-agent has separate conversation history
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Method 1: Manual Spawning
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Spawn a specialist
|
|
||||||
specialist = agent.spawn_sub_agent(
|
|
||||||
specialist_prompt="You are a zettelkasten expert. Focus ONLY on note organization.",
|
|
||||||
agent_id="zettelkasten_processor" # Optional: cache for reuse
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use the specialist
|
|
||||||
result = specialist.chat("Process my fleeting notes", username="jordan")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Delegation (Recommended)
|
|
||||||
|
|
||||||
```python
|
|
||||||
# One-off delegation (specialist not cached)
|
|
||||||
result = agent.delegate(
|
|
||||||
task="Analyze my emails and extract action items",
|
|
||||||
specialist_prompt="You are an email analyst. Extract action items and deadlines.",
|
|
||||||
username="jordan"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cached delegation (specialist reused)
|
|
||||||
result = agent.delegate(
|
|
||||||
task="Create permanent notes from my fleeting notes",
|
|
||||||
specialist_prompt="You are a zettelkasten specialist. Focus on note linking.",
|
|
||||||
username="jordan",
|
|
||||||
agent_id="zettelkasten_processor" # Cached for future use
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 3: LLM-Driven Orchestration (Future)
|
|
||||||
|
|
||||||
The main agent can analyze requests and decide when to delegate:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _should_delegate(self, user_message: str) -> Optional[str]:
|
|
||||||
"""Let LLM decide if delegation is needed."""
|
|
||||||
# Ask LLM: "Should this be delegated? If yes, generate specialist prompt"
|
|
||||||
# Return specialist_prompt if delegation needed, None otherwise
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### Complex Zettelkasten Operations
|
|
||||||
```python
|
|
||||||
# Main agent detects: "This requires deep note processing"
|
|
||||||
specialist = agent.spawn_sub_agent(
|
|
||||||
specialist_prompt="""You are a zettelkasten expert. Your ONLY job is:
|
|
||||||
- Process fleeting notes into permanent notes
|
|
||||||
- Find semantic connections using hybrid search
|
|
||||||
- Create wiki-style links between related concepts
|
|
||||||
Stay focused on knowledge management.""",
|
|
||||||
agent_id="zettelkasten_processor"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Email Intelligence
|
|
||||||
```python
|
|
||||||
specialist = agent.spawn_sub_agent(
|
|
||||||
specialist_prompt="""You are an email analyst. Your ONLY job is:
|
|
||||||
- Summarize email threads
|
|
||||||
- Extract action items and deadlines
|
|
||||||
- Identify patterns in communication
|
|
||||||
Stay focused on email analysis.""",
|
|
||||||
agent_id="email_analyst"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calendar Optimization
|
|
||||||
```python
|
|
||||||
specialist = agent.spawn_sub_agent(
|
|
||||||
specialist_prompt="""You are a calendar optimization expert. Your ONLY job is:
|
|
||||||
- Find scheduling conflicts
|
|
||||||
- Suggest optimal meeting times
|
|
||||||
- Identify time-blocking opportunities
|
|
||||||
Stay focused on schedule management.""",
|
|
||||||
agent_id="calendar_optimizer"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Reduced Context Window**: Specialists don't load SOUL.md, user profiles, or irrelevant memory
|
|
||||||
2. **Focused Performance**: Specialists stay on-task without distractions
|
|
||||||
3. **Token Efficiency**: Smaller system prompts = lower token usage
|
|
||||||
4. **Parallel Execution**: Can spawn multiple specialists simultaneously (future)
|
|
||||||
5. **Learning Over Time**: Main agent learns when to delegate based on patterns
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
No configuration needed! The infrastructure is ready to use. You can:
|
|
||||||
|
|
||||||
1. **Add specialists later**: Define common specialists in a config file
|
|
||||||
2. **LLM-driven delegation**: Let the main agent decide when to delegate
|
|
||||||
3. **Parallel execution**: Spawn multiple specialists for complex workflows
|
|
||||||
4. **Custom workspaces**: Give specialists isolated memory (set `share_memory=False`)
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Code Location
|
|
||||||
- **agent.py**: Lines 25-90 (sub-agent infrastructure)
|
|
||||||
- `spawn_sub_agent()`: Create specialist with custom prompt
|
|
||||||
- `delegate()`: Convenience method for one-off delegation
|
|
||||||
- `is_sub_agent`, `specialist_prompt`: Instance variables
|
|
||||||
- `sub_agents`: Cache dictionary
|
|
||||||
|
|
||||||
### Thread Safety
|
|
||||||
- Sub-agents have their own `_chat_lock`
|
|
||||||
- Safe to spawn from multiple threads
|
|
||||||
- Cached specialists are reused (no duplicate spawning)
|
|
||||||
|
|
||||||
### Memory Sharing
|
|
||||||
- Default: Sub-agents share main memory workspace
|
|
||||||
- Optional: Isolated workspace at `memory_workspace/sub_agents/{agent_id}/`
|
|
||||||
- Shared memory = specialists can access/update zettelkasten vault
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Specialist Registry**: Define common specialists in `config/specialists.yaml`
|
|
||||||
2. **Auto-Delegation**: Main agent auto-detects when to delegate
|
|
||||||
3. **Parallel Execution**: Run multiple specialists concurrently
|
|
||||||
4. **Result Synthesis**: Main agent combines outputs from multiple specialists
|
|
||||||
5. **Learning System**: Track which specialists work best for which tasks
|
|
||||||
|
|
||||||
## Example Workflows
|
|
||||||
|
|
||||||
### Workflow 1: Zettelkasten Processing with Delegation
|
|
||||||
```python
|
|
||||||
# User: "Process my fleeting notes about AI and machine learning"
|
|
||||||
# Main agent detects: complex zettelkasten task
|
|
||||||
|
|
||||||
result = agent.delegate(
|
|
||||||
task="Find all fleeting notes tagged 'AI' or 'machine-learning', process into permanent notes, and discover connections",
|
|
||||||
specialist_prompt="You are a zettelkasten expert. Use hybrid search to find semantic connections. Create permanent notes with smart links.",
|
|
||||||
username="jordan",
|
|
||||||
agent_id="zettelkasten_processor"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Specialist:
|
|
||||||
# 1. search_by_tags(tags=["AI", "machine-learning", "fleeting"])
|
|
||||||
# 2. For each note: permanent_note() with auto-linking
|
|
||||||
# 3. Returns: "Created 5 permanent notes with 18 discovered connections"
|
|
||||||
|
|
||||||
# Main agent synthesizes:
|
|
||||||
# "Sir, I've processed your AI and ML notes. Five concepts emerged with particularly
|
|
||||||
# interesting connections to your existing work on neural architecture..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### Workflow 2: Email + Calendar Coordination
|
|
||||||
```python
|
|
||||||
# User: "Find meetings next week and check if I have email threads about them"
|
|
||||||
|
|
||||||
# Spawn two specialists in parallel (future feature)
|
|
||||||
email_result = agent.delegate(
|
|
||||||
task="Search emails for threads about meetings",
|
|
||||||
specialist_prompt="Email analyst. Extract meeting context.",
|
|
||||||
agent_id="email_analyst"
|
|
||||||
)
|
|
||||||
|
|
||||||
calendar_result = agent.delegate(
|
|
||||||
task="List all meetings next week",
|
|
||||||
specialist_prompt="Calendar expert. Get meeting details.",
|
|
||||||
agent_id="calendar_optimizer"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Main agent synthesizes both results
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: Infrastructure complete, ready to use. Add specialists as patterns emerge!
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# Windows 11 Quick Reference
|
|
||||||
|
|
||||||
Quick command reference for testing and running Ajarbot on Windows 11.
|
|
||||||
|
|
||||||
## First Time Setup (5 Minutes)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Step 1: Navigate to project
|
|
||||||
cd c:\Users\fam1n\projects\ajarbot
|
|
||||||
|
|
||||||
# Step 2: Run automated setup
|
|
||||||
quick_start.bat
|
|
||||||
|
|
||||||
# Step 3: Set API key (if prompted)
|
|
||||||
# Get your key from: https://console.anthropic.com/
|
|
||||||
|
|
||||||
# Step 4: Verify installation
|
|
||||||
python test_installation.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Examples (Choose One)
|
|
||||||
|
|
||||||
### Option 1: Basic Agent Test
|
|
||||||
```powershell
|
|
||||||
python example_usage.py
|
|
||||||
```
|
|
||||||
**What it does:** Tests basic chat and memory
|
|
||||||
|
|
||||||
### Option 2: Pulse & Brain Monitoring
|
|
||||||
```powershell
|
|
||||||
python example_bot_with_pulse_brain.py
|
|
||||||
```
|
|
||||||
**What it does:** Runs cost-effective monitoring
|
|
||||||
**To stop:** Press `Ctrl+C`
|
|
||||||
|
|
||||||
### Option 3: Task Scheduler
|
|
||||||
```powershell
|
|
||||||
python example_bot_with_scheduler.py
|
|
||||||
```
|
|
||||||
**What it does:** Shows scheduled task execution
|
|
||||||
**To stop:** Press `Ctrl+C`
|
|
||||||
|
|
||||||
### Option 4: Multi-Platform Bot
|
|
||||||
```powershell
|
|
||||||
# Generate config file
|
|
||||||
python bot_runner.py --init
|
|
||||||
|
|
||||||
# Edit config (add Slack/Telegram tokens)
|
|
||||||
notepad config\adapters.local.yaml
|
|
||||||
|
|
||||||
# Run bot
|
|
||||||
python bot_runner.py
|
|
||||||
```
|
|
||||||
**To stop:** Press `Ctrl+C`
|
|
||||||
|
|
||||||
## Daily Commands
|
|
||||||
|
|
||||||
### Activate Virtual Environment
|
|
||||||
```powershell
|
|
||||||
cd c:\Users\fam1n\projects\ajarbot
|
|
||||||
.\venv\Scripts\activate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Start Bot
|
|
||||||
```powershell
|
|
||||||
python bot_runner.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Health
|
|
||||||
```powershell
|
|
||||||
python bot_runner.py --health
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
```powershell
|
|
||||||
type logs\bot.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Dependencies
|
|
||||||
```powershell
|
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Key Management
|
|
||||||
|
|
||||||
### Set for Current Session
|
|
||||||
```powershell
|
|
||||||
$env:ANTHROPIC_API_KEY = "sk-ant-your-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set Permanently (System)
|
|
||||||
1. Press `Win + X`
|
|
||||||
2. Click "System"
|
|
||||||
3. Click "Advanced system settings"
|
|
||||||
4. Click "Environment Variables"
|
|
||||||
5. Under "User variables", click "New"
|
|
||||||
6. Variable: `ANTHROPIC_API_KEY`
|
|
||||||
7. Value: `sk-ant-your-key-here`
|
|
||||||
|
|
||||||
### Check if Set
|
|
||||||
```powershell
|
|
||||||
$env:ANTHROPIC_API_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running as Service
|
|
||||||
|
|
||||||
### Quick Background Run
|
|
||||||
```powershell
|
|
||||||
Start-Process python -ArgumentList "bot_runner.py" -WindowStyle Hidden
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stop Background Process
|
|
||||||
```powershell
|
|
||||||
# Find process
|
|
||||||
Get-Process python | Where-Object {$_.CommandLine -like "*bot_runner*"}
|
|
||||||
|
|
||||||
# Stop it (replace <PID> with actual process ID)
|
|
||||||
Stop-Process -Id <PID>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "Python not recognized"
|
|
||||||
```powershell
|
|
||||||
# Add to PATH
|
|
||||||
# Win + X -> System -> Advanced -> Environment Variables
|
|
||||||
# Edit PATH, add: C:\Users\fam1n\AppData\Local\Programs\Python\Python3XX
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Module not found"
|
|
||||||
```powershell
|
|
||||||
.\venv\Scripts\activate
|
|
||||||
pip install -r requirements.txt --force-reinstall
|
|
||||||
```
|
|
||||||
|
|
||||||
### "API key not found"
|
|
||||||
```powershell
|
|
||||||
$env:ANTHROPIC_API_KEY = "sk-ant-your-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Memory
|
|
||||||
```powershell
|
|
||||||
Remove-Item -Recurse -Force memory_workspace
|
|
||||||
python example_usage.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Files Quick Reference
|
|
||||||
|
|
||||||
| File/Folder | Purpose |
|
|
||||||
|-------------|---------|
|
|
||||||
| `agent.py` | Main agent logic |
|
|
||||||
| `bot_runner.py` | Multi-platform bot launcher |
|
|
||||||
| `pulse_brain.py` | Monitoring system |
|
|
||||||
| `example_*.py` | Example scripts to test |
|
|
||||||
| `test_*.py` | Test scripts |
|
|
||||||
| `config/` | Configuration files |
|
|
||||||
| `docs/` | Full documentation |
|
|
||||||
| `adapters/` | Platform integrations |
|
|
||||||
| `memory_workspace/` | Memory database |
|
|
||||||
|
|
||||||
## Model Switching
|
|
||||||
|
|
||||||
Tell your bot via chat:
|
|
||||||
- `/haiku` - Fast, cheap (default)
|
|
||||||
- `/sonnet` - Smart, caching enabled
|
|
||||||
- `/status` - Check current model
|
|
||||||
|
|
||||||
## Need More Help?
|
|
||||||
|
|
||||||
- **Complete Setup Guide:** [SETUP.md](SETUP.md)
|
|
||||||
- **Full Windows Guide:** [docs/WINDOWS_DEPLOYMENT.md](docs/WINDOWS_DEPLOYMENT.md)
|
|
||||||
- **Main Documentation:** [docs/README.md](docs/README.md)
|
|
||||||
@@ -5,7 +5,6 @@ Connects messaging platform adapters to the Agent instance.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
@@ -281,39 +280,3 @@ class AdapterRuntime:
|
|||||||
status["adapters"][adapter.platform_name] = adapter_health
|
status["adapters"][adapter.platform_name] = adapter_health
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
# --- Example Preprocessors and Postprocessors ---
|
|
||||||
|
|
||||||
|
|
||||||
def command_preprocessor(message: InboundMessage) -> InboundMessage:
|
|
||||||
"""Example: Handle bot commands."""
|
|
||||||
if not message.text.startswith("/"):
|
|
||||||
return message
|
|
||||||
|
|
||||||
parts = message.text.split(maxsplit=1)
|
|
||||||
command = parts[0]
|
|
||||||
|
|
||||||
if command == "/status":
|
|
||||||
message.text = "What is your current status?"
|
|
||||||
elif command == "/help":
|
|
||||||
message.text = (
|
|
||||||
"Please provide help information about what you can do."
|
|
||||||
)
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
def markdown_postprocessor(
|
|
||||||
response: str, original_message: InboundMessage
|
|
||||||
) -> str:
|
|
||||||
"""Example: Ensure markdown compatibility for Slack."""
|
|
||||||
if original_message.platform != "slack":
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Convert standard markdown bold to Slack mrkdwn
|
|
||||||
response = response.replace("**", "*")
|
|
||||||
# Slack doesn't support ## headers
|
|
||||||
response = re.sub(r"^#+\s+", "", response, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Allows the Agent to invoke local skills programmatically,
|
|||||||
enabling advanced automation and dynamic behavior.
|
enabling advanced automation and dynamic behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
@@ -83,46 +82,6 @@ class SkillInvoker:
|
|||||||
info["path"] = str(skill_path)
|
info["path"] = str(skill_path)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def invoke_skill_via_cli(
|
|
||||||
self, skill_name: str, *args: str
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Invoke a skill via Claude Code CLI.
|
|
||||||
|
|
||||||
Requires claude-code CLI to be installed and in PATH.
|
|
||||||
For production, integrate with the Agent's LLM directly.
|
|
||||||
"""
|
|
||||||
# Validate skill_name
|
|
||||||
if not skill_name or not skill_name.replace("-", "").replace("_", "").isalnum():
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid skill name: must contain only alphanumeric, "
|
|
||||||
"hyphens, and underscores"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate arguments don't contain shell metacharacters
|
|
||||||
for arg in args:
|
|
||||||
if any(char in str(arg) for char in ['&', '|', ';', '$', '`', '\n', '\r']):
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid argument: contains shell metacharacters"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = ["claude-code", f"/{skill_name}"] + list(args)
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=self.project_root,
|
|
||||||
timeout=60, # Add timeout to prevent hanging
|
|
||||||
)
|
|
||||||
return result.stdout if result.returncode == 0 else None
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("[SkillInvoker] claude-code CLI not found")
|
|
||||||
return None
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
print(f"[SkillInvoker] Skill {skill_name} timed out")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def invoke_skill_via_agent(
|
def invoke_skill_via_agent(
|
||||||
self, skill_name: str, agent: Any, *args: str
|
self, skill_name: str, agent: Any, *args: str
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -193,20 +152,3 @@ def skill_based_preprocessor(
|
|||||||
return message
|
return message
|
||||||
|
|
||||||
return preprocessor
|
return preprocessor
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
invoker = SkillInvoker()
|
|
||||||
|
|
||||||
print("Available skills:")
|
|
||||||
for skill in invoker.list_available_skills():
|
|
||||||
info = invoker.get_skill_info(skill)
|
|
||||||
print(f" /{skill}")
|
|
||||||
if info:
|
|
||||||
print(
|
|
||||||
f" Description: {info.get('description', 'N/A')}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f" User-invocable: "
|
|
||||||
f"{info.get('user-invocable', 'N/A')}"
|
|
||||||
)
|
|
||||||
|
|||||||
3
agent.py
3
agent.py
@@ -1,7 +1,6 @@
|
|||||||
"""AI Agent with Memory and LLM Integration."""
|
"""AI Agent with Memory and LLM Integration."""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from typing import List, Optional, Callable
|
from typing import List, Optional, Callable
|
||||||
|
|
||||||
from hooks import HooksSystem
|
from hooks import HooksSystem
|
||||||
@@ -12,8 +11,6 @@ from tools import TOOL_DEFINITIONS, execute_tool
|
|||||||
|
|
||||||
# Maximum number of recent messages to include in LLM context
|
# Maximum number of recent messages to include in LLM context
|
||||||
MAX_CONTEXT_MESSAGES = 20 # Optimized for Agent SDK flat-rate subscription
|
MAX_CONTEXT_MESSAGES = 20 # Optimized for Agent SDK flat-rate subscription
|
||||||
# Maximum characters of agent response to store in memory
|
|
||||||
MEMORY_RESPONSE_PREVIEW_LENGTH = 500 # Store more context for better memory retrieval
|
|
||||||
# Maximum conversation history entries before pruning
|
# Maximum conversation history entries before pruning
|
||||||
MAX_CONVERSATION_HISTORY = 100 # Higher limit with flat-rate subscription
|
MAX_CONVERSATION_HISTORY = 100 # Higher limit with flat-rate subscription
|
||||||
# Maximum tool execution iterations (generous limit for complex operations like zettelkasten)
|
# Maximum tool execution iterations (generous limit for complex operations like zettelkasten)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Environment variables:
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import signal
|
import signal
|
||||||
import sys
|
|
||||||
import traceback
|
import traceback
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -119,8 +118,8 @@ class BotRunner:
|
|||||||
print(f"[Setup] {len(enabled_tasks)} scheduled task(s) enabled:")
|
print(f"[Setup] {len(enabled_tasks)} scheduled task(s) enabled:")
|
||||||
for task_info in enabled_tasks:
|
for task_info in enabled_tasks:
|
||||||
print(f" - {task_info['name']}: {task_info['schedule']}")
|
print(f" - {task_info['name']}: {task_info['schedule']}")
|
||||||
if task_info.get("send_to_platform"):
|
if task_info.get("send_to"):
|
||||||
print(f" → {task_info['send_to_platform']}")
|
print(f" → {task_info['send_to']}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
# Security Audit Summary
|
|
||||||
|
|
||||||
**Date:** 2026-02-12
|
|
||||||
**Auditors:** 5 Opus 4.6 Agents (Parallel Execution)
|
|
||||||
**Status:** ✅ Critical vulnerabilities fixed
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
A comprehensive security audit was performed on the entire ajarbot codebase using 5 specialized Opus 4.6 agents running in parallel. The audit identified **32 security findings** across 4 severity levels:
|
|
||||||
|
|
||||||
- **Critical:** 3 findings (ALL FIXED)
|
|
||||||
- **High:** 9 findings (ALL FIXED)
|
|
||||||
- **Medium:** 14 findings (6 FIXED, 8 remaining non-critical)
|
|
||||||
- **Low:** 6 findings (informational)
|
|
||||||
|
|
||||||
All critical and high-severity vulnerabilities have been remediated. The codebase is now safe for testing and deployment.
|
|
||||||
|
|
||||||
## Critical Vulnerabilities Fixed
|
|
||||||
|
|
||||||
### 1. Path Traversal in Memory System (CRITICAL → FIXED)
|
|
||||||
**Files:** `memory_system.py` (read_file, update_user, get_user)
|
|
||||||
**Risk:** Arbitrary file read/write anywhere on the filesystem
|
|
||||||
**Fix Applied:**
|
|
||||||
- Added validation that username contains only alphanumeric, hyphens, and underscores
|
|
||||||
- Added path resolution checks using `.resolve()` and `.is_relative_to()`
|
|
||||||
- Prevents traversal attacks like `../../etc/passwd` or `../../.env`
|
|
||||||
|
|
||||||
### 2. Format String Injection in Pulse Brain (CRITICAL → FIXED)
|
|
||||||
**File:** `pulse_brain.py:410`
|
|
||||||
**Risk:** Information disclosure, potential code execution via object attribute access
|
|
||||||
**Fix Applied:**
|
|
||||||
- Replaced `.format(**data)` with `string.Template.safe_substitute()`
|
|
||||||
- All data values converted to strings before substitution
|
|
||||||
- Updated all template strings in `config/pulse_brain_config.py` to use `$variable` syntax
|
|
||||||
|
|
||||||
### 3. Command & Prompt Injection in Skills (CRITICAL → FIXED)
|
|
||||||
**File:** `adapters/skill_integration.py`
|
|
||||||
**Risk:** Arbitrary command execution and prompt injection
|
|
||||||
**Fixes Applied:**
|
|
||||||
- Added skill_name validation (alphanumeric, hyphens, underscores only)
|
|
||||||
- Added argument validation to reject shell metacharacters
|
|
||||||
- Added 60-second timeout to subprocess calls
|
|
||||||
- Wrapped user arguments in `<user_input>` XML tags to prevent prompt injection
|
|
||||||
- Limited argument length to 1000 characters
|
|
||||||
- Changed from privileged "skill-invoker" username to "default"
|
|
||||||
|
|
||||||
## High-Severity Vulnerabilities Fixed
|
|
||||||
|
|
||||||
### 4. FTS5 Query Injection (HIGH → FIXED)
|
|
||||||
**File:** `memory_system.py` (search, search_user methods)
|
|
||||||
**Risk:** Enumerate all memory content via FTS5 query syntax
|
|
||||||
**Fix Applied:**
|
|
||||||
- Created `_sanitize_fts5_query()` static method
|
|
||||||
- Wraps queries in double quotes to treat as phrase search
|
|
||||||
- Escapes double quotes within query strings
|
|
||||||
|
|
||||||
### 5. Credential Exposure in Config Dump (HIGH → FIXED)
|
|
||||||
**File:** `config/config_loader.py:143`
|
|
||||||
**Risk:** API keys and tokens printed to stdout/logs
|
|
||||||
**Fix Applied:**
|
|
||||||
- Added `redact_credentials()` function
|
|
||||||
- Masks credentials showing only first 4 and last 4 characters
|
|
||||||
- Applied to config dump in `__main__` block
|
|
||||||
|
|
||||||
### 6. Thread Safety in Pulse Brain (HIGH → FIXED)
|
|
||||||
**File:** `pulse_brain.py`
|
|
||||||
**Risk:** Race conditions, data corruption, inconsistent state
|
|
||||||
**Fix Applied:**
|
|
||||||
- Added `threading.Lock` (`self._lock`)
|
|
||||||
- Protected all access to `pulse_data` dict
|
|
||||||
- Protected `brain_invocations` counter
|
|
||||||
- Protected `get_status()` method with lock
|
|
||||||
|
|
||||||
## Security Improvements Summary
|
|
||||||
|
|
||||||
| Category | Before | After |
|
|
||||||
|----------|--------|-------|
|
|
||||||
| Path Traversal Protection | ❌ None | ✅ Full validation |
|
|
||||||
| Input Sanitization | ❌ Minimal | ✅ Comprehensive |
|
|
||||||
| Format String Safety | ❌ Vulnerable | ✅ Safe templates |
|
|
||||||
| Command Injection Protection | ❌ Basic | ✅ Validated + timeout |
|
|
||||||
| SQL Injection Protection | ✅ Parameterized | ✅ Parameterized |
|
|
||||||
| Thread Safety | ❌ No locks | ✅ Lock protected |
|
|
||||||
| Credential Handling | ⚠️ Exposed in logs | ✅ Redacted |
|
|
||||||
|
|
||||||
## Remaining Non-Critical Issues
|
|
||||||
|
|
||||||
The following medium/low severity findings remain but do not pose immediate security risks:
|
|
||||||
|
|
||||||
### Medium Severity (Informational)
|
|
||||||
|
|
||||||
1. **No Rate Limiting** (`adapters/runtime.py:84`)
|
|
||||||
- Messages not rate-limited per user
|
|
||||||
- Could lead to API cost abuse
|
|
||||||
- Recommendation: Add per-user rate limiting (e.g., 10 messages/minute)
|
|
||||||
|
|
||||||
2. **User Message Logging** (`adapters/runtime.py:108`)
|
|
||||||
- First 50 chars of messages logged to stdout
|
|
||||||
- May capture sensitive user data
|
|
||||||
- Recommendation: Make message logging configurable, disabled by default
|
|
||||||
|
|
||||||
3. **Placeholder Credentials in Examples**
|
|
||||||
- Example files encourage inline credential replacement
|
|
||||||
- Risk: Accidental commit to version control
|
|
||||||
- Recommendation: All examples already use `os.getenv()` pattern
|
|
||||||
|
|
||||||
4. **SSL Verification Disabled** (`config/pulse_brain_config.py:98`)
|
|
||||||
- UniFi controller check uses `verify=False`
|
|
||||||
- Acceptable for localhost self-signed certificates
|
|
||||||
- Documented with comment
|
|
||||||
|
|
||||||
### Low Severity (Informational)
|
|
||||||
|
|
||||||
1. **No File Permissions on Config Files**
|
|
||||||
- Config files created with default permissions
|
|
||||||
- Recommendation: Set `0o600` on credential files (Linux/macOS)
|
|
||||||
|
|
||||||
2. **Daemon Threads May Lose Data on Shutdown**
|
|
||||||
- All threads are daemon threads
|
|
||||||
- Recommendation: Implement graceful shutdown with thread joins
|
|
||||||
|
|
||||||
## Code Quality Improvements
|
|
||||||
|
|
||||||
In addition to security fixes, the following improvements were made:
|
|
||||||
|
|
||||||
1. **PEP8 Compliance** - All 16 Python files refactored following PEP8 guidelines
|
|
||||||
2. **Type Annotations** - Added return type annotations throughout
|
|
||||||
3. **Code Organization** - Reduced nesting, improved readability
|
|
||||||
4. **Documentation** - Enhanced docstrings and inline comments
|
|
||||||
|
|
||||||
## Positive Security Findings
|
|
||||||
|
|
||||||
The audit found several existing security best practices:
|
|
||||||
|
|
||||||
✅ **SQL Injection Protection** - All database queries use parameterized statements
|
|
||||||
✅ **YAML Safety** - Uses `yaml.safe_load()` (not `yaml.load()`)
|
|
||||||
✅ **No eval/exec** - No dangerous code execution functions
|
|
||||||
✅ **No unsafe deserialization** - No insecure object loading
|
|
||||||
✅ **Subprocess Safety** - Uses list arguments (not shell=True)
|
|
||||||
✅ **Gitignore** - Properly excludes `*.local.yaml` and `.env` files
|
|
||||||
✅ **Environment Variables** - API keys loaded from environment
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Basic functionality testing confirms:
|
|
||||||
- ✅ Code is syntactically correct
|
|
||||||
- ✅ File structure intact
|
|
||||||
- ✅ No import errors introduced
|
|
||||||
- ✅ All modules loadable (pending dependency installation)
|
|
||||||
|
|
||||||
## Recommendations for Deployment
|
|
||||||
|
|
||||||
### Before Production
|
|
||||||
|
|
||||||
1. **Install Dependencies**
|
|
||||||
```powershell
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set API Keys Securely**
|
|
||||||
```powershell
|
|
||||||
$env:ANTHROPIC_API_KEY = "sk-ant-your-key"
|
|
||||||
```
|
|
||||||
Or use Windows Credential Manager
|
|
||||||
|
|
||||||
3. **Review User Mapping**
|
|
||||||
- Map platform user IDs to sanitized usernames
|
|
||||||
- Ensure usernames are alphanumeric + hyphens/underscores only
|
|
||||||
|
|
||||||
4. **Enable Rate Limiting** (if exposing to untrusted users)
|
|
||||||
- Add per-user message rate limiting
|
|
||||||
- Set maximum message queue size
|
|
||||||
|
|
||||||
5. **Restrict File Permissions** (Linux/macOS)
|
|
||||||
```bash
|
|
||||||
chmod 600 config/*.local.yaml
|
|
||||||
chmod 600 memory_workspace/memory_index.db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Monitoring
|
|
||||||
|
|
||||||
Monitor for:
|
|
||||||
- Unusual API usage patterns
|
|
||||||
- Failed validation attempts in logs
|
|
||||||
- Large numbers of messages from single users
|
|
||||||
- Unexpected file access patterns
|
|
||||||
|
|
||||||
## Audit Methodology
|
|
||||||
|
|
||||||
The security audit was performed by 5 specialized Opus 4.6 agents:
|
|
||||||
|
|
||||||
1. **Memory System Agent** - Audited `memory_system.py` for SQL injection, path traversal
|
|
||||||
2. **LLM Interface Agent** - Audited `agent.py`, `llm_interface.py` for prompt injection
|
|
||||||
3. **Adapters Agent** - Audited all adapter files for command injection, XSS
|
|
||||||
4. **Monitoring Agent** - Audited `pulse_brain.py`, `heartbeat.py` for code injection
|
|
||||||
5. **Config Agent** - Audited `bot_runner.py`, `config_loader.py` for secrets management
|
|
||||||
|
|
||||||
Each agent:
|
|
||||||
- Performed deep code analysis
|
|
||||||
- Identified specific vulnerabilities with line numbers
|
|
||||||
- Assessed severity and exploitability
|
|
||||||
- Provided detailed remediation recommendations
|
|
||||||
|
|
||||||
Total audit time: ~8 minutes (parallel execution)
|
|
||||||
Total findings: 32
|
|
||||||
Lines of code analyzed: ~3,500+
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### Security Fixes
|
|
||||||
- `memory_system.py` - Path traversal protection, FTS5 sanitization
|
|
||||||
- `pulse_brain.py` - Format string fix, thread safety
|
|
||||||
- `adapters/skill_integration.py` - Command/prompt injection fixes
|
|
||||||
- `config/config_loader.py` - Credential redaction
|
|
||||||
- `config/pulse_brain_config.py` - Template syntax updates
|
|
||||||
|
|
||||||
### No Breaking Changes
|
|
||||||
All fixes maintain backward compatibility with existing functionality. The only user-facing change is that template strings now use `$variable` instead of `{variable}` syntax in pulse brain configurations.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The ajarbot codebase has been thoroughly audited and all critical security vulnerabilities have been remediated. The application is now safe for testing and deployment on Windows 11.
|
|
||||||
|
|
||||||
**Next Steps:**
|
|
||||||
1. Install dependencies: `pip install -r requirements.txt`
|
|
||||||
2. Run basic tests: `python test_installation.py`
|
|
||||||
3. Test with your API key: `python example_usage.py`
|
|
||||||
4. Review deployment guide: `docs/WINDOWS_DEPLOYMENT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Security Audit Completed:** ✅
|
|
||||||
**Critical Issues Remaining:** 0
|
|
||||||
**Safe for Deployment:** Yes
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Google Calendar API client for managing events."""
|
"""Google Calendar API client for managing events."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List
|
||||||
|
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
@@ -66,7 +66,7 @@ class CalendarClient:
|
|||||||
# Limit days_ahead to 30
|
# Limit days_ahead to 30
|
||||||
days_ahead = min(days_ahead, 30)
|
days_ahead = min(days_ahead, 30)
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(timezone.utc)
|
||||||
time_min = now.isoformat() + "Z"
|
time_min = now.isoformat() + "Z"
|
||||||
time_max = (now + timedelta(days=days_ahead)).isoformat() + "Z"
|
time_max = (now + timedelta(days=days_ahead)).isoformat() + "Z"
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ class CalendarClient:
|
|||||||
else:
|
else:
|
||||||
dt = datetime.fromisoformat(start)
|
dt = datetime.fromisoformat(start)
|
||||||
start_str = dt.strftime("%b %d (all day)")
|
start_str = dt.strftime("%b %d (all day)")
|
||||||
except:
|
except Exception:
|
||||||
start_str = start
|
start_str = start
|
||||||
|
|
||||||
lines.append(f"{i}. {event['summary']} - {start_str}")
|
lines.append(f"{i}. {event['summary']} - {start_str}")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Gmail API client for sending and reading emails."""
|
"""Gmail API client for sending and reading emails."""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import webbrowser
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Thread
|
from typing import Optional
|
||||||
from typing import Dict, Optional
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
@@ -120,14 +119,6 @@ class GoogleOAuthManager:
|
|||||||
|
|
||||||
return self.credentials
|
return self.credentials
|
||||||
|
|
||||||
def needs_refresh_soon(self) -> bool:
|
|
||||||
"""Check if token will expire within 5 minutes."""
|
|
||||||
if not self.credentials or not self.credentials.expiry:
|
|
||||||
return False
|
|
||||||
|
|
||||||
expiry_threshold = datetime.utcnow() + timedelta(minutes=5)
|
|
||||||
return self.credentials.expiry < expiry_threshold
|
|
||||||
|
|
||||||
def run_oauth_flow(self, manual: bool = False) -> bool:
|
def run_oauth_flow(self, manual: bool = False) -> bool:
|
||||||
"""Run OAuth2 authorization flow.
|
"""Run OAuth2 authorization flow.
|
||||||
|
|
||||||
@@ -224,21 +215,3 @@ class GoogleOAuthManager:
|
|||||||
# Atomic rename
|
# Atomic rename
|
||||||
temp_file.replace(self.token_file)
|
temp_file.replace(self.token_file)
|
||||||
|
|
||||||
def revoke_authorization(self) -> bool:
|
|
||||||
"""Revoke OAuth authorization and delete tokens.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if revoked successfully, False otherwise.
|
|
||||||
"""
|
|
||||||
if not self.credentials:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.credentials.revoke(Request())
|
|
||||||
if self.token_file.exists():
|
|
||||||
self.token_file.unlink()
|
|
||||||
print("[OAuth] Authorization revoked successfully")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[OAuth] Failed to revoke authorization: {e}")
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -193,101 +193,6 @@ class PeopleClient:
|
|||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def update_contact(self, resource_name: str, updates: Dict) -> Dict:
|
|
||||||
"""Update an existing contact.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
resource_name: Contact resource name (e.g., "people/c1234567890")
|
|
||||||
updates: Dict with fields to update (given_name, family_name, email, phone, notes)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with success status or error
|
|
||||||
"""
|
|
||||||
if not self._ensure_service():
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get current contact to obtain etag
|
|
||||||
current = (
|
|
||||||
self.service.people()
|
|
||||||
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
|
||||||
.execute()
|
|
||||||
)
|
|
||||||
|
|
||||||
body: Dict[str, Any] = {"etag": current["etag"]}
|
|
||||||
update_fields = []
|
|
||||||
|
|
||||||
if "given_name" in updates or "family_name" in updates:
|
|
||||||
names = current.get("names", [{}])
|
|
||||||
name = names[0] if names else {}
|
|
||||||
body["names"] = [{
|
|
||||||
"givenName": updates.get("given_name", name.get("givenName", "")),
|
|
||||||
"familyName": updates.get("family_name", name.get("familyName", "")),
|
|
||||||
}]
|
|
||||||
update_fields.append("names")
|
|
||||||
|
|
||||||
if "email" in updates:
|
|
||||||
body["emailAddresses"] = [{"value": updates["email"]}]
|
|
||||||
update_fields.append("emailAddresses")
|
|
||||||
|
|
||||||
if "phone" in updates:
|
|
||||||
body["phoneNumbers"] = [{"value": updates["phone"]}]
|
|
||||||
update_fields.append("phoneNumbers")
|
|
||||||
|
|
||||||
if "notes" in updates:
|
|
||||||
body["biographies"] = [{"value": updates["notes"], "contentType": "TEXT_PLAIN"}]
|
|
||||||
update_fields.append("biographies")
|
|
||||||
|
|
||||||
if not update_fields:
|
|
||||||
return {"success": False, "error": "No valid fields to update"}
|
|
||||||
|
|
||||||
result = (
|
|
||||||
self.service.people()
|
|
||||||
.updateContact(
|
|
||||||
resourceName=resource_name,
|
|
||||||
body=body,
|
|
||||||
updatePersonFields=",".join(update_fields),
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"resource_name": result.get("resourceName", resource_name),
|
|
||||||
"updated_fields": update_fields,
|
|
||||||
}
|
|
||||||
|
|
||||||
except HttpError as e:
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
def delete_contact(self, resource_name: str) -> Dict:
|
|
||||||
"""Delete a contact.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
resource_name: Contact resource name (e.g., "people/c1234567890")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with success status or error
|
|
||||||
"""
|
|
||||||
if not self._ensure_service():
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.service.people().deleteContact(
|
|
||||||
resourceName=resource_name,
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
return {"success": True, "deleted": resource_name}
|
|
||||||
|
|
||||||
except HttpError as e:
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
def _format_contact(self, person: Dict) -> Dict:
|
def _format_contact(self, person: Dict) -> Dict:
|
||||||
"""Format a person resource into a simple contact dict."""
|
"""Format a person resource into a simple contact dict."""
|
||||||
names = person.get("names", [])
|
names = person.get("names", [])
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Utility functions for Gmail/Calendar tools."""
|
"""Utility functions for Gmail/Calendar tools."""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import email
|
|
||||||
import re
|
import re
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Usage (standalone test):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def ssh_execute(args: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Run SSH command in thread pool to avoid blocking
|
# Run SSH command in thread pool to avoid blocking
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
result = await loop.run_in_executor(
|
result = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
_execute_ssh_sync,
|
_execute_ssh_sync,
|
||||||
@@ -187,7 +187,7 @@ async def ssh_file_upload(args: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
result = await loop.run_in_executor(
|
result = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
_upload_file_sync,
|
_upload_file_sync,
|
||||||
|
|||||||
@@ -456,10 +456,6 @@ class MemorySystem:
|
|||||||
# Normalize to 0-1, then invert (lower BM25 is better)
|
# Normalize to 0-1, then invert (lower BM25 is better)
|
||||||
normalized = (chunk_data["bm25_score"] - min_bm25) / bm25_range
|
normalized = (chunk_data["bm25_score"] - min_bm25) / bm25_range
|
||||||
bm25_map[chunk_id]["normalized_bm25"] = 1 - normalized
|
bm25_map[chunk_id]["normalized_bm25"] = 1 - normalized
|
||||||
else:
|
|
||||||
# No BM25 results
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 5. Combine scores: 0.7 vector + 0.3 BM25
|
# 5. Combine scores: 0.7 vector + 0.3 BM25
|
||||||
combined_scores = {}
|
combined_scores = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
server:
|
|
||||||
http_listen_port: 9080
|
|
||||||
grpc_listen_port: 0
|
|
||||||
|
|
||||||
positions:
|
|
||||||
filename: /tmp/positions.yaml
|
|
||||||
|
|
||||||
clients:
|
|
||||||
- url: http://loki:3100/loki/api/v1/push
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: syslog_ingest
|
|
||||||
syslog:
|
|
||||||
listen_address: 0.0.0.0:1514
|
|
||||||
listen_protocol: tcp
|
|
||||||
idle_timeout: 60s
|
|
||||||
label_structured_data: yes
|
|
||||||
labels:
|
|
||||||
job: "syslog_combined"
|
|
||||||
relabel_configs:
|
|
||||||
- source_labels: ['__syslog_message_hostname']
|
|
||||||
target_label: 'host'
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# SYSLOG NOISE FILTERS
|
|
||||||
# Estimated ~80-85% volume reduction from Dream Router
|
|
||||||
# Applied: 2026-02-23
|
|
||||||
# ============================================================
|
|
||||||
pipeline_stages:
|
|
||||||
# --- HIGH VOLUME DROPS (~60-70% of all logs) ---
|
|
||||||
|
|
||||||
# mDNS multicast (IPv4) - Apple/Chromecast/IoT discovery
|
|
||||||
# Fires across EVERY VLAN (br0, br2, br5, br10, br11, br12)
|
|
||||||
- drop:
|
|
||||||
expression: 'DST=224\.0\.0\.251'
|
|
||||||
drop_counter_reason: "mdns_ipv4_multicast"
|
|
||||||
|
|
||||||
# mDNS multicast (IPv6)
|
|
||||||
- drop:
|
|
||||||
expression: 'DST=ff02::fb'
|
|
||||||
drop_counter_reason: "mdns_ipv6_multicast"
|
|
||||||
|
|
||||||
# mDNS port catch-all (anything remaining on port 5353)
|
|
||||||
- drop:
|
|
||||||
expression: 'DPT=5353'
|
|
||||||
drop_counter_reason: "mdns_port_5353"
|
|
||||||
|
|
||||||
# --- MEDIUM VOLUME DROPS (~15-20%) ---
|
|
||||||
|
|
||||||
# mca-ctrl / stahtd daemon noise - fires every 2-3 seconds
|
|
||||||
- drop:
|
|
||||||
expression: 'no input for event'
|
|
||||||
drop_counter_reason: "mca_ctrl_stahtd_noise"
|
|
||||||
|
|
||||||
# --- LOW VOLUME DROPS (~3-5%) ---
|
|
||||||
|
|
||||||
# UniFi device discovery broadcasts
|
|
||||||
- drop:
|
|
||||||
expression: 'DPT=10001'
|
|
||||||
drop_counter_reason: "unifi_discovery"
|
|
||||||
|
|
||||||
# hostapd WiFi AP check systemd spam (~every 30s)
|
|
||||||
- drop:
|
|
||||||
expression: 'hostapd-global-check'
|
|
||||||
drop_counter_reason: "hostapd_check_spam"
|
|
||||||
|
|
||||||
# Duplicate DNAT entries for port forwards (keeps the WAN_IN Allow line)
|
|
||||||
- drop:
|
|
||||||
expression: 'PortForward.*DNAT'
|
|
||||||
drop_counter_reason: "duplicate_dnat"
|
|
||||||
|
|
||||||
# Internal ICMP gateway pings - devices checking if gateway alive
|
|
||||||
- drop:
|
|
||||||
expression: 'PROTO=ICMP.*DST=192\.168\.'
|
|
||||||
drop_counter_reason: "internal_icmp_pings"
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# WHAT WE KEEP:
|
|
||||||
# - [WAN_LOCAL]Block → real attack attempts (security value)
|
|
||||||
# - [WAN_IN]Allow → legit inbound traffic log
|
|
||||||
# - Daemon errors/warnings
|
|
||||||
# - DHCP/DNS logs
|
|
||||||
# - mcad interval changes (rare, informational)
|
|
||||||
# - Everything from serviceslab (Proxmox host)
|
|
||||||
# ============================================================
|
|
||||||
102
quick_start.bat
102
quick_start.bat
@@ -1,102 +0,0 @@
|
|||||||
@echo off
|
|
||||||
echo ============================================================
|
|
||||||
echo Ajarbot Quick Start for Windows 11
|
|
||||||
echo ============================================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Check if Python is installed
|
|
||||||
python --version >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [ERROR] Python is not installed or not in PATH
|
|
||||||
echo Please install Python from https://www.python.org/downloads/
|
|
||||||
echo Make sure to check "Add Python to PATH" during installation
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [1/5] Python detected
|
|
||||||
python --version
|
|
||||||
|
|
||||||
REM Check if virtual environment exists
|
|
||||||
if not exist "venv\" (
|
|
||||||
echo.
|
|
||||||
echo [2/5] Creating virtual environment...
|
|
||||||
python -m venv venv
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [ERROR] Failed to create virtual environment
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo Virtual environment created
|
|
||||||
) else (
|
|
||||||
echo.
|
|
||||||
echo [2/5] Virtual environment already exists
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Activate virtual environment
|
|
||||||
echo.
|
|
||||||
echo [3/5] Activating virtual environment...
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [ERROR] Failed to activate virtual environment
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Install dependencies
|
|
||||||
echo.
|
|
||||||
echo [4/5] Installing dependencies...
|
|
||||||
pip install -r requirements.txt --quiet
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [ERROR] Failed to install dependencies
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo Dependencies installed
|
|
||||||
|
|
||||||
REM Check for API key
|
|
||||||
echo.
|
|
||||||
echo [5/5] Checking for API key...
|
|
||||||
if "%ANTHROPIC_API_KEY%"=="" (
|
|
||||||
echo.
|
|
||||||
echo [WARNING] ANTHROPIC_API_KEY not set
|
|
||||||
echo.
|
|
||||||
echo Please set your API key using one of these methods:
|
|
||||||
echo.
|
|
||||||
echo Option 1: Set for current session only
|
|
||||||
echo set ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
||||||
echo.
|
|
||||||
echo Option 2: Add to system environment variables
|
|
||||||
echo Win + X -^> System -^> Advanced -^> Environment Variables
|
|
||||||
echo.
|
|
||||||
echo Option 3: Create .env file
|
|
||||||
echo echo ANTHROPIC_API_KEY=sk-ant-your-key-here ^> .env
|
|
||||||
echo pip install python-dotenv
|
|
||||||
echo.
|
|
||||||
set /p API_KEY="Enter your Anthropic API key (or press Enter to skip): "
|
|
||||||
if not "!API_KEY!"=="" (
|
|
||||||
set ANTHROPIC_API_KEY=!API_KEY!
|
|
||||||
echo API key set for this session
|
|
||||||
) else (
|
|
||||||
echo Skipping API key setup
|
|
||||||
echo You'll need to set it before running examples
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
echo API key found
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ============================================================
|
|
||||||
echo Setup Complete!
|
|
||||||
echo ============================================================
|
|
||||||
echo.
|
|
||||||
echo Your environment is ready. Try these commands:
|
|
||||||
echo.
|
|
||||||
echo python example_usage.py # Basic agent test
|
|
||||||
echo python example_bot_with_pulse_brain.py # Pulse ^& Brain monitoring
|
|
||||||
echo python example_bot_with_scheduler.py # Task scheduler
|
|
||||||
echo python bot_runner.py --init # Generate adapter config
|
|
||||||
echo.
|
|
||||||
echo For more information, see docs\WINDOWS_DEPLOYMENT.md
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -431,26 +431,6 @@ class TaskScheduler:
|
|||||||
return f"Task '{task_name}' executed"
|
return f"Task '{task_name}' executed"
|
||||||
|
|
||||||
|
|
||||||
def integrate_scheduler_with_runtime(
|
|
||||||
runtime: Any,
|
|
||||||
agent: Agent,
|
|
||||||
config_file: Optional[str] = None,
|
|
||||||
) -> TaskScheduler:
|
|
||||||
"""
|
|
||||||
Integrate scheduled tasks with the bot runtime.
|
|
||||||
|
|
||||||
Usage in bot_runner.py:
|
|
||||||
scheduler = integrate_scheduler_with_runtime(runtime, agent)
|
|
||||||
scheduler.start()
|
|
||||||
"""
|
|
||||||
scheduler = TaskScheduler(agent, config_file)
|
|
||||||
|
|
||||||
for adapter in runtime.registry.get_all():
|
|
||||||
scheduler.add_adapter(adapter.platform_name, adapter)
|
|
||||||
|
|
||||||
return scheduler
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
provider="claude", workspace_dir="./memory_workspace"
|
provider="claude", workspace_dir="./memory_workspace"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,416 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Remote Homelab Collection Wrapper
|
|
||||||
# Purpose: Executes the collection script on a remote Proxmox host via SSH
|
|
||||||
# and retrieves the results back to your local machine (WSL/Linux)
|
|
||||||
#
|
|
||||||
# Usage: ./collect-remote.sh [PROXMOX_HOST] [OPTIONS]
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Color codes
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
BOLD='\033[1m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Script configuration
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
COLLECTION_SCRIPT="${SCRIPT_DIR}/collect-homelab-config.sh"
|
|
||||||
REMOTE_SCRIPT_PATH="/tmp/collect-homelab-config.sh"
|
|
||||||
LOCAL_OUTPUT_DIR="${SCRIPT_DIR}"
|
|
||||||
|
|
||||||
# SSH configuration
|
|
||||||
SSH_USER="${SSH_USER:-root}"
|
|
||||||
SSH_PORT="${SSH_PORT:-22}"
|
|
||||||
SSH_OPTS="-o ConnectTimeout=10 -o StrictHostKeyChecking=no"
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Functions
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
log() {
|
|
||||||
local level="$1"
|
|
||||||
shift
|
|
||||||
local message="$*"
|
|
||||||
|
|
||||||
case "${level}" in
|
|
||||||
INFO)
|
|
||||||
echo -e "${BLUE}[INFO]${NC} ${message}"
|
|
||||||
;;
|
|
||||||
SUCCESS)
|
|
||||||
echo -e "${GREEN}[✓]${NC} ${message}"
|
|
||||||
;;
|
|
||||||
WARN)
|
|
||||||
echo -e "${YELLOW}[WARN]${NC} ${message}"
|
|
||||||
;;
|
|
||||||
ERROR)
|
|
||||||
echo -e "${RED}[ERROR]${NC} ${message}" >&2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
banner() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${CYAN}======================================================================${NC}"
|
|
||||||
echo -e "${BOLD}${CYAN} $1${NC}"
|
|
||||||
echo -e "${BOLD}${CYAN}======================================================================${NC}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
${BOLD}Remote Homelab Collection Wrapper${NC}
|
|
||||||
|
|
||||||
${BOLD}USAGE:${NC}
|
|
||||||
$0 PROXMOX_HOST [OPTIONS]
|
|
||||||
|
|
||||||
${BOLD}DESCRIPTION:${NC}
|
|
||||||
Executes the homelab collection script on a remote Proxmox host via SSH,
|
|
||||||
then retrieves the results back to your local machine.
|
|
||||||
|
|
||||||
${BOLD}ARGUMENTS:${NC}
|
|
||||||
PROXMOX_HOST IP address or hostname of your Proxmox server
|
|
||||||
|
|
||||||
${BOLD}OPTIONS:${NC}
|
|
||||||
-u, --user USER SSH username (default: root)
|
|
||||||
-p, --port PORT SSH port (default: 22)
|
|
||||||
-l, --level LEVEL Collection level: basic, standard, full, paranoid
|
|
||||||
(default: standard)
|
|
||||||
-s, --sanitize OPT Sanitization: all, ips, none (default: passwords/tokens only)
|
|
||||||
-o, --output DIR Local directory to store results (default: current directory)
|
|
||||||
-k, --keep-remote Keep the export on the remote host (default: remove after download)
|
|
||||||
-v, --verbose Verbose output
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
${BOLD}ENVIRONMENT VARIABLES:${NC}
|
|
||||||
SSH_USER Default SSH username (default: root)
|
|
||||||
SSH_PORT Default SSH port (default: 22)
|
|
||||||
|
|
||||||
${BOLD}EXAMPLES:${NC}
|
|
||||||
# Basic usage
|
|
||||||
$0 192.168.1.100
|
|
||||||
|
|
||||||
# Full collection with complete sanitization
|
|
||||||
$0 192.168.1.100 --level full --sanitize all
|
|
||||||
|
|
||||||
# Custom SSH user and port
|
|
||||||
$0 proxmox.local --user admin --port 2222
|
|
||||||
|
|
||||||
# Keep results on remote host
|
|
||||||
$0 192.168.1.100 --keep-remote
|
|
||||||
|
|
||||||
# Verbose output with custom output directory
|
|
||||||
$0 192.168.1.100 -v -o ~/backups/homelab
|
|
||||||
|
|
||||||
${BOLD}PREREQUISITES:${NC}
|
|
||||||
1. SSH access to the Proxmox host
|
|
||||||
2. collect-homelab-config.sh in the same directory as this script
|
|
||||||
3. Sufficient disk space on both remote and local machines
|
|
||||||
|
|
||||||
${BOLD}WORKFLOW:${NC}
|
|
||||||
1. Copies collection script to remote Proxmox host
|
|
||||||
2. Executes the script remotely
|
|
||||||
3. Downloads the compressed archive to local machine
|
|
||||||
4. Optionally removes the remote copy
|
|
||||||
5. Extracts the archive locally
|
|
||||||
|
|
||||||
${BOLD}NOTES:${NC}
|
|
||||||
- Requires passwordless SSH or SSH key authentication (recommended)
|
|
||||||
- The script will be run as the specified SSH user (typically root)
|
|
||||||
- Remote execution output is displayed in real-time
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
check_prerequisites() {
|
|
||||||
# Check if collection script exists
|
|
||||||
if [[ ! -f "${COLLECTION_SCRIPT}" ]]; then
|
|
||||||
log ERROR "Collection script not found: ${COLLECTION_SCRIPT}"
|
|
||||||
log ERROR "Ensure collect-homelab-config.sh is in the same directory as this script"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if ssh is available
|
|
||||||
if ! command -v ssh &> /dev/null; then
|
|
||||||
log ERROR "SSH client not found. Please install openssh-client"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if scp is available
|
|
||||||
if ! command -v scp &> /dev/null; then
|
|
||||||
log ERROR "SCP not found. Please install openssh-client"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
test_ssh_connection() {
|
|
||||||
local host="$1"
|
|
||||||
|
|
||||||
log INFO "Testing SSH connection to ${SSH_USER}@${host}:${SSH_PORT}..."
|
|
||||||
|
|
||||||
if ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "exit 0" 2>/dev/null; then
|
|
||||||
log SUCCESS "SSH connection successful"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log ERROR "Cannot connect to ${SSH_USER}@${host}:${SSH_PORT}"
|
|
||||||
log ERROR "Possible issues:"
|
|
||||||
log ERROR " - Host is unreachable"
|
|
||||||
log ERROR " - SSH service is not running"
|
|
||||||
log ERROR " - Incorrect credentials"
|
|
||||||
log ERROR " - Firewall blocking connection"
|
|
||||||
log ERROR ""
|
|
||||||
log ERROR "Try manually: ssh -p ${SSH_PORT} ${SSH_USER}@${host}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_proxmox_host() {
|
|
||||||
local host="$1"
|
|
||||||
|
|
||||||
log INFO "Verifying Proxmox installation on remote host..."
|
|
||||||
|
|
||||||
if ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "test -f /etc/pve/.version" 2>/dev/null; then
|
|
||||||
local pve_version=$(ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "cat /etc/pve/.version" 2>/dev/null)
|
|
||||||
log SUCCESS "Confirmed Proxmox VE installation (version: ${pve_version})"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log WARN "Remote host does not appear to be a Proxmox VE server"
|
|
||||||
log WARN "Proceeding anyway, but collection may fail..."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
upload_script() {
|
|
||||||
local host="$1"
|
|
||||||
|
|
||||||
banner "Uploading Collection Script"
|
|
||||||
|
|
||||||
log INFO "Copying collection script to ${host}..."
|
|
||||||
|
|
||||||
if scp ${SSH_OPTS} -P "${SSH_PORT}" "${COLLECTION_SCRIPT}" "${SSH_USER}@${host}:${REMOTE_SCRIPT_PATH}"; then
|
|
||||||
log SUCCESS "Script uploaded successfully"
|
|
||||||
|
|
||||||
# Make executable
|
|
||||||
ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "chmod +x ${REMOTE_SCRIPT_PATH}"
|
|
||||||
log SUCCESS "Script permissions set"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log ERROR "Failed to upload script"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
execute_remote_collection() {
|
|
||||||
local host="$1"
|
|
||||||
shift
|
|
||||||
local collection_args=("$@")
|
|
||||||
|
|
||||||
banner "Executing Collection on Remote Host"
|
|
||||||
|
|
||||||
log INFO "Running collection script on ${host}..."
|
|
||||||
log INFO "Arguments: ${collection_args[*]}"
|
|
||||||
|
|
||||||
# Build the remote command
|
|
||||||
local remote_cmd="${REMOTE_SCRIPT_PATH} ${collection_args[*]}"
|
|
||||||
|
|
||||||
# Execute remotely and stream output
|
|
||||||
if ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "${remote_cmd}"; then
|
|
||||||
log SUCCESS "Collection completed successfully on remote host"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log ERROR "Collection failed on remote host"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download_results() {
|
|
||||||
local host="$1"
|
|
||||||
local output_dir="$2"
|
|
||||||
|
|
||||||
banner "Downloading Results"
|
|
||||||
|
|
||||||
log INFO "Finding remote export archive..."
|
|
||||||
|
|
||||||
# Find the most recent export archive
|
|
||||||
local remote_archive=$(ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" \
|
|
||||||
"ls -t /root/homelab-export-*.tar.gz 2>/dev/null | head -1" 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ -z "${remote_archive}" ]]; then
|
|
||||||
log ERROR "No export archive found on remote host"
|
|
||||||
log ERROR "Collection may have failed or compression was disabled"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log INFO "Found archive: ${remote_archive}"
|
|
||||||
|
|
||||||
# Create output directory
|
|
||||||
mkdir -p "${output_dir}"
|
|
||||||
|
|
||||||
# Download the archive
|
|
||||||
local local_archive="${output_dir}/$(basename "${remote_archive}")"
|
|
||||||
|
|
||||||
log INFO "Downloading to: ${local_archive}"
|
|
||||||
|
|
||||||
if scp ${SSH_OPTS} -P "${SSH_PORT}" "${SSH_USER}@${host}:${remote_archive}" "${local_archive}"; then
|
|
||||||
log SUCCESS "Archive downloaded successfully"
|
|
||||||
|
|
||||||
# Extract the archive
|
|
||||||
log INFO "Extracting archive..."
|
|
||||||
if tar -xzf "${local_archive}" -C "${output_dir}"; then
|
|
||||||
log SUCCESS "Archive extracted to: ${output_dir}/$(basename "${local_archive}" .tar.gz)"
|
|
||||||
|
|
||||||
# Show summary
|
|
||||||
local extracted_dir="${output_dir}/$(basename "${local_archive}" .tar.gz)"
|
|
||||||
if [[ -f "${extracted_dir}/SUMMARY.md" ]]; then
|
|
||||||
echo ""
|
|
||||||
log INFO "Collection Summary:"
|
|
||||||
echo ""
|
|
||||||
head -30 "${extracted_dir}/SUMMARY.md"
|
|
||||||
echo ""
|
|
||||||
log INFO "Full summary: ${extracted_dir}/SUMMARY.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log ERROR "Failed to extract archive"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log ERROR "Failed to download archive"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_remote() {
|
|
||||||
local host="$1"
|
|
||||||
local keep_remote="$2"
|
|
||||||
|
|
||||||
if [[ "${keep_remote}" == "true" ]]; then
|
|
||||||
log INFO "Keeping export on remote host (--keep-remote specified)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
banner "Cleaning Up Remote Host"
|
|
||||||
|
|
||||||
log INFO "Removing export files from remote host..."
|
|
||||||
|
|
||||||
# Remove the script
|
|
||||||
ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" "rm -f ${REMOTE_SCRIPT_PATH}" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Remove export directories and archives
|
|
||||||
ssh ${SSH_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${host}" \
|
|
||||||
"rm -rf /root/homelab-export-* 2>/dev/null" 2>/dev/null || true
|
|
||||||
|
|
||||||
log SUCCESS "Remote cleanup completed"
|
|
||||||
}
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Main Execution
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
main() {
|
|
||||||
# Parse arguments
|
|
||||||
if [[ $# -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local proxmox_host=""
|
|
||||||
local collection_level="standard"
|
|
||||||
local sanitize_option=""
|
|
||||||
local keep_remote="false"
|
|
||||||
local verbose="false"
|
|
||||||
|
|
||||||
# First argument is the host
|
|
||||||
proxmox_host="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
# Parse remaining options
|
|
||||||
local collection_args=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
-u|--user)
|
|
||||||
SSH_USER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-p|--port)
|
|
||||||
SSH_PORT="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-l|--level)
|
|
||||||
collection_level="$2"
|
|
||||||
collection_args+=("--level" "$2")
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-s|--sanitize)
|
|
||||||
sanitize_option="$2"
|
|
||||||
collection_args+=("--sanitize" "$2")
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-o|--output)
|
|
||||||
LOCAL_OUTPUT_DIR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-k|--keep-remote)
|
|
||||||
keep_remote="true"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-v|--verbose)
|
|
||||||
verbose="true"
|
|
||||||
collection_args+=("--verbose")
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log ERROR "Unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Validate host
|
|
||||||
if [[ -z "${proxmox_host}" ]]; then
|
|
||||||
log ERROR "Proxmox host not specified"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Display configuration
|
|
||||||
banner "Remote Homelab Collection"
|
|
||||||
echo -e "${BOLD}Target Host:${NC} ${proxmox_host}"
|
|
||||||
echo -e "${BOLD}SSH User:${NC} ${SSH_USER}"
|
|
||||||
echo -e "${BOLD}SSH Port:${NC} ${SSH_PORT}"
|
|
||||||
echo -e "${BOLD}Collection Level:${NC} ${collection_level}"
|
|
||||||
echo -e "${BOLD}Output Directory:${NC} ${LOCAL_OUTPUT_DIR}"
|
|
||||||
echo -e "${BOLD}Keep Remote:${NC} ${keep_remote}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Execute workflow
|
|
||||||
check_prerequisites
|
|
||||||
test_ssh_connection "${proxmox_host}" || exit 1
|
|
||||||
verify_proxmox_host "${proxmox_host}"
|
|
||||||
upload_script "${proxmox_host}" || exit 1
|
|
||||||
execute_remote_collection "${proxmox_host}" "${collection_args[@]}" || exit 1
|
|
||||||
download_results "${proxmox_host}" "${LOCAL_OUTPUT_DIR}" || exit 1
|
|
||||||
cleanup_remote "${proxmox_host}" "${keep_remote}"
|
|
||||||
|
|
||||||
banner "Collection Complete"
|
|
||||||
|
|
||||||
log SUCCESS "Homelab infrastructure export completed successfully"
|
|
||||||
log INFO "Results are available in: ${LOCAL_OUTPUT_DIR}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
=== COLLECTION OUTPUT ===
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Starting Homelab Infrastructure Collection
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[INFO] Collection Level: full
|
|
||||||
[INFO] Output Directory: /tmp/homelab-export
|
|
||||||
[INFO] Sanitization: IPs=false | Passwords=false | Tokens=false
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Creating Directory Structure
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Directory structure created at: /tmp/homelab-export
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting System Information
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Proxmox VE version
|
|
||||||
[✓] Collected Hostname
|
|
||||||
[✓] Collected Kernel information
|
|
||||||
[✓] Collected System uptime
|
|
||||||
[✓] Collected System date/time
|
|
||||||
[✓] Collected CPU information
|
|
||||||
[✓] Collected Detailed CPU info
|
|
||||||
[✓] Collected Memory information
|
|
||||||
[✓] Collected Detailed memory info
|
|
||||||
[✓] Collected Filesystem usage
|
|
||||||
[✓] Collected Block devices
|
|
||||||
[✓] Collected LVM physical volumes
|
|
||||||
[✓] Collected LVM volume groups
|
|
||||||
[✓] Collected LVM logical volumes
|
|
||||||
[✓] Collected IP addresses
|
|
||||||
[✓] Collected Routing table
|
|
||||||
[✓] Collected Listening sockets
|
|
||||||
[✓] Collected Installed packages
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Proxmox Configurations
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Datacenter config
|
|
||||||
[✓] Collected Storage config
|
|
||||||
[✓] Collected User config
|
|
||||||
[✓] Collected Auth public key
|
|
||||||
[WARN] Failed to copy directory HA configuration from /etc/pve/ha
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting VM Configurations
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected VM 100 (docker-hub) config
|
|
||||||
[✓] Collected VM 101 (monitoring-docker) config
|
|
||||||
[✓] Collected VM 104 (ubuntu-dev) config
|
|
||||||
[✓] Collected VM 105 (pfSense-Firewall) config
|
|
||||||
[✓] Collected VM 106 (Ansible-Control) config
|
|
||||||
[✓] Collected VM 107 (ubuntu-docker) config
|
|
||||||
[✓] Collected VM 108 (CML) config
|
|
||||||
[✓] Collected VM 114 (haos) config
|
|
||||||
[✓] Collected VM 119 (moltbot) config
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting LXC Container Configurations
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Container 102 (nginx) config
|
|
||||||
[✓] Collected Container 103 (netbox) config
|
|
||||||
[✓] Collected Container 112 (twingate-connector) config
|
|
||||||
[✓] Collected Container 113 (n8n
|
|
||||||
n8n
|
|
||||||
n8n) config
|
|
||||||
[✓] Collected Container 117 (test-cve-database) config
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Network Configurations
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Network interfaces config
|
|
||||||
[WARN] Failed to copy directory Additional interface configs from /etc/network/interfaces.d
|
|
||||||
[✓] Collected SDN configuration
|
|
||||||
[✓] Collected Hosts file
|
|
||||||
[✓] Collected DNS resolver config
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Storage Information
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Storage status
|
|
||||||
[✓] Collected ZFS pool status
|
|
||||||
[✓] Collected ZFS pool list
|
|
||||||
[✓] Collected ZFS datasets
|
|
||||||
[✓] Collected Samba config
|
|
||||||
[✓] Collected iSCSI initiator config
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Backup Configurations
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Vzdump config
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Cluster Information
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[WARN] Failed to execute: pvecm status (Cluster status)
|
|
||||||
[WARN] Failed to execute: pvecm nodes (Cluster nodes)
|
|
||||||
[✓] Collected Cluster resources
|
|
||||||
[✓] Collected Recent tasks
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Guest Information
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected VM list
|
|
||||||
[✓] Collected Container list
|
|
||||||
[✓] Collected All guests (JSON)
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collecting Service Configurations (Advanced)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Collected Systemd services
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Generating Documentation
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Generated README.md
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Generating Summary Report
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Generated SUMMARY.md
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Collection Complete
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
[✓] Total items collected: 53
|
|
||||||
[INFO] Total items skipped: 1
|
|
||||||
[WARN] Total errors: 4
|
|
||||||
[WARN] Review /tmp/homelab-export/collection.log for details
|
|
||||||
|
|
||||||
Export Location: /tmp/homelab-export
|
|
||||||
Summary Report: /tmp/homelab-export/SUMMARY.md
|
|
||||||
Collection Log: /tmp/homelab-export/collection.log
|
|
||||||
|
|
||||||
|
|
||||||
Exit code: 0
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Proxmox SSH Helper - serviceslab (192.168.2.100)
|
|
||||||
Uses paramiko for native Python SSH (no sshpass needed).
|
|
||||||
Usage: python proxmox_ssh.py "command to run"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import paramiko
|
|
||||||
|
|
||||||
PROXMOX_HOST = "192.168.2.100"
|
|
||||||
PROXMOX_USER = "root"
|
|
||||||
PROXMOX_PASS = "Nbkx4mdmay1)"
|
|
||||||
PROXMOX_PORT = 22
|
|
||||||
TIMEOUT = 15
|
|
||||||
|
|
||||||
|
|
||||||
def run_command(command: str) -> tuple:
|
|
||||||
"""Execute a command on the Proxmox server via SSH.
|
|
||||||
Returns (stdout, stderr, exit_code).
|
|
||||||
"""
|
|
||||||
client = paramiko.SSHClient()
|
|
||||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
||||||
try:
|
|
||||||
client.connect(
|
|
||||||
hostname=PROXMOX_HOST,
|
|
||||||
port=PROXMOX_PORT,
|
|
||||||
username=PROXMOX_USER,
|
|
||||||
password=PROXMOX_PASS,
|
|
||||||
timeout=TIMEOUT,
|
|
||||||
look_for_keys=False,
|
|
||||||
allow_agent=False,
|
|
||||||
)
|
|
||||||
stdin, stdout, stderr = client.exec_command(command, timeout=TIMEOUT)
|
|
||||||
exit_code = stdout.channel.recv_exit_status()
|
|
||||||
out = stdout.read().decode("utf-8", errors="replace")
|
|
||||||
err = stderr.read().decode("utf-8", errors="replace")
|
|
||||||
return out, err, exit_code
|
|
||||||
finally:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python proxmox_ssh.py \"command\"")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
cmd = sys.argv[1]
|
|
||||||
out, err, code = run_command(cmd)
|
|
||||||
if out:
|
|
||||||
print(out, end="")
|
|
||||||
if err:
|
|
||||||
print(err, end="", file=sys.stderr)
|
|
||||||
sys.exit(code)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Proxmox SSH Helper - serviceslab (192.168.2.100)
|
|
||||||
# Usage: proxmox_ssh.sh "command to run"
|
|
||||||
|
|
||||||
PROXMOX_HOST="192.168.2.100"
|
|
||||||
PROXMOX_USER="root"
|
|
||||||
PROXMOX_PASS="Nbkx4mdmay1)"
|
|
||||||
|
|
||||||
sshpass -p "$PROXMOX_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${PROXMOX_USER}@${PROXMOX_HOST}" "$1"
|
|
||||||
21
tools.py
21
tools.py
@@ -539,25 +539,6 @@ def execute_tool(tool_name: str, tool_input: Dict[str, Any], healing_system: Any
|
|||||||
return f"Error executing {tool_name}: {error_msg}"
|
return f"Error executing {tool_name}: {error_msg}"
|
||||||
|
|
||||||
|
|
||||||
def _extract_mcp_result(result: Any) -> str:
|
|
||||||
"""Convert an MCP tool result dict to a plain string."""
|
|
||||||
if isinstance(result, dict):
|
|
||||||
if "error" in result:
|
|
||||||
return f"Error: {result['error']}"
|
|
||||||
elif "content" in result:
|
|
||||||
content = result["content"]
|
|
||||||
if isinstance(content, list):
|
|
||||||
# Extract text from content blocks
|
|
||||||
parts = []
|
|
||||||
for block in content:
|
|
||||||
if isinstance(block, dict) and block.get("type") == "text":
|
|
||||||
parts.append(block.get("text", ""))
|
|
||||||
return "\n".join(parts) if parts else str(content)
|
|
||||||
return str(content)
|
|
||||||
return str(result)
|
|
||||||
return str(result)
|
|
||||||
|
|
||||||
|
|
||||||
def _execute_obsidian_tool(
|
def _execute_obsidian_tool(
|
||||||
tool_name: str,
|
tool_name: str,
|
||||||
tool_input: Dict[str, Any],
|
tool_input: Dict[str, Any],
|
||||||
@@ -1096,8 +1077,6 @@ def _obsidian_fallback(tool_name: str, tool_input: Dict[str, Any]) -> Optional[s
|
|||||||
|
|
||||||
Returns None if no fallback is possible for the given tool.
|
Returns None if no fallback is possible for the given tool.
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
if tool_name == "obsidian_read_note":
|
if tool_name == "obsidian_read_note":
|
||||||
# Map to read_file with vault-relative path
|
# Map to read_file with vault-relative path
|
||||||
vault_path = Path("memory_workspace/obsidian")
|
vault_path = Path("memory_workspace/obsidian")
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ _PRICING = {
|
|||||||
"cache_write": 3.75, # Cache creation
|
"cache_write": 3.75, # Cache creation
|
||||||
"cache_read": 0.30, # 90% discount on cache hits
|
"cache_read": 0.30, # 90% discount on cache hits
|
||||||
},
|
},
|
||||||
|
"claude-sonnet-4-6": {
|
||||||
|
"input": 3.00,
|
||||||
|
"output": 15.00,
|
||||||
|
"cache_write": 3.75,
|
||||||
|
"cache_read": 0.30,
|
||||||
|
},
|
||||||
"claude-opus-4-6": {
|
"claude-opus-4-6": {
|
||||||
"input": 15.00,
|
"input": 15.00,
|
||||||
"output": 75.00,
|
"output": 75.00,
|
||||||
|
|||||||
Reference in New Issue
Block a user