Merge branch 'master'

This commit is contained in:
2026-07-01 04:09:28 -05:00
18 changed files with 1475 additions and 341 deletions
+151
View File
@@ -0,0 +1,151 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
JARVIS Agent installer for Windows.
.DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights.
.EXAMPLE
# Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
# Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
#>
$ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
$CONFIG_FILE = "$INSTALL_DIR\config.json"
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Host "`n========================================" -ForegroundColor Yellow
Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Stop existing service if running ─────────────────────────────────────────
$existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
if ($existing) {
Write-Step "Stopping existing JARVIS Agent service..."
if ($existing.Status -eq 'Running') {
Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep 2
}
try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
} catch {}
Write-OK "Existing service removed."
}
# ── Check / install Python ────────────────────────────────────────────────────
Write-Step "Checking Python..."
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
}
# ── Create install dir ────────────────────────────────────────────────────────
Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Download agent script ─────────────────────────────────────────────────────
Write-Step "Downloading JARVIS agent..."
try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
} catch {
Write-Fail "Failed to download agent: $_"
}
# ── Get registration key ──────────────────────────────────────────────────────
$regKey = $env:JARVIS_REG_KEY
if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
$existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
}
# ── Get hostname ──────────────────────────────────────────────────────────────
$hostname = $env:COMPUTERNAME
$customHostname = $env:JARVIS_HOSTNAME
if ($customHostname) { $hostname = $customHostname }
# ── Write config ──────────────────────────────────────────────────────────────
Write-Step "Writing config..."
$cfg = @{
jarvis_url = $JARVIS_URL
registration_key = $regKey
hostname = $hostname
agent_type = 'windows'
ssl_verify = $true
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @('WinDefend', 'Spooler', 'wuauserv')
} | ConvertTo-Json -Depth 5
$cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-OK "Config written to $CONFIG_FILE"
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller — pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
while not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
last_push = now
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# JARVIS Network Scanner — runs on PVE1, pushes nmap results to JARVIS
# Cron: */3 * * * * /usr/local/bin/jarvis-netscan.sh >/dev/null 2>&1
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
SUBNET="10.48.200.0/24"
TMPFILE=$(mktemp)
nmap -sn --send-ip "$SUBNET" 2>/dev/null > "$TMPFILE"
if [ ! -s "$TMPFILE" ]; then
echo "$(date): nmap produced no output" >&2
rm -f "$TMPFILE"
exit 1
fi
JSON=$(python3 - "$TMPFILE" <<'PYEOF'
import sys, re, json
with open(sys.argv[1]) as f:
data = f.read()
devices = []
cur = None
for line in data.splitlines():
line = line.strip()
m = re.match(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?', line)
if m:
if cur:
devices.append(cur)
hn = m.group(1) if m.group(1) and m.group(1) != m.group(2) else ''
cur = {'ip': m.group(2), 'hostname': hn, 'mac': '', 'vendor': ''}
elif cur:
m2 = re.match(r'MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)', line)
if m2:
cur['mac'] = m2.group(1).lower()
cur['vendor'] = '' if m2.group(2) == 'Unknown' else m2.group(2)
if cur:
devices.append(cur)
print(json.dumps({'devices': devices}))
PYEOF
)
rm -f "$TMPFILE"
if [ -z "$JSON" ]; then
echo "$(date): JSON parse failed" >&2
exit 1
fi
RESPONSE=$(curl -sk --max-time 15 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" 2>/dev/null)
echo "$(date): $RESPONSE"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# JARVIS VoIP Phone Probe — runs every minute on PVE1
# Pings all Yealink phones + checks FusionPBX SIP registration (read-only)
# 200.3 is on an external FusionPBX — ping only, no SIP check
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
FUSION_HOST="134.209.72.226"
# IP|alias|extension(none=skip SIP check)|mac
PHONES=(
"10.48.200.2|Yealink — Myron Main (Ext 1000)|1000|80:5e:c0:35:04:77"
"10.48.200.3|Yealink — United Mirror & Glass (External SIP)|none|c4:fc:22:28:63:71"
"10.48.200.43|Yealink T48S — Tommy Main (Ext 1001)|1001|80:5e:0c:15:0c:4f"
"10.48.200.86|Yealink — Myron Vanguard WiFi (Offline During Work Hrs)|none|"
"10.48.200.65|Yealink — Myron Vanguard Work (Ext 1003)|1003|c4:fc:22:13:e1:89"
)
# Get SIP registrations from FusionPBX (read-only)
REG_OUTPUT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
root@$FUSION_HOST "fs_cli -x 'show registrations'" 2>/dev/null || echo "")
# Collect results as TSV, delegate JSON building to python3 to avoid injection
RESULTS=""
for PHONE in "${PHONES[@]}"; do
IFS='|' read -r IP ALIAS EXT MAC <<< "$PHONE"
if ping -c 1 -W 2 "$IP" > /dev/null 2>&1; then
STATUS="online"
else
STATUS="offline"
fi
if [ "$EXT" = "none" ]; then
SIP="external"
elif [ -n "$REG_OUTPUT" ] && echo "$REG_OUTPUT" | grep -q "^${EXT},"; then
SIP="registered"
else
SIP="unregistered"
fi
RESULTS="${RESULTS}${IP}\t${ALIAS}\t${MAC}\t${STATUS}\t${SIP}\t${EXT}\n"
done
JSON=$(printf "%b" "$RESULTS" | python3 -c "
import sys, json
devices = []
for line in sys.stdin:
line = line.rstrip('\n')
if not line:
continue
parts = line.split('\t')
if len(parts) < 6:
continue
ip, alias, mac, status, sip, ext = parts[:6]
devices.append({
'ip': ip, 'alias': alias, 'mac': mac,
'vendor': 'Yealink', 'status': status,
'sip_status': sip, 'extension': ext,
})
print(json.dumps({'devices': devices}))
")
curl -sk --max-time 10 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" > /dev/null 2>&1
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
JARVIS Ping Probe — runs on PVE1 (10.48.200.90), which is on the LAN.
Pings devices that can't run the full agent, then calls JARVIS heartbeat
on their behalf so the dashboard shows live status.
"""
import json
import subprocess
import urllib.request
import urllib.error
import ssl
JARVIS_URL = "http://10.48.200.211"
HOST_HEADER = "jarvis.orbishosting.com"
# Devices to probe: agent_id → api_key
DEVICES = {
"fortigate_gw": "00103aea6fcbf837bc55e11b445a3620",
"yealink_t48s": "2bf8bd7ca8dd31c28fd16aa956e15f88",
"homeassistant_ha": "6f8077dee7a7b4af202bc80886f1223d",
}
# Map agent_id → IP (for ping)
IPS = {
"fortigate_gw": "10.48.200.1",
"yealink_t48s": "10.48.200.43",
"homeassistant_ha": "10.48.200.97",
}
def ping(ip: str) -> bool:
result = subprocess.run(
["ping", "-c", "1", "-W", "2", ip],
capture_output=True, timeout=5
)
return result.returncode == 0
def heartbeat(agent_id: str, api_key: str, alive: bool):
# If device is down we still send heartbeat so JARVIS updates last_seen
# and sets status based on the alive flag via the metric payload
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/heartbeat",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def update_status(agent_id: str, api_key: str, status: str):
"""Push a minimal metric so JARVIS knows if device is up or down."""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({
"type": "system",
"data": {
"hostname": agent_id,
"cpu_percent": 0,
"ping_only": True,
"ping_status": status,
}
}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/metrics",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def main():
for agent_id, api_key in DEVICES.items():
ip = IPS.get(agent_id, "")
alive = ping(ip) if ip else False
status = "online" if alive else "offline"
print(f"{agent_id} ({ip}): {status}", flush=True)
heartbeat(agent_id, api_key, alive)
update_status(agent_id, api_key, status)
if __name__ == "__main__":
main()
+4 -4
View File
@@ -18,13 +18,13 @@ define(chr(39)+'CLAUDE_MODEL'.chr(39), chr(39)+'claude-sonnet-4-6'.chr(39))
define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024); define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024);
define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39)); define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39));
define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'groq/compound-mini'.chr(39)); define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'compound-beta-mini'.chr(39));
define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39)); define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39));
define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30); define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30);
define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.95:11434'.chr(39)); define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.210:11434'.chr(39));
define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.2:1b'.chr(39)); define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:70b'.chr(39)); define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90); define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90);
define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39)); define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39));
+2 -1
View File
@@ -111,7 +111,8 @@ switch ($agentAction) {
// ── HEARTBEAT ──────────────────────────────────────────────────────────── // ── HEARTBEAT ────────────────────────────────────────────────────────────
case 'heartbeat': case 'heartbeat':
update_agent_seen($agent['agent_id'], 'online', trim($data['version'] ?? '') ?: null); $hbStatus = in_array($data['status'] ?? '', ['online','offline']) ? $data['status'] : 'online';
update_agent_seen($agent['agent_id'], $hbStatus, trim($data['version'] ?? '') ?: null);
// Return any pending commands for this agent // Return any pending commands for this agent
$commands = JarvisDB::query( $commands = JarvisDB::query(
+1 -1
View File
@@ -181,7 +181,7 @@ function collect_all(): array {
$results['sites'] = 'skipped (fresh)'; $results['sites'] = 'skipped (fresh)';
} else try { } else try {
$sites = [ $sites = [
"jarvis" => "http://jarvis.orbishosting.com:1972", "jarvis" => "http://127.0.0.1",
'tomsjavajive' => 'https://tomsjavajive.com', 'tomsjavajive' => 'https://tomsjavajive.com',
'epictravelexp'=> 'https://epictravelexpeditions.com', 'epictravelexp'=> 'https://epictravelexpeditions.com',
'parkerslingshotrentals' => 'https://parkerslingshotrentals.com', 'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
+2 -1
View File
@@ -83,7 +83,8 @@ if ($method === 'POST' && $action === 'service') {
// Serve entities from ha_entities table (real-time agent push data) // Serve entities from ha_entities table (real-time agent push data)
$skipDomains = ['sensor','binary_sensor','button','update','select','number', $skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation', 'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button','media_player','scene','water_heater']; 'assist_satellite','input_button','media_player','scene','water_heater',
'alarm_control_panel','automation','script','calendar','notify','weather','camera','siren','remote','todo','lawn_mower'];
$skipKeywords = [ $skipKeywords = [
// HACS / system toggles // HACS / system toggles
'pre_release','get_hacs','matter_server','zerotier','mariadb', 'pre_release','get_hacs','matter_server','zerotier','mariadb',
+244 -13
View File
@@ -1,4 +1,9 @@
/*M!999999\- enable the sandbox mode */ /*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: jarvis_db
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0ubuntu0.24.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@@ -10,6 +15,11 @@
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `agent_commands`
--
DROP TABLE IF EXISTS `agent_commands`; DROP TABLE IF EXISTS `agent_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -28,6 +38,11 @@ CREATE TABLE `agent_commands` (
KEY `idx_created` (`created_at`) KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `agent_metrics`
--
DROP TABLE IF EXISTS `agent_metrics`; DROP TABLE IF EXISTS `agent_metrics`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -40,8 +55,13 @@ CREATE TABLE `agent_metrics` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_agent_time` (`agent_id`,`recorded_at`), KEY `idx_agent_time` (`agent_id`,`recorded_at`),
KEY `idx_recorded` (`recorded_at`) KEY `idx_recorded` (`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=28329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=31422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `alerts`
--
DROP TABLE IF EXISTS `alerts`; DROP TABLE IF EXISTS `alerts`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -58,8 +78,13 @@ CREATE TABLE `alerts` (
`auto_resolve` tinyint(1) DEFAULT 0, `auto_resolve` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_source_key` (`source_key`) KEY `idx_source_key` (`source_key`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `api_cache`
--
DROP TABLE IF EXISTS `api_cache`; DROP TABLE IF EXISTS `api_cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -70,6 +95,80 @@ CREATE TABLE `api_cache` (
PRIMARY KEY (`cache_key`) PRIMARY KEY (`cache_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `appointments`
--
DROP TABLE IF EXISTS `appointments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `appointments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`description` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`start_at` datetime NOT NULL,
`end_at` datetime DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`all_day` tinyint(1) DEFAULT 0,
`reminder_min` int(11) DEFAULT 30,
`alerted` tinyint(1) DEFAULT 0,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_start` (`start_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_jobs`
--
DROP TABLE IF EXISTS `arc_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_jobs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_type` varchar(64) NOT NULL,
`payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`priority` int(11) DEFAULT 0,
`status` enum('queued','running','done','failed','cancelled') DEFAULT 'queued',
`result` longtext DEFAULT NULL,
`error` varchar(2000) DEFAULT NULL,
`created_by` varchar(128) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`started_at` datetime DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_status`
--
DROP TABLE IF EXISTS `arc_status`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_status` (
`id` int(11) NOT NULL DEFAULT 1,
`version` varchar(20) DEFAULT NULL,
`started_at` datetime DEFAULT NULL,
`last_heartbeat` datetime DEFAULT NULL,
`active_jobs` int(11) DEFAULT 0,
`jobs_done` int(11) DEFAULT 0,
`jobs_failed` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `conversations`
--
DROP TABLE IF EXISTS `conversations`; DROP TABLE IF EXISTS `conversations`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -83,8 +182,13 @@ CREATE TABLE `conversations` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_session` (`session_id`), KEY `idx_session` (`session_id`),
KEY `idx_created` (`created_at`) KEY `idx_created` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=335 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ha_entities`
--
DROP TABLE IF EXISTS `ha_entities`; DROP TABLE IF EXISTS `ha_entities`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -102,8 +206,13 @@ CREATE TABLE `ha_entities` (
UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`), UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`),
KEY `idx_domain` (`domain`), KEY `idx_domain` (`domain`),
KEY `idx_updated` (`updated_at`) KEY `idx_updated` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=77909 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_facts`
--
DROP TABLE IF EXISTS `kb_facts`; DROP TABLE IF EXISTS `kb_facts`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -117,8 +226,13 @@ CREATE TABLE `kb_facts` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`) UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`)
) ENGINE=InnoDB AUTO_INCREMENT=26088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=41478 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_intents`
--
DROP TABLE IF EXISTS `kb_intents`; DROP TABLE IF EXISTS `kb_intents`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -133,8 +247,13 @@ CREATE TABLE `kb_intents` (
`active` tinyint(1) DEFAULT 1, `active` tinyint(1) DEFAULT 1,
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_ollama_models`
--
DROP TABLE IF EXISTS `kb_ollama_models`; DROP TABLE IF EXISTS `kb_ollama_models`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -147,8 +266,13 @@ CREATE TABLE `kb_ollama_models` (
`pulled_at` timestamp NULL DEFAULT current_timestamp(), `pulled_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `model_name` (`model_name`) UNIQUE KEY `model_name` (`model_name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_preferences`
--
DROP TABLE IF EXISTS `kb_preferences`; DROP TABLE IF EXISTS `kb_preferences`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -159,8 +283,13 @@ CREATE TABLE `kb_preferences` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `pref_key` (`pref_key`) UNIQUE KEY `pref_key` (`pref_key`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `known_commands`
--
DROP TABLE IF EXISTS `known_commands`; DROP TABLE IF EXISTS `known_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -173,6 +302,11 @@ CREATE TABLE `known_commands` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `metrics_history`
--
DROP TABLE IF EXISTS `metrics_history`; DROP TABLE IF EXISTS `metrics_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -184,8 +318,13 @@ CREATE TABLE `metrics_history` (
`recorded_at` timestamp NULL DEFAULT current_timestamp(), `recorded_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_metric_time` (`metric_name`,`recorded_at`) KEY `idx_metric_time` (`metric_name`,`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=33415 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=34771 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `network_devices`
--
DROP TABLE IF EXISTS `network_devices`; DROP TABLE IF EXISTS `network_devices`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -201,8 +340,13 @@ CREATE TABLE `network_devices` (
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_ip` (`ip`) UNIQUE KEY `uk_ip` (`ip`)
) ENGINE=InnoDB AUTO_INCREMENT=409 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=5556 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `registered_agents`
--
DROP TABLE IF EXISTS `registered_agents`; DROP TABLE IF EXISTS `registered_agents`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -210,10 +354,11 @@ CREATE TABLE `registered_agents` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`agent_id` varchar(128) NOT NULL, `agent_id` varchar(128) NOT NULL,
`hostname` varchar(255) NOT NULL, `hostname` varchar(255) NOT NULL,
`agent_type` enum('linux','homeassistant','proxmox') NOT NULL DEFAULT 'linux', `agent_type` enum('linux','homeassistant','proxmox','windows','macos') NOT NULL DEFAULT 'linux',
`ip_address` varchar(45) DEFAULT NULL, `ip_address` varchar(45) DEFAULT NULL,
`api_key` varchar(64) NOT NULL, `api_key` varchar(64) NOT NULL,
`capabilities` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`capabilities`)), `capabilities` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`capabilities`)),
`version` varchar(32) DEFAULT NULL,
`last_seen` datetime DEFAULT NULL, `last_seen` datetime DEFAULT NULL,
`status` enum('online','offline','unknown') NOT NULL DEFAULT 'unknown', `status` enum('online','offline','unknown') NOT NULL DEFAULT 'unknown',
`created_at` datetime DEFAULT current_timestamp(), `created_at` datetime DEFAULT current_timestamp(),
@@ -221,8 +366,56 @@ CREATE TABLE `registered_agents` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_agent_id` (`agent_id`), UNIQUE KEY `uk_agent_id` (`agent_id`),
KEY `idx_status` (`status`) KEY `idx_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tasks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`notes` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`priority` enum('urgent','high','normal','low') DEFAULT 'normal',
`status` enum('pending','in_progress','done','cancelled') DEFAULT 'pending',
`due_date` date DEFAULT NULL,
`due_time` time DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_status_due` (`status`,`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `usage_patterns`
--
DROP TABLE IF EXISTS `usage_patterns`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `usage_patterns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`intent_name` varchar(64) NOT NULL,
`hour` tinyint(2) NOT NULL,
`dow` tinyint(1) NOT NULL,
`hit_count` int(11) DEFAULT 1,
`last_seen` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_intent_time` (`intent_name`,`hour`,`dow`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`; DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -236,7 +429,7 @@ CREATE TABLE `users` (
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -248,3 +441,41 @@ CREATE TABLE `users` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--
-- Table structure for table `guardian_config`
--
CREATE TABLE IF NOT EXISTS `guardian_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`key_name` varchar(64) NOT NULL,
`value` varchar(255) NOT NULL DEFAULT '',
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key` (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `guardian_events`
--
CREATE TABLE IF NOT EXISTS `guardian_events` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`event_type` varchar(64) NOT NULL,
`severity` enum('info','warning','critical') NOT NULL DEFAULT 'info',
`agent_id` varchar(64) NOT NULL DEFAULT '',
`hostname` varchar(128) NOT NULL DEFAULT '',
`metric` varchar(64) NOT NULL DEFAULT '',
`value` float NOT NULL DEFAULT 0,
`threshold` float NOT NULL DEFAULT 0,
`message` text NOT NULL,
`ai_analysis` text NOT NULL DEFAULT '',
`acknowledged` tinyint(1) NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_severity` (`severity`),
KEY `idx_ack` (`acknowledged`),
KEY `idx_created` (`created_at`),
KEY `idx_agent` (`agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Dump completed on 2026-06-29 23:15:44
+70
View File
@@ -0,0 +1,70 @@
-- JARVIS KB Seed Data
-- Preferences
INSERT INTO kb_preferences (pref_key, pref_value) VALUES
('user_name', 'Myron'),
('user_title', 'Mr. Blair'),
('ai_model', 'llama3.1:8b'),
('timezone', 'America/Chicago')
ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value);
-- Intents: greeting, time, system, network, proxmox, ollama, tasks, HA
INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active) VALUES
-- Greetings
('greeting', '(?i)^(hello|hi|hey|good (morning|afternoon|evening)|what.?s up|howdy)\\b', 'Good {current_time}, {user_title}. All systems are online. How can I assist you?', 'system', 'response', 10, 1),
-- Time / date
('current_time', '(?i)\\b(what.?s the (time|current time)|what time is it|tell me the time)\\b', 'It is currently {current_time}, {user_title}.', NULL, 'response', 9, 1),
('current_date', '(?i)\\b(what.?s (today.?s date|the date)|what day is it|today.?s date)\\b', 'Today is {current_date}, {user_title}.', NULL, 'response', 9, 1),
-- System status
('system_status', '(?i)\\b(system (status|health)|how.?s (the system|everything)|jarvis status|all systems)\\b', 'JARVIS is fully operational, {user_title}. CPU: {cpu_usage}%, Memory: {mem_percent}% used ({mem_used_gb}GB / {mem_total_gb}GB). Disk: {disk_used} used of {disk_total}. Uptime: {uptime}. Network agents: {online_count}/{total_count} online.', 'system', 'response', 8, 1),
('cpu_status', '(?i)\\b(cpu|processor) (usage|load|status|percent|utilization)\\b', 'Current CPU usage is {cpu_usage}%, {user_title}. Load averages: {load_1m} (1m), {load_5m} (5m), {load_15m} (15m).', 'system', 'response', 8, 1),
('memory_status', '(?i)\\b(memory|ram|mem) (usage|status|free|used|available)\\b', 'Memory: {mem_used_gb}GB used of {mem_total_gb}GB ({mem_percent}% utilized), {user_title}. Free: {mem_free_gb}GB.', 'system', 'response', 8, 1),
('disk_status', '(?i)\\b(disk|storage|drive) (usage|space|status|free|used|available)\\b', 'Disk status: {disk_used} used of {disk_total} total, {disk_free} free, {user_title}.', 'system', 'response', 8, 1),
('uptime', '(?i)\\b(uptime|how long.*running|how long.*up|server uptime)\\b', 'JARVIS has been running for {uptime}, {user_title}.', 'system', 'response', 7, 1),
-- Network status
('network_status', '(?i)\\b(network (status|health|agents)|agents (online|status)|how many (agents|devices) (online|running))\\b', 'Network status: {online_count} of {total_count} agents are online, {user_title}.', 'network', 'response', 8, 1),
('network_scan', '(?i)\\b(run (a )?network scan|scan (the )?network|nmap scan|network devices)\\b', 'Initiating network scan, {user_title}.', NULL, 'action', 7, 1),
-- Proxmox
('proxmox_status', '(?i)\\b(proxmox (status|health)|vm (status|count|summary)|virtual machines|how many vms)\\b', 'Proxmox: {vm_running} of {vm_total} VMs/containers running, {user_title}. Host CPU: {pve_cpu_percent}%, Memory: {pve_mem_used_gb}GB / {pve_mem_total_gb}GB ({pve_mem_percent}%).', 'proxmox', 'response', 8, 1),
('vm_suggestions', '(?i)\\b(vm (resources|performance|usage)|check vms|resource usage)\\b', 'Checking VM resource usage, {user_title}.', 'proxmox', 'action', 7, 1),
-- Ollama / AI
('ollama_status', '(?i)\\b(ollama (status|health|models)|ai models|llm status|local (ai|models))\\b', 'Ollama is {status} with {model_count} model(s) available: {available_models}, {user_title}.', 'ollama', 'response', 7, 1),
-- Site health
('site_status', '(?i)\\b(site(s)? (status|health|up|down)|website status|are (the )?sites (up|down))\\b', 'Site health — jarvis: {jarvis}, orbishosting: {orbishosting}, tomtomgames: {tomtomgames}, tomsjavajive: {tomsjavajive}, parkerslingshotrentals: {parkersling}, epictravelexpeditions: {epictravelexp}, {user_title}.', 'sites', 'response', 7, 1),
-- Tasks / planner
('task_count', '(?i)\\b(how many tasks|pending tasks|task (count|summary)|my tasks)\\b', 'You have {pending_count} pending tasks and {overdue_count} overdue, {user_title}.', NULL, 'response', 7, 1),
('planner_briefing', '(?i)\\b((daily )?briefing|what.?s (on|happening) today|today.?s schedule|morning briefing)\\b', 'Fetching your daily briefing, {user_title}.', NULL, 'action', 8, 1),
-- Home Assistant
('ha_lights_on', '(?i)\\b(turn (on|off) (the |all )?lights?|lights? (on|off)|switch (on|off) (the )?lights?)\\b', 'Sending light command, {user_title}.', NULL, 'action', 8, 1),
('ha_scene', '(?i)\\b(activate (a |the )?scene|set (a |the )?scene|home scene)\\b', 'Activating home scene, {user_title}.', NULL, 'action', 7, 1),
-- Jellyfin
('jellyfin_now_playing', '(?i)\\b(what.?s (playing|on)|now playing|jellyfin.*playing|playing.*jellyfin)\\b', 'Checking Jellyfin now playing, {user_title}.', NULL, 'action', 7, 1),
('jellyfin_library', '(?i)\\b(jellyfin (library|media|shows?|movies?)|media library|show.*library)\\b', 'Fetching Jellyfin library, {user_title}.', NULL, 'action', 6, 1),
('jellyfin_pause', '(?i)\\b(pause (jellyfin|playback|media)|stop (playing|jellyfin))\\b', 'Pausing Jellyfin, {user_title}.', NULL, 'action', 7, 1),
-- DO server
('do_status', '(?i)\\b(do (server|status)|digital ocean (status|server)|vps status)\\b', 'Digital Ocean server is {do_status}, {user_title}.', 'do_server', 'response', 7, 1),
-- Focus / panels
('focus_mode', '(?i)\\b(focus (mode|on)|enable focus|concentration mode)\\b', 'Enabling focus mode, {user_title}.', NULL, 'action', 6, 1),
('show_panels', '(?i)\\b(show (all )?panels|expand (all|everything)|full view)\\b', 'Expanding all panels, {user_title}.', NULL, 'action', 6, 1),
-- Help
('help', '(?i)^(help|what can you do|commands|capabilities|what do you know)\\s*\\??$', 'I can help you with: system status, network status, VM/Proxmox status, Ollama AI models, site health, tasks and planner briefings, Jellyfin media, Home Assistant lights and devices, and general questions via Ollama. What would you like to know, {user_title}?', NULL, 'response', 5, 1)
ON DUPLICATE KEY UPDATE
pattern = VALUES(pattern),
response_template = VALUES(response_template),
active = 1;
SELECT COUNT(*) AS intents_seeded FROM kb_intents;
SELECT COUNT(*) AS prefs_seeded FROM kb_preferences;
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=JARVIS Arc Reactor
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/jarvis-arc
ExecStart=/opt/jarvis-arc/venv/bin/python3 /opt/jarvis-arc/reactor.py
Restart=always
RestartSec=10
User=root
StandardOutput=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
StandardError=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
[Install]
WantedBy=multi-user.target
+24 -3
View File
@@ -80,10 +80,31 @@ while IFS= read -r path; do
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
log "OLS reloaded for JARVIS deploy" log "OLS reloaded for JARVIS deploy"
# Sync reactor.py to runtime location if it changed # Patch live config.php with correct Ollama host/model and Groq search model
if echo "$CHANGED" | grep -q 'deploy/reactor.py'; then CONFIG="$path/api/config.php"
if [ -f "$CONFIG" ]; then
sed -i "s|http://10\.48\.200\.95:11434|http://10.48.200.210:11434|g" "$CONFIG"
sed -i "s|'llama3\.2:1b'|'llama3.1:8b'|g" "$CONFIG"
sed -i "s|\"llama3\.2:1b\"|\"llama3.1:8b\"|g" "$CONFIG"
sed -i "s|'llama3\.1:70b'|'llama3.1:8b'|g" "$CONFIG"
sed -i "s|\"llama3\.1:70b\"|\"llama3.1:8b\"|g" "$CONFIG"
sed -i "s|'groq/compound-mini'|'compound-beta-mini'|g;s|\"groq/compound-mini\"|\"compound-beta-mini\"|g" "$CONFIG"
log "Patched config.php: Ollama IP→.210, model→llama3.1:8b, Groq search→compound-beta-mini"
fi
# Sync reactor.py + service file to runtime location if they changed
if echo "$CHANGED" | grep -q 'deploy/reactor.py\|deploy/jarvis-arc.service'; then
mkdir -p /opt/jarvis-arc
cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py
systemctl restart jarvis-arc if echo "$CHANGED" | grep -q 'deploy/jarvis-arc.service'; then
cp "$path/deploy/jarvis-arc.service" /etc/systemd/system/jarvis-arc.service
systemctl daemon-reload
systemctl enable jarvis-arc
log "Arc Reactor service file installed and enabled"
fi
mkdir -p /home/jarvis.orbishosting.com/logs
systemctl restart jarvis-arc 2>/dev/null || \
bash -c "pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &"
log "Arc Reactor updated and restarted (reactor.py changed)" log "Arc Reactor updated and restarted (reactor.py changed)"
fi fi
fi fi
+108 -52
View File
@@ -49,8 +49,8 @@ CLAUDE_API_KEY = "sk-ant-api03-JL6vjFeyEfajQmaTOmsT6AfLLPs2icrIAvvJ0hdi4DuMi0155
CLAUDE_MODEL = "claude-sonnet-4-6" CLAUDE_MODEL = "claude-sonnet-4-6"
GROQ_API_KEY = "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" GROQ_API_KEY = "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"
GROQ_MODEL = "llama-3.3-70b-versatile" GROQ_MODEL = "llama-3.3-70b-versatile"
OLLAMA_HOST = "http://10.48.200.95:11434" OLLAMA_HOST = "http://10.48.200.210:11434"
OLLAMA_MODEL = "llama3.2:1b" OLLAMA_MODEL = "llama3.1:8b"
GMAIL_USER = "myronblair@gmail.com" GMAIL_USER = "myronblair@gmail.com"
GMAIL_PASS = "demsvdylwweacbcx" GMAIL_PASS = "demsvdylwweacbcx"
@@ -175,6 +175,16 @@ async def _ollama_call(messages: list, system: str = "") -> str:
data = await resp.json() data = await resp.json()
return data.get("response", "") return data.get("response", "")
async def _ollama_vision_call(image_b64: str, prompt: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{OLLAMA_HOST}/api/generate",
json={"model": "llava:7b", "prompt": prompt, "images": [image_b64], "stream": False},
timeout=aiohttp.ClientTimeout(total=120),
) as resp:
data = await resp.json()
return data.get("response", "").strip()
async def handle_llm(payload: dict) -> dict: async def handle_llm(payload: dict) -> dict:
message = payload.get("message", "") message = payload.get("message", "")
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.") system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
@@ -658,38 +668,44 @@ async def handle_screenshot(payload: dict) -> dict:
height = result.get("height", 0) height = result.get("height", 0)
file_size = result.get("file_size", 0) file_size = result.get("file_size", 0)
# Run Claude vision analysis if we have an image # Run vision analysis if we have an image — llava first, Claude fallback
analysis = "" analysis = ""
provider_used = "" provider_used = ""
if do_analyze and image_b64: if do_analyze and image_b64:
try: try:
import anthropic analysis = await _ollama_vision_call(image_b64, analyze_prompt)
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY) provider_used = "ollama:llava"
msg = await client.messages.create( log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
model="claude-opus-4-8-20251101",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64",
"media_type": "image/png", "data": image_b64}},
{"type": "text", "text": analyze_prompt},
],
}],
)
analysis = msg.content[0].text if msg.content else ""
provider_used = "claude"
log.info(f"[VISION] Claude analysis complete ({len(analysis)} chars)")
except Exception as e: except Exception as e:
log.warning(f"[VISION] Claude vision failed: {e}") log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
analysis = f"Vision analysis unavailable: {e}" try:
import anthropic
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
msg = await client.messages.create(
model="claude-opus-4-8-20251101",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64",
"media_type": "image/png", "data": image_b64}},
{"type": "text", "text": analyze_prompt},
],
}],
)
analysis = msg.content[0].text if msg.content else ""
provider_used = "claude"
log.info(f"[VISION] Claude fallback analysis complete ({len(analysis)} chars)")
except Exception as e2:
log.warning(f"[VISION] Claude fallback also failed: {e2}")
analysis = f"Vision analysis unavailable: {e2}"
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text": elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
# Text-only sysinfo snapshot — summarize with LLM # Text-only sysinfo snapshot — summarize with Ollama
try: try:
snap_text = json.dumps(result, indent=2)[:3000] snap_text = json.dumps(result, indent=2)[:3000]
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}" prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
analysis = await llm_call([{"role": "user", "content": prompt}], "groq") analysis = await llm_call([{"role": "user", "content": prompt}], "ollama")
provider_used = "groq" provider_used = "ollama"
except Exception as e: except Exception as e:
analysis = f"Analysis unavailable: {e}" analysis = f"Analysis unavailable: {e}"
@@ -742,32 +758,47 @@ async def handle_vision(payload: dict) -> dict:
if not image_b64: if not image_b64:
raise ValueError("No image data provided") raise ValueError("No image data provided")
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}") log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname} provider={provider}")
try: analysis = ""
import anthropic provider_used = provider
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
msg = await client.messages.create( if provider == "ollama" or provider == "llava":
model="claude-opus-4-8-20251101", analysis = await _ollama_vision_call(image_b64, prompt)
max_tokens=2048, provider_used = "ollama:llava"
messages=[{ else:
"role": "user", # Default: llava first, Claude fallback
"content": [ try:
{"type": "image", "source": {"type": "base64", analysis = await _ollama_vision_call(image_b64, prompt)
"media_type": "image/png", "data": image_b64}}, provider_used = "ollama:llava"
{"type": "text", "text": prompt}, log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
], except Exception as e:
}], log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
) try:
analysis = msg.content[0].text if msg.content else "" import anthropic
except Exception as e: client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
raise RuntimeError(f"Vision analysis failed: {e}") msg = await client.messages.create(
model="claude-opus-4-8-20251101",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64",
"media_type": "image/png", "data": image_b64}},
{"type": "text", "text": prompt},
],
}],
)
analysis = msg.content[0].text if msg.content else ""
provider_used = "claude"
except Exception as e2:
raise RuntimeError(f"Vision analysis failed (llava: {e}, claude: {e2})")
# Update stored screenshot if we have an ID # Update stored screenshot if we have an ID
if screenshot_id: if screenshot_id:
await db_execute( await db_execute(
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s", "UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
(analysis, "claude", int(screenshot_id)) (analysis, provider_used, int(screenshot_id))
) )
return { return {
@@ -775,7 +806,7 @@ async def handle_vision(payload: dict) -> dict:
"screenshot_id": screenshot_id, "screenshot_id": screenshot_id,
"prompt": prompt, "prompt": prompt,
"analysis": analysis, "analysis": analysis,
"provider": "claude", "provider": provider_used,
} }
@@ -1022,7 +1053,7 @@ async def guardian_loop() -> None:
"for Myron. Be direct about severity and what action to take. " "for Myron. Be direct about severity and what action to take. "
"No markdown, no headers." "No markdown, no headers."
) )
ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "groq") ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "ollama")
# Update the most recent guardian event with AI analysis # Update the most recent guardian event with AI analysis
await db_execute( await db_execute(
"""UPDATE guardian_events SET ai_analysis=%s """UPDATE guardian_events SET ai_analysis=%s
@@ -1051,7 +1082,7 @@ async def _guardian_inject_chat(message: str) -> None:
"""Write a proactive JARVIS message into the conversations table so the HUD picks it up.""" """Write a proactive JARVIS message into the conversations table so the HUD picks it up."""
try: try:
await db_execute( await db_execute(
"""INSERT INTO conversations (session_id, role, message, created_at) """INSERT INTO conversations (session_id, role, content, created_at)
VALUES ('guardian', 'assistant', %s, NOW())""", VALUES ('guardian', 'assistant', %s, NOW())""",
(message,) (message,)
) )
@@ -1065,7 +1096,7 @@ async def handle_sitrep(payload: dict) -> dict:
payload: { detail: brief|full, provider: groq } payload: { detail: brief|full, provider: groq }
""" """
detail = payload.get("detail", "full") detail = payload.get("detail", "full")
provider = payload.get("provider", "groq") provider = payload.get("provider", "ollama")
log.info(f"[GUARDIAN] SITREP requested (detail={detail})") log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
@@ -1806,7 +1837,7 @@ Keep the tone confident and action-oriented. Format with clear sections. Under 4
# Store in conversations so HUD can surface it # Store in conversations so HUD can surface it
await db_execute( await db_execute(
"INSERT INTO conversations (session_id, role, message, created_at) VALUES ('guardian','assistant',%s,NOW())", "INSERT INTO conversations (session_id, role, content, created_at) VALUES ('guardian','assistant',%s,NOW())",
(review_text,) (review_text,)
) )
@@ -2209,6 +2240,31 @@ async def lifespan(app: FastAPI):
log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}") log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}")
await get_pool() await get_pool()
await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,)) await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,))
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_config (
id INT AUTO_INCREMENT PRIMARY KEY,
key_name VARCHAR(64) NOT NULL,
value VARCHAR(255) NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_key (key_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_events (
id INT AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info',
agent_id VARCHAR(64) NOT NULL DEFAULT '',
hostname VARCHAR(128) NOT NULL DEFAULT '',
metric VARCHAR(64) NOT NULL DEFAULT '',
value FLOAT NOT NULL DEFAULT 0,
threshold FLOAT NOT NULL DEFAULT 0,
message TEXT NOT NULL,
ai_analysis TEXT NOT NULL DEFAULT '',
acknowledged TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_severity (severity),
KEY idx_ack (acknowledged),
KEY idx_created (created_at),
KEY idx_agent (agent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
asyncio.create_task(job_poller()) asyncio.create_task(job_poller())
asyncio.create_task(heartbeat_loop()) asyncio.create_task(heartbeat_loop())
asyncio.create_task(guardian_loop()) asyncio.create_task(guardian_loop())
@@ -2728,13 +2784,13 @@ async def guardian_chat_events(since: str = ""):
"""Return proactive guardian messages injected into conversations.""" """Return proactive guardian messages injected into conversations."""
if since: if since:
rows = await db_fetchall( rows = await db_fetchall(
"SELECT id, message, created_at FROM conversations " "SELECT id, content AS message, created_at FROM conversations "
"WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC", "WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC",
(since,) (since,)
) )
else: else:
rows = await db_fetchall( rows = await db_fetchall(
"SELECT id, message, created_at FROM conversations " "SELECT id, content AS message, created_at FROM conversations "
"WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10" "WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10"
) )
return rows or [] return rows or []
+4 -2
View File
@@ -242,7 +242,9 @@ if ($action) {
$search = strtolower(trim($_GET['search'] ?? '')); $search = strtolower(trim($_GET['search'] ?? ''));
$skipDomains = ['sensor','binary_sensor','button','update','select','number', $skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation', 'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button']; 'assist_satellite','input_button','media_player','scene','water_heater',
'alarm_control_panel','automation','script','calendar','notify',
'weather','camera','siren','remote','todo','lawn_mower'];
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone', $skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
'_siren_on','_email_on','_manual_record','_infrared_', '_siren_on','_email_on','_manual_record','_infrared_',
'do_not_disturb','matter_server','zerotier','mariadb', 'do_not_disturb','matter_server','zerotier','mariadb',
@@ -548,7 +550,7 @@ if ($action) {
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']); j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
} else { bad('Unknown cron worker'); } } else { bad('Unknown cron worker'); }
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') { } elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
shell_exec('pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &'); shell_exec('bash -c "mkdir -p /home/jarvis.orbishosting.com/logs; systemctl restart jarvis-arc 2>/dev/null || (pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &)"');
j(['ok'=>true,'msg'=>'Arc Reactor restarting']); j(['ok'=>true,'msg'=>'Arc Reactor restarting']);
} else { bad('Invalid worker action'); } } else { bad('Invalid worker action'); }
break; break;
+135 -262
View File
@@ -1,278 +1,151 @@
# JARVIS Agent Installer — Windows (PowerShell) #Requires -RunAsAdministrator
# Registers the agent as a proper Windows Service (Win 8.1+, no open window required). <#
# Requires pywin32. Runs the service as LocalSystem. .SYNOPSIS
# JARVIS Agent installer for Windows.
# Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
#
# One-liner (PowerShell as Admin):
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
param( .DESCRIPTION
[string]$JarvisUrl = "", Installs JARVIS Agent as a Windows Service that auto-starts at boot.
[string]$Key = "", Requires: PowerShell 5.1+, internet access, and Administrator rights.
[string]$AgentName = ""
)
# param() defaults don't apply when piped through iex — set here as fallback .EXAMPLE
if (-not $JarvisUrl) { $JarvisUrl = "https://jarvis.orbishosting.com" } # Interactive install (prompts for registration key):
if (-not $Key) { $Key = "f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" } irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
if (-not $AgentName) { $AgentName = $env:COMPUTERNAME.ToLower() }
$ErrorActionPreference = "Stop" # Silent install with key:
$InstallDir = "C:\ProgramData\jarvis-agent" $env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
$AgentScript = "$InstallDir\jarvis-agent-windows.py" #>
$ConfigFile = "$InstallDir\config.json"
$ServiceName = "JARVISAgent"
$OldTaskName = "JARVIS-Agent" # legacy scheduled-task name
Write-Host "" $ErrorActionPreference = 'Stop'
Write-Host " ====================================" -ForegroundColor Cyan $JARVIS_URL = 'https://jarvis.orbishosting.com'
Write-Host " JARVIS Agent Installer v3.1 " -ForegroundColor Cyan $INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
Write-Host " Windows Service Edition " -ForegroundColor Cyan $SERVICE_NAME = 'JARVISAgent'
Write-Host " ====================================" -ForegroundColor Cyan $AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
Write-Host "" $CONFIG_FILE = "$INSTALL_DIR\config.json"
# ── Require admin ────────────────────────────────────────────────────────────── function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
[Security.Principal.WindowsBuiltInRole]::Administrator)) { function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Error "Run PowerShell as Administrator and try again."
}
# ── Prompt if not provided ───────────────────────────────────────────────────── Write-Host "`n========================================" -ForegroundColor Yellow
$JarvisUrl = $JarvisUrl.TrimEnd("/") Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Find or install Python 3 (system-wide so LocalSystem service can reach it) # ── Stop existing service if running ────────────────────────────────────────
Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan $existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
$pythonPath = $null
# System-wide paths — accessible by LocalSystem service account
$systemPaths = @(
"C:\Program Files\Python313\python.exe",
"C:\Program Files\Python312\python.exe",
"C:\Program Files\Python311\python.exe",
"C:\Program Files\Python310\python.exe",
"C:\Program Files\Python39\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
)
function Install-PythonSystemWide {
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget (system-wide)..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { $wingetOk = $true; Write-Host " done." -ForegroundColor Green }
} catch {}
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 explicitly supports Win 8.1+
Write-Host " Downloading Python 3.11 (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Error "Could not download Python. Install from https://python.org choosing 'Install for all users', then re-run."
}
Write-Host " Installing system-wide (silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# Refresh PATH after install
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
}
# ── Search for system-wide Python first ───────────────────────────────────────
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
# ── Fall back to PATH — but flag if it's per-user ─────────────────────────────
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3", "py")) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
} catch {}
}
}
# ── If Python is per-user (AppData), install system-wide so LocalSystem can use it ──
$needsSystemPython = $false
if ($pythonPath -and ($pythonPath -match "AppData")) {
Write-Host " Found per-user Python: $pythonPath" -ForegroundColor Yellow
Write-Host " LocalSystem service needs system-wide Python. Installing..." -ForegroundColor Yellow
$needsSystemPython = $true
} elseif (-not $pythonPath) {
Write-Host " Python 3 not found. Installing system-wide..." -ForegroundColor Yellow
$needsSystemPython = $true
}
if ($needsSystemPython) {
Install-PythonSystemWide
# Locate the newly installed system-wide Python
$pythonPath = $null
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
if (-not $pythonPath) {
Write-Error "System-wide Python not found after install. Open a new Admin PowerShell and re-run."
}
}
Write-Host " Python: $pythonPath" -ForegroundColor Green
# ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
# pip install
$pipResult = & $pythonPath -m pip install --upgrade pywin32 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "pip install pywin32 failed (exit $LASTEXITCODE).`n$pipResult`nTry manually: $pythonPath -m pip install pywin32"
}
# postinstall registers service runner DLLs — non-fatal if it fails
try {
$postResult = & $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>&1
Write-Host " pywin32 installed." -ForegroundColor Green
} catch {
Write-Host " pywin32 installed (postinstall skipped — service should still work)." -ForegroundColor Yellow
}
# ── Create install directory and download agent ────────────────────────────────
Write-Host "[3/6] Downloading agent..." -ForegroundColor Cyan
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/3.1")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " Downloaded to $AgentScript" -ForegroundColor Green
} catch {
Write-Error "Download failed: $_"
}
# ── Write config ───────────────────────────────────────────────────────────────
Write-Host "[4/6] Writing config..." -ForegroundColor Cyan
$agentId = "${AgentName}_windows"
$config = [ordered]@{
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host " Config: $ConfigFile" -ForegroundColor Green
# ── Remove legacy scheduled task if present ────────────────────────────────────
try {
$oldTask = Get-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
if ($oldTask) {
Stop-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $OldTaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " Removed legacy scheduled task '$OldTaskName'." -ForegroundColor Yellow
}
} catch {}
# ── Register Windows service ───────────────────────────────────────────────────
Write-Host "[5/6] Registering Windows service '$ServiceName'..." -ForegroundColor Cyan
# Stop + remove any existing service first
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existing) { if ($existing) {
if ($existing.Status -eq "Running") { Write-Step "Stopping existing JARVIS Agent service..."
Write-Host " Stopping existing service..." -NoNewline if ($existing.Status -eq 'Running') {
& $pythonPath $AgentScript stop 2>&1 | Out-Null Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep -Seconds 3 Start-Sleep 2
Write-Host " stopped." -ForegroundColor Yellow
} }
Write-Host " Removing existing service..." -NoNewline try {
& $pythonPath $AgentScript remove 2>&1 | Out-Null & python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
Start-Sleep -Seconds 2 } catch {}
Write-Host " removed." -ForegroundColor Yellow Write-OK "Existing service removed."
} }
# Install the service (--startup auto = start at boot) # ── Check / install Python ────────────────────────────────────────────────────
& $pythonPath $AgentScript --startup auto install Write-Step "Checking Python..."
if ($LASTEXITCODE -ne 0) { $py = Get-Command python -ErrorAction SilentlyContinue
Write-Error "Service registration failed (exit $LASTEXITCODE). Check that pywin32 postinstall completed." if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
} }
# Configure failure recovery: restart after 5s, 10s, 30s # ── Create install dir ────────────────────────────────────────────────────────
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null Write-Step "Creating install directory..."
Write-Host " Service registered with auto-restart on failure." -ForegroundColor Green New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Start the service ────────────────────────────────────────────────────────── # ── Download agent script ─────────────────────────────────────────────────────
Write-Host "[6/6] Starting service..." -ForegroundColor Cyan Write-Step "Downloading JARVIS agent..."
& $pythonPath $AgentScript start try {
Start-Sleep -Seconds 4 Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
} catch {
Write-Fail "Failed to download agent: $_"
}
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue # ── Get registration key ──────────────────────────────────────────────────────
$status = if ($svc) { $svc.Status } else { "NotFound" } $regKey = $env:JARVIS_REG_KEY
$color = if ($status -eq "Running") { "Green" } else { "Yellow" } if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
Write-Host " Service status: $status" -ForegroundColor $color $existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
}
Write-Host "" # ── Get hostname ──────────────────────────────────────────────────────────────
Write-Host " ====================================" -ForegroundColor Green $hostname = $env:COMPUTERNAME
Write-Host " Installation complete! " -ForegroundColor Green $customHostname = $env:JARVIS_HOSTNAME
Write-Host " ====================================" -ForegroundColor Green if ($customHostname) { $hostname = $customHostname }
Write-Host ""
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White # ── Write config ──────────────────────────────────────────────────────────────
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White Write-Step "Writing config..."
Write-Host " Python : $pythonPath" -ForegroundColor White $cfg = @{
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White jarvis_url = $JARVIS_URL
Write-Host "" registration_key = $regKey
Write-Host " Manage the service:" -ForegroundColor Gray hostname = $hostname
Write-Host " Get-Service JARVISAgent" -ForegroundColor Gray agent_type = 'windows'
Write-Host " Start-Service JARVISAgent" -ForegroundColor Gray ssl_verify = $true
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray poll_interval = 30
Write-Host " Restart-Service JARVISAgent" -ForegroundColor Gray heartbeat_every = 10
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 30 -Wait" -ForegroundColor Gray update_check_hours = 24
Write-Host "" watch_services = @('WinDefend', 'Spooler', 'wuauserv')
Write-Host " To uninstall:" -ForegroundColor Gray } | ConvertTo-Json -Depth 5
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray $cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-Host " & '$pythonPath' '$AgentScript' remove" -ForegroundColor Gray Write-OK "Config written to $CONFIG_FILE"
Write-Host ""
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
+2 -2
View File
@@ -9,7 +9,7 @@ set -e
HOSTNAME_ARG="${1:-$(hostname -s)}" HOSTNAME_ARG="${1:-$(hostname -s)}"
AGENT_TYPE="${2:-linux}" AGENT_TYPE="${2:-linux}"
JARVIS_URL="http://10.48.200.211" JARVIS_URL="${JARVIS_URL:-https://jarvis.orbishosting.com}"
JARVIS_HOST="" JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent" INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent" CONFIG_DIR="/etc/jarvis-agent"
@@ -50,7 +50,7 @@ else
{ {
"jarvis_url": "$JARVIS_URL", "jarvis_url": "$JARVIS_URL",
"host_header": "$JARVIS_HOST", "host_header": "$JARVIS_HOST",
"ssl_verify": false, "ssl_verify": true,
"registration_key": "$REG_KEY", "registration_key": "$REG_KEY",
"hostname": "$HOSTNAME_ARG", "hostname": "$HOSTNAME_ARG",
"agent_type": "$AGENT_TYPE", "agent_type": "$AGENT_TYPE",
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
if not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()