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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions assets/sourceos/bin/turtle-install-launchd
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
# com.sourceos.turtle-mesh-push — GCS mesh sync every 5 minutes
# com.sourceos.turtle-mesh-serve — local mesh dashboard on :7788
#
# Optionally installs (requires --with-searxng):
# com.sourceos.searxng — start sovereign SearXNG container at login
#
# Usage:
# turtle-install-launchd # install both
# turtle-install-launchd --unload # stop + unload both
# turtle-install-launchd --status # show launchctl status
# turtle-install-launchd # install mesh agents
# turtle-install-launchd --with-searxng # also install SearXNG agent
# turtle-install-launchd --unload # stop + unload all loaded agents
# turtle-install-launchd --status # show launchctl status
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand All @@ -18,20 +22,26 @@ LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
LABELS=(com.sourceos.turtle-mesh-push com.sourceos.turtle-mesh-serve)

usage() {
echo "Usage: turtle-install-launchd [--unload|--status]"
echo "Usage: turtle-install-launchd [--with-searxng] [--unload|--status]"
}

status_mode=false
unload_mode=false
with_searxng=false
for arg in "$@"; do
case "$arg" in
--status) status_mode=true ;;
--unload) unload_mode=true ;;
--status) status_mode=true ;;
--unload) unload_mode=true ;;
--with-searxng) with_searxng=true ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $arg" >&2; usage >&2; exit 1 ;;
esac
done

if $with_searxng; then
LABELS+=(com.sourceos.searxng)
fi

if $status_mode; then
for label in "${LABELS[@]}"; do
echo -n " $label: "
Expand Down Expand Up @@ -72,3 +82,6 @@ done
echo ""
echo " Mesh dashboard → http://localhost:7788"
echo " GCS sync → every 5 minutes (requires SOURCEOS_GCS_BUCKET env in ~/.zshrc)"
if $with_searxng; then
echo " SearXNG → http://localhost:8888 (container must exist; run sourceos-searxng-setup.sh first)"
fi
43 changes: 34 additions & 9 deletions assets/sourceos/bin/turtle-mesh-serve
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import queue
import sys
import threading
import time
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

Expand Down Expand Up @@ -60,13 +61,31 @@ def load_jsonl_tail(path: Path, n: int = 50) -> list[dict]:
return items[-n:]


def _searxng_alive() -> bool:
state_file = Path(os.getenv("XDG_STATE_HOME", str(Path.home() / ".local/state"))) / "sourceos" / "searxng-url"
candidates = []
if state_file.exists():
url = state_file.read_text().strip().rstrip("/")
if url:
candidates.append(url)
candidates += ["http://localhost:8888", "http://localhost:8080"]
for url in candidates:
try:
urllib.request.urlopen(f"{url}/healthz", timeout=1)
return True
except Exception:
pass
return False


def gather_state() -> dict:
mesh = load_jsonl_tail(MESH_DIR / "context.jsonl", 60)
active = load_json(MESH_DIR / "active.json")
ci = load_json(STATUS_DIR / "ci.json")
pr = load_json(STATUS_DIR / "pr.json")
noetica= load_json(STATUS_DIR / "noetica.json")
board = load_json(STATUS_DIR / "board.json")
searxng_ok = _searxng_alive()

bb_cands: list[dict] = []
cand_path = BB_SUPPORT / "memory" / "candidates.jsonl"
Expand All @@ -84,15 +103,16 @@ def gather_state() -> dict:
notes.append({"name": p.name, "mtime": p.stat().st_mtime})

return {
"ts": datetime.datetime.now().isoformat(),
"active": active,
"mesh": mesh,
"ci": ci,
"pr": pr,
"noetica": noetica,
"board": board,
"bb_cands": bb_cands,
"notes": notes,
"ts": datetime.datetime.now().isoformat(),
"active": active,
"mesh": mesh,
"ci": ci,
"pr": pr,
"noetica": noetica,
"board": board,
"bb_cands": bb_cands,
"notes": notes,
"searxng_ok": searxng_ok,
}


Expand Down Expand Up @@ -227,6 +247,7 @@ h2{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em
</div>
<div id="status-bar">
<span id="noe-badge"><span class="dot dot-yellow pulse"></span>Noetica …</span>
<span id="searxng-badge"><span class="dot dot-yellow pulse"></span>Search …</span>
<span id="ci-badge">CI —</span>
<span id="pr-badge">PRs —</span>
<span id="board-badge">Board —</span>
Expand Down Expand Up @@ -273,6 +294,10 @@ function render(state) {
? `<span class="dot dot-green"></span>Noetica <span class="badge badge-green">up</span>`
: `<span class="dot dot-red"></span>Noetica <span class="badge badge-red">down</span>`

document.getElementById('searxng-badge').innerHTML = state.searxng_ok
? `<span class="dot dot-green"></span>Search <span class="badge badge-green">up</span>`
: `<span class="dot dot-red"></span>Search <span class="badge badge-red">down</span>`

const ci = state.ci || {}
const ciClass = ci.conclusion === 'success' ? 'badge-green' : ci.conclusion === 'failure' ? 'badge-red' : 'badge-yellow'
document.getElementById('ci-badge').innerHTML = ci.status
Expand Down
36 changes: 34 additions & 2 deletions assets/sourceos/bin/turtle-web-search
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,46 @@ import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

_DEFAULT_SEARXNG = "https://searx.be"
_USER_AGENT = "TurtleTerm/1.0 (sovereign; +https://sourceos.io)"


def _searxng_url() -> str:
"""Resolve the SearXNG base URL with a four-level priority chain:

1. SEARXNG_URL environment variable (explicit override)
2. Persisted URL written by sourceos-searxng-setup.sh
3. Auto-probe localhost:8888 (Docker) then localhost:8080 (Homebrew)
4. Public fallback (https://searx.be)
"""
# 1. Explicit env var wins
if url := os.environ.get("SEARXNG_URL", ""):
return url.rstrip("/")
# 2. Persisted local instance URL
state_file = (
Path(os.getenv("XDG_STATE_HOME", str(Path.home() / ".local/state")))
/ "sourceos"
/ "searxng-url"
)
if state_file.exists():
url = state_file.read_text().strip()
if url:
return url.rstrip("/")
# 3. Try localhost:8888 (Docker), then 8080 (Homebrew)
for local_url in ("http://localhost:8888", "http://localhost:8080"):
try:
urllib.request.urlopen(f"{local_url}/healthz", timeout=1)
return local_url
except Exception: # noqa: BLE001
pass
# 4. Public fallback
return "https://searx.be"


def search(query: str, count: int = 5) -> list[dict]:
"""Query SearXNG and return a ranked list of results."""
base = os.environ.get("SEARXNG_URL", _DEFAULT_SEARXNG).rstrip("/")
base = _searxng_url()
params = urllib.parse.urlencode({
"q": query,
"format": "json",
Expand Down
30 changes: 30 additions & 0 deletions assets/sourceos/launchd/com.sourceos.searxng.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
com.sourceos.searxng.plist
Ensures the sourceos-searxng Docker container starts at login.
Pre-requisite: run sourceos-searxng-setup.sh at least once to create the container.
-->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.sourceos.searxng</string>

<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/docker</string>
<string>start</string>
<string>sourceos-searxng</string>
</array>

<key>RunAtLoad</key>
<true/>

<key>StandardOutPath</key>
<string>/tmp/sourceos-searxng.log</string>

<key>StandardErrorPath</key>
<string>/tmp/sourceos-searxng.err</string>
</dict>
</plist>
120 changes: 120 additions & 0 deletions scripts/sourceos-searxng-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# MIT License — https://sourceos.io
# sourceos-searxng-setup.sh — provision a sovereign SearXNG instance for TurtleTerm.
#
# Option A (preferred): Docker on 127.0.0.1:8888
# Option B (fallback): Homebrew searxng on 127.0.0.1:8080
#
# Usage:
# sourceos-searxng-setup.sh
set -euo pipefail

STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/sourceos"
STATE_FILE="$STATE_DIR/searxng-url"
CONTAINER_NAME="sourceos-searxng"
DOCKER_PORT="8888"
BREW_PORT="8080"

# ── helpers ────────────────────────────────────────────────────────────────────

log() { echo " [searxng] $*"; }
warn() { echo " [searxng] WARN: $*" >&2; }

persist_url() {
mkdir -p "$STATE_DIR"
printf '%s' "$1" > "$STATE_FILE"
log "URL persisted → $STATE_FILE"
}

test_instance() {
local url="$1"
local test_url="${url}/search?q=test&format=json"
log "Testing $url ..."
result=$(curl -sf --max-time 10 "$test_url" 2>/dev/null || true)
if [[ -z "$result" ]]; then
warn "No response from $url — instance may need a moment to start."
return 1
fi
count=$(python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('results',[])))" <<< "$result" 2>/dev/null || echo "?")
log "SearXNG OK: ${count} results"
}

# ── Option A — Docker ──────────────────────────────────────────────────────────

setup_docker() {
log "Docker available — using container path (port $DOCKER_PORT)."

if docker inspect "$CONTAINER_NAME" &>/dev/null; then
log "Container '$CONTAINER_NAME' already exists — starting if not running."
docker start "$CONTAINER_NAME" &>/dev/null || true
else
log "Creating container '$CONTAINER_NAME' ..."
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p "127.0.0.1:${DOCKER_PORT}:8080" \
-e SEARXNG_BASE_URL="http://localhost:${DOCKER_PORT}" \
-e SEARXNG_LIMITER=false \
-e SEARXNG_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" \
searxng/searxng:latest
log "Container started."
fi

# Brief wait for the HTTP stack to bind
sleep 3

local url="http://localhost:${DOCKER_PORT}"
test_instance "$url" || warn "Run 'docker logs $CONTAINER_NAME' if the instance stays unreachable."
persist_url "$url"

echo ""
echo " export SEARXNG_URL=http://localhost:${DOCKER_PORT}"
echo ""
log "Done (Docker). Add the export above to ~/.zshrc if desired."
}

# ── Option B — Homebrew ────────────────────────────────────────────────────────

setup_brew() {
log "Docker not available — falling back to Homebrew."

brew install searxng 2>/dev/null || true

SEARXNG_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/searxng/settings.yml"
if [[ ! -f "$SEARXNG_CFG" ]]; then
mkdir -p "$(dirname "$SEARXNG_CFG")"
cat > "$SEARXNG_CFG" <<YAML
use_default_settings: true
server:
limiter: false
secret_key: "$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
port: ${BREW_PORT}
bind_address: "127.0.0.1"
YAML
log "Created $SEARXNG_CFG"
else
log "Using existing $SEARXNG_CFG"
fi

brew services restart searxng || brew services start searxng
sleep 3

local url="http://localhost:${BREW_PORT}"
test_instance "$url" || warn "Check 'brew services info searxng' if the instance stays unreachable."
persist_url "$url"

echo ""
echo " export SEARXNG_URL=http://localhost:${BREW_PORT}"
echo ""
log "Done (Homebrew). Add the export above to ~/.zshrc if desired."
}

# ── main ───────────────────────────────────────────────────────────────────────

log "Provisioning sovereign SearXNG for TurtleTerm ..."

if docker info &>/dev/null 2>&1; then
setup_docker
else
setup_brew
fi
Loading