chore: remove remaining gitignored files from tracking

- analyze_coverage.py (local coverage analysis script)
- coverage_summary.txt (coverage output)
- mock.env (environment file)
This commit is contained in:
rcourtman 2026-01-28 21:19:52 +00:00
parent a53aa387f3
commit 23ff4d1337
11 changed files with 136 additions and 6019 deletions

View file

@ -1,56 +0,0 @@
import sys
import os
def parse_coverage(filename):
if not os.path.exists(filename):
print(f"File {filename} not found")
return
package_stmts = {}
package_covered = {}
with open(filename, 'r') as f:
lines = f.readlines()
current_mode = ""
for line in lines:
if line.startswith("mode:"):
current_mode = line.split()[1]
continue
parts = line.strip().split(':')
if len(parts) != 2:
continue
file_path = parts[0]
# Package is directory of file_path
package_name = os.path.dirname(file_path)
metrics = parts[1].split()
if len(metrics) != 3:
continue
# start_end = metrics[0]
num_stmts = int(metrics[1])
count = int(metrics[2])
package_stmts[package_name] = package_stmts.get(package_name, 0) + num_stmts
if count > 0:
package_covered[package_name] = package_covered.get(package_name, 0) + num_stmts
results = []
for pkg, total in package_stmts.items():
covered = package_covered.get(pkg, 0)
percent = (covered / total) * 100 if total > 0 else 0
results.append((pkg, percent, covered, total))
# Sort by percentage (ascending)
results.sort(key=lambda x: x[1])
print("Package Coverage Report (Bottom 20):")
for pkg, pct, cov, tot in results[:20]:
print(f"{pct:6.2f}% ({cov}/{tot}) {pkg}")
if __name__ == "__main__":
parse_coverage("coverage.out")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,96 +0,0 @@
package tools
import (
"context"
"fmt"
)
// registerControlToolsConsolidated registers the consolidated pulse_control tool
func (e *PulseToolExecutor) registerControlToolsConsolidated() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_control",
Description: `Control Proxmox VMs/LXC containers or execute WRITE commands on infrastructure.
IMPORTANT: For READ operations (grep, cat, tail, logs, ps, status checks), use pulse_read instead.
This tool is for WRITE operations that modify state.
Types:
- guest: Start, stop, restart, shutdown, or delete VMs and LXC containers
- command: Execute commands that MODIFY state (restart services, write files, etc.)
USE pulse_control FOR:
- Guest control: start/stop/restart/delete VMs and LXCs
- Service management: systemctl restart, service start/stop
- Package management: apt install, yum update
- File modification: echo > file, sed -i, rm, mv, cp
DO NOT use pulse_control for:
- Reading logs → use pulse_read action=exec or action=logs
- Checking status → use pulse_read action=exec
- Reading files → use pulse_read action=file
- Finding files → use pulse_read action=find
Examples:
- Restart VM: type="guest", guest_id="101", action="restart"
- Restart service: type="command", command="systemctl restart nginx", target_host="webserver"
For Docker container control, use pulse_docker.
Note: Delete requires the guest to be stopped first.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "Control type: guest or command",
Enum: []string{"guest", "command"},
},
"guest_id": {
Type: "string",
Description: "For guest: VMID or name",
},
"action": {
Type: "string",
Description: "For guest: start, stop, shutdown, restart, delete",
Enum: []string{"start", "stop", "shutdown", "restart", "delete"},
},
"command": {
Type: "string",
Description: "For command type: the shell command to execute",
},
"target_host": {
Type: "string",
Description: "For command type: hostname to run command on",
},
"run_on_host": {
Type: "boolean",
Description: "For command type: run on host (default true)",
},
"force": {
Type: "boolean",
Description: "For guest stop: force stop without graceful shutdown",
},
},
Required: []string{"type"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeControl(ctx, args)
},
RequireControl: true,
})
}
// executeControl routes to the appropriate control handler based on type
// Handler functions are implemented in tools_control.go
func (e *PulseToolExecutor) executeControl(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
controlType, _ := args["type"].(string)
switch controlType {
case "guest":
return e.executeControlGuest(ctx, args)
case "command":
return e.executeRunCommand(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: guest, command", controlType)), nil
}
}

View file

@ -3,507 +3,85 @@ package tools
import (
"context"
"fmt"
"strconv"
"strings"
)
// getCommandContext returns information about how to run commands on a resource.
// This helps the AI understand what commands to use with pulse_control.
type CommandContext struct {
// How to run commands: "direct" (agent on resource), "via_host" (agent on parent host)
Method string `json:"method"`
// The target_host value to use with pulse_control
TargetHost string `json:"target_host"`
// Example command pattern (what to pass to pulse_control)
Example string `json:"example"`
// For containers running inside this resource (e.g., Docker in LXC)
NestedExample string `json:"nested_example,omitempty"`
}
// getCLIAccessPattern returns context about the resource type.
// Does NOT prescribe how to access - the AI should determine that based on available agents.
func getCLIAccessPattern(resourceType, hostID, resourceID string) string {
switch resourceType {
case "lxc":
return fmt.Sprintf("LXC container on Proxmox node '%s' (VMID %s)", hostID, resourceID)
case "vm":
return fmt.Sprintf("VM on Proxmox node '%s' (VMID %s)", hostID, resourceID)
case "docker":
return fmt.Sprintf("Docker container '%s' on host '%s'", resourceID, hostID)
case "host":
return fmt.Sprintf("Host '%s'", hostID)
default:
return ""
}
}
// commonServicePaths contains typical log/config paths for well-known services
// These are fallbacks when discovery doesn't find specific paths
var commonServicePaths = map[string]struct {
LogPaths []string
ConfigPaths []string
DataPaths []string
}{
"jellyfin": {
LogPaths: []string{"/var/log/jellyfin/", "/config/log/"},
ConfigPaths: []string{"/etc/jellyfin/", "/config/"},
DataPaths: []string{"/var/lib/jellyfin/", "/config/data/"},
},
"plex": {
LogPaths: []string{"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/"},
ConfigPaths: []string{"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/"},
DataPaths: []string{"/var/lib/plexmediaserver/"},
},
"sonarr": {
LogPaths: []string{"/config/logs/"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/config/"},
},
"radarr": {
LogPaths: []string{"/config/logs/"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/config/"},
},
"prowlarr": {
LogPaths: []string{"/config/logs/"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/config/"},
},
"lidarr": {
LogPaths: []string{"/config/logs/"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/config/"},
},
"postgresql": {
LogPaths: []string{"/var/log/postgresql/", "/var/lib/postgresql/data/log/"},
ConfigPaths: []string{"/etc/postgresql/", "/var/lib/postgresql/data/"},
DataPaths: []string{"/var/lib/postgresql/data/"},
},
"mysql": {
LogPaths: []string{"/var/log/mysql/", "/var/lib/mysql/"},
ConfigPaths: []string{"/etc/mysql/"},
DataPaths: []string{"/var/lib/mysql/"},
},
"mariadb": {
LogPaths: []string{"/var/log/mysql/", "/var/lib/mysql/"},
ConfigPaths: []string{"/etc/mysql/"},
DataPaths: []string{"/var/lib/mysql/"},
},
"nginx": {
LogPaths: []string{"/var/log/nginx/"},
ConfigPaths: []string{"/etc/nginx/"},
DataPaths: []string{"/var/www/"},
},
"homeassistant": {
LogPaths: []string{"/config/home-assistant.log"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/config/"},
},
"frigate": {
LogPaths: []string{"/config/logs/"},
ConfigPaths: []string{"/config/"},
DataPaths: []string{"/media/frigate/"},
},
"redis": {
LogPaths: []string{"/var/log/redis/"},
ConfigPaths: []string{"/etc/redis/"},
DataPaths: []string{"/var/lib/redis/"},
},
"mongodb": {
LogPaths: []string{"/var/log/mongodb/"},
ConfigPaths: []string{"/etc/mongod.conf"},
DataPaths: []string{"/var/lib/mongodb/"},
},
"grafana": {
LogPaths: []string{"/var/log/grafana/"},
ConfigPaths: []string{"/etc/grafana/"},
DataPaths: []string{"/var/lib/grafana/"},
},
"prometheus": {
LogPaths: []string{"/var/log/prometheus/"},
ConfigPaths: []string{"/etc/prometheus/"},
DataPaths: []string{"/var/lib/prometheus/"},
},
"influxdb": {
LogPaths: []string{"/var/log/influxdb/"},
ConfigPaths: []string{"/etc/influxdb/"},
DataPaths: []string{"/var/lib/influxdb/"},
},
}
// getCommonServicePaths returns fallback paths for a service type
func getCommonServicePaths(serviceType string) (logPaths, configPaths, dataPaths []string) {
// Normalize service type (lowercase, remove version numbers)
normalized := strings.ToLower(serviceType)
// Try to match against known services
for key, paths := range commonServicePaths {
if strings.Contains(normalized, key) {
return paths.LogPaths, paths.ConfigPaths, paths.DataPaths
}
}
return nil, nil, nil
}
// registerDiscoveryTools registers AI-powered infrastructure discovery tools
func (e *PulseToolExecutor) registerDiscoveryTools() {
// registerDiscoveryToolsConsolidated registers the consolidated pulse_discovery tool
func (e *PulseToolExecutor) registerDiscoveryToolsConsolidated() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_get_discovery",
Description: `Get AI-discovered information about a specific resource (VM, LXC container, Docker container, or host).
Name: "pulse_discovery",
Description: `Get deep AI-discovered information about services (log paths, config locations, service details).
Returns: JSON with service type, version, config paths, data paths, CLI access command, and discovered facts.
Actions:
- get: Trigger discovery and get detailed info for a specific resource. Use this when you need deep context about a container/VM (where logs are, config paths, service details). Requires resource_type, resource_id, and host_id - use pulse_query action="search" first if you don't know these.
- list: Search existing discoveries only. Will NOT find resources that haven't been discovered yet. Use action="get" to trigger discovery for new resources.
Use when: You need detailed context about a resource before proposing remediation commands, or when investigating what services are running on infrastructure.
Workflow for investigating applications:
1. Use pulse_query action="search" to find the resource by name
2. Use pulse_discovery action="get" with the resource details to get deep context (log paths, config locations)
3. Use pulse_control type="command" to run commands (check logs, query app state, etc.)
The discovery includes:
- Service type and version (e.g., "Frigate NVR v0.13.2")
- Configuration file locations
- Data/storage paths
- CLI access command (e.g., "pct exec 101 -- <command>")
- Discovered facts (ports, GPUs, connected services, etc.)
- User-added notes
This information is critical for proposing correct remediation commands that match the actual service configuration.`,
Examples:
- Trigger discovery: action="get", resource_type="docker", resource_id="jellyfin", host_id="docker-host-1"
- Search existing: action="list", service_type="postgresql"`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"action": {
Type: "string",
Description: "Discovery action: get or list",
Enum: []string{"get", "list"},
},
"resource_type": {
Type: "string",
Description: "Type of resource: 'vm', 'lxc', 'docker', or 'host'",
Description: "For get: resource type (vm, lxc, docker, host)",
Enum: []string{"vm", "lxc", "docker", "host"},
},
"resource_id": {
Type: "string",
Description: "Resource identifier (VMID for VM/LXC, container name for Docker, hostname for host)",
Description: "For get: resource identifier (VMID, container name, hostname)",
},
"host_id": {
Type: "string",
Description: "Node/host where the resource runs. For VM/LXC: the PVE node from 'node' field. For Docker: the Docker host. For host type: same as resource_id.",
Description: "For get: node/host where resource runs",
},
},
Required: []string{"resource_type", "resource_id", "host_id"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeGetDiscovery(ctx, args)
},
})
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_list_discoveries",
Description: `List all AI-discovered infrastructure information with optional filtering.
Returns: JSON array of discoveries with service types, versions, and summaries.
Use when: You need an overview of what services are running across infrastructure, or want to find specific service types.
Filters:
- type: Filter by resource type (vm, lxc, docker, host)
- host: Filter by host/node ID
- service_type: Filter by discovered service type (e.g., "frigate", "postgresql")`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "Optional: Filter by resource type",
Description: "For list: filter by resource type",
Enum: []string{"vm", "lxc", "docker", "host"},
},
"host": {
Type: "string",
Description: "Optional: Filter by host/node ID",
Description: "For list: filter by host/node ID",
},
"service_type": {
Type: "string",
Description: "Optional: Filter by discovered service type",
Description: "For list: filter by service type (e.g., frigate, postgresql)",
},
"limit": {
Type: "integer",
Description: "Maximum number of results (default: 50)",
Description: "For list: maximum results (default: 50)",
},
},
Required: []string{"action"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeListDiscoveries(ctx, args)
return exec.executeDiscovery(ctx, args)
},
})
}
func (e *PulseToolExecutor) executeGetDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.discoveryProvider == nil {
return NewTextResult("Discovery service not available."), nil
// executeDiscovery routes to the appropriate discovery handler based on action
// Handler functions are implemented in tools_discovery.go
func (e *PulseToolExecutor) executeDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
action, _ := args["action"].(string)
switch action {
case "get":
return e.executeGetDiscovery(ctx, args)
case "list":
return e.executeListDiscoveries(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: get, list", action)), nil
}
resourceType, _ := args["resource_type"].(string)
resourceID, _ := args["resource_id"].(string)
hostID, _ := args["host_id"].(string)
if resourceType == "" {
return NewErrorResult(fmt.Errorf("resource_type is required")), nil
}
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
if hostID == "" {
return NewErrorResult(fmt.Errorf("host_id is required - use the 'node' field from search or get_resource results")), nil
}
// For LXC and VM types, resourceID should be a numeric VMID.
// If a name was passed, try to resolve it to a VMID.
if (resourceType == "lxc" || resourceType == "vm") && e.stateProvider != nil {
if _, err := strconv.Atoi(resourceID); err != nil {
// Not a number - try to resolve the name to a VMID
state := e.stateProvider.GetState()
resolved := false
if resourceType == "lxc" {
for _, c := range state.Containers {
if strings.EqualFold(c.Name, resourceID) && c.Node == hostID {
resourceID = fmt.Sprintf("%d", c.VMID)
resolved = true
break
}
}
} else if resourceType == "vm" {
for _, vm := range state.VMs {
if strings.EqualFold(vm.Name, resourceID) && vm.Node == hostID {
resourceID = fmt.Sprintf("%d", vm.VMID)
resolved = true
break
}
}
}
if !resolved {
return NewErrorResult(fmt.Errorf("could not resolve resource name '%s' to a VMID on host '%s'", resourceID, hostID)), nil
}
}
}
// First try to get existing discovery
discovery, err := e.discoveryProvider.GetDiscoveryByResource(resourceType, hostID, resourceID)
if err != nil {
return NewErrorResult(fmt.Errorf("failed to get discovery: %w", err)), nil
}
// Compute CLI access pattern (always useful, even if discovery fails)
cliAccess := getCLIAccessPattern(resourceType, hostID, resourceID)
// If no discovery exists, trigger one
if discovery == nil {
discovery, err = e.discoveryProvider.TriggerDiscovery(ctx, resourceType, hostID, resourceID)
if err != nil {
// Even on failure, provide cli_access so AI can investigate manually
return NewJSONResult(map[string]interface{}{
"found": false,
"resource_type": resourceType,
"resource_id": resourceID,
"host_id": hostID,
"cli_access": cliAccess,
"message": fmt.Sprintf("Discovery failed: %v", err),
"hint": "Use pulse_control with type='command' to investigate. Try checking /var/log/ for logs.",
}), nil
}
}
if discovery == nil {
// No discovery but provide cli_access for manual investigation
return NewJSONResult(map[string]interface{}{
"found": false,
"resource_type": resourceType,
"resource_id": resourceID,
"host_id": hostID,
"cli_access": cliAccess,
"message": "Discovery returned no data. The resource may not be accessible.",
"hint": "Use pulse_control with type='command' to investigate. Try listing /var/log/ or checking running processes.",
}), nil
}
// Use fallback cli_access if discovery didn't provide one
responseCLIAccess := discovery.CLIAccess
if responseCLIAccess == "" {
responseCLIAccess = cliAccess
}
// Use fallback paths for known services if discovery didn't find specific ones
responseConfigPaths := discovery.ConfigPaths
responseDataPaths := discovery.DataPaths
var responseLogPaths []string
if discovery.ServiceType != "" {
fallbackLogPaths, fallbackConfigPaths, fallbackDataPaths := getCommonServicePaths(discovery.ServiceType)
if len(responseConfigPaths) == 0 && len(fallbackConfigPaths) > 0 {
responseConfigPaths = fallbackConfigPaths
}
if len(responseDataPaths) == 0 && len(fallbackDataPaths) > 0 {
responseDataPaths = fallbackDataPaths
}
if len(fallbackLogPaths) > 0 {
responseLogPaths = fallbackLogPaths
}
}
// Return the discovery information
response := map[string]interface{}{
"found": true,
"id": discovery.ID,
"resource_type": discovery.ResourceType,
"resource_id": discovery.ResourceID,
"host_id": discovery.HostID,
"hostname": discovery.Hostname,
"service_type": discovery.ServiceType,
"service_name": discovery.ServiceName,
"service_version": discovery.ServiceVersion,
"category": discovery.Category,
"cli_access": responseCLIAccess,
"config_paths": responseConfigPaths,
"data_paths": responseDataPaths,
"confidence": discovery.Confidence,
"discovered_at": discovery.DiscoveredAt,
"updated_at": discovery.UpdatedAt,
}
// Add log paths if we have them (from fallback or discovery)
if len(responseLogPaths) > 0 {
response["log_paths"] = responseLogPaths
}
// Add facts if present
if len(discovery.Facts) > 0 {
facts := make([]map[string]string, 0, len(discovery.Facts))
for _, f := range discovery.Facts {
facts = append(facts, map[string]string{
"category": f.Category,
"key": f.Key,
"value": f.Value,
})
}
response["facts"] = facts
}
// Add user notes if present
if discovery.UserNotes != "" {
response["user_notes"] = discovery.UserNotes
}
// Add AI reasoning for context
if discovery.AIReasoning != "" {
response["ai_reasoning"] = discovery.AIReasoning
}
// Add listening ports if present
if len(discovery.Ports) > 0 {
ports := make([]map[string]interface{}, 0, len(discovery.Ports))
for _, p := range discovery.Ports {
port := map[string]interface{}{
"port": p.Port,
"protocol": p.Protocol,
}
if p.Process != "" {
port["process"] = p.Process
}
if p.Address != "" {
port["address"] = p.Address
}
ports = append(ports, port)
}
response["ports"] = ports
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeListDiscoveries(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.discoveryProvider == nil {
return NewTextResult("Discovery service not available."), nil
}
filterType, _ := args["type"].(string)
filterHost, _ := args["host"].(string)
filterServiceType, _ := args["service_type"].(string)
limit := intArg(args, "limit", 50)
var discoveries []*ResourceDiscoveryInfo
var err error
// Get discoveries based on filters
if filterType != "" {
discoveries, err = e.discoveryProvider.ListDiscoveriesByType(filterType)
} else if filterHost != "" {
discoveries, err = e.discoveryProvider.ListDiscoveriesByHost(filterHost)
} else {
discoveries, err = e.discoveryProvider.ListDiscoveries()
}
if err != nil {
return NewErrorResult(fmt.Errorf("failed to list discoveries: %w", err)), nil
}
// Filter by service type if specified
if filterServiceType != "" {
filtered := make([]*ResourceDiscoveryInfo, 0)
filterLower := strings.ToLower(filterServiceType)
for _, d := range discoveries {
if strings.Contains(strings.ToLower(d.ServiceType), filterLower) ||
strings.Contains(strings.ToLower(d.ServiceName), filterLower) {
filtered = append(filtered, d)
}
}
discoveries = filtered
}
// Apply limit
if len(discoveries) > limit {
discoveries = discoveries[:limit]
}
// Build response
results := make([]map[string]interface{}, 0, len(discoveries))
for _, d := range discoveries {
result := map[string]interface{}{
"id": d.ID,
"resource_type": d.ResourceType,
"resource_id": d.ResourceID,
"host_id": d.HostID,
"hostname": d.Hostname,
"service_type": d.ServiceType,
"service_name": d.ServiceName,
"service_version": d.ServiceVersion,
"category": d.Category,
"cli_access": d.CLIAccess,
"confidence": d.Confidence,
"updated_at": d.UpdatedAt,
}
// Add key facts count
if len(d.Facts) > 0 {
result["facts_count"] = len(d.Facts)
}
// Add ports count
if len(d.Ports) > 0 {
result["ports_count"] = len(d.Ports)
}
results = append(results, result)
}
response := map[string]interface{}{
"discoveries": results,
"total": len(results),
}
if filterType != "" {
response["filter_type"] = filterType
}
if filterHost != "" {
response["filter_host"] = filterHost
}
if filterServiceType != "" {
response["filter_service_type"] = filterServiceType
}
return NewJSONResult(response), nil
}

View file

@ -1,87 +0,0 @@
package tools
import (
"context"
"fmt"
)
// registerDiscoveryToolsConsolidated registers the consolidated pulse_discovery tool
func (e *PulseToolExecutor) registerDiscoveryToolsConsolidated() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_discovery",
Description: `Get deep AI-discovered information about services (log paths, config locations, service details).
Actions:
- get: Trigger discovery and get detailed info for a specific resource. Use this when you need deep context about a container/VM (where logs are, config paths, service details). Requires resource_type, resource_id, and host_id - use pulse_query action="search" first if you don't know these.
- list: Search existing discoveries only. Will NOT find resources that haven't been discovered yet. Use action="get" to trigger discovery for new resources.
Workflow for investigating applications:
1. Use pulse_query action="search" to find the resource by name
2. Use pulse_discovery action="get" with the resource details to get deep context (log paths, config locations)
3. Use pulse_control type="command" to run commands (check logs, query app state, etc.)
Examples:
- Trigger discovery: action="get", resource_type="docker", resource_id="jellyfin", host_id="docker-host-1"
- Search existing: action="list", service_type="postgresql"`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"action": {
Type: "string",
Description: "Discovery action: get or list",
Enum: []string{"get", "list"},
},
"resource_type": {
Type: "string",
Description: "For get: resource type (vm, lxc, docker, host)",
Enum: []string{"vm", "lxc", "docker", "host"},
},
"resource_id": {
Type: "string",
Description: "For get: resource identifier (VMID, container name, hostname)",
},
"host_id": {
Type: "string",
Description: "For get: node/host where resource runs",
},
"type": {
Type: "string",
Description: "For list: filter by resource type",
Enum: []string{"vm", "lxc", "docker", "host"},
},
"host": {
Type: "string",
Description: "For list: filter by host/node ID",
},
"service_type": {
Type: "string",
Description: "For list: filter by service type (e.g., frigate, postgresql)",
},
"limit": {
Type: "integer",
Description: "For list: maximum results (default: 50)",
},
},
Required: []string{"action"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeDiscovery(ctx, args)
},
})
}
// executeDiscovery routes to the appropriate discovery handler based on action
// Handler functions are implemented in tools_discovery.go
func (e *PulseToolExecutor) executeDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
action, _ := args["action"].(string)
switch action {
case "get":
return e.executeGetDiscovery(ctx, args)
case "list":
return e.executeListDiscoveries(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: get, list", action)), nil
}
}

View file

@ -5,325 +5,60 @@ import (
"fmt"
)
// registerPMGTools registers Proxmox Mail Gateway query tools
func (e *PulseToolExecutor) registerPMGTools() {
// registerPMGToolsConsolidated registers the consolidated pulse_pmg tool
func (e *PulseToolExecutor) registerPMGToolsConsolidated() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_get_pmg_status",
Description: `Get Proxmox Mail Gateway instance status and health.
Name: "pulse_pmg",
Description: `Query Proxmox Mail Gateway status and statistics.
Returns: JSON with instances array containing id, name, host, status, version, and nodes (with status, role, uptime, load).
Types:
- status: Instance status and health (nodes, uptime, load)
- mail_stats: Mail flow statistics (counts, spam, virus, bounces)
- queues: Mail queue status (active, deferred, hold)
- spam: Spam quarantine statistics and score distribution
Use when: User asks about mail gateway status, PMG health, or mail server infrastructure.`,
Examples:
- Get status: type="status"
- Get specific instance: type="status", instance="pmg01"
- Get mail stats: type="mail_stats"
- Get queue status: type="queues"
- Get spam stats: type="spam"`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "PMG query type",
Enum: []string{"status", "mail_stats", "queues", "spam"},
},
"instance": {
Type: "string",
Description: "Optional: specific PMG instance name or ID",
},
},
Required: []string{"type"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeGetPMGStatus(ctx, args)
},
})
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_get_mail_stats",
Description: `Get mail flow statistics (counts, spam, virus, bounces).
Returns: JSON with mail statistics including total in/out, spam in/out, virus counts, bounces, greylist count, and average processing time.
Use when: User asks about mail flow, email statistics, spam counts, or virus detections.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"instance": {
Type: "string",
Description: "Optional: specific PMG instance name or ID",
},
},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeGetMailStats(ctx, args)
},
})
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_get_mail_queues",
Description: `Get mail queue status (active, deferred, hold).
Returns: JSON with queue status for each node including active, deferred, hold, incoming counts and oldest message age.
Use when: User asks about mail queues, deferred messages, mail delivery issues, or queue backlogs.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"instance": {
Type: "string",
Description: "Optional: specific PMG instance name or ID",
},
},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeGetMailQueues(ctx, args)
},
})
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_get_spam_stats",
Description: `Get spam quarantine statistics and distribution.
Returns: JSON with quarantine counts (spam, virus, attachment, blacklisted) and spam score distribution buckets.
Use when: User asks about spam statistics, quarantine status, or spam score distribution.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"instance": {
Type: "string",
Description: "Optional: specific PMG instance name or ID",
},
},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeGetSpamStats(ctx, args)
return exec.executePMG(ctx, args)
},
})
}
func (e *PulseToolExecutor) executeGetPMGStatus(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
// executePMG routes to the appropriate PMG handler based on type
func (e *PulseToolExecutor) executePMG(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
pmgType, _ := args["type"].(string)
switch pmgType {
case "status":
return e.executeGetPMGStatus(ctx, args)
case "mail_stats":
return e.executeGetMailStats(ctx, args)
case "queues":
return e.executeGetMailQueues(ctx, args)
case "spam":
return e.executeGetSpamStats(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: status, mail_stats, queues, spam", pmgType)), nil
}
instanceFilter, _ := args["instance"].(string)
state := e.stateProvider.GetState()
if len(state.PMGInstances) == 0 {
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
}
var instances []PMGInstanceSummary
for _, pmg := range state.PMGInstances {
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
continue
}
var nodes []PMGNodeSummary
for _, node := range pmg.Nodes {
nodes = append(nodes, PMGNodeSummary{
Name: node.Name,
Status: node.Status,
Role: node.Role,
Uptime: node.Uptime,
LoadAvg: node.LoadAvg,
})
}
instances = append(instances, PMGInstanceSummary{
ID: pmg.ID,
Name: pmg.Name,
Host: pmg.Host,
Status: pmg.Status,
Version: pmg.Version,
Nodes: nodes,
})
}
if len(instances) == 0 && instanceFilter != "" {
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
}
// Ensure non-nil slices
for i := range instances {
if instances[i].Nodes == nil {
instances[i].Nodes = []PMGNodeSummary{}
}
}
response := PMGStatusResponse{
Instances: instances,
Total: len(instances),
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeGetMailStats(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
instanceFilter, _ := args["instance"].(string)
state := e.stateProvider.GetState()
if len(state.PMGInstances) == 0 {
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
}
// If filtering, find that specific instance
for _, pmg := range state.PMGInstances {
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
continue
}
if pmg.MailStats == nil {
if instanceFilter != "" {
return NewTextResult(fmt.Sprintf("No mail statistics available for PMG instance '%s'.", instanceFilter)), nil
}
continue
}
response := MailStatsResponse{
Instance: pmg.Name,
Stats: PMGMailStatsSummary{
Timeframe: pmg.MailStats.Timeframe,
TotalIn: pmg.MailStats.CountIn,
TotalOut: pmg.MailStats.CountOut,
SpamIn: pmg.MailStats.SpamIn,
SpamOut: pmg.MailStats.SpamOut,
VirusIn: pmg.MailStats.VirusIn,
VirusOut: pmg.MailStats.VirusOut,
BouncesIn: pmg.MailStats.BouncesIn,
BouncesOut: pmg.MailStats.BouncesOut,
BytesIn: pmg.MailStats.BytesIn,
BytesOut: pmg.MailStats.BytesOut,
GreylistCount: pmg.MailStats.GreylistCount,
RBLRejects: pmg.MailStats.RBLRejects,
AverageProcessTimeMs: pmg.MailStats.AverageProcessTimeMs,
},
}
return NewJSONResult(response), nil
}
if instanceFilter != "" {
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
}
return NewTextResult("No mail statistics available from any PMG instance."), nil
}
func (e *PulseToolExecutor) executeGetMailQueues(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
instanceFilter, _ := args["instance"].(string)
state := e.stateProvider.GetState()
if len(state.PMGInstances) == 0 {
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
}
// Collect queue data from all instances (or filtered instance)
for _, pmg := range state.PMGInstances {
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
continue
}
var queues []PMGQueueSummary
for _, node := range pmg.Nodes {
if node.QueueStatus != nil {
queues = append(queues, PMGQueueSummary{
Node: node.Name,
Active: node.QueueStatus.Active,
Deferred: node.QueueStatus.Deferred,
Hold: node.QueueStatus.Hold,
Incoming: node.QueueStatus.Incoming,
Total: node.QueueStatus.Total,
OldestAgeSeconds: node.QueueStatus.OldestAge,
})
}
}
if len(queues) == 0 {
if instanceFilter != "" {
return NewTextResult(fmt.Sprintf("No queue data available for PMG instance '%s'.", instanceFilter)), nil
}
continue
}
response := MailQueuesResponse{
Instance: pmg.Name,
Queues: queues,
}
return NewJSONResult(response), nil
}
if instanceFilter != "" {
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
}
return NewTextResult("No mail queue data available from any PMG instance."), nil
}
func (e *PulseToolExecutor) executeGetSpamStats(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
instanceFilter, _ := args["instance"].(string)
state := e.stateProvider.GetState()
if len(state.PMGInstances) == 0 {
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
}
for _, pmg := range state.PMGInstances {
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
continue
}
quarantine := PMGQuarantineSummary{}
if pmg.Quarantine != nil {
quarantine = PMGQuarantineSummary{
Spam: pmg.Quarantine.Spam,
Virus: pmg.Quarantine.Virus,
Attachment: pmg.Quarantine.Attachment,
Blacklisted: pmg.Quarantine.Blacklisted,
Total: pmg.Quarantine.Spam + pmg.Quarantine.Virus + pmg.Quarantine.Attachment + pmg.Quarantine.Blacklisted,
}
}
var distribution []PMGSpamBucketSummary
for _, bucket := range pmg.SpamDistribution {
distribution = append(distribution, PMGSpamBucketSummary{
Score: bucket.Score,
Count: bucket.Count,
})
}
response := SpamStatsResponse{
Instance: pmg.Name,
Quarantine: quarantine,
Distribution: distribution,
}
if response.Distribution == nil {
response.Distribution = []PMGSpamBucketSummary{}
}
return NewJSONResult(response), nil
}
if instanceFilter != "" {
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
}
return NewTextResult("No spam statistics available from any PMG instance."), nil
}

View file

@ -1,64 +0,0 @@
package tools
import (
"context"
"fmt"
)
// registerPMGToolsConsolidated registers the consolidated pulse_pmg tool
func (e *PulseToolExecutor) registerPMGToolsConsolidated() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_pmg",
Description: `Query Proxmox Mail Gateway status and statistics.
Types:
- status: Instance status and health (nodes, uptime, load)
- mail_stats: Mail flow statistics (counts, spam, virus, bounces)
- queues: Mail queue status (active, deferred, hold)
- spam: Spam quarantine statistics and score distribution
Examples:
- Get status: type="status"
- Get specific instance: type="status", instance="pmg01"
- Get mail stats: type="mail_stats"
- Get queue status: type="queues"
- Get spam stats: type="spam"`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "PMG query type",
Enum: []string{"status", "mail_stats", "queues", "spam"},
},
"instance": {
Type: "string",
Description: "Optional: specific PMG instance name or ID",
},
},
Required: []string{"type"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executePMG(ctx, args)
},
})
}
// executePMG routes to the appropriate PMG handler based on type
func (e *PulseToolExecutor) executePMG(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
pmgType, _ := args["type"].(string)
switch pmgType {
case "status":
return e.executeGetPMGStatus(ctx, args)
case "mail_stats":
return e.executeGetMailStats(ctx, args)
case "queues":
return e.executeGetMailQueues(ctx, args)
case "spam":
return e.executeGetSpamStats(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: status, mail_stats, queues, spam", pmgType)), nil
}
}

View file

@ -1,10 +0,0 @@
# Mock Mode Configuration
PULSE_MOCK_MODE=false
PULSE_MOCK_NODES=7
PULSE_MOCK_VMS_PER_NODE=5
PULSE_MOCK_LXCS_PER_NODE=8
PULSE_MOCK_DOCKER_HOSTS=3
PULSE_MOCK_DOCKER_CONTAINERS=12
PULSE_MOCK_RANDOM_METRICS=true
PULSE_MOCK_STOPPED_PERCENT=20
PULSE_LICENSE_PUBLIC_KEY="OzbVzmg+TaSGt0eWzDVpn0QkqhOzJqUbOFvSF3AmuRU="